Перебирать список файлов с пробелами

165

Я хочу перебрать список файлов. Этот список является результатом команды 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

Что я могу сделать, чтобы избежать разделения на пробелы?

Теги:

10 ответов

202
Лучший ответ

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

find . -iname "foo*" | while read f
do
    # ... loop body
done
  • 28
    Это очень чисто. И заставляет меня чувствовать себя лучше, чем менять IFS в сочетании с циклом for
  • 14
    Это разделит один путь к файлу, который содержит \ n. ОК, их не должно быть, но их можно создать: touch "$(printf "foo\nbar")"
Показать ещё 8 комментариев
145

Существует несколько способов достижения этого.

Если вы хотите придерживаться оригинальной версии, это можно сделать следующим образом:

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"
  • 1
    Принял ваш ответ: самый полный и интересный - я не знал о $IFS и синтаксисе < <(cmd) . Еще одна вещь остается неясной для меня, почему $ in $'\0' ? Большое спасибо.
  • 0
    @gregseth: это синтаксис bash для буквального escape-символа. Например, если вы говорите CTRL + V, а затем нажимаете TAB, вы вставляете буквенную вкладку. Однако это не будет выглядеть правильно при копировании и вставке в другое место, но синтаксис $'\t' будет оцениваться как вкладка и работает так же. Это просто удобный способ передавать определенные символы командам, не беспокоясь о том, что оболочка их искажает.
Показать ещё 10 комментариев
27

Существует также очень простое решение: полагаться на 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).

  • 1
    Синтаксис "${file}" сотворил для меня чудеса.
12
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
  • 6
    как примечание стороны, это будет работать, только если вы хотите выполнить команду. Встроенная оболочка не будет работать таким образом.
11
find . -name "fo*" -print0 | xargs -0 ls -l

См. man xargs.

6

Поскольку вы не выполняете какой-либо другой тип фильтрации с помощью find, вы можете использовать следующее из bash 4.0:

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

**/ будет соответствовать нулевым или более каталогам, поэтому полный шаблон будет соответствовать foo* в текущем каталоге или в любых подкаталогах.

1

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

Мне также понравился пример с глупыми файлами.:)

$ 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
0

find имеет аргумент -exec который перебирает результаты поиска и выполняет произвольную команду. Например:

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

Здесь {} представляет найденные файлы, и их перенос в "" позволяет результирующей команде оболочки обрабатывать пробелы в имени файла.

Во многих случаях вы можете заменить это последнее \; (который запускает новую команду) с помощью \+, который поместит несколько файлов в одну команду (хотя не обязательно все они одновременно, см. man find для более подробной информации).

0

Хорошо - мой первый пост о переполнении стека!

Хотя мои проблемы с этим всегда были в 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
}

Надеюсь это поможет!

0

В некоторых случаях здесь, если вам просто нужно скопировать или переместить список файлов, вы можете также перевести этот список в awk.
Важно \"" "\" в поле $0 (вкратце ваши файлы, один список строк = один файл).

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

Ещё вопросы

Сообщество Overcoder
Наверх
Меню