Розуміння "IFS = read -r рядок"


60

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

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

Я також розумію, що read -r lineзбереже дані з stdinзмінної з ім'ям line:

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

Однак як команда може призначити змінне значення? І робить він спочатку зберегти дані stdinв змінній , lineа потім дати значення lineдля IFS?


Відповіді:


104

Деякі люди мають таке помилкове поняття, що readце команда читати рядок. Це не.

readчитає слова з (можливо, продовження зворотної косої риски), де слова $IFSрозмежовані і зворотна косою рисою може бути використана для виходу з роздільників (або продовження рядків).

Родовим синтаксисом є:

read word1 word2... remaining_words

readчитає STDIN один байт в той час , поки він не знайде неекранований символ нового рядка (або кінець вхідного тексту), розщеплюється , що в відповідності зі складними правилами і зберігає результат цього поділу на $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'.

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

У оболонці Борна:

var=value cmd

Якщо cmdце вбудований (як readє), varзалишається встановленим valueпісля cmdзакінчення. Це особливо важливо, $IFSтому що в оболонці Борна $IFSвикористовується для розділення всього, а не тільки розширень. Крім того, якщо ви видалите пробіл із символу $IFSоболонки Борна, він "$@"більше не працює.

У оболонці Bourne перенаправлення складеної команди змушує її запускатись в нижній частині корпусу (у ранніх версіях навіть такі речі, як, read var < fileабо exec 3< file; read var <&3не працювали), тому в оболонці Bourne рідко можна було використовувати readбудь-що, крім введення користувача в термінал (де сенс обробки продовження рядка мав сенс)

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

Це в основному те саме, що head -n 1за винятком того, що він читає один байт за один раз, щоб переконатися, що він не читає більше одного рядка. У цих системах ви можете:

line=`line`

Звичайно, це означає нерестування нового процесу, виконайте команду і прочитайте її вихід через трубу, так що набагато менш ефективний, ніж ksh IFS= read -r line, але все ж набагато більш інтуїтивно зрозумілий.


3
+1 Дякую за корисну інформацію про різні способи лікування простору / вкладки проти "інших" в IFS в баші ... Я знав, що до них по-різному ставляться, але це пояснення все це дуже спрощує. (І розуміння між bash (та іншими позиційними оболонками) та регулярними 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"
Mike S

8

Теорія

Тут грають два поняття:

  • IFSє роздільником поля введення, що означає, що читання рядків буде розділене на основі символів у IFS. У командному рядку, IFSяк правило, будь-які символи пробілу, тому командний рядок розбивається на пробіли.
  • Робити щось на кшталт VAR=value commandозначає "змінити середовище командування так, щоб VARмати значення value". В основному, команда commandбуде бачити VARтаке значення value, але будь-яка команда, виконана після цього, все ще буде VARмати попереднє значення. Іншими словами, ця змінна буде змінена лише для цього твердження.

В цьому випадку

Отже, коли ви робите IFS= read -r line, ви встановлюєте IFSпорожній рядок (жоден символ не буде використовуватися для розбиття, тому розшарування не відбудеться), щоб readпрочитати весь рядок і побачити його як одне слово, яке буде призначено lineзмінній. Зміни IFSвпливають лише на цей оператор, так що будь-які наступні команди не впливатимуть на зміну.

Як бічна записка

У той час як команда правильно і буде працювати , як задумано, установка IFSв даному випадку НЕ Міць 1 НЕ буде необхідності. Як написано на bashсторінці man у 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змінити десь раніше в сценарії.


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]

Завдяки rici за вказівку на цю різницю.


Те, що вам не вистачає, - це те, що якщо IFS не буде змінено, read -r lineобрізає провідні та відсталі пробіли перед призначенням вхідної lineзмінної.
rici

@rici Я підозрював щось подібне, але лише перевіряв символи IFS між словами, а не ведучими / кінцевими. Дякуємо, що вказали на цей факт!
jlliagre

очищення IFS також запобіжить призначенню декількох змінних (побічний ефект). IFS= read a b <<< 'aa bb' ; echo "-$a-$b-"покаже-aa bb--
kyodev
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.