Используя «while read…», 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

Может кто-нибудь сказать мне, что отличает эти две команды? Благодаря ~

user3094631
источник

Ответы:

24

Это не просто эхо против печати

Сначала давайте разберемся, что происходит с read a b cдеталью. readвыполнит разделение слов на основе значения IFSпеременной по умолчанию, которое является space-tab-newline, и подгонит все на основе этого. Если есть больше входных данных, чем переменные для его хранения, он поместит разделенные части в первые переменные, а то, что не может быть подогнано - перейдет в последнюю. Вот что я имею в виду:

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как это очень часто используемая структура.

эхо против поведения 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 или любого C-подобного языка, который имеет printf()функцию в нем. Посмотрите этот превосходный ответ Тердона на тему printfпротив echo. Хотя вы можете настроить вывод в соответствии с вашей конкретной оболочкой в ​​вашей конкретной версии Ubuntu, если вы собираетесь переносить скрипты на разные системы, вам, вероятно, следует предпочесть, printfа не echo. Возможно, вы начинающий системный администратор, работающий с машинами Ubuntu и CentOS, или, может быть, даже FreeBSD - кто знает - поэтому в таких случаях вам придется делать выбор.

Цитата из руководства bash, раздел SHELL BUILTIN COMMANDS

читать [-ers] [-a имя] [-d раздел] [-i текст] [-n nchars] [-N nchars] [-p приглашение] [-t тайм-аут] [-u fd] [имя ... ]

Одна строка читается из стандартного ввода или из файлового дескриптора fd, предоставленного в качестве аргумента опции -u, и первое слово присваивается первому имени, второе слово - второму имени и т. Д. С остатком слова и их промежуточные разделители, присвоенные фамилии. Если из входного потока прочитано меньше слов, чем имён, оставшимся именам присваиваются пустые значения. Символы в IFS используются для разделения строки на слова с использованием тех же правил, которые оболочка использует для раскрытия (описано выше в разделе «Разделение слов»).

Сергей Колодяжный
источник
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как вы ожидаете здесь).
Чарльз Даффи
См. Разделы ПРИМЕНЕНИЕ ИСПОЛЬЗОВАНИЕ и ОБОСНОВАНИЕ спецификации POSIX для по echoадресу pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html , в которых подробно описывается echoповедение, которое не задано при наличии любого -nили обратной косой черты в любом месте ввода, и указывается, что printfиспользоваться вместо
Чарльз Даффи
@CharlesDuffy Я когда-нибудь писал в своей, что я ожидаю, что это будет работать во всех версиях bash? А какая у вас проблема с моим ответом? Если вы считаете, что у вас есть лучшее решение, напишите его как ответ, а не пытайтесь доказать, что люди ошибаются. Я слишком часто проходил этот путь с такими людьми, как ты, поэтому, пожалуйста, остановись на этих комментариях, подобных этому. Это все, что я должен сказать.
Терренс
3
@CharlesDuffy в Ask Ubuntu мы иногда пишем ответы, которые нельзя перенести в другие дистрибутивы Linux. Поскольку ваш представитель здесь всего 103 балла, вы могли этого не заметить. Ваша репутация в Stack Overflow составляет 123,3 тыс. Баллов, так что вы, очевидно, знаете массу вещей о * NIX системах в целом. Я думаю, что вы, Террас и Серг в порядке, но смотрите на тему с разных сторон. Я повторяю (не каламбур), что вам нужно написать ответ, который будет * NIX переносимым или, по крайней мере, переносимым в дистрибутивах Linux.
WinEunuuchs2Unix
2
@CharlesDuffy Хотя я согласен с вашим мнением, ответы должны быть переносимыми, в конце концов, это сайт, ориентированный на Ubuntu, поэтому пользователи должны использовать инструменты, специфичные для Ubuntu. Так что предупреждение несколько подразумевается. Давайте не будем вдаваться в чрезмерно длинные дискуссии. Вы правы в том, что следует учитывать и другие оболочки, и новички, читающие, чтобы учиться на ответах, также должны быть рассмотрены. Короткого комментария, вероятно, было бы достаточно, чтобы сделать точку.
Сергей Колодяжный