11

Используя цикл, такой как

for i in `find . -name \*.txt` 

сломается, если в некоторых именах файлов есть пробелы.

Какую технику я могу использовать, чтобы избежать этой проблемы?

5 ответов5

11

В идеале вы вообще так не делаете, потому что правильно анализировать имена файлов в скрипте оболочки всегда сложно (исправьте это для пробелов, у вас все равно будут проблемы с другими встроенными символами, в частности с новой строкой). Это даже указано как первая запись на странице 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 вместо оболочки.

11

Используйте find -print0 и направьте его в xargs -0 , или напишите свою собственную маленькую C-программу и направьте ее в свою маленькую C-программу. Это то, для чего были изобретены -print0 и -0 .

Сценарии оболочки - не лучший способ обработки имен файлов с пробелами в них: вы можете сделать это, но это становится неуклюжим.

2

Вы можете установить "внутренний разделитель полей" (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 разделяется на него. Поскольку ни одно имя файла не может содержать этот символ (что я знаю), это также всегда безопасно. Это в основном полезно в простых случаях; и , как правило , не является отличной заменой для полного цикл.

1

Использование 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
Заметки
1

Я не согласен с 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\; `)

Всё ещё ищете ответ? Посмотрите другие вопросы с метками .