Почему `if IFS = read` используется так часто, вместо` IFS =; пока читаешь ..`?

81

Кажется, что в обычной практике установка IFS выходит за пределы цикла while, чтобы не повторять установку его для каждой итерации ... Является ли это просто привычным стилем "monkey see, monkey do", как это было для этой обезьяны до Я читаю человека, читаю , или мне не хватает какой-то тонкой (или явно очевидной) ловушки?

Peter.O
источник

Ответы:

82

Ловушка в том, что

IFS=; while read..

устанавливает IFSдля всего окружения оболочки вне цикла, тогда как

while IFS= read

переопределяет его только для readвызова (за исключением оболочки Bourne). Вы можете проверить, что делает цикл как

while IFS= read xxx; ... done

потом после такой петли echo "blabalbla $IFS ooooooo"печатает

blabalbla
 ooooooo

тогда как после

IFS=; read xxx; ... done

то IFS остается переопределены: теперь echo "blabalbla $IFS ooooooo"печатает

blabalbla  ooooooo

Так что если вы используете вторую форму, вы должны помнить , чтобы сбросить: IFS=$' \t\n'.


Вторая часть этого вопроса была объединена здесь , поэтому я удалил соответствующий ответ отсюда.

rozcietrzewiacz
источник
Ладно, кажется, что потенциальная «ловушка» - игнорирование сброса внешнего IFS ... Но я действительно задаюсь вопросом, есть ли еще что-то еще ... Я пробую здесь что-то, довольно лихорадочно, и я обратите внимание, что установка IFS в списке команд while ведет себя по-разному, в зависимости от того, следует ли за ним двоеточие. Я не понимаю этого поведения (пока), и теперь мне интересно, есть ли какие-то особые соображения на этом уровне ... например. while IFS=X readне разделяется X, но while IFS=X; read...
Peter.O
(Вы имели в виду полу двоеточие, верно?) Второй whileне имеет особого смысла - условие для while концов на этой точкой с запятой, так что нет никакого фактического цикла ... readстановится только первая команда в цикле один-элемент ... Или нет ? А как же doтогда ..?
rozcietrzewiacz
1
Нет, подождите - вы правы, вы можете иметь несколько команд в whileусловии (до do).
rozcietrzewiacz
Ох ... определенно, вы можете иметь их ... как вы поняли ... но они, похоже, не любят точку с запятой ... (и цикл будет продолжать цикл до бесконечности, пока последняя команда не вернет - ноль кода выхода) ... теперь мне интересно, находится ли ловушка в другом секторе целиком; понимание того, как работает список команд while , например. почему IFS=работает, но IFS=Xне работает ... (или, может быть, я какое-то время занимался этим вопросом ... кофе-брейк нужен :)
Peter.O
1
$ rozcietrzewiacz .. Упс ... Я не заметил ваше обновление, когда я переместил свое обновление (как упомянуто в предыдущем комментарии) .. Это выглядит интересно, и это начинает иметь смысл ... но даже на ночь ... Птица, как я, очень поздно ... (Я только что услышал утренних птиц:) ... Тем не менее, я немного собрался с силами и прочитал твои примеры ... Я думаю, что у меня это есть, на самом деле я ' Я уверен, что у вас есть, но я должен спать :) ... Это почти Эврика! момент ... спасибо
Peter.O
45

Давайте посмотрим на пример с тщательно продуманным вводным текстом:

text=' hello  world\
foo\bar'

Это две строки, первая из которых начинается с пробела и заканчивается обратной косой чертой. Во-первых, давайте посмотрим на то, что происходит без каких-либо мер предосторожности read(но с помощью printf '%s\n' "$text"осторожной печати $textбез риска расширения). (Ниже приведена $ ‌подсказка оболочки.)

$ printf '%s\n' "$text" |
  while read line; do printf '%s\n' "[$line]"; done
[hello worldfoobar]

readуничтожение обратной косой черты: обратная косая черта-новая строка приводит к игнорированию новой строки, а обратная косая черта - все игнорирует эту первую обратную косую черту. Чтобы избежать обратного слеша, мы используем специально read -r.

$ printf '%s\n' "$text" |
  while read -r line; do printf '%s\n' "[$line]"; done
[hello  world\]
[foo\bar]

Это лучше, у нас есть две линии, как и ожидалось. Две строки почти содержат желаемое содержимое: двойной пробел между helloи worldбыл сохранен, потому что он находится внутри lineпеременной. С другой стороны, начальное пространство было съедено. Это потому, что readчитает столько слов, сколько вы передаете переменным, за исключением того, что последняя переменная содержит остаток строки - но она все равно начинается с первого слова, то есть начальные пробелы отбрасываются.

Итак, чтобы буквально прочитать каждую строку, нам нужно убедиться, что разделение слов не происходит. Мы делаем это, устанавливая IFSпеременную в пустое значение.

$ printf '%s\n' "$text" |
  while IFS= read -r line; do printf '%s\n' "[$line]"; done
[ hello  world\]
[foo\bar]

Обратите внимание, как мы установили IFS специально для продолжительности readвстроенного . В IFS= read -r lineустанавливает переменную окружения IFS(пустое значение) , специально для исполнения read. Это пример общего простого синтаксиса команды : (возможно, пустая) последовательность назначений переменных, за которой следует имя команды и ее аргументы (также вы можете добавлять перенаправления в любой точке). Поскольку readэта переменная является встроенной, она фактически никогда не попадает в среду внешнего процесса; тем не менее, ценность $IFS- это то, что мы назначаем там, пока readвыполняется ». Обратите внимание, что readэто не специальная встроенная функция , поэтому назначение выполняется только в течение его продолжительности.

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

Сравните это с фрагментом кода, который просматривает файлы в двоеточии. Список имен файлов читается из файла, по одному имени файла в строке.

IFS=":"; set -f
while IFS= read -r name; do
  for dir in $PATH; do
    ## At this point, "$IFS" is still ":"
    if [ -e "$dir/$name" ]; then echo "$dir/$name"; fi
  done
done <filenames.txt

Если бы цикл был while IFS=; read -r name; do …, то for dir in $PATHне разбился бы на разделенные $PATHдвоеточием компоненты. Если бы код был IFS=; while read …, было бы еще более очевидно, что IFSон не установлен :в теле цикла.

Конечно, было бы возможно восстановить значение IFSпосле выполнения read. Но это потребовало бы знания предыдущего значения, что является дополнительным усилием. IFS= readэто простой способ (и, удобно, также самый короткий путь).

¹ И, если readон прерывается перехваченным сигналом, возможно, во время выполнения перехвата - это не указано в POSIX и зависит от оболочки на практике.

жилль
источник
4
Спасибо, Жиль ... очень хорошая экскурсия ... (вы имели в виду "set -f"?) .... Теперь, для читателя, чтобы перефразировать то, что уже было сказано, я хотел бы подчеркнуть проблему, которая была я смотрю на это неправильно. Прежде всего, это тот факт, что конструкция while IFS= read(без точки с запятой =) не является специальной формой, whileили, IFSили из read.. Конструкция является общей: то есть. anyvar=anyvalue anycommand, Отсутствие ;после установки anyvarделает объем anyvar локального до anycommand.. пока - делать / сделать цикл 100% связан с местной сферой any_var.
Peter.O
3

Помимо (уже выяснены) IFSобзорных различия между while IFS='' read, IFS=''; while readи while IFS=''; readидиомы (за команду против сценария / оболочки по всей IFSвидимости переменной), то забирать домой урок заключается в том, что вы теряете ведущие и завершающие пробелы из строки ввода , если переменная IFS устанавливается на (содержит) пробел.

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

Поэтому установка переменной IFS для пустой строки - это не плохая идея, поскольку она гарантирует, что начальный и конечный пробелы в строке не будут удалены.

Читайте также: Bash, чтение построчно из файла, с IFS

(
shopt -s nullglob
touch '  file with spaces   '
IFS=$' \t\n' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
IFS='' read -r file <<<"$(printf '%s' *file*with*spaces*)"
ls -l "$file"
)
джон
источник
+1 отличная демонстрация, очистка после 'rm * file * с *
пробелом
0

Вдохновленный ответом Юзема

Если вы хотите установить IFSреальный характер, это сработало для меня

iconv -f cp1252 zapni.tv.php | while IFS='#' read -d'#' line
do
  echo "$line"
done
Стивен Пенни
источник