it-swarm-ru.tech

Перебирая файлы с пробелами в именах?

Я написал следующий скрипт для сравнения выходных данных двух директоров со всеми одинаковыми файлами:

#!/bin/bash

for file in `find . -name "*.csv"`  
do
     echo "file = $file";
     diff $file /some/other/path/$file;
     read char;
done

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

Пример вывода команды find:

./zQuery - abc - Do Not Prompt for Date.csv
160
Amir Afghani

Короткий ответ (ближайший к вашему ответу, но обрабатывает пробелы)

OIFS="$IFS"
IFS=$'\n'
for file in `find . -type f -name "*.csv"`  
do
     echo "file = $file"
     diff "$file" "/some/other/path/$file"
     read line
done
IFS="$OIFS"

Лучший ответ (также обрабатывает символы подстановки и новые строки в именах файлов)

find . -type f -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

Лучший ответ (на основе ответ Жиля )

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'

Или еще лучше, чтобы избежать запуска по одному sh на файл:

find . -type f -name '*.csv' -exec sh -c '
  for file do
    echo "$file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
  done
' sh {} +

Длинный ответ

У вас есть три проблемы:

  1. По умолчанию оболочка разделяет выходные данные команды на пробелы, табуляции и переносы строк
  2. Имена файлов могут содержать символы подстановки, которые будут расширены
  3. Что если существует каталог, имя которого заканчивается на *.csv?

1. Расщепление только по новым строкам

Чтобы выяснить, для чего установить file, оболочка должна взять вывод find и ​​как-то интерпретировать его, иначе file будет просто полным выводом find.

Оболочка считывает переменную IFS, которая по умолчанию имеет значение <space><tab><newline>.

Затем он просматривает каждый символ в выводе find. Как только он видит какой-либо символ, находящийся в IFS, он думает, что отмечает конец имени файла, поэтому он устанавливает file для любых символов, которые он видел до сих пор, и запускает цикл. Затем он начинает с того места, где остановился, чтобы получить следующее имя файла, и запускает следующий цикл и т.д., Пока не достигнет конца вывода.

Так что это эффективно делает это:

for file in "zquery" "-" "abc" ...

Чтобы сказать, что нужно разделить ввод только на новые строки, вам нужно сделать

IFS=$'\n'

перед вашей командой for ... find.

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

Если вы используете sh или dash вместо ksh93, bash или zsh, вам нужно вместо этого написать IFS=$'\n':

IFS='
'

Этого, вероятно, достаточно, чтобы ваш скрипт работал, но если вы заинтересованы в правильной обработке некоторых других угловых случаев, читайте дальше ...

2. Расширение $file без подстановочных знаков

Внутри цикла, где вы делаете

diff $file /some/other/path/$file

оболочка пытается расширить $file (снова!).

Он может содержать пробелы, но поскольку мы уже установили IFS выше, это не будет проблемой здесь.

Но он также может содержать символы подстановки, такие как * или ?, что может привести к непредсказуемому поведению. (Спасибо Жилю за то, что указал на это.)

Чтобы запретить Shell расширять символы подстановки, поместите переменную в двойные кавычки, например,.

diff "$file" "/some/other/path/$file"

Та же проблема может также укусить нас в

for file in `find . -name "*.csv"`

Например, если у вас были эти три файла

file1.csv
file2.csv
*.csv

(очень маловероятно, но все же возможно)

Это было бы так, как будто вы бежали

for file in file1.csv file2.csv *.csv

который будет расширен до

for file in file1.csv file2.csv *.csv file1.csv file2.csv

в результате чего file1.csv и file2.csv будут обработаны дважды.

Вместо этого мы должны сделать

find . -name "*.csv" -print | while IFS= read -r file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
done

read читает строки из стандартного ввода, разбивает строку на слова в соответствии с IFS и ​​сохраняет их в указанных вами именах переменных.

Здесь мы говорим не разбивать строку на слова, а сохранять строку в $file.

Также обратите внимание, что read line изменился на read line </dev/tty.

Это потому, что внутри цикла стандартный ввод поступает из find через конвейер.

Если бы мы только что сделали read, это заняло бы часть или все имя файла, и некоторые файлы были бы пропущены.

/dev/tty - это терминал, с которого пользователь запускает скрипт. Обратите внимание, что это приведет к ошибке, если скрипт запускается через cron, но я полагаю, что это не важно в этом случае.

Тогда что, если имя файла содержит символы новой строки?

Мы можем справиться с этим, изменив -print на -print0 и используя read -d '' в конце конвейера:

find . -name "*.csv" -print0 | while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read char </dev/tty
done

Это заставляет find ставить нулевой байт в конце каждого имени файла. Нулевые байты являются единственными символами, не разрешенными в именах файлов, поэтому они должны обрабатывать все возможные имена файлов, независимо от того, насколько они странны.

Чтобы получить имя файла на другой стороне, мы используем IFS= read -r -d ''.

Там, где мы использовали read выше, мы использовали разделитель строк по умолчанию для новой строки, но теперь find использует null в качестве разделителя строк. В bash вы не можете передать NUL-символ в аргументе команде (даже встроенной), но bash понимает -d '' как значение - разделить NUL , Поэтому мы используем -d '', чтобы read использовал тот же разделитель строк, что и find. Обратите внимание, что, кстати, -d $'\0' также работает, потому что bash, не поддерживающий байты NUL, обрабатывает его как пустую строку.

Чтобы быть точным, мы также добавляем -r, который говорит, что не обрабатывать обратную косую черту в именах файлов специально. Например, без -r, \<newline> удаляются, а \n преобразуется в n.

Более переносимый способ написания этого, который не требует bash или zsh или запоминания всех вышеприведенных правил о нулевых байтах (опять же, спасибо Жилю):

find . -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read char </dev/tty
' {} ';'

3. Пропуск каталогов, имена которых заканчиваются на * .csv

find . -name "*.csv"

также будет соответствовать каталогам, которые называются something.csv.

Чтобы избежать этого, добавьте -type f в команду find.

find . -type f -name '*.csv' -exec sh -c '
  file="$0"
  echo "$file"
  diff "$file" "/some/other/path/$file"
  read line </dev/tty
' {} ';'

Как указывает glenn jackman , в обоих этих примерах команды, выполняемые для каждого файла, выполняются в подоболочке, поэтому, если вы измените какие-либо переменные внутри цикла, они будут забыты.

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

i=0
while IFS= read -r -d '' file; do
    echo "file = $file"
    diff "$file" "/some/other/path/$file"
    read line </dev/tty
    i=$((i+1))
done < <(find . -type f -name '*.csv' -print0)
echo "$i files processed"

Обратите внимание, что если вы попытаетесь скопировать и вставить это в командной строке, read line будет использовать echo "$i files processed", так что эта команда не запустится.

Чтобы избежать этого, вы можете удалить read line </dev/tty и отправить результат на пейджер, например less.


[~ # ~] отмечает [~ # ~]

Я удалил точки с запятой (;) внутри цикла. Вы можете положить их обратно, если хотите, но они не нужны.

В наши дни $(command) встречается чаще, чем `command`. Это происходит главным образом потому, что проще написать $(command1 $(command2)), чем `command1 \`command2\``.

read char на самом деле не читает символ. Он читает целую строку, поэтому я изменил ее на read line.

218
Mikel

Этот сценарий завершается ошибкой, если любое имя файла содержит пробелы или символы-заглушки оболочки \[?*. Команда find выводит одно имя файла на строку. Затем команда подстановки `find …` оценивается командной оболочкой следующим образом:

  1. Выполните команду find, получите ее вывод.
  2. Разделите вывод find на отдельные слова. Любой символ пробела является разделителем слов.
  3. Для каждого Word, если это шаблон с шаблоном, разверните его до списка файлов, которые ему соответствуют.

Например, предположим, что в текущем каталоге есть три файла с именами `foo* bar.csv, foo 1.txt и foo 2.txt.

  1. Команда find возвращает ./foo* bar.csv.
  2. Оболочка разделяет эту строку на пробел, создавая два слова: ./foo* и bar.csv.
  3. Так как ./foo* содержит метасимвол глобализации, он расширен до списка подходящих файлов: ./foo 1.txt и ./foo 2.txt.
  4. Поэтому цикл for выполняется последовательно с ./foo 1.txt, ./foo 2.txt и bar.csv.

Вы можете избежать большинства проблем на этом этапе, смягчив разделение слов и отключив глобализацию. Чтобы смягчить разбиение слов, установите для переменной IFS один символ новой строки; таким образом, вывод find будет разбит только на новые строки, и пробелы останутся. Чтобы отключить глобализацию, запустите set -f. Тогда эта часть кода будет работать до тех пор, пока в имени файла не будет символа новой строки.

IFS='
'
set -f
for file in $(find . -name "*.csv"); do …

(Это не является частью вашей проблемы, но я рекомендую использовать $(…) вместо `…`. Они имеют то же значение, но в версии для обратной цитаты есть странные правила цитирования.)

Ниже приведена еще одна проблема: diff $file /some/other/path/$file должен быть

diff "$file" "/some/other/path/$file"

В противном случае значение $file разбивается на слова, и слова обрабатываются как шаблоны глобуса, как в приведенной выше команде подстановки. Если вы должны помнить одну вещь о программировании оболочки, помните следующее: всегда используйте двойные кавычки вокруг раскрытия переменных ($foo) и подстановок команд ($(bar)), если вы не знаете, что хотите разбить , (Выше мы знали, что хотим разбить вывод find на строки.)

Надежный способ вызова find - это запуск команды для каждого найденного файла:

find . -name '*.csv' -exec sh -c '
  echo "$0"
  diff "$0" "/some/other/path/$0"
' {} ';'

В этом случае другой подход заключается в сравнении двух каталогов, хотя вы должны явно исключить все "скучные" файлы.

diff -r -x '*.txt' -x '*.ods' -x '*.pdf' … . /some/other/path
22
Gilles 'SO- stop being evil'

Я удивлен, что не вижу readarray упомянутого. Это делает это очень легко, когда используется в сочетании с оператором <<<:

$ touch oneword "two words"

$ readarray -t files <<<"$(ls)"

$ for file in "${files[@]}"; do echo "|$file|"; done
|oneword|
|two words|

Использование конструкции <<<"$expansion" также позволяет разбивать переменные, содержащие символы новой строки, на массивы, например:

$ string=$(dmesg)
$ readarray -t lines <<<"$string"
$ echo "${lines[0]}"
[    0.000000] Initializing cgroup subsys cpuset

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

6
blujay

Циклически просматривайте любые файлы ( любые включенные специальные символы) с помощью абсолютно безопасный поиск (см. Ссылку для документации):

exec 9< <( find "$absolute_dir_path" -type f -print0 )
while IFS= read -r -d '' -u 9
do
    file_path="$(readlink -fn -- "$REPLY"; echo x)"
    file_path="${file_path%x}"
    echo "START${file_path}END"
done
6
l0b0

Афайк найти есть все, что вам нужно.

find . -okdir diff {} /some/other/path/{} ";"

find заботится о том, чтобы вызывать программы без проблем. -okdir подскажет вам перед diff (вы уверены, что да/нет).

Никакой Shell, никакой шутки, джокеров, пи, па, по.

Как примечание: если вы объединяете find с for/while/do/xargs, в большинстве случаев вы делаете это неправильно. :)

4
user unknown

Я удивлен, что никто не упомянул очевидное zsh решение здесь:

for file (**/*.csv(ND.)) {
  do-something-with $file
}

((D) также включить скрытые файлы, (N) чтобы избежать ошибки, если совпадений нет, (.) ограничить обычными файлами.)

bash4.3 и ​​выше теперь также поддерживают это частично:

shopt -s globstar nullglob dotglob
for file in **/*.csv; do
  [ -f "$file" ] || continue
  [ -L "$file" ] && continue
  do-something-with "$file"
done
4
Stéphane Chazelas

Имена файлов с пробелами в них выглядят как несколько имен в командной строке, если они не заключены в кавычки. Если ваш файл называется "Hello World.txt", строка diff будет расширена до:

diff Hello World.txt /some/other/path/Hello World.txt

который выглядит как четыре имени файла. Просто заключите в кавычки аргументы:

diff "$file" "/some/other/path/$file"
2
Ross Smith

Двойное цитирование - твой друг.

diff "$file" "/some/other/path/$file"

В противном случае содержимое переменной получает Word-split.

1
geekosaur

В bash4 вы также можете использовать встроенную функцию mapfile, чтобы установить массив, содержащий каждую строку, и выполнить итерации в этом массиве.

$ tree 
.
├── a
│   ├── a 1
│   └── a 2
├── b
│   ├── b 1
│   └── b 2
└── c
    ├── c 1
    └── c 2

3 directories, 6 files
$ mapfile -t files < <(find -type f)
$ for file in "${files[@]}"; do
> echo "file: $file"
> done
file: ./a/a 2
file: ./a/a 1
file: ./b/b 2
file: ./b/b 1
file: ./c/c 2
file: ./c/c 1
1
kitekat75