En utilisant «while read…», echo et printf obtiennent des résultats différents

14

Selon cette question " Utilisation" pendant la lecture ... "dans un script Linux "

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

résultat:

1, 2, 3 4 5 6

mais quand je remplace echoparprintf

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

résultat

1, 2, 3
4, 5, 6

Quelqu'un pourrait-il me dire ce qui rend ces deux commandes différentes? Merci ~

user3094631
la source

Réponses:

24

Ce n'est pas seulement écho vs printf

Tout d'abord, comprenons ce qui se passe avec la read a b cpièce. readeffectuera la division des mots en fonction de la valeur par défaut de la IFSvariable qui est espace-tab-newline, et adaptera tout en fonction de cela. S'il y a plus d'entrée que les variables pour le contenir, il adaptera les pièces divisées aux premières variables, et ce qui ne peut pas être ajusté - ira en dernier. Voici ce que je veux dire:

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

C'est exactement comme cela qu'il est décrit dans bashle manuel de (voir la citation à la fin de la réponse).

Dans votre cas, ce qui se passe est que 1 et 2 correspondent aux variables a et b et c prend tout le reste, ce qui est 3 4 5 6 .

Ce que vous verrez aussi souvent, c'est que les gens utilisent while IFS= read -r line; do ... ; done < input.txtpour lire les fichiers texte ligne par ligne. Encore une fois, IFS=est là pour une raison de contrôler le fractionnement de mots, ou plus précisément - de le désactiver et de lire une seule ligne de texte dans une variable. S'il n'était pas là, readj'essaierais d'adapter chaque mot individuel en linevariable. Mais c'est une autre histoire, que je vous encourage à étudier plus tard, car while IFS= read -r variablec'est une structure très fréquemment utilisée.

écho vs comportement printf

echofait ce que vous attendez ici. Il affiche vos variables exactement comme les reada disposées. Cela a déjà été démontré dans la discussion précédente.

printfest très spécial, car il continuera à ajuster les variables dans la chaîne de format jusqu'à ce qu'elles soient toutes épuisées. Ainsi, lorsque vous printf "%d, %d, %d \n" $a $b $cimprimez, la chaîne de formatage comporte 3 décimales, mais il y a plus d'arguments que 3 (car vos variables se développent en fait jusqu'à 1,2,3,4,5,6). Cela peut sembler déroutant, mais existe pour une raison comme un comportement amélioré par rapport à ce que fait la fonction réelle printf() en langage C.

Ce que vous avez également fait ici qui affecte la sortie, c'est que vos variables ne sont pas citées, ce qui permet au shell (pas printf) de décomposer les variables en 6 éléments distincts. Comparez cela en citant:

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

Exactement parce que la $cvariable est citée, elle est maintenant reconnue comme une chaîne entière 3 4, et elle ne correspond pas au %dformat, qui est juste un seul entier

Faites maintenant la même chose sans citer:

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

printf dit encore: "OK, vous avez 6 éléments mais le format affiche seulement 3, donc je vais continuer à ajuster les trucs et laisser vide tout ce que je ne peux pas faire correspondre à l'entrée réelle de l'utilisateur".

Et dans tous ces cas, vous n'avez pas à me croire sur parole. Il suffit de courir strace -e trace=execveet de voir par vous-même ce que la commande "voit" réellement:

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

Notes complémentaires

Comme Charles Duffy l'a correctement souligné dans les commentaires, basha sa propre fonction intégrée printf, qui est ce que vous utilisez dans votre commande, straceappellera en fait la /usr/bin/printfversion, pas la version du shell. Mis à part des différences mineures, pour notre intérêt pour cette question particulière, les spécificateurs de format standard sont les mêmes et le comportement est le même.

Ce qui doit également être gardé à l'esprit, c'est que la printfsyntaxe est beaucoup plus portable (et donc préférée) que echo, sans compter que la syntaxe est plus familière au C ou à tout langage de type C qui a une printf()fonction. Voir cette excellente réponse par terdon au sujet de printfvs echo. Bien que vous puissiez faire la sortie adaptée à votre shell spécifique sur votre version spécifique d'Ubuntu, si vous allez porter des scripts sur différents systèmes, vous devriez probablement préférerprintf plutôt qu'écho. Peut-être êtes-vous un administrateur système débutant travaillant avec des machines Ubuntu et CentOS, ou peut-être même FreeBSD - qui sait - dans de tels cas, vous devrez faire des choix.

Citation du manuel bash, section SHELL BUILTIN COMMANDS

lire [-ers] [-a aname] [-d delim] [-i texte] [-n nchars] [-N nchars] [-p prompt] [-t timeout] [-u fd] [nom ... ]

Une ligne est lue à partir de l'entrée standard ou du descripteur de fichier fd fourni comme argument à l'option -u, et le premier mot est affecté au premier nom, le deuxième mot au deuxième nom, et ainsi de suite, avec le reste mots et leurs séparateurs intermédiaires affectés au nom de famille. S'il y a moins de mots lus dans le flux d'entrée que de noms, les noms restants reçoivent des valeurs vides. Les caractères dans IFS sont utilisés pour diviser la ligne en mots en utilisant les mêmes règles que le shell utilise pour l'expansion (décrites ci-dessus sous la division des mots).

Sergiy Kolodyazhnyy
la source
2
Une différence notable entre le stracecas et l'autre - strace printfutilise /usr/bin/printf, tandis que printfdirectement dans bash utilise le shell intégré du même nom. Ils ne seront pas toujours identiques - par exemple, l'instance bash a des spécificateurs de format %qet, dans les nouvelles versions, $()Tpour le formatage de l'heure.
Charles Duffy
7

Ce n'est qu'une suggestion et ne vise pas du tout à remplacer la réponse de Sergiy. Je pense que Sergiy a écrit une excellente réponse pour expliquer pourquoi ils sont différents dans l'impression. Comment la variable sur la lecture est affectée avec le reste dans la $cvariable comme 3 4 5 6après 1et 2sont affectées à aet b. echone divisera pas la variable pour vous où le printfsera avec le %ds.

Vous pouvez cependant leur faire en gros vous donner les mêmes réponses en manipulant l'écho des nombres au début de la commande:

Dans /bin/bashvous pouvez utiliser:

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

En /bin/shvous pouvez simplement utiliser:

echo "1 2 3 \n4 5 6"

Bash utilise le -e pour activer les caractères \ escape où sh n'en a pas besoin car il est déjà activé. \nle fait créer une nouvelle ligne, donc maintenant la ligne d'écho est divisée en deux lignes distinctes qui peuvent maintenant être utilisées deux fois pour votre instruction de boucle d'écho:

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

Ce qui à son tour produit la même sortie en utilisant la printfcommande:

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

Dans 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 

J'espère que cela t'aides!

Terrance
la source
echo -en'est pas seulement une extension de POSIX - mais tout ce echoqui n'émet pas -esur sa sortie étant donné que l'entrée défie / brise réellement la spécification des lettres noires. (bash n'est pas conforme par défaut, mais est conforme à la norme lorsque les deux posixet les xpg_echoindicateurs sont définis; ces derniers peuvent être définis par défaut au moment de la compilation, ce qui signifie que toutes les versions de bash ne fonctionneront pas echo -ecomme vous vous y attendez ici).
Charles Duffy
Voir les sections UTILISATION DE L' APPLICATION et JUSTIFICATION de la spécification POSIX pour echoà pubs.opengroup.org/onlinepubs/9699919799/utilities/echo.html , décrivant explicitement que echole comportement de ce n'est pas spécifié en présence d'un -nou de barres obliques inverses n'importe où dans l'entrée, et le conseillant printfêtre utilisé à la place.
Charles Duffy
@CharlesDuffy Ai-je déjà écrit dans le mien que je m'attendais à ce que cela fonctionne dans toutes les versions de bash? Et quel problème avez-vous avec ma réponse? Si vous sentez que vous avez une meilleure solution, écrivez-la comme réponse au lieu d'essayer de prouver que les gens ont tort. J'ai trop souvent traversé cette voie avec des gens comme vous, alors arrêtez-vous avec ces commentaires comme celui-ci. C'est tout ce que j'ai à dire.
Terrance
3
@CharlesDuffy dans Ask Ubuntu, nous écrivons parfois des réponses qui ne sont pas transportables vers d'autres distributions Linux. Comme votre représentant ici ne compte que 103 points, vous ne l'avez peut-être pas remarqué. Votre représentant sur Stack Overflow est cependant de 123,3 K points, donc vous connaissez évidemment une tonne de choses sur les systèmes * NIX en général. Je pense que vous, Terrace et Serg êtes d'accord, mais regardez le fil sous différents angles. Je fais écho (sans jeu de mots) au sentiment que vous avez écrit une réponse qui est * NIX portable, ou du moins portable sur les distributions Linux.
WinEunuuchs2Unix
2
@CharlesDuffy Bien que je sois d'accord avec votre sentiment, les réponses devraient être portables, c'est après tout un site orienté Ubuntu, donc les outils spécifiques à Ubuntu devraient être assumés par les utilisateurs. L'avertissement est donc quelque peu implicite. N'entrons pas dans des discussions trop longues. Vous avez raison de considérer d'autres coquilles, et les débutants qui lisent pour apprendre des réponses doivent également être pris en compte. Un bref commentaire serait probablement suffisant pour faire valoir ce point.
Sergiy Kolodyazhnyy