Понимание «IFS = read -r line»

61

Я, очевидно, понимаю, что можно добавить значение к внутренней переменной разделителя полей. Например:

$ IFS=blah
$ echo "$IFS"
blah
$ 

Я также понимаю, что read -r lineбудет сохранять данные из stdinпеременной с именем line:

$ read -r line <<< blah
$ echo "$line"
blah
$ 

Однако, как команда может назначить значение переменной? И делает он сначала сохранить данные stdinв переменной , lineа затем дать значение lineдля IFS?

Мартин
источник
3
Связанный: unix.stackexchange.com/q/169716/38906
cuonglm

Ответы:

104

У некоторых людей есть ошибочное представление, что readэто команда для чтения строки. Это не.

readчитает слова из строки (возможно, с обратной косой чертой), где слова $IFSразделяются и обратная косая черта может использоваться для экранирования (или продолжения строк).

Общий синтаксис:

read word1 word2... remaining_words

readчитает стандартный ввод один байт в то время , пока он не найдет неэкранированный символ новой строки (или конца входного текста), расщепляется , что в соответствии со сложными правилами и сохраняет результат этого расщепления $word1, $word2... $remaining_words.

Например, на входе, как:

  <tab> foo bar\ baz   bl\ah   blah\
whatever whatever

и со значением по умолчанию $IFS, read a b cназначит:

  • $afoo
  • $bbar baz
  • $cblah blahwhatever whatever

Теперь, если передан только один аргумент, он не становится read line. Это все еще read remaining_words. Обработка обратной косой черты все еще выполняется, символы пробелов IFS по-прежнему удаляются с начала и до конца.

-rОпция удаляет обработку обратной косой. Так что та же самая команда с выше -rвместо назначит

  • $afoo
  • $bbar\
  • $cbaz bl\ah blah\

Теперь для части разделения важно понимать, что есть два класса символов для $IFS: пробельные символы IFS (а именно пробел и табуляция (и новая строка, хотя здесь это не имеет значения, если вы не используете -d), что также происходит быть в значении по умолчанию $IFS) и другие. Обработка этих двух классов персонажей различна.

С IFS=:( :причем не в качестве IFS символ пробела), вход как :foo::bar::бы разделилась на "", "foo", "", barи ""(и дополнительно ""с некоторыми реализациями , хотя это не имеет значения , за исключением read -a). Хотя, если мы заменим это :пробелом, разбиение будет сделано только на fooи bar. То есть ведущие и конечные игнорируются, а их последовательности рассматриваются как единое целое. Существуют дополнительные правила при объединении пробельных и непробельных символов $IFS. Некоторые реализации могут добавлять / удалять специальную обработку, удваивая символы в IFS ( IFS=::или IFS=' ').

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

Даже с символами IFS, не являющимися пробелами, если строка ввода содержит один (и только один) из этих символов, и это последний символ в строке (как IFS=: read -r wordна входе, подобном foo:) с оболочками POSIX (нет, zshни в некоторых pdkshверсиях), этот ввод рассматривается как одно fooслово, потому что в этих оболочках символы $IFSрассматриваются как терминаторы , поэтому wordбудут содержать foo, а не foo:.

Итак, канонический способ чтения одной строки ввода с помощью readвстроенной функции:

IFS= read -r line

(обратите внимание, что для большинства readреализаций это работает только для текстовых строк, поскольку символ NUL не поддерживается, кроме как в zsh).

Использование var=value cmdсинтаксиса гарантирует, что он IFSбудет установлен по-разному только на время выполнения этой cmdкоманды.

Историческая справка

readВстроенный был введен Bourne оболочки и уже читать слова , а не линии. Есть несколько важных отличий от современных оболочек POSIX.

Оболочка Bourne readне поддерживает -rопцию (которая была введена оболочкой Korn), поэтому нет способа отключить обработку обратной косой черты, кроме предварительной обработки ввода чем-то вроде sed 's/\\/&&/g'этого.

Оболочка Bourne не имела такого понятия двух классов символов (которое снова было введено ksh). В оболочке Борна все символы пройти такое же лечение , как IFS пробельные символы делают в KSH, то есть IFS=: read a b cна входе , как foo::barбы назначить barна $b, а не пустую строку.

В оболочке Борна, с:

var=value cmd

Если cmdвстроенный (как readесть), varостается установленным valueпосле того, cmdкак закончил. Это особенно важно, $IFSпоскольку в оболочке Bourne $IFSиспользуется для разделения всего, а не только расширений. Кроме того, если вы удалите символ пробела из $IFSоболочки Bourne, он "$@"больше не будет работать.

В оболочке Bourne перенаправление составной команды приводит к тому, что она запускается в подоболочке (в самых ранних версиях даже такие вещи, как read var < fileили exec 3< file; read var <&3не работают), поэтому в оболочке Bourne редко можно было использовать readчто-либо, кроме ввода пользователя на терминале (где имеет смысл обработка продолжения строки)

Некоторые Unices (например, HP / UX, есть еще один util-linux) по-прежнему имеют lineкоманду для чтения одной строки ввода (которая раньше была стандартной командой UNIX вплоть до версии 2 спецификации Single UNIX ).

Это в основном то же самое, head -n 1за исключением того, что он читает по одному байту за раз, чтобы убедиться, что он не читает более одной строки. На этих системах вы можете сделать:

line=`line`

Конечно, это означает порождение нового процесса, выполнение команды и чтение ее результатов по каналу, что намного менее эффективно, чем ksh IFS= read -r line, но все же намного более интуитивно понятно.

Стефан Шазелас
источник
3
+1 Спасибо за полезную информацию о различных методах обработки пробелов и табуляции и «других» в IFS в bash ... Я знал, что с ними обращались по-разному, но это объяснение сильно упрощает все это. (И понимание между bash (и другими оболочками posix) и регулярными shразличиями также полезно для написания переносимых скриптов!)
Оливье Дюлак
По крайней мере bash-4.4.19, while read -r; do echo "'$REPLY'"; doneработает как while IFS= read -r line; do echo "'$line'"; done.
x-yuri
Это: «... ошибочное представление о том, что чтение - это команда чтения строки ...», заставляет меня думать, что если использование readчтения строки является ошибочным, должно быть что-то еще. Что это за ошибочное понятие? Или это первое утверждение технически правильно, но на самом деле ошибочное понятие таково: «чтение - это команда для чтения слов из строки. Поскольку она настолько мощная, вы можете использовать ее для чтения строк из файла, выполнив: IFS= read -r line»
Майк С
8

Теория

Здесь действуют две концепции:

  • IFSявляется разделителем поля ввода, что означает, что прочитанная строка будет разделена на основе символов в IFS. В командной строке IFSобычно используются любые пробельные символы, поэтому командная строка разделяется пробелами.
  • Выполнение чего-то подобного VAR=value commandозначает «изменить окружение команды так, VARчтобы оно имело значение value». По сути, команда commandбудет VARиметь значение value, имеющее значение , но любая команда, выполненная после этого, все равно будет VARиметь свое предыдущее значение. Другими словами, эта переменная будет изменена только для этого оператора.

В этом случае

Таким образом, при выполнении IFS= read -r lineзадания вы устанавливаете IFSпустую строку (для разделения не будет использоваться ни один символ, поэтому разделения не произойдет), чтобы readпрочитать всю строку и увидеть ее как одно слово, которое будет присвоено lineпеременной. Изменения IFSвлияют только на этот оператор, поэтому изменение не повлияет на следующие команды.

Как примечание стороны

В то время как команда правильно и будет работать , как задумано, установка IFSв данном случае не может 1 не быть необходимым. Как написано на bashстранице руководства во readвстроенном разделе:

Одна строка считывается из стандартного ввода [...], и первое слово присваивается первому имени, второе слово - второму имени и т. Д., А оставшиеся слова и их промежуточные разделители присваиваются фамилии . Если из входного потока прочитано меньше слов, чем имён, оставшимся именам присваиваются пустые значения. Символы в IFSиспользуются для разделения строки на слова. [...]

Поскольку у вас есть только lineпеременная, ей будут присвоены все слова, так что если вам не нужны никакие из предшествующих и завершающих пробельных символов 1, вы можете просто написать read -r lineи покончить с этим.

[1] Так же, как пример того, как значение unsetили $IFSзначение по умолчанию заставит readрассматривать начальные / конечные пробелы IFS , вы можете попробовать:

echo ' where are my spaces? ' | { 
    unset IFS
    read -r line
    printf %s\\n "$line"
} | sed -n l

Запустите его, и вы увидите, что предшествующие и конечные символы не выживут, если IFSне установлены. Кроме того, некоторые странные вещи могут произойти, если $IFSв сценарии нужно что-то изменить ранее.

user43791
источник
5

Вы должны прочитать это заявление в двух частях, первая очищает значение переменной IFS, т.е. эквивалентно более читаемым IFS="", второй читает lineпеременную из стандартного ввода, read -r line.

Что характерно в этом синтаксисе, так это то, что влияние IFS является временным и действует только для readкоманды.

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

Редактировать:

Он -rпредназначен для того, чтобы ввод, заканчивающийся на, \не обрабатывался специально, т. Е. Для включения обратной косой черты в lineпеременную, а не в качестве символа продолжения, чтобы разрешить многострочный ввод.

$ read line; echo "[$line]"   
abc\
> def
[abcdef]
$ read -r line; echo "[$line]"  
abc\
[abc\]

Очистка IFS имеет побочный эффект предотвращения чтения, чтобы обрезать потенциальные начальные и конечные пробелы или символы табуляции, например:

$ echo "   a b c   " | { IFS= read -r line; echo "[$line]" ; }   
[   a b c   ]
$ echo "   a b c   " | { read -r line; echo "[$line]" ; }     
[a b c]

Спасибо Ричи за то, что указал на эту разницу.

jlliagre
источник
Чего вам не хватает, так это того, что если IFS не изменился, read -r lineон обрежет начальные и конечные пробелы, прежде чем назначить входные данные для lineпеременной.
Ричи
@rici Я подозревал что-то подобное, но проверял только символы IFS между словами, а не начальные / конечные. Спасибо за указание на этот факт!
Jlliagre
очистка IFS также предотвратит присвоение нескольких переменных (побочный эффект). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"покажет-aa bb--
кёдев