wiki:GnuLinuxAdminShell

Ce chapitre fait partie du cours d'aministration de GNU/Linux.

Programmation Shell

1. Environnement

Lorsque vous lancez un shell, un environnement spécifique est mise en place. Les fichiers de configuration permettant de définir cet environnement sont en général multiples et variés, dépendant du contexte (cf. login shell) ou du type de shell (csh, bash, etc). Par exemple au lancement d'un login shell Bash sous Debian, les fichiers suivants sont lus:

/etc/profile environnement par défaut pour les Bourne Shells
/etc/environment environnement global pour tous les processus (en général, la langue via LANG)
/etc/bash.bashrc environnement par défaut de Bash
~/.profile ce fichier est lu par tous les Bourne Shells dans le cas d'un login shell
~/.bashrc ce fichier est toujours lu au démarrage du shell Bash

A noter que PAM peut également interférer dans le cas d'un login shell. Suivant le mécanisme d'authentification (ssh, login, etc), le module pam_env peut être invoqué; consulter alors /etc/security/pam_env.conf.

Dans un compte utilisateur fraîchement créé, on peut par exemple éditer le fichier ~/.bashrc, et mettre à profit la coloration syntaxique de ls et la mise en place d'aliases populaires en retirant quelques commentaires :

export LS_OPTIONS='--color=auto'
eval `dircolors`
alias ls='ls $LS_OPTIONS'
alias ll='ls $LS_OPTIONS -l'
alias la='ls $LS_OPTIONS -la'

Si vous effectuez une modification de l'un de ces fichiers, elle ne sera prise en compte qu'au prochain démarrage du shell ou au prochain login. Vous pouvez forcer le rechargement immédiat de la plupart des fichiers de configuration (sauf quand l'enchaînement de ceux-ci est périlleux) avec la commande source qui exécute le fichier exactement comme si vous le tapiez dans le shell en cours. Ces deux lignes sont équivalentes:

$ source ~/.bashrc
$ . ~/.bashrc

Trucs et astuces Bash

  • L'historique des commandes est enregistré automatiquement dans le fichier pointé par la variable HISTFILE (par défaut ~/.bash_history). Il est pratique d'augmenter la capacité de stockage de l'historique afin d'avoir un enregistrement exhaustif de son travail, on peut modifier les valeurs de HISTSIZE et HISTFILESIZE par exemple dans son ~/.bashrc à cet effet (consulter man bash pour plus d'informations).
  • Vous ne pouvez pas éditer directement l'historique, car celui-ci est chargé en mémoire au lancement du shell, puis écrit en entier en fin de session. Toute manipulation depuis le shell est écrasée. A noter que si le fichier d'historique n'est pas défini (par ex: unset HISTFILE), votre session n'est pas enregistrée lorsque vous la quittez.
  • Le fichier ~/.bash_logout est executé quand on quitte le shell. Ceci peut etre mis à profit pour effacer la console avec la commande clear, et être sûr que personne ne puisse utiliser le défilement de l'historique après que vous ayez quitté la console.
  • Si vous mettez la ligne set bell-style none dans le fichier ~/.inputrc, les bips ou flashs d'écrans sont éliminés (ceci est lié à l'utilisation de la bibliothèque gettext).

2. Programmation

Exécution

Ecrire un programme shell revient à enregistrer une séquence de commandes shell dans un fichier. Tout ce qui sera écrit dans nos programme peut être testé directement en ligne de commande (y compris les constructions sur plusieurs lignes comme les if ... then... fi). Pour lancer un programme shell, les façons les plus classiques sont:

$ sh programme.sh
$ sh -x programme.sh

La deuxième invocation (flag -x) active une trace d'exécution et permet de débugger le programme. Notez que l'extension .sh n'est pas une convention obligatoire, et son omission ne pose aucun problème. Si vous voulez pouvoir exécuter un programme shell directement, l'alternative est la suivante:

$ chmod +x programme.sh
$ ./programme.sh

Précéder le nom du programme par ./ est obligatoire si le chemin courant n'est pas dans la variable PATH (par ex: PATH=.:/usr/bin:/bin). Enfin pour que cette invocation fonctionne, il faut préciser le shell et ses arguments éventuels dans le corps même du programme. On va donc commencer un programme shell par ceci:

#!/bin/bash
...

Le code spécial #! est appelé le shebang (prononcez chibangue). Il doit être suivi d'un interpréteur et de ses options éventuelles. Ici nous sommes sûr que Bash sera invoqué. A noter que si vous invoquez /bin/sh (le shell "par défaut"), vous pouvez rencontrer à l'exécution des dialectes différents de shell (csh, bourne, etc) et mettre en défaut votre script de façon inattendue.

TODO: "./monscript: not found" avec les CRLF.

Comportement

Une commande Unix bien élevée suit en général les règles suivantes:

  1. La commande doit vérifier la validité de ses arguments
  2. La commande doit être raisonnablement auto-documentée
  3. L'auteur et la révision doivent être traçables
  4. Si la commande est invoquée sans arguments, elle ne doit effectuer aucune écriture ou modification; sinon un dialogue de confirmation avec une explication claire doit intervenir

Pour l'illustration, nous allons effectuer un programme simple qui effectue une sauvegarde complète d'un noyau Linux complet (modules, configuration, map, image). Nous créons donc le fichier kernel_backup.sh avec ce contenu:

#!/bin/bash

# kernel_backup.sh: effectue la sauvegarde d'un noyau Linux et de ses
# donnees afferentes dans un unique tarball (image, modules, etc). Le
# tarball est créé dans le répertoire courant.

# 2006-08-25  Vincent CARON - Premiere version
# 2006-08-26  Vincent CARON - Correction de la documentation

if [ $# != 1 ]; then
  echo "Utilisation: $0 kernel_version" >&2
  exit 1
fi
KERNEL_VERSION="$1"

Jusqu'ici, nous auto-documentons la commande via des commentaires et donnons un nom explicite à la commande (en général de type sujet_verbe en anglais): règle 2 vérifiée.

Nous gardons une trace de l'auteur, de la date et de la modification effectuée: règle 3 vérifiée.

Ensuite nous vérifions que l'utilisateur passe également 1 argument, sinon nous documentons l'utilisation attendue de la commande (écriture dans la sortie d'erreur, et sortie avec un code non nul). Si cette commande est appelée sans arguments, elle échoue donc. Règles 1 et 4 vérifiées.

Bonne pratiques

Le shell est avant tout un langage basé sur l'anglais. Utilisez l'anglais pour les éléments de programmation (noms de variables principalement). Si votre préférez interagir avec un programme en français, limitez-vous aux éléments qui s'adressent aux humains: commentaires dans le programme, sorties d'erreur (echo).

Utilisez des variables avec un nom explicite (n'abbréviez pas, vous pouvez taper 5 à 10 caractères de plus sans vous faire mal aux doigts), toujours en majuscule. Dès que vous utilisez une même donnée à au moins deux endroits différents, centralisez-la dans une seule variable: vous éviterez des fautes de frappe subtiles, et simplifierez la relecture et la modification du programme.

Assignez les variables spéciales à des variables explicites pour donner un sens immédiatement compréhensible au relecteur. Par ex., KERNEL_VERSION="$1" est la façon la plus explicite d'exprimer que le premier argument de la commande doit être un numéro de version de noyau. Dans la suite du programme, cette variable longue sera plus compréhensible. Enfin, si vous modifiez l'interface de votre commande, il suffira de modifier cette seule assignation sans remettre en cause le reste du programme.

Gestion des erreurs

Le shell est particulièrement adapté pour séquencer le lancement de différents programmes et gérer leurs interactions. En général les programmes que vous invoquez ont une gestion d'erreur standard, prévisible et documentée. Il s'agit de faire de même.

Toute erreur doit être gérée. Suivant le but global du script, cette gestion peut consister à:

  • Ignorer l'erreur et continuer (la commande en erreur a en général déjà affiché un message d'alerte)
  • Arrêter le programme
  • Prendre un chemin différent dans le programme
  • Interpréter l'erreur et prendre une des trois dernières décision

Il est important de se mettre à la place de l'utilisateur, et de lui fournir les informations qui l'intéressent le plus:

  • La source de l'erreur (ex: accès refusé à /dev/cdrom)
  • La conséquence de l'erreur (ex: programme abandonné, un résultat partiel est dans travail.txt)
  • Eventuellement, une suggestion pour corriger l'erreur si celle-ci est répandue (ex: êtes-vous dans le groupe cdrom ?)

Méthodologie

La façon la plus simple de programmer une bonne gestion d'erreur consiste à tester à chaque étape le cas d'erreur, et le traiter immédiatement. Ainsi les tests apparaissent de façon linéaire. Ceci est à opposer à la programmation "imbriquée" où le séquencement progresse en rentrant dans des tests en cascade (rapidement illisible et délicat à maintenir).

Reprenons le fil de notre programme kernel_backup.sh:

KERNEL_IMAGE="/boot/vmlinuz-$KERNEL_VERSION"
KERNEL_CONFIG="/boot/config-$KERNEL_VERSION"
KERNEL_MAP="/boot/System.map-$KERNEL_VERSION"
KERNEL_MODULES="/lib/modules/$KERNEL_VERSION"

if [ ! -f "$KERNEL_IMAGE" ]; then
  echo "Error: kernel image file '$KERNEL_IMAGE' not found" >&2
  exit 1
fi

KERNEL_FILES="$KERNEL_IMAGE $KERNEL_CONFIG $KERNEL_MAP $KERNEL_MODULES"
KERNEL_BACKUP="kernel_backup-$KERNEL_VERSION.tar.gz"

tar czf "$KERNEL_BACKUP" $KERNEL_FILES

Afin d'améliorer la lisibilité et de nommer ce que nous allons manipuler, nous déclarons un lot de variables utilisant le même préfixe et les mêmes conventions que précédemment. L'idée est que dans les commandes suivantes on puisse "lire en anglais dans le texte" ce que cette dernière effectue.

L'usage des guillemets dans la valeur de l'assignation est généralement recommandé, ceci vous évite à vérifier les cas où celui-ci est obligatoire (ie. dès que la valeur littérale contient un caractère d'espacement).

L'utilisation des guillemets pour récupérer la valeur d'une variable (ex: $KERNEL_FILES) est plus subtil: cela dépend si vous voulez que cette variable puisse générer un argument ou plusieurs. Pour tester la présence du fichier, nous savons que le test -f attend un argument et un seul: les guillemets garantissent que si la valeur de KERNEL_IMAGE contient un espace, le script continuera à fonctionner normalement. Dans le cas de la commande tar, il faut que le nom du tarball généré par ($KERNEL_BACKUP) ne soit qu'un argument, tandis qu'il faut que les différents fichiers à archiver constituent bien des arguments distincts (omission des guillemets obligatoires pour KERNEL_FILES). Ceci demande un peut d'entraînement !

Si notre programme s'exécute jusqu'à la fin (commande tar), il sortira avec le code d'erreur de cette dernière commande. Il serait équivalent (et facultatif) de finir votre programme par exit $?.

Automatisation

Si vous voulez automatiser une tâche, comme par exemple sauvegarder votre noyau complet sur une autre machine, vous pourriez créer ce cronjob (cf. recommandations de votre distribution):

cd /mnt/nfs-server-1/backups && /usr/local/bin/kernel_backup.sh 2.4.32-1

Notre programme de sauvegarde créant son archive dans le répertoire courant, il nous faut d'abord sélectionner ce répertoire courant. On voit tout de suite que ce comportement est bénéfique:

  • il oblige l'administrateur qui automatise la tâche à le choisir et le spécifier (au lieu de le cacher dans le programme lui-même - on dit mettre en dur !)
  • si le chemin n'existe pas (serveur NFS en panne...), une erreur est générée et le backup n'est pas effectué; c'est en général une très bonne idée, continuer l'exécution de la tâche sans connaître le chemin de destination est potentiellement dangereux (écrasement de fichiers locaux, remplissage de partition)

Enfin vous constaterez rapidement que notre programme a un défaut ennuyeux. Le démon cron a la très bonne idée d'envoyer immédiatement par email à l'administrateur la sortie d'une commande qui a généré une erreur. Et nous recevons tous les jours un avertissement sous cette forme:

tar: Removing leading `/' from member names

Tout se déroule bien, mais tar nous prévient que par mesure de sécurité, il a stocké les noms de fichiers comme relatifs (sans le préfixe /). Il ne reste plus qu'à corriger ceci, deux solutions s'offrent à vous:

  • utiliser l'option -C / de tar et n'utiliser que des chemins relatifs (à la racine, s'entend)
  • utiliser l'option -P qui conserve ce /: mais êtes-vous sûr que cet avertissement était là pour rien ?

Nous laissons cet exercice au lecteur, et n'oubliez pas de rajouter une trace de votre correctif dans votre programme !

[Oral: étude du programme de restauration correspondant, aperçu de {{{sed}}} et des expressions régulières]

Last modified 14 years ago Last modified on Sep 4, 2006, 5:57:33 PM