В чем разница между 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"
- только если предыдущая команда выполнена успешно, удалить исходный файл