Використовуючи "під час читання ...", echo та printf отримують різні результати


14

Відповідно до цього питання " Використання" під час читання ... "у скрипті Linux "

echo '1 2 3 4 5 6' | while read a b c;do echo "$a, $b, $c"; done

результат:

1, 2, 3 4 5 6

але коли я замінюю echoнаprintf

echo '1 2 3 4 5 6' | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done

результат

1, 2, 3
4, 5, 6

Невже хтось скаже мені, чим відрізняються ці дві команди? Спасибі ~

Відповіді:


24

Це не просто відлуння проти printf

Спочатку давайте розберемося, що відбувається з read a b cчастиною. readвиконає розділення слів на основі значення за замовчуванням IFSзмінної, яка є пробіл-табуляція-новий рядок, і підходить до всього, що ґрунтується на цьому. Якщо є більше вхідних даних, ніж змінні для її зберігання, він вмістить розділені частини на перші змінні, а те, що неможливо встановити - перейде в останню. Ось що я маю на увазі:

bash-4.3$ read a b c <<< "one two three four"
bash-4.3$ echo $a
one
bash-4.3$ echo $b
two
bash-4.3$ echo $c
three four

Саме так це описано в bashпосібнику (див. Цитату в кінці відповіді).

У вашому випадку відбувається те, що 1 і 2 вписуються в змінні a і b, а c приймає все інше, що є 3 4 5 6.

Те, що ви також побачите багато разів, - це те, що люди використовують while IFS= read -r line; do ... ; done < input.txtдля читання текстових файлів по черзі. Знову ж таки, IFS=є причина контролювати розділення слів, а точніше - відключити його та прочитати один рядок тексту в змінну. Якби його не було, readнамагалися б укласти кожне окреме слово у lineзмінну. Але це ще одна історія, яку я закликаю вас вивчити пізніше, оскільки while IFS= read -r variableце дуже часто використовувана структура.

echo vs printf поведінка

echoробить те, що ви тут очікували. Він відображає ваші змінні точно так, як readїх упорядкував. Це було вже продемонстровано в попередній дискусії.

printfє дуже особливим, тому що він буде продовжувати пристосовувати змінні до рядка формату, поки всі вони не будуть вичерпані. Отже, коли ви виконайте printf "%d, %d, %d \n" $a $b $cprintf, ви бачите рядок формату з 3 десятковими знаками, але аргументів більше, ніж 3 (оскільки ваші змінні фактично розширюються до окремих 1,2,3,4,5,6). Це може здатися заплутаним, але існує з тієї причини, як поліпшена поведінка, ніж реальна printf() функція в мові С.

Що ви також зробили тут, що впливає на вихід, - це те, що ваші змінні не цитуються, що дозволяє оболонці (не printf) розбивати змінні на 6 окремих елементів. Порівняйте це з цитуванням:

bash-4.3$ read a b c <<< "1 2 3 4"
bash-4.3$ printf "%d %d %d\n" "$a" "$b" "$c"
bash: printf: 3 4: invalid number
1 2 3

Саме тому $c, що котирується змінна, вона тепер визнається як один цілий рядок 3 4, і він не відповідає %dформату, який є лише одним цілим числом

Тепер зробіть те ж саме, не цитуючи:

bash-4.3$ printf "%d %d %d\n" $a $b $c
1 2 3
4 0 0

printf знову каже: "Гаразд, у вас є 6 елементів, але формат показує лише 3, тому я буду тримати придатний матеріал і залишати порожнім все, що не можу відповідати фактичному вводу від користувача".

І в усіх цих випадках вам не потрібно приймати моє слово за це. Просто запустіть strace -e trace=execveі переконайтеся, що насправді команда "бачить":

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" $a $b $c
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3", "4"], [/* 80 vars */]) = 0
1 2 3
4 0 0
+++ exited with 0 +++

bash-4.3$ strace -e trace=execve printf "%d %d %d\n" "$a" "$b" "$c"
execve("/usr/bin/printf", ["printf", "%d %d %d\\n", "1", "2", "3 4"], [/* 80 vars */]) = 0
1 2 printf: 3 4’: value not completely converted
3
+++ exited with 1 +++

Додаткові нотатки

Як належним чином зауважив у коментарях Чарльз Даффі, у нього bashє своя вбудована система printf, яку ви використовуєте у своїй команді, straceнасправді буде називати /usr/bin/printfверсію, а не оболонку. Крім незначних відмінностей, для нашого інтересу до цього конкретного питання специфікатори стандартного формату однакові, а поведінка однакова.

Що також слід пам’ятати, це те, що printfсинтаксис є набагато більш портативним (і, отже, кращим), ніж echo, не кажучи вже про те, що синтаксис більш звичний для C або будь-якої мови, подібної С, яка має printf()в ньому функцію. Дивіться цей чудовий відповідь на terdon на тему printfпроти echo. Хоча ви можете зробити висновок з урахуванням вашої конкретної оболонки для вашої конкретної версії Ubuntu, якщо ви збираєтесь переносити сценарії в різні системи, вам, мабуть, слід віддати перевагу, printfа не відлуння. Можливо, ви початківець системний адміністратор, який працює з машинами Ubuntu та CentOS, або, можливо, навіть FreeBSD - хто це знає - тому в таких випадках вам доведеться робити вибір.

Цитата з інструкції з bash, розділ SHELL BUILTIN COMMANDS

read [-ers] [-a aname] [-d delim] [-i текст] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [name ... ]

Один рядок зчитується зі стандартного вводу або з дескриптора файлу fd, який подається як аргумент до опції -u, і перше слово присвоюється першому імені, другому - другому і т. Д., Залишившись слова та їх втручаються роздільники, присвоєні прізвищу. Якщо з вхідного потоку читається менше слів, ніж імен, решта імен присвоюються порожніми значеннями. Символи IFS використовуються для розділення рядка на слова, використовуючи ті самі правила, які використовує оболонка для розширення (описано вище в розділі Word).


2
Одна примітна різниця між straceрегістром та іншим - strace printfце використання /usr/bin/printf, тоді як printfбезпосередньо в bash використовується оболонка, вбудована за однойменною назвою. Вони не завжди будуть ідентичними - наприклад, екземпляр bash має специфікатори формату %qта, у нових версіях, $()Tформат часу.
Чарльз Даффі

7

Це лише пропозиція і зовсім не призначена для заміни відповіді Сергія. Я думаю, що Сергій написав чудову відповідь, чому вони відрізняються друком. Як змінна на читання отримує призначення з рештою в $cзмінної, 3 4 5 6після того, як 1і 2призначаються aі b. echoне розділить змінну для вас, де printfбуде з %ds.

Однак ви можете змусити їх дати вам однакові відповіді, маніпулюючи відлунням чисел на початку команди:

У /bin/bashви можете використовувати:

echo -e "1 2 3 \n4 5 6"

У /bin/shви можете просто використовувати:

echo "1 2 3 \n4 5 6"

Bash використовує -e для ввімкнення символів \ escape, де sh не потрібен, як це вже ввімкнено. \nзмушує його створити новий рядок, тому тепер ехо-лінія розділена на два окремі рядки, які тепер можна використовувати два рази для вашої операції циклу ехо:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6

Що в свою чергу виробляє той самий вихід із використанням printfкоманди:

:~$ echo -e "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

В sh

$ echo "1 2 3 \n4 5 6" | while read a b c; do echo "$a, $b, $c"; done
1, 2, 3
4, 5, 6
$ echo "1 2 3 \n4 5 6" | while read a b c ;do printf "%d, %d, %d \n" $a $b $c; done
1, 2, 3 
4, 5, 6 

Сподіваюся, це допомагає!


echo -eце не просто розширення до POSIX - але й echoте, що не випромінює -eна своєму виході, враховуючи, що вхід насправді викликає / порушує специфікацію чорної літери. (bash за замовчуванням не відповідає, але відповідає стандарту, коли встановлені обидва posixта xpg_echoпрапори; останні можуть бути за замовчуванням під час компіляції, це означає, що не всі версії bash будуть працювати з тим, echo -eяк ви очікуєте тут).
Чарльз Даффі

Див. Розділи ВИКОРИСТАННЯ ВИКОРИСТАННЯ та RATIONALE специфікації POSIX на веб- echoсайті pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html , чітко описуючи те echo, що поведінка не визначено у присутності будь-якого -nабо зворотного косого рису в будь- якому місці та вказуючи, що printfвикористовувати замість цього.
Чарльз Даффі

@CharlesDuffy Я коли-небудь писав у моєму, що очікую, що це буде працювати у всіх версіях bash? І яка проблема у вас з моєю відповіддю? Якщо ви відчуваєте, що маєте краще рішення, напишіть це як відповідь, а не намагаючись доводити людей неправильно. Я занадто багато разів пройшов цей шлях з такими людьми, як ви, тому, будь ласка, припиніть такі коментарі. Це все, що я маю сказати.
Терранс

3
@CharlesDuffy в Ask Ubuntu ми іноді пишемо відповіді, які не можна перенести в інші дистрибутиви Linux. Оскільки у вас є лише 103 бали, ви, можливо, цього не помічали. Ваша представниця в Stack Overflow - це 123,3K балів, так що ви, очевидно, знаєте безліч речей про * NIX-системи взагалі. Я думаю, у Тебе, Тераса і Серг все в порядку, але дивишся на нитку з різних ракурсів. Я повторюю (не призначений для каламбуру) почуття до вас, щоб написати відповідь, яка є * NIX-портативною або, принаймні, портативною у дистрибутивах Linux.
WinEunuuchs2Unix

2
@CharlesDuffy Хоча я погоджуюся з вашими настроями, відповіді повинні бути переносними, але це врешті-решт, орієнтований на Ubuntu сайт, тому користувачі повинні брати на себе спеціальні інструменти для Ubuntu. Тож попередження дещо мається на увазі. Не будемо вдаватися до надмірно тривалих дискусій. Ви маєте рацію, що слід враховувати й інші оболонки, а також слід враховувати і новачків, які читають, щоб дізнатись відповіді. Короткий коментар, ймовірно, буде достатнім, щоб висловити суть.
Сергій Колодяжний
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.