Usando "enquanto lê ...", echo e printf obtêm resultados diferentes

14

De acordo com esta pergunta " Usando" enquanto lê ... "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 eu substituo echoporprintf

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 torna esses dois comandos diferentes? Obrigado ~

user3094631
fonte

Respostas:

24

Não é apenas eco vs printf

Primeiro, vamos entender o que acontece com a read a b cparte. readexecutará a divisão de palavras com base no valor padrão da IFSvariável que é space-tab-newline e ajustará tudo com base nisso. Se houver mais informações do que as variáveis ​​para mantê-lo, ele encaixará partes divididas nas primeiras variáveis, e o que não pode ser ajustado - será o ú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 bashmanual (veja a citação no final da resposta).

No seu caso, o que acontece é que, 1 e 2 se encaixam nas variáveis ​​aeb, ec toma tudo o 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.txtpara ler arquivos de texto linha por linha. Novamente, IFS=aqui está um motivo para controlar a divisão de palavras, ou mais especificamente - desative-a e leia uma única linha de texto em uma variável. Se não estivesse lá, readestaria tentando ajustar cada palavra individual em linevariável. Mas essa é outra história, que eu encorajo você a estudar mais tarde, pois while IFS= read -r variableé uma estrutura usada com muita frequência.

comportamento de eco vs printf

echofaz o que você esperaria aqui. Ele exibe suas variáveis ​​exatamente como as readorganizou. Isso já foi demonstrado na discussão anterior.

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

O que você também fez aqui que 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 a citação:

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 $cvariável é citada, agora é reconhecida como uma sequência inteira 3 4e não se ajusta ao %dformato, que é apenas um único número 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 continuarei ajustando as coisas e deixando em branco o que não puder corresponder à entrada real do usuário".

E em todos esses casos, você não precisa aceitar minha palavra. Basta executar strace -e trace=execvee 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, bashtem seu próprio built-in printf, que é o que você está usando em seu comando, stracena verdade chamará /usr/bin/printfversão, não a versão do shell. Além de pequenas diferenças, para nosso interesse nessa 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 lembrado é que a printfsintaxe é muito mais portátil (e, portanto, preferida) do que echo, sem mencionar que a sintaxe é mais familiar para C ou qualquer linguagem semelhante a C que tenha printf()função nela. Veja este excelente resposta por terdon sobre o tema da printfvs echo. Embora você possa fazer a saída adaptada ao seu shell específico na sua versão específica do Ubuntu, se você estiver portando scripts em diferentes sistemas, provavelmente prefere o printfeco. Talvez você seja um administrador de sistemas iniciante trabalhando com máquinas Ubuntu e CentOS, ou talvez até FreeBSD - quem sabe - então, nesses casos, você terá que fazer escolhas.

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

leia [-ers] [-a aname] [-d delim] [-i text] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [nome ... ]

Uma linha é lida a partir da entrada padrão ou do descritor de arquivo fd fornecido como argumento para a opção -u, e a primeira palavra é atribuída ao primeiro nome, a segunda palavra ao segundo nome e assim por diante, com as sobras palavras e seus separadores intermediários atribuídos ao sobrenome. Se houver menos palavras lidas no fluxo de entrada do que nomes, os nomes restantes receberão 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 (descritas acima em Separação de palavras).

Sergiy Kolodyazhnyy
fonte
2
Uma diferença notável entre o stracecaso e o outro - strace printfestá usando /usr/bin/printf, enquanto printfdiretamente no bash está usando o shell incorporado com o mesmo nome. Eles nem sempre serão idênticos - por exemplo, a instância do bash possui especificadores de formato %qe, em novas versões, $()Tpara formatação de horário.
Charles Duffy
7

Esta é apenas uma sugestão e não pretende substituir a resposta de Sergiy. Acho que Sergiy escreveu uma ótima resposta sobre por que eles são diferentes na impressão. Como a variável na leitura é atribuída com o restante na $cvariável como 3 4 5 6depois 1e 2é atribuída a ae b. echonão vai dividir a variável para você onde printfvai com os %ds.

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

Em /bin/bashvocê pode usar:

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

Em /bin/shvocê pode apenas usar:

echo "1 2 3 \n4 5 6"

O Bash usa o -e para ativar os caracteres \ escape nos quais sh não precisa, pois já está ativado. \nfaz 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 instruçã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

Que por sua vez produz a mesma saída usando o printfcomando:

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

Dentro 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 isto ajude!

Terrance
fonte
echo -enão é apenas uma extensão do POSIX - mas qualquer echoque não seja -eemitida em sua saída, uma vez que a entrada está realmente desafiando / quebrando as especificações de letras pretas. (o bash não é compatível por padrão, mas está em conformidade com o padrão quando os dois posixe os xpg_echosinalizadores são definidos; o último pode ser padronizado em tempo de compilação, o que significa que nem todas as versões do bash funcionarão echo -econforme o esperado aqui).
Charles Duffy
Consulte as seções USO DO APLICATIVO e RATIONALE da especificação POSIX echoem pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html , descrevendo explicitamente que echoo comportamento desse tipo não é especificado na presença de -nbarras invertidas em qualquer lugar da entrada e aconselhando que printfser usado em seu lugar.
Charles Duffy
@CharlesDuffy Eu já escrevi no meu que espero que isso funcione em todas as versões do bash? E que problema você tem com a minha resposta? Se você acha que tem uma solução melhor, escreva-a como resposta, em vez de tentar provar que as pessoas estão erradas. Eu já passei por esse caminho muitas vezes com pessoas como você, então pare com esses comentários como este. Isso é tudo o que tenho a dizer.
Terrance
3
@CharlesDuffy no Ask Ubuntu, às vezes escrevemos respostas que não são transportáveis ​​para outras distribuições Linux. Como seu representante aqui é de apenas 103 pontos, você pode não ter percebido isso. O seu representante no Stack Overflow, no entanto, é de 123,3K pontos, então você obviamente conhece muitas coisas sobre os sistemas * NIX em geral. Acho que você, Terrace e Serg estão bem, mas estão olhando o fio de diferentes ângulos. Repito (sem trocadilhos) o sentimento de que você escreva uma resposta que seja * NIX portátil, ou pelo menos portátil nas distribuições Linux.
WinEunuuchs2Unix
2
@CharlesDuffy Embora eu concorde com as suas respostas de opinião, elas devem ser portáteis, afinal este é o site orientado ao Ubuntu, portanto, as ferramentas específicas do Ubuntu devem ser assumidas pelos usuários. Portanto, o aviso está um pouco implícito. Não vamos entrar em discussões excessivamente longas. Você está certo que outras conchas devem ser consideradas, e novatos lendo para aprender com as respostas também devem ser considerados. Um breve comentário provavelmente seria suficiente para expressar o argumento.
22817 Sergiy Kolodyazhnyy