Maîtrise des commandes externes : optimiser les performances des scripts Bash

Découvrez des gains de performance cachés dans vos scripts Bash en maîtrisant l'utilisation des commandes externes. Ce guide explique le surcoût important causé par le lancement répété de processus comme `grep` ou `sed`. Apprenez des techniques pratiques et actionnables pour remplacer les appels externes par des fonctions intégrées efficaces de Bash, effectuer des opérations par lots avec des utilitaires puissants et optimiser les boucles de lecture de fichiers afin de réduire considérablement le temps d'exécution dans les tâches d'automatisation à haut débit.

Maîtrise des commandes externes : optimiser les performances des scripts Bash

Le script Bash le plus rapide est souvent celui qui lance le moins de programmes.

Bash est bon pour le travail de collage : lire un fichier, décider quoi faire, lancer un autre outil, vérifier le code de sortie, et passer à la suite. Ce n'est pas un langage de traitement de données haute performance. Le piège est d'utiliser Bash comme si chaque petite opération sur une chaîne nécessitait sed, chaque comparaison expr, et chaque boucle sur des fichiers un nouveau grep. Ce style fonctionne sur dix lignes. Il devient douloureux sur 200 000 lignes.

Le coût est le démarrage des processus. Quand un script exécute grep, sed, awk, cut, tr, date, ou basename, le shell doit créer un autre processus et attendre qu'il se termine. Un appel n'est pas un problème. Un appel à l'intérieur d'une grande boucle est un modèle qui mérite d'être corrigé.

Commencez par rechercher les commandes à l'intérieur des boucles :

grep -nE 'for |while ' script.sh
grep -nE 'grep|sed|awk|cut|tr|expr|basename|dirname|cat' script.sh

Cela ne signifie pas que chaque correspondance est mauvaise. Un seul awk sur un fichier entier est généralement acceptable. Un sed lancé une fois par ligne est le genre de chose qui transforme un script de maintenance en une panne mystérieuse lors d'un déploiement.

Remplacer les petits appels externes par Bash lui-même

Les gains les plus faciles concernent l'arithmétique, la longueur des chaînes, les préfixes, les suffixes et les substitutions simples. Bash sait déjà faire tout cela.

Arithmétique externe :

# Utilise l'utilitaire externe 'expr'
RESULTAT=$(expr $A + $B)

Arithmétique intégrée :

RESULTAT=$((A + B))

Substitution de chaîne externe :

MA_CHAINE="bonjour le monde"
NOUVELLE_CHAINE=$(echo "$MA_CHAINE" | sed 's/monde/univers/')

Expansion de paramètre :

MA_CHAINE="bonjour le monde"
NOUVELLE_CHAINE=${MA_CHAINE/monde/univers}
printf '%s\n' "$NOUVELLE_CHAINE"
Tâche Méthode inefficace (externe) Méthode efficace (intégrée)
Extraction de sous-chaîne `echo "$STR" cut -c 1-5`
Vérification de longueur expr length "$STR" ${#STR}
Supprimer le suffixe basename "$fichier" .log ${fichier%.log}
Supprimer le chemin basename "$chemin" ${chemin##*/}
Supprimer le nom de fichier dirname "$chemin" ${chemin%/*}
Remplacer la première occurrence sed 's/foo/bar/' ${valeur/foo/bar}
Remplacer toutes les occurrences sed 's/foo/bar/g' ${valeur//foo/bar}

Préférez [[ ... ]] pour les conditionnels Bash. C'est un mot-clé du shell, il gère proprement la correspondance de motifs et évite certaines surprises de guillemets qui apparaissent avec [ ... ].

if [[ $nom == *.log && -s $nom ]]; then
  printf 'fichier journal non vide : %s\n' "$nom"
fi

Ne forcez pas trop cette approche. Le remplacement de motifs Bash n'est pas un moteur regex complet. Si la règle est vraiment complexe, un seul passage awk ou perl est plus propre et généralement plus rapide qu'une expansion de shell astucieuse.

Effectuer des opérations par lots plutôt que de répéter le travail

Si un outil peut traiter plusieurs entrées en une seule exécution, fournissez-lui plusieurs entrées. Cela est particulièrement important pour grep, awk, sed, find, les outils de compression, les clients de téléchargement, et tout ce qui se connecte à un service réseau.

Cette boucle lance un grep par fichier :

for fichier in *.log; do
  grep "ERREUR" "$fichier" > "${fichier}.erreurs"
done

Si vous n'avez besoin que d'un seul résultat combiné, utilisez un seul grep :

grep "ERREUR" *.log > toutes_erreurs.txt

Si vous avez besoin d'une sortie par fichier, demandez-vous si la séparation est vraiment nécessaire. Parfois, l'outil en aval peut lire un préfixe de nom de fichier à partir de grep -H :

grep -H "ERREUR" *.log > erreurs-avec-noms.txt

Pour les transformations orientées lignes, simplifiez les chaînes grep | awk en un seul programme awk :

awk '/donnees/ {print $1}' entree.txt | sort > sortie.txt

Cela exécute toujours sort, et c'est très bien. Le tri est exactement le genre de travail qu'un outil externe devrait faire. Le changement utile est de supprimer le cat inutile et le grep séparé.

Lire les fichiers sans cat

La boucle standard de lecture de ligne est ennuyeuse pour une bonne raison :

while IFS= read -r ligne; do
  printf 'Traitement : %s\n' "$ligne"
done < fichier.txt

IFS= préserve les espaces de début et de fin. -r empêche read de traiter les antislashs comme des échappements. La redirection maintient la boucle dans le shell actuel, ce qui est important si la boucle met à jour des variables dont vous avez besoin plus tard.

Cette version semble inoffensive mais est généralement pire :

cat fichier.txt | while read -r ligne; do
  compteur=$((compteur + 1))
done
printf '%s\n' "$compteur"

Dans Bash, un segment de pipeline s'exécute généralement dans un sous-shell, donc compteur peut ne pas être mis à jour dans le shell parent. De plus, cela lance cat sans aucun bénéfice.

Utilisez la substitution de processus lorsque l'entrée est réellement produite par une commande :

while IFS= read -r fichier; do
  printf 'gros fichier : %s\n' "$fichier"
done < <(find /var/log -type f -size +100M)

Ici, find fait un vrai travail. Garder la boucle dans le shell actuel reste utile.

Utiliser find -exec ... + et xargs avec précaution

Les boucles sur les fichiers sont une source courante de lenteur accidentelle :

for fichier in $(find . -name '*.tmp'); do
  rm "$fichier"
done

Cela échoue avec les espaces et lance rm de manière répétée. Utilisez l'exécution par lots :

find . -name '*.tmp' -exec rm -f {} +

La forme + passe plusieurs chemins à chaque invocation de rm. La forme plus ancienne \; exécute la commande une fois par chemin.

Pour les commandes qui bénéficient de la concurrence, xargs -P peut réduire le temps réel :

xargs -n 1 -P 4 curl -fsS -O < urls.txt

Utilisez -0 lorsque des noms de fichiers sont impliqués :

find uploads -type f -print0 | xargs -0 -n 50 -P 4 ./traiter-fichier

Le parallélisme n'est pas gratuit. Quatre tâches curl peuvent être plus rapides qu'une. Quarante peuvent vous faire limiter par une API ou saturer une petite machine.

Mesurez avant de tout réécrire

La bonne optimisation dépend de là où le temps est passé. Utilisez d'abord un chronométrage simple :

time ./script.sh

Pour les scripts qui utilisent beaucoup de processus, strace -c sous Linux peut montrer si le script passe son temps à créer des processus, ouvrir des fichiers ou attendre des entrées/sorties :

strace -f -c ./script.sh

Le traçage du shell peut révéler des commandes répétées :

PS4='+ $SECONDS ${BASH_SOURCE}:${LINENO}: '
bash -x ./script.sh

Si le script passe 95 % de son temps à attendre une exportation de base de données, remplacer ${valeur/foo/bar} n'aura pas d'importance. S'il exécute sed 300 000 fois, cela en aura.

Sachez quand les outils externes sont meilleurs

Objectif Meilleur outil (généralement) Remarques
Extraction et filtrage de champs awk Meilleur que les boucles Bash pour le texte tabulaire.
Édition de flux sed Bon pour un seul passage sur un fichier.
Parcours de fichiers find Plus sûr que d'analyser ls.
JSON jq N'analysez pas JSON avec cut.
Tâches parallèles xargs -P ou GNU parallel Ajoutez des limites et gérez les échecs.
Traitement de gros textes awk, perl, Python Souvent plus clair qu'un Bash héroïque.

Les fonctions intégrées de Bash sont rapides, mais la maintenabilité l'emporte toujours. Je préfère maintenir un script awk clair que 40 lignes d'expansion de paramètres fragile que seul l'auteur original comprend.

Une liste de vérification pratique pour la révision

Quand un script Bash semble lent, parcourez-le dans cet ordre :

  1. Trouvez les commandes externes à l'intérieur des boucles.
  2. Remplacez les opérations arithmétiques et sur les chaînes simples par l'expansion Bash.
  3. Supprimez les appels inutiles à cat.
  4. Regroupez les arguments de fichiers avec grep, awk, sed, find -exec ... +, ou xargs.
  5. Gardez les boucles de lecture de lignes dans le shell actuel lorsque les variables doivent survivre à la boucle.
  6. Mesurez à nouveau.

Vous n'avez pas besoin de transformer chaque script en exercice de benchmark. Les gros gains viennent généralement de quelques endroits évidents : une commande par ligne, une commande par fichier, ou une commande par élément d'API. Corrigez ceux-ci, gardez le script lisible, et arrêtez-vous lorsque le temps d'exécution n'est plus un problème.