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


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.