Цикличное выполнение через файлы с пробелами на имена? [дубликат]

Я часто задавался вопросом сам. Выделения Википедии, которые основной отличительный признак - то, что способность имеет полноэкранный текстовый режим (проклятия) интерфейс. То, что можно использовать больше всего apt-get аргументы команды с aptitude самостоятельно просто проектное решение помочь apt-get пользователи для перемещения в aptitude и наоборот.

Я никогда не использовал wajig, но документация предполагает, что это - просто сценарий, который знает, передаете ли Вы его a deb файл (когда это работает dpkg) или apt имя пакета (когда это работает apt-get вместо этого). Вы могли испытать его и видеть если, именно это это делает?

Конечно, реальная разница:

gaurav@fern:~$ apt-get moo
         (__) 
         (oo) 
   /------\/ 
  / |    ||   
 *  /\---/\ 
    ~~   ~~   
...."Have you mooed today?"...
gaurav@fern:~$ aptitude moo
There are no Easter Eggs in this program.

152
21.03.2011, 02:17
10 ответов

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

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

Лучший ответ (на основе ответа Gilles)

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 выше, который не будет проблемой здесь.

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

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

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

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

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

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

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

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.

206
27.01.2020, 19:28
  • 1
    while в конвейере может создать проблемы с созданной подоболочкой (переменные в блоке цикла, не видимом после того, как команда завершится, например). С ударом я использовал бы замену перенаправления ввода и процесса: while read -r -d $'\0' file; do ...; done < <(find ... -print0) –  glenn jackman 18.03.2011, 03:23
  • 2
    Несомненно, или использование heredoc: while read; do; done <<EOF "$(find)" EOF. Не настолько легкий читать как бы то ни было. –  Mikel 18.03.2011, 03:41
  • 3
    @glenn jackman: Я пытался добавить больше объяснения сейчас. Я просто делал его лучше или хуже? –  Mikel 18.03.2011, 04:36
  • 4
    Вам не нужно IFS, -print0, while и read если Вы обрабатываете find к его полному, как показано ниже в моем решении. –  user unknown 20.03.2011, 01:10
  • 5
    Ваше первое решение справится с любым символом кроме новой строки, если Вы также выключите globbing с set -f. –  Gilles 'SO- stop being evil' 04.04.2011, 22:28

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

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

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

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

Можно избежать большинства проблем на данном этапе путем снижения разделения слова и выключения globbing. Для снижения разделения слова установите IFS переменная к единственному символу новой строки; этим путем вывод find будет только разделен в новых строках, и пробелы останутся. Для выключения globbing работать 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 разделяется на слова, и слова рассматривают как шаблоны шарика, как с командой substitutio выше. Если необходимо помнить одну вещь о программировании оболочки, помните это: всегда используйте двойные кавычки вокруг переменных расширений ($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
21
27.01.2020, 19:28
  • 1
    я забыл о подстановочных знаках как другая причина заключить в кавычки правильно.Спасибо! :-) –  Mikel 18.03.2011, 04:34
  • 2
    вместо find -exec sh -c 'cmd 1; cmd 2' ";", необходимо использовать find -exec cmd 1 {} ";" -exec cmd 2 {} ";", потому что оболочка должна замаскировать параметры, но найти, не делает. В особом случае здесь, отзовитесь эхом, "0$" не должен быть часть сценария, просто добавить - печать после ';'. Вы не включали вопрос продолжиться, но даже который может быть сделан находкой, как показано ниже в моем решении. ;) –  user unknown 20.03.2011, 01:25
  • 3
    @userunknown: использование {} как подстрока параметра в find -exec не является портативным, вот почему оболочка необходима. Я не понимаю то, что Вы подразумеваете “под оболочкой, должен замаскировать параметры”; если это о заключении в кавычки, мое решение правильно заключается в кавычки. Вы правы что echo часть могла быть выполнена -print вместо этого. -okdir довольно недавний GNU, находят расширение, это не доступно везде. Я не включал ожидание для продолжения, потому что я полагаю, что чрезвычайно плохой UI и автор вопроса могут легко поместить read в отрывке оболочки, если он хочет. –  Gilles 'SO- stop being evil' 20.03.2011, 01:59
  • 4
    Заключение в кавычки является формой маскирования, не так ли? Я не понимаю Ваш комментарий о том, что является портативным, и что нет. Ваш пример (2-й от нижней части) использование - должностное лицо для вызова sh и использование {} - таким образом, где мой пример (около-okdir) менее портативен? find . -name "*.csv" -exec diff {} /some/other/path/{} ";" -print –  user unknown 20.03.2011, 03:05
  • 5
    “Маскирование” не является общей терминологией в литературе оболочки, таким образом, необходимо будет объяснить, что Вы имеете в виду, хотите ли Вы быть понятыми. Мое использование в качестве примера {} только однажды и в отдельном аргументе; другие случаи (используемый дважды или как подстрока) не являются портативными. “Портативный” означает, что это будет работать над всеми системами Unix; хорошая инструкция является спецификацией Unix POSIX / Единственной спецификацией Unix. –  Gilles 'SO- stop being evil' 20.03.2011, 03:15

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

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
27.01.2020, 19:28
  • 1
    Спасибо за упоминание -d ''. Я не понял это $'\0' совпал с '', но это, кажется. Хорошее решение, также. –  Mikel 04.04.2011, 14:30
  • 2
    мне нравится отделение находки и в то время как, благодарит. –  dhill 11.04.2013, 19:32
  • 3
    черт возьми l0b0, почему я всегда заканчиваю тем, что нашел Ваши решения на SE? ;) –  Dominik R 03.02.2017, 15:07
  • 4
    Черт возьми @DominikR, теперь существует еще по крайней мере две вещи, которые я не знаю - почему Вы всегда заканчиваете тем, что нашли мои решения и почему Вы попросили, чтобы я ответил, что :p –  l0b0 03.02.2017, 21:00

Afaik находят, имеет все, в чем Вы нуждаетесь.

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

найдите берет себя, забота о вызове программ savely.-okdir предложит Вам перед разностью (Вы уверенный да/нет).

Никакая включенная оболочка, никакой globbing, шутники, пи, pa, почтовый.

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

4
27.01.2020, 19:28
  • 1
    Спасибо за ответ. Почему Вы делаете его неправильно, если Вы объединяетесь, находят с for/while/do/xargs? –  Amir Afghani 18.03.2011, 16:56
  • 2
    Найдите уже выполняет итерации по подмножеству файлов. Большинство людей, которые обнаруживаются с вопросами, могло просто использовать одно из действий (-хорошо (dir) - должностное лицо (dir), - удаляют) в сочетании с""; или + (позже для параллельного вызова). Главная причина сделать так, что Вы не должны возиться с параметрами файла, маскируя их для оболочки. Не то, чтобы важный: Вы не должны новые процессы все время, меньше памяти, больше скорости. более короткая программа. –  user unknown 18.03.2011, 23:05
  • 3
    Не здесь, чтобы сокрушить Ваш дух, но выдержать сравнение: time find -type f -exec cat "{}" \; с time find -type f -print0 | xargs -0 -I stuff cat stuff. xargs версия была быстрее на 11 секунд при обработке 10 000 пустых файлов. Будьте осторожны при утверждении этого в большинстве случаев объединение find с другими утилитами является неправильным. -print0 и -0 есть ли для контакта с пробелами в именах файлов при помощи нулевого байта как разделитель объекта, а не пространство. –  Jonathan Komar 05.07.2017, 14:00
  • 4
    @JonathanKomar: Ваш коммандос находки/должностного лица занял 11,7 с в моей системе с 10 000 файлов, xargs версией 9.7 s, time find -type f -exec cat {} + как предложено в моем предыдущем комментарии занял 0,1 с. Обратите внимание, что различие в подмозаике между "им является неправильным" и "Вы делаете его неправильно", особенно при украшении smilie. Вы, например, делаете это неправильно?;) BTW, пробелы в имени файла не являются никакой проблемой для вышеупомянутой команды и находят в целом. Грузовой культовый программист? И между прочим, объединение находит с другими инструментами, прекрасен, просто xargs является большую часть времени лишним. –  user unknown 05.07.2017, 15:48
  • 5
    @userunknown я объяснил, как мои соглашения о коде с пробелами для потомства (образование будущих средств просмотра), и не подразумевали, что Ваш код не делает. + поскольку параллельные вызовы очень быстры, как Вы упомянули. Я не сказал бы что грузовой культовый программист, потому что эта способность использовать xargs таким образом пригождается в многочисленных случаях. Я соглашаюсь больше с философией Unix: сделайте одну вещь и сделайте это хорошо (используйте программы отдельно или в комбинации, чтобы сделать задание). find обходит тонкую грань туда. –  Jonathan Komar 06.07.2017, 10:21

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

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

который похож на четыре имен файлов. Просто поместите кавычки вокруг аргументов:

diff "$file" "/some/other/path/$file"
2
27.01.2020, 19:28
  • 1
    Это помогает, но это не решает мою проблему. Я все еще вижу случаи, где файл разделяется на несколько маркеров. –  Amir Afghani 18.03.2011, 02:37
  • 2
    Этот ответ вводит в заблуждение. Проблема for file in `find . -name "*.csv"` команда. Если существует названный файл Hello World.csv, file будет установлен на ./Hello и затем к World.csv. Заключение в кавычки $file не поможет. –  G-Man Says 'Reinstate Monica' 04.03.2015, 21:11

Двойное заключение в кавычки является Вашим другом.

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

Иначе содержание переменной разделяется на слово.

1
27.01.2020, 19:28
  • 1
    Это вводит в заблуждение. Проблема for file in `find . -name "*.csv"` команда. Если существует названный файл Hello World.csv, file будет установлен на ./Hello и затем к World.csv. Заключение в кавычки $file не поможет. –  G-Man Says 'Reinstate Monica' 04.03.2015, 21:11

Пробелами в значениях можно избежать как простыми для конструкции цикла

for CHECK_STR in `ls -l /root/somedir`
do 
echo "CHECKSTR $CHECK_STR"
done

ls-l root/somedir содержит мой файл с пробелами

Вывод выше моего файла с пробелами

избегать этого вывода, простое решение (замечают двойные кавычки),

for CHECK_STR in "`ls -l /root/somedir`"
do 
echo "CHECKSTR $CHECK_STR"
done

произведите мой файл с пробелами

испытанный удар

-3
27.01.2020, 19:28
  • 1
    “ Цикличное выполнение через файлы” – именно это говорит вопрос. Ваше решение произведет все ls -l вывод сразу. Это эффективно эквивалентно с echo "CHECKSTR `ls -l /root/somedir`". –  manatwork 13.05.2013, 10:02
[113343]С помощью функции builtin mapfile можно также установить массив, содержащий каждую строку, и выполнить по нему итерацию.

1
27.01.2020, 19:28

Удивительно, что здесь еще никто не упомянул очевидное 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
3
27.01.2020, 19:28

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

$ touch oneword "two words"

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

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

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

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

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

5
27.01.2020, 19:28

Теги

Похожие вопросы