Pourquoi ne pas utiliser les backticks avec la boucle for

Il y a quelque time, j'ai posté une réponse à une question sur les scripts. Quelqu'un a fait remarquer que je ne devrais pas utiliser la command suivante:

for x in $(cat file); do something; done 

mais au lieu de cela:

 while read f; do something; done < file 

L' article Usless Use of Cat suppose d'expliquer tout le problème, mais la seule explication est:

Les backticks sont carrément dangereux, sauf si vous savez que le résultat des backticks sera inférieur ou égal à la durée de la command line que votre shell peut accepter. (En fait, il s'agit d'une limitation du kernel.La constante ARG_MAX dans votre limits.h devrait vous dire combien votre propre système peut prendre. POSIX exige que ARG_MAX soit au less 4 096 octets.)

Si j'ai bien compris cela, bash (?) Devrait planter si j'utilise la sortie du très gros file dans la command (il devrait dépasser ARG_MAX dans le file limits.h). J'ai donc vérifié ARG_MAX avec la command:

 > grep ARG_MAX /usr/src/kernels/$(uname -r)/include/uapi/linux/limits.h #define ARG_MAX 131072 /* # bytes of args + environ for exec() */ 

Ensuite, j'ai créé un file contenant du text sans espaces:

 > ls -l -rw-r--r--. 1 root root 100000000 Aug 21 15:37 in_file 

Puis je cours:

 for i in $(cat in_file); do echo $i; done 

aaaand rien de terrible n'est arrivé.

Alors qu'est-ce que je devrais faire pour vérifier si / comment tout ce 'ne pas utiliser chat avec boucle' chose est dangereux?

Cela dépend de ce que le file est censé contenir. S'il est destiné à contenir une list de shell globs séparés par IFS comme (en supposant la valeur par défaut de $IFS ):

 /var/log/*.log /var/adm/*~ /some/dir/*.txt 

Alors for i in $(cat file) serait la voie à suivre. Comme c'est ce que fait le $(cat file) guillemets: appliquez l'opérateur split + glob sur la sortie du cat file dépouillé de ses caractères de fin de ligne. Donc, il serait boucle sur chaque nom de file résultant des expansions de ces globs (sauf dans les cas où les globs ne correspondent à aucun file où cela laisserait le glob là mais non expansé).

Si vous vouliez parcourir chaque ligne de file délimitée, vous feriez:

 while IFS= read -r line <&3; do { something with "$line" } 3<&- done 3< file 

Avec une boucle for , vous pouvez parcourir chaque ligne non vide avec:

 IFS = '
 '# split sur newline seulement (en fait des séquences de nouvelles lignes et
   # ignorant les principaux et les suivants car newline est un
   # Caractère d'espacement IFS )
 set -o noglob # désactive la partie glob de l'opérateur split + glob:
 pour la ligne dans $ (file cat);  faire
    quelque chose avec "$ line"
 terminé

Cependant un:

 while read line; do something with "$line" done < file 

Ça n'a pas beaucoup de sens. C'est lire le contenu du file d'une manière très compliquée où les caractères de $IFS et les barres obliques inverses sont traités spécialement.

Dans tous les cas, le text ARG_MAX limite le text que vous citez se trouve sur l'appel système execve() (sur la taille cumulée des arguments et des variables d'environnement), donc s'applique uniquement aux cas où une command du système de files est en cours d'exécution avec l'extension éventuellement très longue de l'opérateur split + glob appliqué à la substitution de command (ce text est trompeur et erroné sur plusieurs counts).

Cela s'appliquerait par exemple dans:

 cat -- $(cat file) # with shell implementations where cat is not builtin 

Mais pas dans:

 for i in $(cat file) 

où il n'y a pas d'appel système execve() impliqué.

Comparer:

 bash-4.4$ echo '/*/*/*/*' > file bash-4.4$ true $(cat file) bash-4.4$ n=0; for f in $(cat file); do ((n++)); done; echo "$n" 523696 bash-4.4$ /bin/true $(cat file) bash: /bin/true: Argument list too long 

C'est correct avec la true command embeddede de bash ou la boucle for , mais pas lors de l'exécution de /bin/true . Notez que le file n'a que 9 octets de large mais que l'extension de $(cat file) est de plusieurs mégaoctets car le $(cat file) /*/*/*/* glob est développé par le shell.

Plus de lecture à:

  • Comprendre "IFS = lire la ligne -r"?
  • Pourquoi boucler la mauvaise pratique de sortie de la découverte?
  • CP: nombre maximal de files source arguments pour l'utilitaire de copy
  • Implications sécuritaires de l'oubli de la citation d'une variable dans les shell bash / POSIX

@chepner a expliqué la différence dans les commentaires:

for i in $(cat in_file) ne for i in $(cat in_file) pas les lignes du file, il itère sur les mots résultant du contenu du file soumis à l'expansion des mots et des noms de files.

Pour l'impact sur les performances et l'utilisation des ressources, j'ai fait un petit benchmark pour les deux cas en utilisant des inputs avec des lignes 1M (environ 19M) et en mesurant le time et l'utilisation de la memory avec /usr/bin/time -v :

test1.sh:

 #!/bin/bash while read x do echo $x > /dev/null done < input 

Résultats:

 Command being timed: "./test1.sh" User time (seconds): 12.41 System time (seconds): 2.03 Percent of CPU this job got: 110% Elapsed (wall clock) time (h:mm:ss or m:ss): 0:13.07 Maximum resident set size (kbytes): 3088 

test2.sh:

 #!/bin/bash for x in $(cat input) do echo $x > /dev/null done 

Résultats:

 Command being timed: "./test2.sh" User time (seconds): 17.19 System time (seconds): 3.13 Percent of CPU this job got: 109% Elapsed (wall clock) time (h:mm:ss or m:ss): 0:18.51 Maximum resident set size (kbytes): 336356 

J'ai envoyé la sortie complète des deux tests à pastebin . Avec bash en utilisant for i in $(cat ...) utilise beaucoup plus de memory et fonctionne également plus lentement. Cependant, les résultats peuvent varier en fonction de l'exécution de ces mêmes tests sur un autre shell.

while loops peuvent être problématiques, notamment en ce sens qu'elles mangent l'input standard par défaut (donc ssh -n ) donc si vous avez besoin d'une input standard pour autre chose, une boucle while échouera

 $ find . -name "*.pm" | while read f; do aspell check $f; done $ 

ne fait rien car aspell veut un terminal qui est occupé par une list de noms de modules perl; une boucle for est plus appropriée (en supposant que les noms de files ne seront pas divisés par les règles de division de mots POSIX):

 $ for f in $(find . -name \*.pm); do aspell check $f; done ... 

comme cela n'utilise pas l'input standard comme while fait par défaut.

En outre, while est sujet à une perte de données silencieuse (et se comporte différemment pour cette même input):

 $ echo -n mmm silent data loss | while read line; do echo $line; done $ for i in $(echo -n mmm silent data loss); do echo $i; done mmm silent data loss $ 

Donc, les arguments peuvent être faits que while c'est dangereux et ne devrait pas être utilisé, selon le context.