Ітерація над списком файлів з пробілами


201

Я хочу повторити список файлів. Цей список є результатом findкоманди, тому я придумав:

getlist() {
  for f in $(find . -iname "foo*")
  do
    echo "File found: $f"
    # do something useful
  done
}

Це добре, за винятком випадків, коли у файлі є пробіли у своєму імені:

$ ls
foo_bar_baz.txt
foo bar baz.txt

$ getlist
File found: foo_bar_baz.txt
File found: foo
File found: bar
File found: baz.txt

Що я можу зробити, щоб уникнути розколу на пробіли?


Це в основному специфічний підзаголовок Коли обернути лапки навколо змінної оболонки?
трійка

Відповіді:


253

Ви можете замінити ітерацію на основі слова на лінійну:

find . -iname "foo*" | while read f
do
    # ... loop body
done

31
Це надзвичайно чисто. І змушує мене відчувати себе приємніше, ніж змінювати IFS у поєднанні з циклом for
Петрік,

15
Це розділить один шлях до файлу, який містить \ n. Гаразд, їх не повинно бути навколо, але їх можна створити:touch "$(printf "foo\nbar")"
Оллі Сондерс

4
Щоб запобігти будь-якій інтерпретації вхідних даних (зворотні косої риски, провідні та відсталі пробіли), використовуйте IFS= while read -r fзамість цього.
mklement0

2
Ця відповідь показує більш безпечне поєднання findциклу та час.
травень

5
Схоже , вказуючи на очевидному, але майже у всіх простих випадках -execбудуть чистий , ніж явний цикл: find . -iname "foo*" -exec echo "File found: {}" \;. Крім того , у багатьох випадках ви можете замінити , що в минулому \;з +покласти багато файлів в одній команді.
naught101

152

Існує кілька працездатних способів досягти цього.

Якщо ви хочете тісно дотримуватися оригінальної версії, це можна зробити так:

getlist() {
        IFS=$'\n'
        for file in $(find . -iname 'foo*') ; do
                printf 'File found: %s\n' "$file"
        done
}

Це все ще не вдасться, якщо імена файлів мають в собі буквальні нові рядки, але пробіли не порушать його.

Однак возитися з IFS не потрібно. Ось мій кращий спосіб зробити це:

getlist() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: %s\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

Якщо вам здається, що < <(command)синтаксис не знайомий, вам слід прочитати про підміну процесу . Перевагою цього for file in $(find ...)є те, що файли з пробілами, новинками та іншими символами правильно обробляються. Це працює тому, що findпри -print0використанні null(ака \0) в якості термінатора для кожного імені файлу, і, на відміну від нового рядка, null не є юридичним символом у імені файлу.

Перевага в цьому перед майже еквівалентною версією

getlist() {
        find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
                printf 'File found: %s\n' "$file"
        done
}

Хіба що будь-яке призначення змінної в тілі циклу while зберігається. Тобто, якщо ви підключитесь так, whileяк вище, тіло корпусу whileзнаходиться в нижній частині корпусу, що може бути не тим, що ви хочете.

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

EDIT : Ось хороший тестовий сценарій, щоб ви могли зрозуміти різницю між різними спробами вирішення цієї проблеми

#!/usr/bin/env bash

dir=/tmp/getlist.test/
mkdir -p "$dir"
cd "$dir"

touch       'file not starting foo' foo foobar barfoo 'foo with spaces'\
    'foo with'$'\n'newline 'foo with trailing whitespace      '

# while with process substitution, null terminated, empty IFS
getlist0() {
    while IFS= read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# while with process substitution, null terminated, default IFS
getlist1() {
    while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done < <(find . -iname 'foo*' -print0)
}

# pipe to while, newline terminated
getlist2() {
    find . -iname 'foo*' | while read -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# pipe to while, null terminated
getlist3() {
    find . -iname 'foo*' -print0 | while read -d $'\0' -r file ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, default IFS
getlist4() {
    for file in "$(find . -iname 'foo*')" ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}

# for loop over subshell results, newline terminated, newline IFS
getlist5() {
    IFS=$'\n'
    for file in $(find . -iname 'foo*') ; do
            printf 'File found: '"'%s'"'\n' "$file"
    done
}


# see how they run
for n in {0..5} ; do
    printf '\n\ngetlist%d:\n' $n
    eval getlist$n
done

rm -rf "$dir"

1
Прийняв вашу відповідь: найповніше і найцікавіше - я не знав про $IFSі < <(cmd)синтаксис. Ще одне залишається незрозумілим для мене, чому $в $'\0'? Дуже дякую.
gregseth

2
+1, але вам слід додати ... while IFS= read... для обробки файлів, які починаються або закінчуються пробілом.
Гордон Девіссон

1
Існує один застереження до процесу заміщення процесу. Якщо у вас є запит всередині циклу (або ви читаєте з STDIN будь-яким іншим способом), введення буде заповнене матеріалами, які ви подаєте в цикл. (можливо, це слід додати до відповіді?)
andsen

2
@uvsmtid: Це питання було позначене тегами, bashтому я почував себе в безпеці, використовуючи особливості bash. Заміна процесу не переноситься на інші оболонки (сама sh, швидше за все, не отримає такого значного оновлення).
сорпігал

2
Поєднання IFS=$'\n'з forперешкоджає розділенню слів між внутрішніми рядками, але все-таки робить результуючі рядки предметом глобалізації, тому такий підхід не є повністю надійним (якщо ви також не вимкнете глобалінг спочатку). Хоча це read -d $'\0'працює, це злегка вводить в оману те, що він говорить про те, що ви можете використовувати $'\0'для створення NULs - ви не можете: a \0в рядку, цитованому ANSI C, ефективно закінчує рядок, так що -d $'\0'фактично те саме, що -d ''.
mklement0

29

Існує також дуже просте рішення: покладатися на баш-глобінг

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"
$ ls
stupid   file 3  stupid file1     stupid file2
$ for file in *; do echo "file: '${file}'"; done
file: 'stupid   file 3'
file: 'stupid file1'
file: 'stupid file2'

Зауважте, що я не впевнений, що це поведінка за замовчуванням, але я не бачу спеціальних налаштувань у моєму знімку, тому я б сказав, що це повинно бути "безпечним" (перевірено на OSX та ubuntu).


13
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"

6
як бічна примітка, це буде працювати лише в тому випадку, якщо ви хочете виконати команду. Вбудована оболонка не працюватиме таким чином.
Олексій


6

Оскільки ви не робите жодного іншого типу фільтрації find, ви можете використовувати наступне з bash4.0:

shopt -s globstar
getlist() {
    for f in **/foo*
    do
        echo "File found: $f"
        # do something useful
    done
}

Збіг **/буде відповідати нулю або більше каталогів, тому повний зразок буде відповідати foo*в поточному каталозі або будь-яких підкаталогах.


3

Мені дуже подобається цикл та ітерація масиву, тому я думаю, що я додам цю відповідь до суміші ...

Мені також сподобався дурний приклад файлу marchelbling. :)

$ mkdir test
$ cd test
$ touch "stupid file1"
$ touch "stupid file2"
$ touch "stupid   file 3"

Всередині тестового каталогу:

readarray -t arr <<< "`ls -A1`"

Це додає кожний рядок списку файлів у масив bash, названий arrз будь-якого видаленого останнього рядка.

Скажімо, ми хочемо дати цим файлам кращі назви ...

for i in ${!arr[@]}
do 
    newname=`echo "${arr[$i]}" | sed 's/stupid/smarter/; s/  */_/g'`; 
    mv "${arr[$i]}" "$newname"
done

$ {! arr [@]} розширюється до 0 1 2, тому "$ {arr [$ i]}" є i- м елементом масиву. Цитати навколо змінних важливі для збереження пробілів.

Результат - три перейменовані файли:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3

2

findмає -execаргумент, який перетинає результати пошуку та виконує довільну команду. Наприклад:

find . -iname "foo*" -exec echo "File found: {}" \;

Тут {}представлені знайдені файли, і їх завершення ""дозволяє команді оболонки, що виходить, мати справу з пробілами у назві файлу.

У багатьох випадках ви можете замінити цю останню \;(яка запускає нову команду) на a \+, яка дозволить розмістити декілька файлів в одній команді (не обов'язково всі вони одразу, хоча див. man findДокладніше).


0

У деяких випадках тут, якщо вам просто потрібно скопіювати або перемістити список файлів, ви можете передати цей список, щоб пробудити також.
Важливо \"" "\"навколо поля $0(стисло файли, один рядок-список = один файл).

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'

0

Гаразд - моє перше повідомлення про стек переповнення!

Хоча мої проблеми з цим завжди були в csh, а не розбивати рішення, яке я представляю, я впевнений, буде працювати в обох. Проблема полягає в інтерпретації оболонки повернення "ls". Ми можемо видалити "ls" з проблеми, просто використовуючи розширення оболонки *підстановки - але це дає помилку "без збігу", якщо в поточній (або зазначеній папці) файлах немає файлів - щоб обійти це, ми просто розширимо розширення, щоб включити дот-файли таким чином: * .*- це завжди дасть результати, оскільки файли. і .. завжди буде присутній. Тож у csh ми можемо використовувати цю конструкцію ...

foreach file (* .*)
   echo $file
end

якщо ви хочете відфільтрувати стандартні точкові файли, то це досить просто ...

foreach file (* .*)
   if ("$file" == .) continue
   if ("file" == ..) continue
   echo $file
end

Код у першій публікації цього потоку буде записаний таким чином:

getlist() {
  for f in $(* .*)
  do
    echo "File found: $f"
    # do something useful
  done
}

Сподіваюсь, це допомагає!


0

Ще одне рішення для роботи ...

Мета була:

  • вибирати / фільтрувати назви файлів рекурсивно в каталогах
  • обробляти всі імена (незалежно від місця на шляху ...)
#!/bin/bash  -e
## @Trick in order handle File with space in their path...
OLD_IFS=${IFS}
IFS=$'\n'
files=($(find ${INPUT_DIR} -type f -name "*.md"))
for filename in ${files[*]}
do
      # do your stuff
      #  ....
done
IFS=${OLD_IFS}


Думаю, для конструктивного зауваження, але: 1– це актуальна проблема, 2- оболонка могла розвинутись у часі… як усі я припускаю; 3- Жодна відповідь вище не могла б задовольнити Пряме дозвіл pb, не змінюючи проблеми і не відміняючи :-)
Vince B
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.