Используя цикл, такой как
for i in `find . -name \*.txt`
сломается, если в некоторых именах файлов есть пробелы.
Какую технику я могу использовать, чтобы избежать этой проблемы?
В идеале вы вообще так не делаете, потому что правильно анализировать имена файлов в скрипте оболочки всегда сложно (исправьте это для пробелов, у вас все равно будут проблемы с другими встроенными символами, в частности с новой строкой). Это даже указано как первая запись на странице BashPitfalls.
Тем не менее, есть способ почти сделать то, что вы хотите:
oIFS=$IFS
IFS=$'\n'
find . -name '*.txt' | while read -r i; do
# use "$i" with whatever you're doing
done
IFS=$oIFS
Не забывайте также указывать $i
при его использовании, чтобы потом не интерпретировать пробелы. Также не забудьте установить $IFS
обратно после его использования, потому что если вы этого не сделаете, это приведет к ошибкам позже.
Это имеет приложенный один нюанс: то , что происходит внутри цикла в while
может иметь место в субоболочке, в зависимости от точной оболочки , которую вы используете, поэтому переменные параметры не могут сохраняться. Версия цикла for
избегает этого, но ценой того, что даже если вы примените решение $IFS
чтобы избежать проблем с пробелами, у вас возникнут проблемы, если find
вернет слишком много файлов.
В какой-то момент правильным решением для всего этого становится выполнение этого на языке, таком как Perl или Python вместо оболочки.
Используйте find -print0
и направьте его в xargs -0
, или напишите свою собственную маленькую C-программу и направьте ее в свою маленькую C-программу. Это то, для чего были изобретены -print0
и -0
.
Сценарии оболочки - не лучший способ обработки имен файлов с пробелами в них: вы можете сделать это, но это становится неуклюжим.
Вы можете установить "внутренний разделитель полей" (IFS
) на что-то другое, чем пространство для разделения аргументов цикла, например
ORIGIFS=${IFS}
NL='
'
IFS=${NL}
for i in $(find . -name '*.txt'); do
IFS=${ORIGIFS}
#do stuff
done
IFS=${ORIGIFS}
Я сбрасываю IFS
после его использования в find, в основном потому, что он выглядит красиво, я думаю. Я не видел никаких проблем с установкой новой строки, но я думаю, что это "чище".
Другой метод, в зависимости от того, что вы хотите сделать с выводом из find
, это либо напрямую использовать -exec
с командой find
, либо использовать -print0
и передать его в xargs -0
. В первом случае find
заботится о экранировании имени файла. В случае -print0
find
выводит свой вывод с нулевым разделителем, а затем xargs
разделяется на него. Поскольку ни одно имя файла не может содержать этот символ (что я знаю), это также всегда безопасно. Это в основном полезно в простых случаях; и , как правило , не является отличной заменой для полного цикл.
find -print0
с xargs -0
Использование find -print0
сочетании с xargs -0
полностью устойчиво к допустимым именам файлов и является одним из наиболее расширяемых доступных методов. Например, допустим, вы хотели получить список всех файлов PDF в текущем каталоге. Вы могли бы написать
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 echo
Он найдет каждый PDF-файл (через -iname '*.pdf'
) в текущем каталоге (.
) И в любом подкаталоге и передаст каждый из них в качестве аргумента команде echo
. Поскольку мы указали -n 1
вариант, xargs
будет передавать только один аргумент в то время , чтобы echo
Если бы мы пропустили эту опцию, xargs
бы как можно больше echo
. (Вы можете echo short input | xargs --show-limits
чтобы увидеть, сколько байтов разрешено в командной строке.)
xargs
?Мы можем ясно увидеть влияние, которое xargs
оказывает на его ввод, и, в частности, влияние -n
, используя скрипт, который выводит свои аргументы более точно, чем echo
.
$ cat > echoArgs.sh <<'EOF'
#!/bin/bash
echo "Number of arguments: $#"
[[ $# -eq 0 ]] && exit
for i in $(seq 1 $#); do
echo "Arg $i: <$1>"
shift
done
EOF
$ find . -iname '*.pdf' -print0 | xargs -0 ./echoArgs.sh
$ find . -iname '*.pdf' -print0 | xargs -0 -n 1 ./echoArgs.sh
Обратите внимание, что он отлично обрабатывает пробелы и переводы строк,
$ touch 'A space-age
new line of vending machines.pdf'
$ find . -iname '*space*' -print0 | xargs -0 -n 1 ./echoArgs.sh
что было бы особенно проблематичным со следующим общим решением:
chmod +x ./echoArgs.sh
for file in $(ls *spacey*); do
./echoArgs.sh "$file"
done
Заметки
Я не согласен с bash
bashers, потому что bash
, наряду с набором инструментов * nix, весьма искусен в работе с файлами (включая те, чьи имена имеют встроенный пробел).
На самом деле, find
дает вам точный контроль над выбором файлов для обработки ... Что касается bash, вам действительно нужно только осознать, что вы должны превратить вас в bash words
; как правило, с помощью "двойных кавычек", или другого механизма, такого как IFS, или find's {}
Обратите внимание, что в большинстве / многих ситуациях вам не нужно устанавливать и сбрасывать IFS; просто используйте IFS локально, как показано в примерах ниже. Все три отлично справляются с пробелами. Также вам не нужна "стандартная" структура цикла, потому что find's \;
фактически петля; просто поместите свою логику цикла в функцию bash (если вы не вызываете стандартный инструмент).
IFS=$'\n' find ~/ -name '*.txt' -exec function-or-util {} \;
И еще два примера
IFS=$'\n' find ~/ -name '*.txt' -exec printf 'Hello %s\n' {} \;
IFS=$'\n' find ~/ -name '*.txt' -exec echo {} \+ |sed 's/home//'
'find also allows you to pass multiple filenames as args to you script ..(if it suits your need: use
+instead
\; `)