Используя "while read…",echo и printf получают разные результаты

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

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

2 ответа

Решение

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

Во-первых, давайте разберемся, что происходит с 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 это очень часто используемая структура.

эхо против поведения printf

echo делает то, что вы ожидаете здесь. Он отображает ваши переменные в точности как read устроил их. Это уже было продемонстрировано в предыдущем обсуждении.

printf очень особенный, потому что он будет продолжать подгонку переменных к строке формата, пока все они не будут исчерпаны. Итак, когда вы делаете printf "%d, %d, %d \n" $a $b $c printf видит строку формата с 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 а не эхо. Может быть, вы начинающий системный администратор, работающий с машинами Ubuntu и CentOS, или, может быть, даже FreeBSD - кто знает - поэтому в таких случаях вам придется делать выбор.

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

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

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

Это всего лишь предложение и вовсе не должно заменить ответ Сергея. Я думаю, что Сергей написал отличный ответ о том, почему они отличаются в печати. Как переменная в чтении присваивается с оставшимися в $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 

Надеюсь это поможет!

Другие вопросы по тегам