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

202

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

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

gregseth
источник
Это в основном конкретный случай, когда нужно заключать в кавычки переменную оболочки?
tripleee

Ответы:

253

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

find . -iname "foo*" | while read f
do
    # ... loop body
done
Мартин Клейтон
источник
31
Это очень чисто. И заставляет меня чувствовать себя лучше, чем менять IFS в сочетании с циклом for
Derrick
15
Это разделит один путь к файлу, который содержит \ n. Хорошо, их не должно быть рядом, но они могут быть созданы:touch "$(printf "foo\nbar")"
Олли Сондерс
4
Чтобы предотвратить любую интерпретацию ввода (обратные слэши, начальные и конечные пробелы), используйте IFS= while read -r fвместо этого.
mklement0
2
Этот ответ показывает более безопасную комбинацию findи цикл while.
Moi
5
Похоже , указывая на очевидном, но почти во всех простых случаях -execбудут чист , чем явный цикл: find . -iname "foo*" -exec echo "File found: {}" \;. Кроме того , во многих случаях вы можете заменить , что в прошлом \;с +положить много файлов в одной команде.
naught101
153

Есть несколько реальных способов сделать это.

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

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

РЕДАКТИРОВАТЬ : Вот хороший тестовый скрипт, чтобы вы могли понять разницу между различными попытками решения этой проблемы

#!/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"
sorpigal
источник
1
Принял ваш ответ: самый полный и интересный - я не знал $IFSи о < <(cmd)синтаксисе. Еще одна вещь остается неясной для меня, почему $в $'\0'? Большое спасибо.
gregseth
2
+1, но вы должны добавить ... while IFS= read... для обработки файлов, которые начинаются или заканчиваются пробелами.
Гордон Дэвиссон
1
Существует одно предупреждение для решения о замене процесса. Если у вас есть какой-либо запрос внутри цикла (или вы читаете из STDIN любым другим способом), ввод будет заполнен материалом, который вы вводите в цикл. (может быть, это следует добавить к ответу?)
andsens
2
@uvsmtid: Этот вопрос был помечен, bashпоэтому я чувствовал себя в безопасности, используя специфичные для bash функции. Подстановка процессов не переносима на другие оболочки (сама sh вряд ли когда-либо получит такое значительное обновление).
Сорпигал
2
Комбинация IFS=$'\n'с forпредотвращает расщепление слов во внутренней строке, но при этом приводит к тому, что результирующие строки становятся объектами глобализации, поэтому этот подход не является полностью надежным (если только вы сначала не отключите глобализацию). Хотя это read -d $'\0'работает, оно немного вводит в заблуждение, так как предполагает, что вы можете использовать $'\0'для создания NUL - вы не можете: a \0в строке ANSI C в кавычках эффективно завершает строку, так что -d $'\0'фактически совпадает с -d ''.
mklement0
29

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

marchelbling
источник
13
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
Каролий хорват
источник
6
как примечание стороны, это будет работать, только если вы хотите выполнить команду. Встроенная оболочка не будет работать таким образом.
Алекс
11
find . -name "fo*" -print0 | xargs -0 ls -l

См man xargs.

Торп
источник
6

Поскольку вы не выполняете никакой другой тип фильтрации с помощью find, вы можете использовать следующее начиная с bash4.0:

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

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

chepner
источник
3

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

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

$ 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
terafl0ps
источник
2

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

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

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

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

naught101
источник
0

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

find . -iname "foo*" | awk '{print "mv \""$0"\" ./MyDir2" | "sh" }'
Стив
источник
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

Еще одно решение для работы ...

Целью было:

  • рекурсивно выбирать / фильтровать имена файлов в каталогах
  • обрабатывать каждое имя (любое пространство в пути ...)
#!/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}

Винс Б
источник
Спасибо за конструктивное замечание, но: 1 - это актуальная проблема, 2 - оболочка могла эволюционировать во времени ... как и все, я полагаю; 3- Ни один из приведенных выше ответов не может удовлетворить ПРЯМОЕ разрешение pb без изменения проблемы или устранения проблемы :-)
Винс Б.