В чем разница между find с -exec и xargs?

Пытаясь изучить скрипты Bash Я хочу выполнить какую-то команду для всех файлов в моем текущем каталоге, которые удовлетворяют определенному условию. С помощью

find -name *.flac

Конкретно хочу конвертировать .flac в .mp3, Я могу найти все файлы. Однако я не вижу разницы в выполнении команды с использованием какой-либо опции -exec за find и используя xargs, Например

find -name *.flac | xargs -i ffmpeg -i {} {}.mp3

по сравнению с

find -name *.flac -exec ffmpeg -i {} {}.mp3 \;

Кто-то может указать на разницу? Какая лучше практика? Каковы преимущества / недостатки?

Также: если бы я хотел одновременно удалить исходный файл, как бы я добавил вторую команду в приведенном выше коде?

3 ответа

Резюме:

Если вы не намного лучше знакомы с xargs чем -exec, вы, вероятно, захотите использовать -exec когда вы используете find,

поскольку xargs это отдельная программа, вызов которой, вероятно, будет несколько менее эффективным, чем использование -exec что является особенностью find программа. Обычно мы не хотим вызывать дополнительную программу, если она не дает каких-либо дополнительных преимуществ с точки зрения надежности, производительности или удобочитаемости. поскольку find ... -exec ... предоставляет возможность запуска команд со списком аргументов (как xargs делает) если это возможно, нет никаких преимуществ использования xargs с find над -exec, В случае ffmpeg, мы должны указать входные и выходные файлы, поэтому мы не можем повысить производительность, используя ни один метод для построения списка аргументов, и с xargs удалить нелогичное оригинальное расширение имени файла сложнее.

Какие xargs делает

Примечание: подробный флаг (который печатает построенную команду с ее аргументами) в xargs является -t и интерактивный флаг (который заставляет пользователя запрашивать подтверждение для работы с каждым аргументом) -p , Вы можете найти оба из них полезными для понимания и тестирования его поведения.

xargs пытается превратить свой STDIN (обычно STDOUT предыдущей команды, которая была передана на него) в список аргументов какой-либо команды.

command1 | xargs command2 [output of command1 will be appended here]

Поскольку STDOUT или STDIN - это просто поток текста (это также то, почему вы не должны анализировать вывод ls), xargs легко спотыкается Он читает аргументы как разделенные пробелами или символами новой строки. Имена файлов могут содержать пробелы и могут даже содержать символы новой строки, и такие имена файлов будут вызывать неожиданное поведение. Допустим, у вас есть файл с именем foo bar, Когда список, содержащий это имя файла, передается в xargs, он пытается запустить данную команду на foo и на bar,

Та же проблема возникает при вводе command foo bar и вы знаете, что можете избежать этого, указав пробел или полное имя, например command foo\ bar или же command "foo bar", но даже если мы сможем процитировать список, переданный xargs мы обычно этого не хотим, потому что не хотим, чтобы весь список обрабатывался как один аргумент. Стандартное решение этого заключается в использовании нулевого символа в качестве разделителя, поскольку имена файлов не могут его содержать:

find path test(s) -print0 | xargs -0 command

Это вызывает find добавить нулевой символ к каждому имени файла вместо пробела, и xargs рассматривать только нулевой символ как разделитель.

Проблемы по-прежнему могут возникать, если команда не принимает несколько аргументов или если список аргументов очень длинный.

В этом случае вы используете ffmpeg, который ожидает, что входные файлы будут указаны первыми, а выходные файлы будут указаны последними. Мы можем сказать ffmpeg какие файлы использовать в качестве входных данных явно с -i флаг, но нам нужно указать имя выходного файла (из которого обычно угадывается формат, хотя мы также можем его указать). Итак, для создания подходящих команд вам нужно использовать опцию замены строки (-I или же -i) из xargs чтобы указать как входные, так и выходные файлы:

... | xargs -I{} command {} {}.out

(в документации сказано, что -i не рекомендуется для этой цели, и мы должны использовать -I вместо этого, но я не уверен почему. Когда используешь -I необходимо указать замену ({} обычно используется) сразу после опции. С -i Вы можете не указывать замену, но {} понимается по умолчанию.)

-I опция заставляет список команд разделяться только на новые строки, а не пробелы, поэтому, если вы уверены, что ваши имена файлов не будут содержать новые строки, вам не нужно использовать -print0 | xargs -0 когда вы используете -I, Если вы не уверены, вы все равно можете использовать более безопасный синтаксис:

find -name "*.flac" -print0 | xargs -0I{} ffmpeg -i {} {}.mp3

Тем не менее, выигрыш в производительности xargs (что позволяет нам выполнить команду один раз со списком аргументов) здесь потеряно, так как ffmpeg должен быть запущен один раз для каждой пары входных и выходных файлов (вы можете легко увидеть это, предварительно echo в ffmpeg проверить вышеуказанную команду). Это также создает нелогичное имя файла и не позволяет запускать несколько команд. Чтобы сделать последнее, вы можете позвонить bash, как в ответе десерта:

... | xargs -I{} bash -c 'ffmpeg -i {} {}.mp3 && rm {}'

но переименовать это сложно.

Как -exec это отличается

Когда вы используете -exec возможность find найденные файлы передаются команде в качестве аргументов после -exec, Они не превращены в текст. С синтаксисом:

find ... -exec command {} \;

command запускается один раз для каждого найденного файла. С синтаксисом

find ... -exec command {} +

список аргументов составляется из найденных файлов, так что мы можем выполнить команду только один раз (или только столько раз, сколько требуется) для нескольких файлов, что дает выигрыш в производительности, обеспечиваемый xargs, Однако, поскольку аргументы имени файла не созданы из потока текста, используя -exec не имеет проблемы xargs имеет разрывы на пробелы и другие специальные символы.

С ffmpeg мы не можем использовать + по той же причине, что и xargs не дал никакого выигрыша в производительности; поскольку нам нужно указать и вход, и выход, команда должна запускаться для каждого файла отдельно. Мы должны использовать некоторую форму

find -name "*.flac" -exec ffmpeg -i {} {}.out \;

Это, опять же, даст вам довольно нелогично названный файл, как объясняется в ответе десерта, так что вы можете захотеть удалить его, так как ответ десерта объясняет, как делать с манипуляциями со строками (нелегко сделать в xargs; еще одна причина использовать -exec). Также объясняется, как выполнить несколько команд для файла, чтобы вы могли безопасно удалить исходный файл после успешного преобразования.

Вместо того, чтобы повторять рекомендацию десерта, с которой я согласен, я предложу альтернативу find, что позволяет подобную гибкость bash -c после -exec; удар for цикл:

shopt -s globstar           # allow recursive globbing with **
for f in ./**/*.flac; do    # for all files ending with .flac
   # convert them, stripping the original extension from the new filename
   echo ffmpeg -i "$f" "${f%.flac}.mp3" &&
   echo rm -v "$f"          # if that succeeded, delete the original
done
shopt -u globstar           # turn recursive globbing off

Удалить echo Если после тестирования они действительно работают с файлами.

ffmpeg не узнает -- чтобы отметить конец параметров, чтобы избежать имен файлов, начинающихся с - интерпретируясь как варианты, мы используем ./ указывать текущий каталог вместо того, чтобы начинать с ** так что все пути начинаются с ./ вместо произвольных имен файлов. Это означает, что нам не нужно использовать -- с rm (который это признает) либо.


Примечание: вы должны указать свои -name проверить выражение, если оно содержит какие-либо символы подстановки, в противном случае оболочка расширит их, если это возможно (т. е. если они соответствуют каким-либо файлам в текущем каталоге), прежде чем они будут переданы find так, во-первых, используйте

find -name "*.flac"

предотвратить неожиданное поведение.

Как Занна и Десерт уже ответили -exec должно быть предпочтительным, когда xargs в этом нет необходимости ("Обычно мы не хотим вызывать дополнительную программу, если она не дает никаких дополнительных преимуществ с точки зрения надежности, производительности или читабельности".)

Хотя это совершенно правильно, я хочу добавить, что xargs в сочетании с -P Флаг может обеспечить существенную выгоду с точки зрения производительности.

xargs будет запускать процессы параллельно, обеспечивая многопоточность, похожую, но более гибкую, чем parallel команда.

-P max-procs, --max-procs=max-procs
              Run up to max-procs processes at a time; the default is 1.  If max-procs is 0, xargs will run as many processes as possible at a time.  Use the -n option or the -L option with -P; other‐
              wise chances are that only one exec will be done. 
              [...]

Это особенно помогает с процессами, которые сами по себе не являются многопоточными. В твоем случае ffmpeg позаботится о многопоточности, так что это не поможет или даже отрицательно скажется на производительности.

find . -name "*.ext" -print0 | xargs -0 -i -P 20 command -in {} -out {}.out

Обычно пытаются вызвать как можно меньше команд, но в вашем случае я думаю, что это дело вкуса - я бы пошел с -exec, используя это так:

find . -name '*.flac' -exec bash -c 'ffmpeg -i "$0" "${0%flac}mp3" && rm "$0"' {} \;

Хитрость заключается в том, чтобы позвонить bash с -c Таким образом, вы можете не только выполнять несколько команд, но и использовать подстановку параметров Bash для удаления flac заканчивая вашими именами файлов - я полагаю, вы на самом деле не хотите заканчивать файлами с именем filename.flac.mp3, не так ли?

Пояснения

  • bash -c '…' {} - запустить команду (ы) в bash с именем файла в качестве первого аргумента (доступно с $0)
  • ${0%flac} - полоса flac от конца имени файла
  • && rm "$0" - только если предыдущая команда выполнена успешно, удалить исходный файл
Другие вопросы по тегам