web-dev-qa-db-fra.com

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

Selon cette question " tilisation de" while read ... "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 echo par printf

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 différencie ces deux commandes? Merci ~

14
user3094631

Ce n'est pas juste echo vs printf

Commençons par comprendre ce qui se passe avec la partie read a b c. read effectuera le fractionnement des mots en fonction de la valeur par défaut de la variable IFS, qui est space-tab-newline, et s'adaptera à tout en fonction de cela. S'il y a plus d'entrées que de variables à retenir, les pièces fractionnées seront incluses dans les premières variables, et ce qui ne peut pas être ajusté - entrera 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 ce qui est décrit dans le manuel de bash (voir la citation à la fin de la réponse).

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

Ce que vous verrez également très souvent, c’est que les gens utilisent while IFS= read -r line; do ... ; done < input.txt pour lire les fichiers texte ligne par ligne. Encore une fois, IFS= est ici pour une raison de contrôler le fractionnement de Word, ou plus précisément - de le désactiver, et de lire une seule ligne de texte dans une variable. Si ce n'était pas là, read essaierait de faire correspondre chaque mot à la variable line. Mais c’est une autre histoire que je vous encourage à étudier plus tard, car while IFS= read -r variable est une structure très fréquemment utilisée.

comportement echo vs printf

echo fait ce que vous attendez ici. Il affiche vos variables exactement comme read les a organisées. Cela a déjà été démontré lors de discussions précédentes.

printf est très spécial, car il continuera d'ajuster les variables dans la chaîne de format jusqu'à ce qu'elles soient toutes épuisées. Donc, quand vous faites printf "%d, %d, %d \n" $a $b $c printf voit une chaîne de format avec 3 décimales, mais il y a plus d'arguments que 3 (car vos variables se développent en réalité à des valeurs individuelles 1, 2, 3, 4, 5, 6). Cela peut paraître déroutant, mais existe pour une raison en tant que comportement amélioré par rapport à ce que la fonction réelle printf() utilise en langage C.

Ce que vous avez également fait ici qui affecte la sortie est que vos variables ne sont pas citées, ce qui permet au shell (et non à printf) de ventiler les variables en 6 éléments distincts. Comparez cela avec la citation:

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 variable $c est citée, elle est maintenant reconnue comme une chaîne entière, 3 4, et elle ne correspond pas au format %d, qui n’est qu’un entier.

Maintenant, faites 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 à nouveau: "OK, vous avez 6 éléments mais le format n'en affiche que 3, je vais donc continuer à ajuster les éléments et laisser en blanc tout ce que je ne peux pas mettre en correspondance avec la saisie réelle de l'utilisateur".

Et dans tous ces cas, vous n'êtes pas obligé de prendre ma parole pour cela. Il suffit de lancer strace -e trace=execve et de constater par vous-même ce que la commande "voit" en réalité:

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 bien souligné dans les commentaires, bash possède sa propre printf intégrée, qui correspond à ce que vous utilisez dans votre commande. strace appellera en fait la version /usr/bin/printf, et non la version de 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 identique.

Il convient également de garder à l’esprit que la syntaxe printf est bien plus portable (et donc préférable) que echo, sans compter que la syntaxe est plus familière pour C ou tout langage de type C possédant la fonction printf(). Voir ceci excellente réponse de terdon au sujet de printf vs echo. Bien que vous puissiez adapter la sortie à votre Shell spécifique pour votre version spécifique d’Ubuntu, si vous allez porter des scripts sur différents systèmes, vous devriez probablement préférer printf à l’écran. Vous êtes peut-être un administrateur système débutant travaillant avec des machines Ubuntu et CentOS, ou peut-être même FreeBSD - qui sait - alors vous devrez faire des choix dans de tels cas.

Extrait du manuel bash, section Shell BUILTIN COMMANDS

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

Une ligne est lue à partir de l'entrée standard ou du descripteur de fichier fd fourni en tant qu'argument de l'option -u, et le premier mot est attribué au premier nom, le second mot au deuxième nom, etc., avec les restants. mots et leurs séparateurs intervenant dans le nom de famille. S'il y a moins de mots lus dans le flux d'entrée que de noms, les noms restants se voient attribuer 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 dans Fractionnement de mots).

24
Sergiy Kolodyazhnyy

Ceci est seulement une suggestion et ne vise pas à remplacer la réponse de Sergiy. Je pense que Sergiy a écrit une excellente réponse sur la raison pour laquelle ils sont différents en matière d’impression. Comment la variable sur la lecture est affectée avec le reste dans la variable $c en tant que 3 4 5 6 après 1 et 2 sont attribués à a et b. echo ne divisera pas la variable pour vous, alors que printf le sera avec le %ds.

Cependant, vous pouvez leur demander de vous donner les mêmes réponses en manipulant l'écho des nombres au début de la commande:

Dans /bin/bash, vous pouvez utiliser:

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

Dans /bin/sh, vous pouvez simplement utiliser:

echo "1 2 3 \n4 5 6"

Bash utilise le -e pour activer les caractères\escape lorsque sh n’en a pas besoin car il est déjà activé. \n provoque la création d'une nouvelle ligne. La ligne echo est maintenant divisée en deux lignes distinctes qui peuvent désormais être utilisées deux fois pour votre déclaration echo loop:

:~$ 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 avec la commande 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 

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!

7
Terrance