Розуміння IFS


71

Наступні кілька потоків на цьому веб-сайті та StackOverflow були корисними для розуміння того, як IFSпрацює:

Але у мене все ще є короткі запитання. Я вирішив запитати їх у тому ж самому дописі, оскільки, думаю, це може допомогти кращим майбутнім читачам:

Q1. IFSзазвичай обговорюється в контексті "поділу поля". Чи розділення поля те саме, що розділення слів ?

Q2: специфікація POSIX говорить :

Якщо значення IFS є нульовим, розділення поля не проводиться.

Чи встановлення IFS=збігається з параметром " IFSnull"? Це те, що мається на увазі, якщо встановити його 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. У баші та подібних снарядах можна зробити щось подібне IFS=$' \t\n'. В іншому випадку ви можете вставити буквальні коди управління, використовуючи [space] CTRL+V [tab] CTRL+V [enter]. Якщо ви плануєте це зробити, однак, краще використовувати іншу змінну, щоб тимчасово зберегти старе IFSзначення, а потім відновити його (або тимчасово замінити його для однієї команди за допомогою var=foo commandсинтаксису).
    • Перший фрагмент коду покладе весь прочитаний рядок, дослівно $line, оскільки немає роздільників полів, для яких можна виконати розбиття слів. Але майте на увазі, що оскільки багато оболонок використовують cstrings для зберігання рядків, перший примірник NUL все ще може спричинити появу його достроково припиненого.
    • Другий фрагмент коду може не вводити точну копію вводу $line. Наприклад, якщо є кілька послідовних роздільників полів, вони будуть перетворені на один екземпляр першого елемента. Це часто визнається втратою навколишнього пробілу.
    • Третій фрагмент коду буде виконувати так само, як і другий, за винятком того, що він розділиться лише на пробіл (не на звичайний пробіл, вкладку чи новий рядок).

3
Відповідь на Q2 помилкова: порожній IFSі невстановлений IFSдуже різні. Відповідь на Q4 частково неправильна: внутрішні роздільники тут не торкаються, лише провідні та зворотні.
Жиль

3
@ Gilles: У другому кварталі жодна з трьох заданих номіналів не відноситься до невстановлених IFSзначень, всі вони означають IFS=.
Стефан Гіменез

@Gilles У другому кварталі я ніколи не говорив, що вони однакові. І внутрішні роздільники чіпали, як показано тут: IFS=' ' ; foo=( bar baz qux ) ; echo "${#foo[@]}". (Е, що? Там повинно бути кілька роздільників простору, двигун SO продовжує знімати їх).
Кріс Даун

2
@ StéphaneGimenez, Chris: О, так, вибачте за Q2, я неправильно прочитав питання. Для Q4 ми говоримо read; остання змінна захоплює все, що залишилося, крім останнього роздільника і залишає внутрішні роздільники всередині.
Жиль

1
Жил частково вірно стосується пробілів, які не видаляються читанням. Прочитайте мою відповідь для деталей.

22

Q1: Так. «Розбиття поля» та «розділення слів» - це два терміни для одного поняття.

Q2: Так. Якщо IFSне встановлено (тобто після того, як unset IFS), воно еквівалентно IFSбути встановлений $' \t\n'(пробіл, вкладки і нового рядка). Якщо IFSвстановлено порожнє значення (ось що означає "null" тут) (тобто після 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 порожній, і все ще застосовується глобалізація, тому вам все одно потрібно цитувати змінні (навіть якщо ви відключити глобус)
Stéphane Chazelas

13

Q1. Розщеплення поля.

Чи розділення поля те саме, що розділення слів?

Так, обидва вказують на одну і ту ж ідею.

Q2: Коли значення IFS недійсне ?

Чи встановлено так IFS=''само, як null, те саме, що і порожній рядок?

Так, усі три означають те саме: Розбиття на поле / слово не повинно здійснюватися. Також це впливає на поля друку (як і у випадку echo "$*"), всі поля будуть об'єднані разом без місця.

Q3: (частина a) Не встановлено IFS.

У специфікації POSIX я прочитав наступне :

Якщо IFS не встановлено, оболонка повинна вести себе так, як якщо б значення IFS було <space><tab> <newline> .

Що точно рівнозначно:

За допомогою unset IFSоболонки оболонка повинна поводитись так, як якщо IFS за замовчуванням.

Це означає, що "Розбиття поля" буде точно таким же зі значенням IFS за замовчуванням або не встановлено.
Це НЕ означає, що IFS працюватиме однаково в будь-яких умовах. Будучи більш конкретним, виконуючи OldIFS=$IFSзадасть вар OldIFSдо нуля , а не за замовчуванням. І намагаючись повернути IFS назад, як це, IFS=OldIFSвстановить IFS нульовим, а не збереже його невстановленим, як це було раніше. Стережись !!.

Q3: (частина б) Відновлення 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, частину a, вище).

2.- Поміняйте символи.

Як вирішення, заміна значень табуляції та нового рядка спрощує встановлення значення IFS, а потім воно працює аналогічно.

Встановіть для IFS <space><newline> <tab> :

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(з вар НЕ цитується) там на 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 недійсний, не виконує жодного "розбиття поля", але новий рядок видаляється (як очікувалося).

Наступні три рядки, оскільки 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 з однаковою поведінкою.


Питання та пов'язана документація не говорять про unsetте IFS, як пояснено в коментарях прийнятої відповіді тут.
ILMostro_7
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.