Ответ 1
Вы можете заменить итерацию на основе слов линейной:
find . -iname "foo*" | while read f
do
# ... loop body
done
Я хочу перебрать список файлов. Этот список является результатом команды 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
Что я могу сделать, чтобы избежать разделения на пробелы?
Вы можете заменить итерацию на основе слов линейной:
find . -iname "foo*" | while read f
do
# ... loop body
done
Существует несколько способов достижения этого.
Если вы хотите придерживаться оригинальной версии, это можно сделать следующим образом:
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
(aka \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: здесь хороший тест script, чтобы вы могли понять разницу между различными попытками решения этой проблемы.
#!/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"
Существует также очень простое решение: полагаться на bash globbing
$ 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).
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
find . -name "fo*" -print0 | xargs -0 ls -l
См. man xargs
.
Поскольку вы не выполняете какой-либо другой тип фильтрации с помощью find
, вы можете использовать следующее из bash
4.0:
shopt -s globstar
getlist() {
for f in **/foo*
do
echo "File found: $f"
# do something useful
done
}
**/
будет соответствовать нулевым или более каталогам, поэтому полный шаблон будет соответствовать foo*
в текущем каталоге или в любых подкаталогах.
Мне очень нравятся циклы и итерация массива, поэтому я полагаю, что добавлю этот ответ в микс...
Мне также понравился пример с глупыми файлами.:)
$ 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
find
имеет аргумент -exec
который перебирает результаты поиска и выполняет произвольную команду. Например:
find . -iname "foo*" -exec echo "File found: {}" \;
Здесь {}
представляет найденные файлы, и их перенос в ""
позволяет результирующей команде оболочки обрабатывать пробелы в имени файла.
Во многих случаях вы можете заменить это последнее \;
(который запускает новую команду) с помощью \+
, который поместит несколько файлов в одну команду (хотя не обязательно все они одновременно, см. man find
для более подробной информации).
В некоторых случаях здесь, если вам просто нужно скопировать или переместить список файлов, вы можете также перевести этот список в awk.
Важно \"" "\"
в поле $0
(вкратце ваши файлы, один список строк = один файл).
find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'
Хорошо - мой первый пост о переполнении стека!
Хотя мои проблемы с этим всегда были в csh, а не в bash, решение, которое я представляю, будет работать в обоих случаях. Проблема заключается в интерпретации оболочки возвращаемых значений "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
}
Надеюсь это поможет!
Еще одно решение делает работу...
Целью было:
#!/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}