Usando “while read…”, o echo e o printf obtêm resultados diferentes

14

De acordo com esta pergunta " Using" while read .. "em um script linux "

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

resultado:

1, 2, 3 4 5 6

mas quando substituo echo por printf

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

resultado

1, 2, 3
4, 5, 6

Alguém poderia me dizer o que faz com que esses dois comandos sejam diferentes? Obrigado ~

    
por user3094631 22.07.2017 / 06:03

2 respostas

24

Não é apenas eco vs printf

Primeiro, vamos entender o que acontece com read a b c part. read executará a divisão de palavras com base no valor padrão de IFS variable que é space-tab-newline e ajustará tudo com base nisso. Se houver mais entrada do que as variáveis para segurá-la, ela encaixará as partes divididas em primeiras variáveis, e o que não pode ser ajustado - entrará no último. Aqui está o que eu quero dizer:

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

É exatamente assim que é descrito no manual do bash (veja a citação no final da resposta).

No seu caso, o que acontece é que 1 e 2 se encaixam nas variáveis aeb, e c pega todo o resto, que é 3 4 5 6 .

O que você também verá muitas vezes é que as pessoas usam while IFS= read -r line; do ... ; done < input.txt para ler arquivos de texto linha por linha. Novamente, IFS= está aqui por um motivo para controlar a divisão de palavras ou, mais especificamente, desativá-lo e ler uma única linha de texto em uma variável. Se não estivesse lá, read estaria tentando encaixar cada palavra individual na variável line . Mas essa é outra história, que eu encorajo você a estudar mais tarde, já que while IFS= read -r variable é uma estrutura usada com frequência.

comportamento echo vs printf

echo faz o que você espera aqui. Ele exibe suas variáveis exatamente como read as organizou. Isso já foi demonstrado em discussões anteriores.

printf é muito especial, porque continuará ajustando variáveis na string de formato até que todas estejam esgotadas. Então, quando você faz printf "%d, %d, %d \n" $a $b $c printf vê a string de formato com 3 casas decimais, mas há mais argumentos do que 3 (porque suas variáveis realmente se expandem para individuais 1,2,3,4,5,6). Isso pode parecer confuso, mas existe por um motivo como um comportamento melhorado do que a função real printf() faz na linguagem C.

O que você também fez aqui e afeta a saída é que suas variáveis não são citadas, o que permite que o shell (não printf ) divida as variáveis em 6 itens separados. Compare isso com as citações:

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

Exatamente porque a variável $c está entre aspas, agora ela é reconhecida como uma string inteira, 3 4 e não cabe no formato %d , que é apenas um único inteiro

Agora faça o mesmo sem citar:

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

printf novamente diz: "OK, você tem 6 itens lá, mas o formato mostra apenas 3, então eu continuarei ajustando o material e deixando em branco o que eu não puder corresponder à entrada real do usuário".

E em todos esses casos você não precisa acreditar na minha palavra. Basta executar strace -e trace=execve e ver por si mesmo o que o comando realmente "vê":

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 +++

Notas adicionais

Como Charles Duffy apontou corretamente nos comentários, bash tem seu próprio printf , que é o que você está usando em seu comando, strace na verdade chama /usr/bin/printf version, não shell versão. Além de pequenas diferenças, para nosso interesse nesta questão em particular, os especificadores de formato padrão são os mesmos e o comportamento é o mesmo.

O que também deve ser mantido é que a sintaxe printf é muito mais portátil (e portanto preferida) do que echo , sem mencionar que a sintaxe é mais familiar a C ou qualquer linguagem semelhante a C que possua printf() função nele. Veja esta excelente resposta por terdon sobre o assunto de printf vs echo . Embora você possa fazer a saída feita sob medida para o seu shell específico em sua versão específica do Ubuntu, se você estiver portando scripts em sistemas diferentes, provavelmente deve preferir printf em vez de eco. Talvez você seja um administrador de sistema iniciante que trabalha com máquinas Ubuntu e CentOS, ou talvez até com o FreeBSD - quem sabe - então, nesses casos, você terá que fazer escolhas.

Citação do manual do bash, seção SHELL BUILTIN COMMANDS

  

ler [-ers] [-um aname] [-d delim] [-i texto] [-n nchars] [-N nchars]   [-p prompt] [-t timeout] [-u fd] [nome ...]

     

Uma linha é lida da entrada padrão, ou do descritor de arquivo fd fornecido como um argumento para a opção -u, e a   primeiro             palavra é atribuída ao primeiro nome, a segunda palavra ao segundo nome, e assim por diante, com as palavras restantes e sua intervenção   separa‐             atribuídos ao último nome. Se houver menos palavras lidas no fluxo de entrada do que nomes, os nomes restantes serão   atribuído             valores vazios. Os caracteres no IFS são usados para dividir a linha em palavras usando as mesmas regras que o shell usa para   expansão             (descrito acima no Word Splitting).

    
por Sergiy Kolodyazhnyy 22.07.2017 / 06:35
7

Esta é apenas uma sugestão e não pretende substituir a resposta de Sergiy.  Eu acho que o Sergiy escreveu uma ótima resposta do porque eles são diferentes na impressão. Como a variável na leitura é atribuída com o restante na variável $c , pois 3 4 5 6 após 1 e 2 são atribuídos a a e b . echo não dividirá a variável para você, onde printf será com o %d s.

Você pode, no entanto, fazê-los basicamente dar-lhe as mesmas respostas, manipulando o eco dos números no início do comando:

Em /bin/bash , você pode usar:

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

Em /bin/sh você pode usar:

echo "1 2 3 \n4 5 6"

O Bash usa o -e para ativar os \ caracteres de escape, onde sh não precisa dele, pois já está ativado. \n faz com que ele crie uma nova linha, então agora a linha de eco é dividida em duas linhas separadas que agora podem ser usadas duas vezes para sua declaração de loop de eco:

:~$ 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

O qual, por sua vez, produz a mesma saída usando o comando 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 

Em 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 

Espero que isso ajude!

    
por Terrance 22.07.2017 / 07:20