переменная IFS и другой результат с перечислением файлов с циклом

5

Я хочу получить список файлов в текущем каталоге и его подкаталогах (я хочу использовать однострочный скрипт):

IFS=$(echo -en "\n\b");
for FILE in $(find -type f); do echo "$FILE"; done

Обычно это работает, как и ожидалось, но недавно, с моим списком файлов:

file_.doc
file_0.doc
file_ [2006_02_25] .doc
file_ [2016_06_16] .odt
file_ [2016_06_16] .pdf
file_ [16-6-2006] .doc
file_.pdf
file_ 4-4-2006.doc

выход:

./file_.doc                                                                                                                                                 
./file_0.doc                                                                                                                                                
./file_0.doc
./file_[2016_06_16].odt
./file_[2016_06_16].pdf
./file_0.doc
./file_.pdf
./file_ 4-4-2006.doc

Если я изменю переменную IFS на:

IFS=$(echo -en "\n");

тогда вывод будет (исправлен):

./file_.doc
./file_0.doc
./file_[2006_02_25].doc
./file_[2016_06_16].odt
./file_[2016_06_16].pdf
./file_[16-6-2006].doc
./file_.pdf
./file_ 4-4-2006.doc

Я прочитал, что '\b'это необходимо , и нашел решение, которое использует printfвместоecho .

Мои вопросы:

1) Не могли бы вы объяснить, что отличало эти результаты?

2) Решение, использующее printfвыше, может быть альтернативой echo -en "\n\b"?

Duqu
источник

Ответы:

4

Вывод команды подстановки подвергается разбиению по словам (которое вы позаботились, установив IFS) и глобализации имени файла. [abc]Конструкция является «соответствует ни одному из персонажей a, b, c» как обычно, и [2006_02_25].docспички 0.doc.


В Bash / ksh / zsh вы можете использовать двойную звездочку, чтобы получить все файлы в дереве каталогов (рекурсивно, а не только текущий каталог). Это должно найти те же файлы, что и ваш пример:

shopt -s globstar      # in Bash
# set -o globstar      # in ksh
for file in **/* ; do
    [[ -f $file ]] || continue     # check it's a regular file, like find -type f
    ...
done

Конечно, find это мощный инструмент, поэтому его использование может быть проще, если у вас много условий. Если вы это сделаете, вы должны отключить глобализацию имени файла set -fв дополнение к исправлению IFS:

set -f
IFS=$'\n'
for file in $(find -type f -some -other -conditions) ; do
    ...
done

или используйте while readвместо этого цикл с заменой процесса:

while IFS= read -r file ; do 
    ...
done < <(find -type f -some -other -conditions)

(Выше приведено аналогичное find ... | while ..., но оно обходит проблему последней части конвейера, выполняемой в подоболочке.)

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

В Bash, по крайней мере, есть способ заставить новые строки работать с именами файлов. Установка readразделителя на пустую строку позволяет ему эффективно использовать байт NUL в качестве разделителя. Так:

while IFS= read -d '' -r file ; do 
    ...
done < <(find -type f -some -other -conditions -print0)

Несмотря на то, что это начинает становиться непривычным для написания, вам нужны все эти опции, readчтобы заставить его работать без искажения ввода.


Что касается IFS... Установка IFS=$(echo -en "\n");устанавливает IFSв пустую строку (потому что подстановка команды съедает завершающий символ новой строки), что не приводит к разделению. Вывод в этом случае кажется правильным, так как вы получаете все выходные за findодин раз, а не построчно. Это также скрывает проблему с глобализацией имени файла, поскольку полная многострочная строка не соответствует ни одному имени файла и передается как есть.

Вы можете увидеть разницу, если сделаете что-то еще, кроме как просто напечатать значение цикла. Попробуйте добавить несколько разделителей:

IFS=$(echo -en "\n")       # same as IFS=
for FILE in $(find -type f); do echo "<$FILE>"; done

Безусловно, самый простой способ установить IFSновую строку во всем, кроме самых простых стандартных оболочек, - это IFS=$'\n'.

ilkkachu
источник
Очень подробное объяснение!
Duqu
4

Не делать $( find ... ). Это вызовет генерацию имени файла (globbing), и некоторые из ваших имен файла будут интерпретированы как шаблоны globbing, которые соответствуют другим именам файла. Например, шаблон file_[2006_02_25].docи file_[16-6-2006].docсовпадения, file_0.docпоэтому это имя файла встречается вместо этих двух шаблонов.

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

Вместо этого просто используйте find(и не изменяйте IFS):

find . type -f -print

Если вы хотите сделать другие вещи с этими файлами, то вы можете сделать это в -exec:

find . -type f -exec sh -c 'printf "Found the file %s\n" "$@"' sh {} +

Если вы просто хотите обработать файлы в текущем каталоге, вы можете просто

for name in *; do
    printf 'Found the name %s\n' "$name"
done

Связанный:

Кусалананда
источник
Для рекурсивного получения файлов внутри подкаталогов (я хочу получить имя файла в качестве переменных), мне удается использовать цикл с тестированием, если записи являются каталогами, и заходить в каждый каталог, я мог бы сделать это, но я хочу знать, если это "безопасный"? Спасибо!
Duqu
1
@duqu Вы можете сделать это (если вы всегда не забываете цитировать расширения переменных), но это неудобно. Было бы лучше, если бы вы использовали -execдля выполнения необходимый код для каждого файла или группы файлов. Я не смог написать что-то более конкретное по этому поводу, потому что вопрос не в том, как это сделать, а в том, почему вы получаете разные результаты с помощью своего кода.
Кусалананда