Як переглядати назви файлів, повернених знахідкою?


223
x=$(find . -name "*.txt")
echo $x

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

Звичайно, я можу додатково розділити їх порожнім, щоб отримати список, але я впевнений, що є кращий спосіб зробити це.

То який найкращий спосіб провести цикл за результатами findкоманди?


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

1
Щодо баунті : головна ідея тут - отримати канонічну відповідь, яка висвітлює всі можливі випадки (назви файлів з новими рядками, проблемні символи ...). Ідея полягає в тому, щоб потім використовувати ці імена файлів, щоб зробити якісь речі (викликайте іншу команду, виконайте певне перейменування ...). Дякую!
fedorqui 'ТАК перестаньте шкодити'

Не забувайте, що ім’я файлу чи папки може містити ".txt", а потім пробіл та інший рядок, наприклад "something.txt something" або "something.txt"
Yahya Yahyaoui

Використовуйте масив, а не вар. x=( $(find . -name "*.txt") ); echo "${x[@]}"Тоді ви можете пройти циклfor item in "${x[@]}"; { echo "$item"; }
Іван

Відповіді:


394

TL; DR: Якщо ви просто тут для найправильнішої відповіді, ви, мабуть, хочете моїх особистих уподобань find . -name '*.txt' -exec process {} \;(див. Внизу цієї публікації). Якщо у вас є час, прочитайте решту, щоб побачити кілька різних способів та проблеми з більшістю з них.


Повна відповідь:

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

for i in $x; do # Not recommended, will break on whitespace
    process "$i"
done

Краще, виріжте тимчасову змінну x:

for i in $(find -name \*.txt); do # Not recommended, will break on whitespace
    process "$i"
done

Це набагато краще Glob , коли ви можете. Безпечний простір для файлів у поточному каталозі:

for i in *.txt; do # Whitespace-safe but not recursive.
    process "$i"
done

Ввімкнувши globstar опцію, ви можете розмістити всі відповідні файли в цьому каталозі та всі підкаталоги:

# Make sure globstar is enabled
shopt -s globstar
for i in **/*.txt; do # Whitespace-safe and recursive
    process "$i"
done

У деяких випадках, наприклад, якщо імена файлів уже є у файлі, можливо, вам доведеться використовувати read :

# IFS= makes sure it doesn't trim leading and trailing whitespace
# -r prevents interpretation of \ escapes.
while IFS= read -r line; do # Whitespace-safe EXCEPT newlines
    process "$line"
done < filename

readможна безпечно використовувати в поєднанні з find, якщо правильно встановити роздільник:

find . -name '*.txt' -print0 | 
    while IFS= read -r -d '' line; do 
        process "$line"
    done

Для більш складних пошукових запитів ви, мабуть, захочете скористатися findабо з його -execопцією, або з-print0 | xargs -0 :

# execute `process` once for each file
find . -name \*.txt -exec process {} \;

# execute `process` once with all the files as arguments*:
find . -name \*.txt -exec process {} +

# using xargs*
find . -name \*.txt -print0 | xargs -0 process

# using xargs with arguments after each filename (implies one run per filename)
find . -name \*.txt -print0 | xargs -0 -I{} process {} argument

findтакож можна вписати в каталог кожного файлу перед запуском команди, використовуючи -execdirзамість -exec, і можна зробити інтерактивним (підказка перед запуском команди для кожного файлу), використовуючи -okзамість-exec (або -okdirзамість -execdir).

*: Технічно обидва findта xargs(за замовчуванням) виконують команду з якомога більше аргументів, скільки їх може вміститися в командному рядку стільки разів, скільки потрібно, щоб пройти всі файли. На практиці, якщо у вас дуже велика кількість файлів, це не має значення, і якщо ви перевищуєте довжину, але потрібні вони всі в одному командному рядку, ви SOL знайдете інший спосіб.


4
Варто відзначити , що у випадку з done < filenameі прямують разом з трубою STDIN не може бути використана більше (→ не більше інтерактивного матеріалу всередині циклу), але в тих випадках , коли це необхідно, можна використовувати 3<замість <і додати <&3або -u3до readчастина, в основному з допомогою окремого дескриптора файлу. Крім того, я вважаю, що read -d ''це те саме, read -d $'\0'але зараз я не можу знайти жодної офіційної документації.
phk

1
для i в * .txt; не працює, якщо файли не відповідають. Наприклад, потрібен один тест xtra, наприклад [[-e $ i]]
Майкл Брукс

2
Я втратив цю частину: -exec process {} \;і я здогадуюсь, що це зовсім інше питання - що це означає і як я маніпулюю нею? Де хороший Q / A або док. на ньому?
Алекс Холл

1
@AlexHall ви завжди можете переглядати сторінки чоловіка ( man find). У цьому випадку -execповідомляється findвиконати таку команду, що закінчується ;(або +), де {}буде замінено ім'ям файлу, який він обробляє (або, якщо +він використовується, всі файли, які зробили його до цього стану).
Кевін

3
@phk -d ''краще, ніж -d $'\0'. Останнє не тільки довше, але також говорить про те, що ви можете передавати аргументи, що містять нульові байти, але ви не можете. Перший нульовий байт позначає кінець рядка. У bash $'a\0bc'- це те саме, що aі $'\0'є таким же, як $'\0abc'або просто порожня рядок ''. help readзаявляє, що " Перший символ delim використовується для припинення введення ", тому використання ''в якості роздільника є трохи хаком. Перший символ у порожньому рядку - це нульовий байт, який завжди позначає кінець рядка (навіть якщо ви явно не записуєте його).
Socowi

114

Що б ви не робили, не використовуйте forцикл :

# Don't do this
for file in $(find . -name "*.txt")
do
    code using "$file"
done

Три причини:

  • Щоб цикл for рівномірно розпочався, він findповинен працювати до завершення.
  • Якщо ім'я файлу має будь-який пробіл (включаючи пробіл, вкладку чи новий рядок), він буде розглядатися як два окремих імені.
  • Хоча зараз малоймовірно, ви можете перекрити буфер командного рядка. Уявіть, якщо буфер вашого командного рядка містить 32 КБ, а ваш forцикл повертає 40 КБ тексту. Цей останній 8 КБ буде скинутий з вашого forциклу, і ви ніколи цього не дізнаєтесь.

Завжди використовуйте while readконструкцію:

find . -name "*.txt" -print0 | while read -d $'\0' file
do
    code using "$file"
done

Цикл виконується під час виконання findкоманди. Плюс ця команда буде працювати, навіть якщо ім'я файлу буде повернуто з пробілом у ньому. І ви не переповните буфер командного рядка.

-print0Використовуватиме NULL в якості файлу роздільник замість символу нового рядка і -d $'\0'буде використовувати NULL в якості роздільника при читанні.


3
Він не працюватиме з новими рядками у назви файлів. Використовуйте -execзамість find .
користувач невідомий

2
@userunknown - Ти з цим прав. -execє найбезпечнішим, оскільки він зовсім не використовує оболонку. Однак НЛ у назвах файлів зустрічається досить рідко. Простори в іменах файлів досить поширені. Головне - не використовувати forцикл, який рекомендували багато плакатів.
Девід В.

1
@userunknown - Тут Я це виправив, тому тепер він піклується про файли з новими рядками, вкладками та будь-яким іншим пробілом. Вся справа в повідомленні полягає в тому, щоб сказати ОП не використовувати for file $(find)через проблеми, пов'язані з цим.
Девід В.

4
Якщо ви можете використовувати -exec, це краще, але бувають випадки, коли вам справді потрібне ім'я, повернене до оболонки. Наприклад, якщо ви хочете видалити розширення файлів.
Бен Резер

5
Скористайтеся -rопцією, щоб read: -r raw input - disables interpretion of backslash escapes and line-continuation in the read data
Дайра Хопвуд

102
find . -name "*.txt"|while read fname; do
  echo "$fname"
done

Примітка: цей метод та (другий) метод, показаний bmargulies, безпечні для використання з пробілом у назвах файлів / папок.

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

find . -name '*.txt' -exec echo "{}" \;

Це {}- заповнювач знайденого елемента, а \;використовується для припинення -execприсудка.

А для повноти дозвольте додати ще один варіант - ви повинні любити * nix способи за їх універсальність:

find . -name '*.txt' -print0|xargs -0 -n 1 echo

Це відокремило б надруковані елементи \0символом, який не дозволений у жодній із файлових систем у назвах файлів чи папок, наскільки мені відомо, і тому повинен охоплювати всі основи. xargsпіднімає їх один за одним потім ...


3
Не вдалося, якщо новий рядок у імені файлу.
користувач невідомий

2
@user невідомо: ви праві, це справа, яку я взагалі не розглядав, і, я думаю, дуже екзотична. Але я відповідно підкоригував свою відповідь.
0xC0000022L

5
Ймовірно , варто відзначити, що find -print0і xargs -0обидва розширення GNU і не портативні (POSIX) аргументи. Неймовірно корисно в тих системах, які їх мають!
Toby Speight

1
Це також не вдається з назви файлів, що містять зворотні косої риси (які read -rвиправлятимуться), або назви файлів, що закінчуються у пробілі (який IFS= readби виправлено). Звідси BashFAQ №1, що передбачаєwhile IFS= read -r filename; do ...
Чарльз Даффі

1
Ще одна проблема з цим полягає в тому, що схоже тіло циклу виконується в одній оболонці, але це не так, наприклад exit, не буде працювати, як очікувалося, і змінні, встановлені в тілі циклу, не будуть доступні після циклу.
EM0,

17

Імена файлів можуть містити пробіли та навіть контрольні символи. Пробіли є роздільниками (за замовчуванням) для розширення оболонки в bash, і в результаті цього x=$(find . -name "*.txt")питання взагалі не рекомендується. Якщо find отримує ім'я файлу з пробілами, наприклад, "the file.txt"ви отримаєте 2 окремі рядки для обробки, якщо ви обробляєте xцикл. Ви можете покращити це, змінивши роздільник (bash IFSVariable), наприклад, на \r\n, але назви файлів можуть включати контрольні символи - тому це не є (повністю) безпечним методом.

З моєї точки зору, є 2 рекомендовані (і безпечні) схеми для обробки файлів:

1. Використовуйте для розширення циклу та імені файлу:

for file in ./*.txt; do
    [[ ! -e $file ]] && continue  # continue, if file does not exist
    # single filename is in $file
    echo "$file"
    # your code here
done

2. Використовуйте підстановку find-read-while & Process

while IFS= read -r -d '' file; do
    # single filename is in $file
    echo "$file"
    # your code here
done < <(find . -name "*.txt" -print0)

Зауваження

на рисунку 1:

  1. bash повертає шаблон пошуку ("* .txt"), якщо не знайдено відповідний файл - значить, потрібен додатковий рядок "продовжити, якщо файл не існує". див. Посібник з Bash, Розширення назви файлів
  2. nullglobДля уникнення цієї додаткової лінії може використовуватися параметр оболонки .
  3. "Якщо встановлено failglobпараметр оболонки, і не знайдено збігів, друкується повідомлення про помилку і команда не виконується." (з посібника Bash вище)
  4. параметр оболонки globstar: "Якщо встановлено, шаблон" ** ", який використовується в контексті розширення імені файлу, відповідатиме всім файлам, нульовим або більше директоріям і підкаталогам. Якщо за шаблоном дотримується" / ", відповідають лише каталоги та підкаталоги." див. Посібник з Bash, Shopt Builtin
  5. інші варіанти розширення імен файлів: extglob, nocaseglob, dotglobі змінна оболонкиGLOBIGNORE

на схемі 2:

  1. імена файлів можуть містити пробіли, вкладки, пробіли, нові рядки, ... для обробки файлових файлів безпечним способом, findпри цьому -print0використовується: ім'я файлу друкується з усіма контрольними символами та закінчується NUL. див. також Manpage Gnu Findutils, Небезпечна обробка імені файлів , безпечна обробка імені файлів , незвичні символи у назви файлів . Дивіться Девід А. Уілер нижче для детального обговорення цієї теми.

  2. Існує кілька можливих шаблонів для обробки результатів пошуку в циклі часу. Інші (Кевін, Девід В.) показали, як це зробити за допомогою труб:

    files_found=1 find . -name "*.txt" -print0 | while IFS= read -r -d '' file; do # single filename in $file echo "$file" files_found=0 # not working example # your code here done [[ $files_found -eq 0 ]] && echo "files found" || echo "no files found"
    Якщо ви спробуєте цей фрагмент коду, ви побачите, що він не працює: files_foundзавжди "вірно", і код завжди лунає "файлів не знайдено". Причина полягає в тому, що кожна команда конвеєра виконується в окремій підрозділі, тому змінена змінна всередині циклу (окрема підзарядка) не змінює змінну в головному скрипті оболонки. Ось чому я рекомендую використовувати заміну процесу як "кращу", більш корисну, більш загальну модель.
    Див. Я встановлюю змінні в циклі, який знаходиться в конвеєрі. Чому вони зникають ... (із FAQ Greg's Bash) для детальної дискусії на цю тему.

Додаткові довідки та джерела:


8

(Оновлено, щоб включати в себе чудове покращення швидкості @ Socowi)

З будь-яким, $SHELLхто його підтримує (dash / zsh / bash ...):

find . -name "*.txt" -exec $SHELL -c '
    for i in "$@" ; do
        echo "$i"
    done
' {} +

Зроблено.


Оригінальна відповідь (коротша, але повільніше):

find . -name "*.txt" -exec $SHELL -c '
    echo "$0"
' {} \;

1
Повільна як патока (оскільки вона запускає оболонку для кожного файлу), але це працює. +1
dawg

1
Замість цього \;ви можете використовувати +для передачі якомога більше файлів до одного exec. Потім використовуйте "$@"всередині скрипту оболонки для обробки всіх цих параметрів.
Socowi

3
У цьому коді є помилка. У циклі відсутній перший результат. Це тому, що $@опускає це, оскільки це, як правило, назва сценарію. Нам просто потрібно додати dummyміж ними, 'і {}це може зайняти місце імені сценарію, забезпечуючи, щоб усі збіги оброблялися циклом.
BCartolo

Що робити, якщо мені потрібні інші змінні за межами щойно створеної оболонки?
Jodo

OTHERVAR=foo find . -na.....має дозволити вам отримати доступ $OTHERVARзсередини новоствореної оболонки.
user569825

6
# Doesn't handle whitespace
for x in `find . -name "*.txt" -print`; do
  process_one $x
done

or

# Handles whitespace and newlines
find . -name "*.txt" -print0 | xargs -0 -n 1 process_one

3
for x in $(find ...)порушиться для будь-якого імені файлу з пробілом у ньому. Те ж саме, find ... | xargsякщо ви не використовуєте -print0та-0
glenn jackman

1
Використовуйте find . -name "*.txt -exec process_one {} ";"замість цього. Чому ми повинні використовувати xargs для збору результатів, які ми вже маємо?
користувач невідомий

@userunknown Добре, що все залежить від того, що process_oneє. Якщо це заповнювач для фактичної команди , то переконайтеся, що це спрацює (якщо ви виправите помилку помилки та додасте лапки завершення після "*.txt). Але якщо process_oneце визначена користувачем функція, ваш код не працюватиме.
токсалот

@toxalot: Так, але це не буде проблемою записати функцію в сценарій для виклику.
користувач невідомий

4

Ви можете зберігати свій findвихід у масиві, якщо бажаєте пізніше використовувати висновок як:

array=($(find . -name "*.txt"))

Тепер для друку кожного елемента в новому рядку, ви можете використовувати forциклічне повторення всіх елементів масиву, або ви можете використовувати оператор printf.

for i in ${array[@]};do echo $i; done

або

printf '%s\n' "${array[@]}"

Ви також можете використовувати:

for file in "`find . -name "*.txt"`"; do echo "$file"; done

Це надрукує кожне ім'я файлу в новому рядку

Щоб надрукувати findвихід лише у формі списку, ви можете скористатися одним із наступних:

find . -name "*.txt" -print 2>/dev/null

або

find . -name "*.txt" -print | grep -v 'Permission denied'

Це видалить повідомлення про помилки та надасть лише ім'я файлу як вихід у новому рядку.

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


1
Прокручування масиву провалюється з пробілами в іменах файлів.
EM0

Ви повинні видалити цю відповідь. Він не працює з пробілами у назвах імен файлів або каталогах.
jww

4

Якщо ви можете припустити, що імена файлів не містять нових рядків, ви можете прочитати вихід findмасиву Bash, використовуючи наступну команду:

readarray -t x < <(find . -name '*.txt')

Примітка:

  • -tвикликає readarrayпозбавлення нових рядків.
  • Він не працюватиме, якщо readarrayзнаходиться в трубі, отже, процес заміни.
  • readarray доступний з Bash 4.

Bash 4.4 і вище також підтримує -dпараметр для визначення роздільника. Використання нульового символу замість нового рядка для розмежування імен файлів працює також у рідкісному випадку, коли імена файлів містять нові рядки:

readarray -d '' x < <(find . -name '*.txt' -print0)

readarrayможна також викликати, як і mapfileз тими ж параметрами.

Довідка: https://mywiki.wooledge.org/BashFAQ/005#Loading_lines_from_a_file_or_stream


Це найкраща відповідь! Працює з: * Пробілами у іменах * Немає відповідних файлів * exitпід час перегляду результатів
EM0,

Не працює з усіма можливими іменами, хоча - для цього вам слід скористатисяreadarray -d '' x < <(find . -name '*.txt' -print0)
Чарльз Даффі

3

Мені подобається використовувати find, який спочатку присвоюється змінній, а IFS переходить на новий рядок наступним чином:

FilesFound=$(find . -name "*.txt")

IFSbkp="$IFS"
IFS=$'\n'
counter=1;
for file in $FilesFound; do
    echo "${counter}: ${file}"
    let counter++;
done
IFS="$IFSbkp"

Про всяк випадок, якщо ви хочете повторити більше дій на одному і тому ж наборі DATA і виявити дуже повільно на вашому сервері (високий рівень використання / I / 0)


2

Ви можете помістити повернуті імена файлів findу такий масив:

array=()
while IFS=  read -r -d ''; do
    array+=("$REPLY")
done < <(find . -name '*.txt' -print0)

Тепер ви можете просто прокрутити масив, щоб отримати доступ до окремих елементів і робити з ними все, що завгодно.

Примітка. Це безпека білого простору.


1
З Башем 4.4 або вище , ви можете використовувати одну команду замість циклу: mapfile -t -d '' array < <(find ...). Налаштування IFSне потрібно для mapfile.
Socowi

1

ґрунтуючись на інших відповідях та коментарях @phk, використовуючи fd # 3:
(який все ще дозволяє використовувати stdin всередині циклу)

while IFS= read -r f <&3; do
    echo "$f"

done 3< <(find . -iname "*filename*")

-1

find <path> -xdev -type f -name *.txt -exec ls -l {} \;

Тут буде перераховано файли та дані про атрибути.


-5

Як щодо того, якщо ви використовуєте grep замість знаходження?

ls | grep .txt$ > out.txt

Тепер ви можете прочитати цей файл, і назви файлів мають форму списку.


6
Ні, не робіть цього. Чому не слід розбирати вихід ls . Це крихке, дуже тендітне.
fedorqui 'ТАК перестаньте шкодити'
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.