Перебирать список файлов с пробелами - PullRequest
185 голосов
/ 12 августа 2011

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

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

Ответы [ 11 ]

229 голосов
/ 12 августа 2011

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

find . -iname "foo*" | while read f
do
    # ... loop body
done
149 голосов
/ 12 августа 2011

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

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

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 (он же \0) в качестве терминатора для каждого имени файла и, в отличие от новой строки, ноль не является допустимым символом в имени файла.

Преимущество этого перед почти эквивалентной версией

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"
30 голосов
/ 04 февраля 2014

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

13 голосов
/ 12 августа 2011
find . -iname "foo*" -print0 | xargs -L1 -0 echo "File found:"
11 голосов
/ 12 августа 2011
find . -name "fo*" -print0 | xargs -0 ls -l

См. man xargs.

6 голосов
/ 09 февраля 2014

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

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

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

2 голосов
/ 12 декабря 2018

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

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

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

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

2 голосов
/ 09 апреля 2017

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

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

$ 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 th элемент массива.Кавычки вокруг переменных важны для сохранения пробелов.

В результате получаются три переименованных файла:

$ ls -1
smarter_file1
smarter_file2
smarter_file_3
0 голосов
/ 21 июня 2019

Другое решение, выполняющее работу ...

Цель была:

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


0 голосов
/ 05 ноября 2018

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

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

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...