Понимание IFS

71

Следующие несколько тем на этом сайте и StackOverflow были полезны для понимания того, как IFSработает:

Но у меня все еще есть несколько коротких вопросов. Я решил спросить их в том же посте, так как думаю, что это может помочь лучшим будущим читателям:

Q1. IFSобычно обсуждается в контексте «разделения поля». Является ли разделение полей так же , как слово расщепления ?

Q2: спецификация POSIX гласит :

Если значение IFS равно нулю, разделение полей не производится.

Установка IFS=совпадает с установкой IFSна ноль? Это то, что подразумевается под установкой его empty stringтоже?

Q3: В спецификации POSIX я прочитал следующее:

Если IFS не установлен, оболочка должна вести себя так, как если бы значение IFS было <space>, <tab> and <newline>

Скажем, я хочу восстановить значение по умолчанию IFS. Как мне это сделать? (более конкретно, как я имею в виду <tab>и <newline>?)

Q4: наконец, как бы этот код:

while IFS= read -r line
do    
    echo $line
done < /path_to_text_file

вести себя, если мы изменим первую строку на

while read -r line # Use the default IFS value

или:

while IFS=' ' read -r line
Амелио Васкес-Рейна
источник

Ответы:

28
  1. Да, они одинаковы.
  2. Да.
  3. В bash и подобных оболочках вы могли бы сделать что-то вроде IFS=$' \t\n'. В противном случае вы можете вставить буквенные управляющие коды, используя [space] CTRL+V [tab] CTRL+V [enter]. Однако если вы планируете сделать это, лучше использовать другую переменную, чтобы временно сохранить старое IFSзначение, а затем восстановить его (или временно переопределить для одной команды, используя var=foo commandсинтаксис).
    • Первый фрагмент кода поместит всю строку, дословно прочитанную, так $lineкак нет разделителей полей, для которых нужно разделять слова. Имейте в виду, однако, что, поскольку многие оболочки используют cstrings для хранения строк, первый экземпляр NUL может по-прежнему вызывать преждевременное завершение его появления.
    • Второй фрагмент кода может не помещать точную копию ввода в $line. Например, если имеется несколько последовательных разделителей полей, они будут преобразованы в один экземпляр первого элемента. Это часто признается потерей окружающего пробела.
    • Третий фрагмент кода будет работать так же, как и второй, за исключением того, что он будет разделен только пробелом (не обычным пробелом, символом табуляции или новой строкой).
Крис Даун
источник
3
Ответ на вопрос 2 неправильный: пустое IFSи неустановленное IFSзначения очень разные. Ответ на вопрос 4 отчасти неправильный: внутренние разделители здесь не затрагиваются, только ведущие и конечные.
Жиль "ТАК ... перестать быть злым"
3
@ Жиль: во втором квартале ни одно из трех названных конфессий не относится к неустановленному IFS, все они имеют в виду IFS=.
Стефан Гименес
@ Жиль В Q2, я никогда не говорил, что они были одинаковыми. И внутренние разделители трогали, как показано здесь: IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (Э-э, что? Там должно быть несколько разделителей пробела, так что движок SO продолжает их удалять).
Крис Даун
2
@ StéphaneGimenez, Крис: Да, извините за вопрос 2, я неправильно понял вопрос. Для Q4 мы говорим о read; последняя переменная захватывает все, что осталось, кроме последнего разделителя, и оставляет внутренние разделители внутри.
Жиль "ТАК - перестань быть злым"
1
Жиль частично прав насчет того, что места не удаляются чтением. Прочитайте мой ответ для деталей.
22

Q1: да. «Разделение полей» и «разделение слов» являются двумя терминами одного и того же понятия.

Q2: да. Если IFSне установлено (то есть после unset IFS), это эквивалентно IFSустановке $' \t\n'(пробел, табуляция и перевод строки). Если IFSустановлено пустое значение (это то, что здесь означает «ноль») (т. Е. После IFS=или IFS=''или IFS=""), разделение полей вообще не выполняется (и $*, как правило, использует первый символ $IFS, использует пробел).

Q3: Если вы хотите иметь IFSповедение по умолчанию , вы можете использовать unset IFS. Если вы хотите IFSявно установить это значение по умолчанию, вы можете поместить пространство буквенных символов, табуляцию, символ новой строки в одинарные кавычки. В ksh93, bash или zsh вы можете использовать IFS=$' \t\n'. В частности, если вы хотите избежать буквального символа табуляции в исходном файле, вы можете использовать

IFS=" $(echo t | tr t \\t)
"

Q4: с IFSустановленным на пустое значение, read -r lineустанавливает lineна всю строку, кроме завершающей новой строки. С помощью IFS=" "пробелов в начале и конце строки обрезаются. При значении по умолчанию IFSвкладки и пробелы обрезаются.

Жиль "ТАК - перестань быть злым"
источник
2
Q2 отчасти неправильно. Если IFS пуст, «$ *» объединяется без разделителей. (поскольку $@в контекстах, не включенных в список, есть некоторые различия между оболочками IFS=; var=$@). Следует отметить, что когда IFS пуст, разделение слов не выполняется, но $ var по-прежнему расширяется до без аргумента вместо пустого аргумента, когда $ var пуст и применяется глобализация, поэтому вам все равно нужно заключать в кавычки переменные (даже если вы отключить сглаживание)
Стефан Шазелас
13

Q1. Расщепление поля.

Разделение полей - это то же самое, что и разделение слов?

Да, оба указывают на одну и ту же идею.

Q2: Когда IFS является нулевым ?

Это IFS=''то же самое, что и пустая строка?

Да, все три означают одно и то же: разделение полей / слов не должно выполняться. Кроме того, это влияет на поля печати (как в случае echo "$*"), все поля будут объединены вместе без пробелов.

Q3: (часть а) Отключить IFS.

В спецификации POSIX я прочитал следующее :

Если IFS не установлен, оболочка должна вести себя так, как если бы значение IFS было <space> <tab> <newline> .

Что в точности эквивалентно:

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

Это означает, что «Разделение поля» будет точно таким же, как и значение IFS по умолчанию, или не будет установлено.
Это НЕ означает, что IFS будет работать одинаково в любых условиях. Чтобы быть более точным, выполнение OldIFS=$IFSустановит для переменной OldIFSзначение null , а не по умолчанию. И попытка установить IFS обратно, как это, IFS=OldIFSустановит для IFS значение null, а не оставить его неустановленным, как это было раньше. Осторожно !!.

Q3: (часть b) Восстановить IFS.

Как я могу восстановить значение IFS по умолчанию. Скажем, я хочу восстановить значение по умолчанию IFS. Как мне это сделать? (более конкретно, как я могу обратиться к <tab> и <newline> ?)

Для zsh, ksh и bash (AFAIK) для IFS может быть установлено значение по умолчанию:

IFS=$' \t\n'        # works with zsh, ksh, bash.

Готово, больше ничего не нужно читать.

Но если вам нужно переустановить IFS для sh, это может стать сложным.

Давайте посмотрим с самого простого на комплект без недостатков (кроме сложности).

1.- Отключить IFS.

Мы могли бы просто unset IFS(Прочтите Q3 часть а выше).

2.- Поменять местами символы.

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

Установите IFS на <пробел> <новая строка> <вкладка> :

sh -c 'IFS=$(echo " \n\t"); printf "%s" "$IFS"|xxd'      # Works.

3.- Простой? решение:

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

IFS =»   
'

Где последовательность, набранная вручную, была:, IFS='spacetabnewline'последовательность, которая на самом деле была правильно набрана выше (Если вам нужно подтвердить, отредактируйте этот ответ). Но копирование / вставка из вашего браузера сломается, потому что браузер будет сжимать / скрывать пробелы. Это затрудняет совместное использование кода, как написано выше.

4.- Полное решение.

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

Нам нужен код, который «производит» ожидаемое значение. Но, даже если концептуально правильно, этот код НЕ будет устанавливать трейлинг \n:

sh -c 'IFS=$(echo " \t\n"); printf "%s" "$IFS"|xxd'      # wrong.

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

Нам нужно использовать трюк для sh:

sh -c 'IFS="$(printf " \t\nx")"; IFS="${IFS%x}"; printf "$IFS"|xxd'  # Correct.

Альтернативный способ может состоять в том, чтобы установить IFS в качестве значения среды из bash (например), а затем вызвать sh (версии, которые принимают IFS для установки через среду), как это:

env IFS=$' \t\n' sh -c 'printf "%s" "$IFS"|xxd'

Короче говоря, sh делает сброс IFS по умолчанию довольно странным приключением.

Q4: в фактическом коде:

Наконец, как бы этот код:

while IFS= read -r line
do
    echo $line
done < /path_to_text_file

вести себя, если мы изменим первую строку на

while read -r line # Use the default IFS value

или:

while IFS=' ' read -r line

Во-первых: я не знаю, есть ли echo $line(с указанием var NOT) на porpouse или нет. Он вводит второй уровень «разделения поля», который не имеет чтения. Поэтому я отвечу на оба. :)

С этим кодом (чтобы вы могли подтвердить). Вам понадобится полезный xxd :

#!/bin/ksh
# Correctly set IFS as described above.
defIFS="$(printf " \t\nx")"; defIFS="${defIFS%x}";
IFS="$defIFS"
printf "IFS value: "
printf "%s" "$IFS"| xxd -p

a='   bar   baz   quz   '; l="${#a}"
printf "var value          : %${l}s-" "$a" ; printf "%s\n" "$a" | xxd -p

printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x--          : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf 'Values      quoted :\n' ""  # With values quoted:
printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null    quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS default quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space   quoted : %${l}s-" "$line" ;
    printf "%s" "$line" |xxd -p; done;

printf '%s\n' "Values unquoted :"   # Now with values unquoted:
printf "%s\n" "$a" | while IFS='x' read -r line; do
    printf "IFS --x-- unquoted : "
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS='' read -r line; do
    printf "IFS null  unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

printf "%s\n" "$a" | while IFS="$defIFS" read -r line; do
    printf "IFS defau unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

unset IFS; printf "%s\n" "$a" | while read -r line; do
    printf "IFS unset unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done
    IFS="$defIFS"   # set IFS back to default.

printf "%s\n" "$a" | while IFS=' ' read -r line; do
    printf "IFS space unquoted : ";
    printf "%s, " $line; printf "%s," $line |xxd -p; done

Я получил:

$ ./stackexchange-Understanding-IFS.sh
IFS value: 20090a
var value          :    bar   baz   quz   -20202062617220202062617a20202071757a2020200a
IFS --x--          :    bar   baz   quz   -20202062617220202062617a20202071757a202020
Values      quoted :
IFS null    quoted :    bar   baz   quz   -20202062617220202062617a20202071757a202020
IFS default quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS unset   quoted :       bar   baz   quz-62617220202062617a20202071757a
IFS space   quoted :       bar   baz   quz-62617220202062617a20202071757a
Values unquoted :
IFS --x-- unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS null  unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS defau unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS unset unquoted : bar, baz, quz, 6261722c62617a2c71757a2c
IFS space unquoted : bar, baz, quz, 6261722c62617a2c71757a2c

Первое значение - это только правильное значение IFS='spacetabnewline'

Следующая строка - это все шестнадцатеричные значения, которые $aимеет переменная var , и новая строка '0a' в конце, которая будет передана каждой команде чтения.

Следующая строка, для которой IFS имеет значение null, не выполняет никакого «разделения поля», но новая строка удаляется (как и ожидалось).

Следующие три строки, поскольку IFS содержит пробел, удаляют начальные пробелы и устанавливают в строке var оставшееся сальдо.

Последние четыре строки показывают, что будет делать переменная без кавычек. Значения будут разделены на (несколько) пробелов и будут напечатаны как:bar,baz,qux,


источник
4

unset IFS очищает IFS, даже если впоследствии предполагается, что IFS равен "\ t \ n":

$ echo "'$IFS'"
'   
'
$ IFS=""
$ echo "'$IFS'"
''
$ unset IFS
$ echo "'$IFS'"
''
$ IFS=$' \t\n'
$ echo "'$IFS'"
'   
'
$

Протестировано на bash версий 4.2.45 и 3.2.25 с таким же поведением.

derekm
источник
Вопрос и связанная документация не говорить о unsetо IFS, как объяснено в комментариях принятого ответа здесь.
ILMostro_7