Programmation défensive en bash

closeCet article a été publié il y a 3 ans 11 mois 1 jour, il est donc possible qu’il ne soit plus à jour. Les informations proposées sont donc peut-être expirées.

Nouvelle traduction aujourd’hui, d’un article qui risque sans prévenir de disparaître. La programmation défensive consiste à structurer son code pour limiter au strict minimum les surfaces d’attaques. Le JDN a un article qui résume bien des concepts, mais ici, on va s’attarder sur les scripts Bash. En effet, comme avec PHP on peut vite faire de la merde, et il est préférable de suivre certaines pratiques pour que leur qualité ne soit pas trop catastrophique quand il seront réutilisés ailleurs. Je ne les suis pas toutes, mais j’ai trouvé utile de les partager avec vous.

Variables globales immutables

  • Essayez de limiter le nombre de variables globales
  • Nommage en majuscules
  • déclarations en lecture seule
  • Utilisez des variables globales pour remplacer les cryptiques $1, $0, etc
  • Les globales que j’utilise pratiquement systématiquement dans mes scripts :

Tout est local

Toutes les variables devraient être locales.

  • des paramètres « auto-documentés »
  • Habituellement pour les boucles ont utilise « i », il est vital de la déclarer comme locale
  • les variables locales ne fonctionnent pas dans un contexte global :

main()

  • Permet de conserver toutes les variables comme locales
  • Intuitif pour la programmation fonctionnelle
  • La seule commande globale dans le code est : main

Tout est une fonction

  • Le seul code qui tourne globalement est
    • Les déclarations globales qui sont immutables
    • main
  • Permet de garder un code propre
  • Les procédures sont plus descriptives

  • Le deuxième exemple est bien meilleur. Rechercher des fichiers est le problème de temporary_files() et pas celui de main(). Le code est également mieux « testable », via un test unitaire sur temporary_files().
  • Si vous testez la première version, vous mélangez la logique de recherche du reste de main.

Comme vous le voyez, ce test ne concerne pas main().

Des fonctions de debug

  • Lancez le programme avec le drapeau -x :

  • Débuggez une portion du code en utilisant set -x et set +x, ce qui va afficher les messages de débug pour le code entouré des commandes.

  • Affichez le nom de la fonction et ses arguments :

Donc, en appelant la fonction :

affichera sur la sortie standard :

Clarté du code

Qu’est-ce que fait ce code ?

Laissez votre code parler :

Chaque ligne fait une seule chose

  • Sectionnez vos commandes avec des antislash . Par exemple :

Peut être écrit plus clairement :

  • Les symboles doivent être en début de ligne. Mauvais exemple de symboles à la fin :

Bon exemple où l’on voit clairement la connexion entre les les lignes et les symboles :

Afficher l’usage

Ne faites pas ça :

Ça devrait être une fonction :

echo est répété à chaque ligne. A la place on a ‘Here Document’ :

Attention à bien utiliser des tabulations ‘\t’ pour le début de chaque ligne. Dans vim vous pouvez utiliser cette astuce si votre tabulation consiste en 4 espaces :

Arguments de ligne de commande

Voici un exemple de complément à la fonction usage d’au-dessus. Ce code est tiré de l’article bash shell script to use getopts with gnu style long positional parameters sur le blog de Kirk :

On l’utilise ensuite de cette façon en utilisant la variable immutable ARGS qu’on a défini au début du script :

Tests unitaires

  • Très important dans les langages de haut niveau
  • Utilisez shunit2 pour vos tests unitaires

Voici un autre exemple sur la commande df :

Ici il y a une exception, pour les tests je déclare DF au niveau global et pas en lecture seule. C’est parce que shunit2 n’autorise pas les changements de fonctions au niveau global.


Les plus aguerris au développement logiciel trouveront peut-être évidents ces constructions de scripts, pour ma part j’avais déjà lu certains de ces conseils, notamment pour Python. C’est sympa de voir qu’on peut réutiliser les mêmes concepts pour Bash, par défaut c’est un langage particulièrement permissif, à l’image de PHP. Il n’en faut pas plus pour que les deux se trainent une réputation désastreuse.

10 Commentaires
Le plus ancien
Le plus récent
Commentaires en ligne
Afficher tous les commentaires
Éric
Éric
27/04/2020 19:18

Merci pour ce beau recueil de bonnes pratiques !

Toto
Toto
28/04/2020 09:15

Salut,

Bizarre, il manque quelques règles de protection. $ARGS est un tableau. Il faut protéger les valeurs avec ${ARGS[@]}, non ?

Un avis sur shellcheck ? À mon avis la référence dans l’analyze statique de code shell.

Super article au demeurant ! Merci !!

sebt3
sebt3
06/05/2020 18:02
Répondre à  Toto

Shellcheck, c’est que du bonheur surtout qu’il peut générer des rapports intégrable très facilement à un bon CI, mais tu vas finir par être obligé de mettre des « # shellcheck disable= » partout 😛
Pis, shunit, on en parle ? 😛

Sky
Sky
28/04/2020 16:16

Merci de l’article.
En plus de la majorité de ces pratiques j’utilise également cet entête :

Issu de cet excellent article : http://redsymbol.net/articles/unofficial-bash-strict-mode/

Thibault
Thibault
29/04/2020 11:22

Super intéressant cet article, dans mes favoris sur Bash. La partie sur l’usage est maintenant une évidence, alors que je tourne sur du echo dans tous mes scripts :facepalm

Da Scritch
04/05/2020 18:33

Excellent article, avec plein de choses auxquelles je n’aurais pas pensé comme les mot clés « local » et « readonly ». J’aurais juste ajouté le « set -e » qui est vraiment indispensable, car ilvaut mieux un script correctement planté que des opérations qui marchotent dangereusement.

Par contre, pour parser la ligne de commande ;, j’ai écarté getopts, peu lisible au final et surtout pas toujours présent sur les busybox.

Ma petite recette (désolé pour l’autopromo), expliquée ici https://dascritch.net/post/2018/01/08/En-20-lignes-pas-plus-%3A-g%C3%A9rer-les-options-d-appel-de-tes-scripts-bash

Ban
Ban
04/05/2020 21:52

Intéressant, mais il manque ce qui me semble le plus important pour toute idée de défensivité en BaSH : le quoting. Il faut grosso modo quoter *toutes* les variables, sauf les cas particuliers où on veut l’expansion. Par exemple, le snippet tout simple que tu utilises pour obtenir PROGNAME a un comportement que tu n’attends probablement pas si $0 contient des espaces : # avec un script "/tmp/foo bar.bash" : $ bash foo\ bar.bash foo # avec un script "/tmp/a b c d.bash" : $ bash a\ b\ c\ d.bash basename: extra operand ‘c’ Try 'basename --help' for more information. # ouch. Tout ça… Lire la suite »

Mirabellette
Mirabellette
06/05/2020 16:53

Un super article et des commentaires tout aussi pertinent, merci beaucoup !

sebt3
sebt3
06/05/2020 17:59

Salut,
Super article. Quelques propositions :
– Pour bien marquer le passage des arguments, l’appel a main se fait avec :
main « $@ »
– Tes fonctions de tests aident énormément à la lisibilité, mais pour plus de concision, perso, je les écris comme :
is_empty() { [[ -z $1 ]] }
– Connais-tu https://github.com/qzb/is.sh ?

NicK
NicK
19/05/2020 22:23

Super article mais j’ai jamais vu des scripts bash écrits ainsi.
En général si tu arrives aux scripts bash c’est que tu es déjà dans le serveur/vM /whatthefuck et que l’attaquant est déjà utilisateur avec pas mal de privilèges.