Виконати команду над усіма файлами в каталозі


290

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

Наприклад, для запуску команди на 1 файл:

$ cmd [option] [filename] > results.out

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

Відповіді:


425

Наступний баш код передасть $ файл команді, де $ файл буде представляти кожен файл у / 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.
Стю-ау

43
+1, і це просто коштувало мені всієї колекції шпалер. всі після мене, використовуйте подвійні лапки. "$ file"
Behrooz

Якщо вихідний файл однаковий у циклі, набагато ефективніше перенаправляти його за межі циклу done >results.out(і, мабуть, тоді ви можете перезаписати замість додавання, як я припустив тут).
tripleee

Як ви отримуєте окремі файли результатів, які призначені для власних вхідних файлів?
Тимофій Лебідь

1
будьте обережні, використовуючи цю команду для величезної кількості файлів у dir. Використовуйте замість find -exec.
kolisko

182

Як щодо цього:

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

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


1
Це правильний спосіб обробки файлів. Використання циклу for є схильним до помилок з багатьох причин. Також сортування може бути здійснено за допомогою інших команд, таких як statі sort, що, звичайно, залежить від критеріїв сортування.
tuxdna

1
якби я хотів запустити дві команди, як би я зв'язав їх після -execпараметра? Чи потрібно мені обертати їх в окремі цитати чи щось таке?
frei

findзавжди найкращий варіант, тому що ви можете фільтрувати за шаблоном імен файлів за допомогою параметра, -nameі ви можете це зробити в одній команді.
Жоао Піментел Феррейра

3
@frei відповідь на ваше запитання тут: stackoverflow.com/a/6043896/1243247, але в основному просто додайте -execваріанти:find . -name "*.txt" -exec echo {} \; -exec grep banana {} \;
João Pimentel Ferreira

2
як можна вказати ім'я файлу як варіант?
Тоскан

54

Я роблю це на своєму малиновому пі з командного рядка, запустивши:

for i in *;do omxplayer "$i";done

7

Прийняті / голосовані відповіді чудові, але їм не вистачає декількох дрібнозернистих деталей. У цій публікації висвітлено випадки, як краще впоратися, коли помилка розширення імені шляху оболонки (глобус) не вдається, коли назви файлів містять вбудовані символи нових рядків / тире і переміщення виводу команди перенаправляється з циклу for-циклу під час запису результатів у файл.

При запуску розширення глобальної оболонки з використанням *є можливість розширення не вдатися, якщо в каталозі відсутні файли, а нерозширена рядок глобулів буде передана команді, яку слід запустити, що може мати небажані результати. 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опцію оболонки для розмежування файлів за допомогою цього байта NULL, використовуючи -dпараметр readкоманди, ми можемо зробити нижче

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

Оголошення nullglobі і 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

яка в основному помістить вміст вашої команди для кожної ітерації вхідного файлу в stdout, і коли цикл закінчиться, відкрийте цільовий файл один раз для запису вмісту stdout та збереження його. Еквівалентна findверсія тієї самої була б

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

1
+1 для перевірки наявності файлу. Якщо шукаєте у неіснуючому dir, файл $ містить рядок регулярного вираження "/ invald_dir / *", що не є дійсним ім'ям файлу.
cdalxndr

3

Один з швидких і брудних способів, який іноді робить роботу:

find directory/ | xargs  Command 

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

find . | xargs wc -l

8
@Hubert Чому у ваших іменах є нові рядки ?!
musicin3d

2
це не питання "чому", це питання правильності - імена файлів не повинні містити символи для друку, вони навіть не повинні бути дійсними послідовностями UTF-8. Крім того, те, що новий рядок дуже сильно залежить від кодування, одне кодування another це інше. Дивіться сторінку коду 437
Хуберт Каріо

2
cmon, справді? це працює 99,9% часу, і він сказав "швидкий і брудний"
Едоардо,

Я не прихильник "швидких і брудних" (AKA "зламаних") сценаріїв Баша. Рано чи пізно це закінчується такими речами, як відомий "Moved ~/.local/share/steam. Ran 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

Що досить важко читати, тому давайте його зруйнувати.

спочатку CD в каталог зі своїми файлами,

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

mkdir -p ../docs/"$i"зробити цей каталог у папці документів поза папкою, що містить ваші файли. Що створює додаткову папку з тим самим іменем, що і цей файл.

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

cp "$i" "../docs/$i" Скопіюйте фактичний файл

echo "$i -> ../docs/$i" Відлуння того, що ти зробив

; done Живіть щасливо назавжди


Примітка: для **роботи globstarпотрібно встановити параметр оболонки:shopt -s globstar
Хуберт Каріо

2

Можна використовувати xarg

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

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

-d '\n'make output of 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


це не спрацює, якщо у файлах є нові рядки у своїх іменах
Хуберт Каріо

1
@HubertKario Ви можете прочитати більше про -print0для findі -0для xargsяких використовується нульовий символ замість будь-якого пробілу (включаючи переведення рядків).
tuxdna

так, використання -print0- це щось, що допомагає, але весь конвеєр повинен використовувати щось подібне, а sortце не так
Хуберт Каріо

1

Я думаю, що просте рішення:

sh /dir/* > ./result.txt

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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.