Используя «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 22.07.2017 в 06:03
источник

2 ответа

24

Это не просто echo vs printf

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

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 $c printf видит строку формата с 3 десятичными знаками, но есть больше аргументов, чем 3 (потому что ваши переменные фактически расширяются до отдельных 1,2,3,4,5,6). Это может показаться запутанным, но существует по той причине, что улучшено поведение от того, что делает функция real printf() на языке C.

То, что вы также сделали здесь, что влияет на результат, заключается в том, что ваши переменные не котируются, что позволяет оболочке (а не 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 , а не shell версия. Помимо незначительных различий, для нашего интереса к этому конкретному вопросу спецификаторы стандартного формата одинаковы, а поведение одинаковое.

Также следует иметь в виду, что синтаксис printf гораздо более переносимый (и, следовательно, предпочтительный), чем echo , не говоря уже о том, что синтаксис более знаком с C или любым C-подобным языком, который имеет printf() . См. отличный ответ terdon по теме printf vs echo . Хотя вы можете сделать вывод, адаптированный к вашей конкретной оболочке, на вашей конкретной версии Ubuntu, если вы собираетесь переносить скрипты в разные системы, вам, скорее всего, следует предпочесть printf , а не echo. Возможно, вы начинающий системный администратор, работающий с машинами Ubuntu и CentOS, или, может быть, даже с FreeBSD - кто знает - поэтому в таких случаях вам придется делать выбор.

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

  

прочитать [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars]   [-p prompt] [-t timeout] [-u fd] [имя ...]

     

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

    
ответ дан Sergiy Kolodyazhnyy 22.07.2017 в 06:35
источник
7

Это только предположение и вовсе не означает, что Сергей ответит.  Я думаю, что Сергей написал отличный ответ о том, почему они отличаются в печати. Как переменная в read присваивается оставшейся в переменной $c как 3 4 5 6 после 1 и 2 , назначается a и b . echo не будет разделять переменную для вас, где printf будет с %d s.

Однако вы можете заставить их в основном дать вам те же ответы, манипулируя эхом чисел в начале команды:

В /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 

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

    
ответ дан Terrance 22.07.2017 в 07:20