Выполнить команду для всех файлов в каталоге

291

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

Например, чтобы запустить команду для 1 файла:

$ cmd [option] [filename] > results.out
themaestro
источник
3
Я хотел бы добавить к вопросу. Можно ли это сделать с помощью xargs? например, ls <directory> | xargs cmd [options] {filenames put in here automatically by xargs} [more arguments] > results.out
Ozair Kafray
2
Может, но вы, вероятно , не хотите использоватьls для вождения xargs. Если cmdэто вообще грамотно написано, возможно, вы можете просто сделать cmd <wildcard>.
tripleee

Ответы:

427

Следующий код bash передаст $ file команде, где $ file будет представлять каждый файл в / dir

for file in /dir/*
do
  cmd [option] "$file" >> results.out
done

пример

el@defiant ~/foo $ touch foo.txt bar.txt baz.txt
el@defiant ~/foo $ for i in *.txt; do echo "hello $i"; done
hello bar.txt
hello baz.txt
hello foo.txt
Андрей Логвинов
источник
23
Если в нем нет файлов /dir/, цикл все равно выполняется один раз со значением '*' $file, что может быть нежелательно. Чтобы избежать этого, включите nullglob на время цикла. Добавьте эту строку перед циклом shopt -s nullglobи эту строку после цикла shopt -u nullglob #revert nullglob back to it's normal default state.
Stew-au
43
+1, и это просто стоило мне всей моей коллекции обоев. все после меня, используйте двойные кавычки. "$ file"
Behrooz
Если выходной файл в цикле одинаковый, гораздо эффективнее перенаправить вне цикла done >results.out(и, вероятно, тогда вы можете перезаписать вместо добавления, как я и предполагал здесь).
tripleee
Как вы получаете отдельные файлы результатов, которые имеют собственные имена для своих входных файлов?
Тимоти Свон
1
будьте осторожны, используя эту команду для огромного количества файлов в каталоге. Вместо этого используйте find -exec.
Колиско
183

Как насчет этого:

find /some/directory -maxdepth 1 -type f -exec cmd option {} \; > results.out
  • -maxdepth 1Аргумент предотвращает рекурсивный поиск find в любых подкаталогах. (Если вы хотите, чтобы такие вложенные каталоги обрабатывались, вы можете пропустить это.)
  • -type -f указывает, что будут обрабатываться только простые файлы.
  • -exec cmd option {}говорит ему запускаться cmdс указанным optionдля каждого найденного файла, с заменой имени файла на{}
  • \; обозначает конец команды.
  • Наконец, выходные данные всех отдельных cmdисполнений перенаправляются на results.out

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

Джим Льюис
источник
1
Это правильный способ обработки файлов. Использование цикла for подвержено ошибкам по многим причинам. Также сортировку можно выполнить с помощью других команд, таких как statи sort, что, конечно, зависит от критериев сортировки.
Tuxdna
1
если бы я хотел запустить две команды, как бы я связал их после -execопции? Должен ли я обернуть их в одинарные кавычки или что-то?
фрай
findвсегда лучший вариант, потому что вы можете фильтровать по шаблону имени файла с опцией, -nameи вы можете сделать это в одной команде.
Жоао Пиментел Феррейра
3
@frei ответ на ваш вопрос здесь: stackoverflow.com/a/6043896/1243247, но в основном просто добавьте -execварианты:find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira
2
как вы можете ссылаться на имя файла в качестве опции?
Тоскан
55

Я делаю это на моем Raspberry Pi из командной строки, выполнив:

for i in *;do omxplayer "$i";done
robgraves
источник
7

Принятые / высоко оцененные ответы великолепны, но в них отсутствуют некоторые мелкие детали. В этом посте рассматриваются случаи, как лучше справляться с неудачным раскрытием имени оболочки (glob), когда имена файлов содержат встроенные символы новой строки / тире и перемещение направления вывода команды из цикла for при записи результатов в файл.

При запуске расширения оболочки glob с использованием *существует возможность сбоя раскрытия, если в каталоге нет файлов, а в команду, которая будет запущена, будет передана нераскрытая строка глобуса, что может привести к нежелательным результатам. bashОболочка обеспечивает расширенный вариант оболочки для этого с помощью nullglob. Таким образом, цикл в основном выглядит следующим образом внутри каталога, содержащего ваши файлы

 shopt -s nullglob

 for file in ./*; do
     cmdToRun [option] -- "$file"
 done

Это позволяет безопасно выйти из цикла for, когда выражение ./*не возвращает никаких файлов (если каталог пуст)

или совместимым образом POSIX ( nullglobэто bashспецифический)

 for file in ./*; do
     [ -f "$file" ] || continue
     cmdToRun [option] -- "$file"
 done

Это позволяет вам войти в цикл, когда выражение не выполнится один раз, и [ -f "$file" ]проверить условие, является ли нераскрытая строка ./*допустимым именем файла в этом каталоге, чего не было бы. Таким образом, в этом случае сбой, используя, continueмы возвращаемся к forциклу, который не будет выполняться в дальнейшем

Также обратите внимание на использование --перед передачей аргумента имени файла. Это необходимо, потому что, как отмечалось ранее, имена файлов оболочки могут содержать тире в любом месте имени файла. Некоторые из команд оболочки интерпретируют это и рассматривают их как параметр команды, когда имя не указано правильно в кавычках, и выполняют команду, думая, если указан флаг.

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


Двойные кавычки имен файлов правильно решают случаи, когда имена содержат символы глобуса или пробелы. Но имена файлов * nix также могут содержать в себе новые строки. Таким образом, мы ограничиваем имена файлов единственным символом, который не может быть частью действительного имени файла - null byte ( \0). Поскольку bashвнутренне используются Cстроки стиля, в которых нулевые байты используются для обозначения конца строки, это правильный кандидат для этого.

Таким образом, используя printfопцию оболочки для разделения файлов с этим нулевым байтом, используя -dопцию readкоманды, мы можем сделать ниже

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done

Символы nullgloband printfи обернуты вокруг, (..)что означает, что они в основном выполняются в под-оболочке (дочерняя оболочка), потому что, чтобы избежать nullglobвозможности отразить на родительской оболочке, после завершения команды. -d ''Вариант readкоманды является не POSIX совместимым, поэтому нуждается в bashоболочке для этого нужно сделать. С помощью findкоманды это можно сделать как

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0)

Для findреализаций, которые не поддерживают -print0(кроме реализаций GNU и FreeBSD), это можно эмулировать с помощьюprintf

find . -maxdepth 1 -type f -exec printf '%s\0' {} \; | xargs -0 cmdToRun [option] --

Другим важным исправлением является перемещение направления из цикла for, чтобы уменьшить количество файловых операций ввода-вывода. При использовании внутри цикла оболочка должна выполнять системные вызовы дважды для каждой итерации цикла for, один раз для открытия и один раз для закрытия дескриптора файла, связанного с файлом. Это станет узким местом для вашей производительности при выполнении больших итераций. Рекомендуемое предложение - переместить его за пределы цикла.

Расширяя приведенный выше код с помощью этих исправлений, вы можете сделать

( shopt -s nullglob; printf '%s\0' ./* ) | while read -rd '' file; do
    cmdToRun [option] -- "$file"
done > results.out

который будет в основном помещать содержимое вашей команды для каждой итерации ввода вашего файла в стандартный вывод, а когда цикл завершится, откройте целевой файл один раз для записи содержимого стандартного вывода и его сохранения. Эквивалентная findверсия того же

while IFS= read -r -d '' file; do
    cmdToRun [option] -- "$file"
done < <(find -maxdepth 1 -type f -print0) > results.out
Inian
источник
1
+1 за проверку того, что файл существует. При поиске в несуществующем dir $ file содержит строку регулярного выражения "/ invalidd_dir / *", а не допустимое имя файла.
cdalxndr
3

Один быстрый и грязный способ, который иногда выполняет свою работу:

find directory/ | xargs  Command 

Например, чтобы найти количество строк во всех файлах в текущем каталоге, вы можете сделать:

find . | xargs wc -l
Рахул
источник
8
@Hubert Почему у вас есть переводы строк в ваших именах файлов ?!
musicin3d
2
это не вопрос «почему», это вопрос правильности - имена файлов не обязательно должны содержать печатные символы, они даже не должны быть действительными последовательностями UTF-8. Кроме того, то, что является новой строкой, очень сильно зависит от кодировки, одна кодировка ♀ является новой строкой другого. См. Кодовую страницу 437
Хьюберт Карио
2
правда, правда? это работает 99,9% времени, и он сказал «быстро и грязно»
Эдоардо
Я не фанат "быстрых и грязных" (ака "сломанных") скриптов Bash. Рано или поздно это заканчивается такими вещами, как знаменитый «Переехал ~/.local/share/steam. Запустил пар. Он удалил все в системе, принадлежащей пользователю». отчет об ошибке.
снижение активности
Это также не будет работать с файлами, в имени которых есть пробелы.
Shamas S - Восстановить Монику
2

Мне нужно было скопировать все файлы .md из одного каталога в другой, так что вот что я сделал.

for i in **/*.md;do mkdir -p ../docs/"$i" && rm -r ../docs/"$i" && cp "$i" "../docs/$i" && echo "$i -> ../docs/$i"; done

Который довольно трудно читать, поэтому давайте разберем его.

сначала перейдите в каталог с вашими файлами,

for i in **/*.md; для каждого файла в вашем шаблоне

mkdir -p ../docs/"$i"сделайте этот каталог в папке docs вне папки, содержащей ваши файлы. Который создает дополнительную папку с тем же именем, что и этот файл.

rm -r ../docs/"$i" удалить лишнюю папку, созданную в результате mkdir -p

cp "$i" "../docs/$i" Скопируйте фактический файл

echo "$i -> ../docs/$i" Эхо, что ты сделал

; done Жить долго и счастливо

Эрик Вули
источник
Примечание: для **работы globstarдолжен быть установлен параметр оболочки:shopt -s globstar
Hubert Kario
2

Ты можешь использовать xarg

ls | xargs -L 1 -d '\n' your-desired-command

-L 1 вызывает пропуск 1 предмет за раз

-d '\n'сделать вывод из lssplit'ed на основе новой строки.

Аль Мамун
источник
1

Основываясь на подходе @Jim Lewis:

Вот быстрое решение, использующее findи сортирующее файлы по дате их изменения:

$ find  directory/ -maxdepth 1 -type f -print0 | \
  xargs -r0 stat -c "%y %n" | \
  sort | cut -d' ' -f4- | \
  xargs -d "\n" -I{} cmd -op1 {} 

Для сортировки смотрите:

http://www.commandlinefu.com/commands/view/5720/find-files-and-list-them-sorted-by-modification-time

tuxdna
источник
это не будет работать, если в файлах есть новые строки в их именах
Hubert Kario
1
@HubertKario. Возможно, вы захотите узнать больше о том, -print0для которого findи -0для xargsкоторого используются пустые символы вместо пробелов (включая переводы строки).
смокинг
да, использование -print0- это то, что помогает, но весь конвейер должен использовать что-то вроде этого, а sortэто не так
Хуберт Карио
1

я думаю, что простое решение:

sh /dir/* > ./result.txt
yovie
источник
2
Вы правильно поняли вопрос? Это просто попытается запустить каждый файл в каталоге через оболочку - как если бы это был скрипт.
Rdas
1

Максимальная глубина

Я нашел, что это хорошо работает с ответом Джима Льюиса, просто добавьте немного так:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
$ find . -maxdepth 1 -type f -name '*.sh' -exec {} \; > results.out

Порядок сортировки

Если вы хотите выполнить в порядке сортировки, измените его следующим образом:

$ export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -maxdepth 2 -type f -name '*.sh' | sort | bash > results.out

Просто для примера, это будет выполнено в следующем порядке:

bash: 1: ./assets/main.sh
bash: 2: ./builder/clean.sh
bash: 3: ./builder/concept/compose.sh
bash: 4: ./builder/concept/market.sh
bash: 5: ./builder/concept/services.sh
bash: 6: ./builder/curl.sh
bash: 7: ./builder/identity.sh
bash: 8: ./concept/compose.sh
bash: 9: ./concept/market.sh
bash: 10: ./concept/services.sh
bash: 11: ./product/compose.sh
bash: 12: ./product/market.sh
bash: 13: ./product/services.sh
bash: 14: ./xferlog.sh

Неограниченная глубина

Если вы хотите выполнить на неограниченной глубине при определенных условиях, вы можете использовать это:

export DIR=/path/dir && cd $DIR && chmod -R +x *
find . -type f -name '*.sh' | sort | bash > results.out

затем поместите поверх каждого файла в дочерних каталогах, как это:

#!/bin/bash
[[ "$(dirname `pwd`)" == $DIR ]] && echo "Executing `realpath $0`.." || return

и где-нибудь в теле родительского файла:

if <a condition is matched>
then
    #execute child files
    export DIR=`pwd`
fi
Chetabahana
источник