Oui, nous voyons un certain nombre de choses comme:
while read line; do
echo $line | cut -c3
done
Ou pire:
for line in `cat file`; do
foo=`echo $line | awk '{print $2}'`
echo whatever $foo
done
(ne rigole pas, j'en ai vu beaucoup).
Généralement des débutants en scripts shell. Ce sont des traductions littérales naïves de ce que vous feriez dans des langages impératifs tels que C ou python, mais ce n'est pas ainsi que vous faites les choses dans les shells, et ces exemples sont très inefficaces, complètement peu fiables (pouvant potentiellement poser des problèmes de sécurité), et si vous réussissez un jour pour corriger la plupart des bugs, votre code devient illisible.
Conceptuellement
En C ou dans la plupart des autres langues, les blocs de construction ne représentent qu'un niveau au-dessus des instructions de l'ordinateur. Vous dites à votre processeur quoi faire et ensuite quoi faire. Vous prenez votre processeur par la main et vous le gérez: vous ouvrez ce fichier, vous lisez autant d'octets, vous faites ceci, vous le faites avec cela.
Les coquillages sont une langue de niveau supérieur. On peut dire que ce n'est même pas une langue. Ils sont avant tous les interprètes en ligne de commande. Le travail est effectué à l'aide des commandes que vous exécutez et le shell n'a pour but que de les orchestrer.
Un des grands avantages d'Unix est le pipe et les flux stdin / stdout / stderr par défaut que toutes les commandes gèrent par défaut.
En 45 ans, nous n'avons pas trouvé meilleur que cette API pour exploiter la puissance des commandes et les faire coopérer à une tâche. C'est probablement la raison principale pour laquelle les gens utilisent encore des coquilles aujourd'hui.
Vous avez un outil de coupe et un outil de translittération, et vous pouvez simplement faire:
cut -c4-5 < in | tr a b > out
Le shell se contente de faire la plomberie (ouvrir les fichiers, configurer les tuyaux, invoquer les commandes) et quand tout est prêt, il coule sans que le shell ne fasse rien. Les outils font leur travail simultanément, efficacement, à leur propre rythme, avec suffisamment de mémoire tampon pour qu’aucun ne bloque l’autre, il est tout simplement magnifique et pourtant si simple.
Invoquer un outil a cependant un coût (et nous le développerons sur le point de performance). Ces outils peuvent être écrits avec des milliers d’instructions en C. Un processus doit être créé, l’outil doit être chargé, initialisé, puis nettoyé, le processus détruit et attendu.
Invoquer, cut
c'est comme ouvrir le tiroir de la cuisine, prendre le couteau, l'utiliser, le laver, le sécher, le remettre dans le tiroir. Quand tu fais:
while read line; do
echo $line | cut -c3
done < file
C'est comme pour chaque ligne du fichier, extraire l' read
outil du tiroir de la cuisine (très maladroit parce qu'il n'a pas été conçu pour cela ), lire une ligne, laver votre outil de lecture, le remettre dans le tiroir. Planifiez ensuite une réunion pour l' outil echo
et cut
, sortez-les du tiroir, appelez-les, nettoyez-les, séchez-les, remettez-les dans le tiroir, etc.
Certains de ces outils ( read
et echo
) sont construits dans la plupart des coques, mais cela ne fait guère de différence ici echo
et cut
doit encore être exécuté dans des processus séparés.
C'est comme couper un oignon mais laver son couteau et le remettre dans le tiroir de la cuisine entre chaque tranche.
Ici, le moyen le plus évident consiste à sortir votre cut
outil du tiroir, à trancher tout votre oignon et à le remettre dans le tiroir une fois le travail terminé.
IOW, dans les shells, en particulier pour traiter du texte, vous appelez le moins d’utilitaires possible et les faites coopérer à la tâche. Vous n’exécutez pas des milliers d’outils en ordre en attendant que chacun d’entre eux démarre, nettoient avant d’exécuter le suivant.
Pour en savoir plus, lisez bien Bruce . Les outils internes de traitement de texte de bas niveau dans les shells (à l'exception peut-être de zsh
) sont limités, encombrants et ne conviennent généralement pas au traitement de texte général.
Performance
Comme indiqué précédemment, l'exécution d'une commande a un coût. Un coût énorme si cette commande n'est pas intégrée, mais même si elles sont intégrées, le coût est élevé.
Et les shells n’ont pas été conçus pour fonctionner comme ça, ils ne prétendent pas être des langages de programmation performants. Ils ne sont pas, ils sont juste des interprètes en ligne de commande. Donc, peu d'optimisation a été faite sur ce front.
En outre, les shells exécutent des commandes dans des processus distincts. Ces blocs de construction ne partagent pas une mémoire ou un état commun. Quand vous faites un fgets()
ou fputs()
en C, c'est une fonction dans stdio. stdio conserve les mémoires tampons internes d'entrée et de sortie pour toutes les fonctions stdio, afin d'éviter de faire des appels système coûteux trop souvent.
Les utilitaires shell même BUILTIN correspondant ( read
, echo
, printf
) ne peuvent pas le faire. read
est destiné à lire une ligne. S'il lit après le caractère de nouvelle ligne, cela signifie que la prochaine commande que vous exécuterez le manquera. Il read
faut donc lire l’entrée un octet à la fois (certaines implémentations ont une optimisation si l’entrée est un fichier normal dans la mesure où elles lisent des morceaux et recherchent en arrière, mais cela ne fonctionne que pour des fichiers normaux et bash
ne lit par exemple que des morceaux de 128 octets, c’est-à-dire encore beaucoup moins que les utilitaires de texte feront).
Idem côté sortie, echo
ne peut pas simplement mettre sa sortie en mémoire tampon, il doit la sortir immédiatement car la prochaine commande que vous exécuterez ne partagera pas cette mémoire tampon.
Évidemment, exécuter les commandes de manière séquentielle signifie que vous devez les attendre, c'est une petite danse de planificateur qui donne le contrôle depuis le shell aux outils. Cela signifie également (par opposition à l'utilisation d'instances longues dans un pipeline) que vous ne pouvez pas exploiter plusieurs processeurs en même temps lorsqu'ils sont disponibles.
Entre cette while read
boucle et l'équivalent (supposé) cut -c3 < file
, dans mon test rapide, il y a un rapport de temps de processeur d'environ 40000 lors de mes tests (une seconde par rapport à une demi-journée). Mais même si vous utilisez uniquement des commandes intégrées au shell:
while read line; do
echo ${line:2:1}
done
(ici avec bash
), cela reste autour de 1: 600 (une seconde contre 10 minutes).
Fiabilité / lisibilité
Il est très difficile d'obtenir ce code correctement. Les exemples que j'ai donnés sont trop souvent vus à l'état sauvage, mais ils ont beaucoup d'insectes.
read
est un outil pratique qui peut faire beaucoup de choses différentes. Il peut lire les entrées de l'utilisateur, les diviser en mots pour les stocker dans différentes variables. read line
ne lit pas une ligne d'entrée, ou peut-être lit-il une ligne d'une manière très spéciale. En fait, il lit les mots de l'entrée, ces mots séparés par une $IFS
barre oblique inversée pouvant être utilisée pour échapper aux séparateurs ou au caractère de nouvelle ligne.
Avec la valeur par défaut de $IFS
, sur une entrée comme:
foo\/bar \
baz
biz
read line
va stocker "foo/bar baz"
dans $line
, pas " foo\/bar \"
comme vous le souhaitiez.
Pour lire une ligne, vous avez besoin de:
IFS= read -r line
Ce n’est pas très intuitif, mais c’est comme ça, souvenez-vous que les coquillages ne devaient pas être utilisés de la sorte.
Pareil pour echo
. echo
étend les séquences. Vous ne pouvez pas l'utiliser pour des contenus arbitraires comme le contenu d'un fichier aléatoire. Vous avez besoin printf
ici à la place.
Et bien sûr, il y a l' oubli typique de citer votre variable dans laquelle tout le monde tombe. Donc c'est plus:
while IFS= read -r line; do
printf '%s\n' "$line" | cut -c3
done < file
Maintenant, quelques mises en garde supplémentaires:
- sauf que
zsh
, cela ne fonctionne pas si l'entrée contient des caractères NUL alors qu'au moins les utilitaires de texte GNU n'auraient pas le problème.
- s'il y a des données après la dernière nouvelle ligne, elles seront ignorées
- Dans la boucle, stdin est redirigé. Vous devez donc faire attention à ce que ses commandes ne lisent pas à partir de stdin.
- pour les commandes dans les boucles, nous ne faisons pas attention à leur succès ou non. Habituellement, les conditions d'erreur (disque plein, erreurs de lecture, etc.) seront mal gérées, généralement plus mal qu'avec le bon équivalent.
Si nous voulons aborder certaines de ces questions ci-dessus, cela devient:
while IFS= read -r line <&3; do
{
printf '%s\n' "$line" | cut -c3 || exit
} 3<&-
done 3< file
if [ -n "$line" ]; then
printf '%s' "$line" | cut -c3 || exit
fi
Cela devient de moins en moins lisible.
La transmission de données à des commandes via les arguments ou l'extraction de leur sortie dans des variables soulève un certain nombre d'autres problèmes:
- la limitation de la taille des arguments (certaines implémentations d'utilitaires de texte ont également une limite, même si l'effet de celles qui sont atteintes est généralement moins problématique)
- le caractère NUL (également un problème avec les utilitaires de texte).
- arguments pris comme options quand ils commencent par
-
(ou +
parfois)
- diverses bizarreries de diverses commandes généralement utilisées dans ces boucles comme
expr
, test
...
- les opérateurs (limités) de manipulation de texte de divers shells qui gèrent des caractères multi-octets de manière incohérente.
- ...
Considérations de sécurité
Lorsque vous commencez à utiliser des variables shell et des arguments de commandes , vous entrez dans un champ de mines.
Si vous oubliez de citer vos variables , oubliez le marqueur de fin d'option , travaillez dans des environnements locaux avec des caractères multi-octets (la norme de nos jours), vous êtes certain de présenter des bogues qui deviendront tôt ou tard des vulnérabilités.
Quand vous voudrez peut-être utiliser des boucles.
À déterminer
yes
écrire dans un fichier si rapidement?