Vous avez un problème que vous voulez résoudre en écrivant un script Bash. Malheureusement, vous ne savez pas comment vous lancer. Une méthode est de commencer directement en codant les parties du script qui viennent facliement et d'écrire les parties difficiles en pseudo-code.
#!/bin/bash NBARG=1 # A besoin d'un nom comme argument. E_MAUVAISARGS=65 if [ nombre-d-arguments différent-de "$NBARG" ] # ^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^ # Impossible de trouver comment coder ceci #+ ... donc écrivez-le en pseudo-code. then echo "Usage: nom-du-script nom" # ^^^^^^^^^^^^^ Encore du pseudo-code. exit $E_MAUVAISARGS fi . . . exit 0 # Plus tard, remplacez le pseudo-code par du code fonctionnel. # La ligne 6 devient : if [ $# -ne "$NBARG" ] # La ligne 12 devient : echo "Usage: `basename $0` nom"
Pour un exemple d'utilisation de pseudo-code, voir l'exercice Square Root.
Pour conserver un enregistrement des scripts utilisateur lancés lors de certaines sessions ou lors d'un certain nombre de sessions, ajoutez les lignes suivantes à chaque script dont vous voulez garder la trace. Ceci va conserver un fichier d'enregistrement des noms de script et des heures d'appel.
# Ajoute (>>) ce qui suit à la fin de chaque script tracé. whoami>> $FICHIER_SAUVEGARDE # Utilisateur appelant le script. echo $0>> $FICHIER_SAUVEGARDE # Nom du script. date>> $FICHIER_SAUVEGARDE # Date et heure. echo>> $FICHIER_SAUVEGARDE # Ligne blanche comme séparateur. # Bien sûr, FICHIER_SAUVEGARDE défini et exporté comme variable d'environnement #+ dans ~/.bashrc (quelque chose comme ~/.scripts-run)
L'opérateur >> ajoute des lignes dans un fichier. Qu'en est-il si vous voulez ajouter une ligne au début d'un fichier existant, c'est-à-dire la coller au tout début ?
fichier=donnees.txt titre="***Ceci est la ligne de titre des fichiers texte de données***" echo $titre | cat - $fichier >$fichier.new # "cat -" concatène stdout dans $fichier. # Le résultat final est l'écriture d'un nouveau fichier avec $titre ajouté au #+ *début*.
C'est une variante simplifiée du script de l'Exemple 18.13, « Ajouter une ligne au début d'un fichier » donnée plus tôt. Bien sûr, sed peut aussi le faire.
Un script shell peut agir comme une commande interne à l'intérieur d'un autre script shell, d'un script Tcl ou d'un script wish, voire même d'un Makefile. Il peut être appelé comme une commande shell externe dans un programme C en utilisant l'appel system(), c'est-à-dire system("nom_du_script");.
Configurer une variable avec le contenu d'un script sed ou awk embarqué accroît la lisibilité de l'emballage shell qui l'entoure. Voir l'Exemple A.1, « mailformat : Formater un courrier électronique » et l'Exemple 14.20, « Utiliser export pour passer une variable à un script awk embarqué ».
Réunissez les fichiers contenant vos définitions et vos fonctions les plus utiles. Quand nécessaire, « incluez » un ou plus de ces « fichiers bibliothèque » dans des scripts avec soit le point (.) soit la commande source.
# BIBLIOTHEQUE SCRIPT # ------ ------- # Note : # Pas de "#!" ici. # Pas de code exécuté immédiatement non plus. # Définition de variables ici ROOT_UID=0 # Root a l'identifiant utilisateur ($UID) 0. E_NOTROOT=101 # Pas d'erreur de l'utilisateur root. MAXRETVAL=255 # Code de retour (positif) maximum d'une fonction. SUCCESS=0 FAILURE=-1 # Fonctions Usage () # Message "Usage :". { if [ -z "$1" ] # Pas d'argument passé. then msg=nom_du_fichier else msg=$@ fi echo "Usage: `basename $0` "$msg"" } Verifier_si_root () # Vérifier si le script tourne en tant que root. { # À partir de l'exemple "ex39.sh". if [ "$UID" -ne "$ROOT_UID" ] then echo "Doit être root pour lancer ce script." exit $E_NOTROOT fi } Creer_Nom_Fichier_Temporaire () # Crée un nom de fichier temporaire "unique". { # À partir de l'exemple "ex51.sh". prefixe=temp suffixe=`eval date +%s` Tempfilename=$prefixe.$suffixe } est_alpha2 () # Teste si la chaine de caractères *entière* est # alphabétique. { # À partir de l'exemple "isalpha.sh". [ $# -eq 1 ] || return $FAILURE case $1 in *[!a-zA-Z]*|"") return $FAILURE;; *) return $SUCCESS;; esac # Merci, S.C. } abs () # Valeur absolue. { # Attention : Valeur de retour maximum = 255. E_ARGERR=-999999 if [ -z "$1" ] # Il est nécessaire de passer un argument. then return $E_ARGERR # Code d'erreur évident renvoyé. fi if [ "$1" -ge 0 ] # Si non-négatif, then # absval=$1 # reste tel quel, else # Sinon, let "absval = (( 0 - $1 ))" # change son signe. fi return $absval } tolower () # Convertit le(s) chaîne(s) de caractères passées comme { #+ argument(s) en minuscule. if [ -z "$1" ] # Si aucun argument n'est passé, then #+ envoyez un message d'erreur echo "(null)" #+ (message d'erreur étant un pointeur null style C) return #+ et sort de la fonction. fi echo "$@" | tr A-Z a-z # Transforme tous les arguments passés ($@). return # Utilisez la substituion de commande pour initialiser une variable à la sortie #+ d'une commande. # Par exemple : # anciennevar="Un EnseMBle dE LetTres miNusCuleS Et MaJuscuLeS" # nouvellevar=`tolower "$anciennevar"` # echo "$nouvellevar" # un ensemble de lettre minuscules et majuscules # # Exercice : Réécrire cette fonction pour changer le(s) argument(s) minuscule(s) #+ en majuscules ... toupper() [facile]. }
Utiliser des en-têtes de commentaires pour accroître la clarté et la compréhension des scripts.
## Attention. rm -rf *.zzy ## Les options "-rf" de "rm" sont très dangereux, ##+ spécialement avec des caractères joker. #+ Suite de la ligne. # Ceci est la ligne 1 #+ d'un commentaire multi-ligne. #+ et ceci est la ligne finale. #* Note. #o Elément d'une liste. #> Autre point de vue. while [ "$var1" != "end" ] #> while test "$var1" != "end"
Une utilisation particulièrement intelligente des constructions if-test permet de mettre en commentaires des blocs de code.
#!/bin/bash BLOC_COMMENTAIRE= # Essayez d'initialiser la variable ci-dessus autrement pour une #+ surprise peu plaisante. if [ $BLOC_COMMENTAIRE ]; then Bloc de commentaires -- ================================= Ceci est une ligne de commentaires. Ceci est une autre ligne de commentaires. Ceci est encore une autre ligne de commentaires. ================================= echo "Ceci ne s'affichera pas." Les blocs de commentaires sont sans erreur ! Youpi ! fi echo "Sans commentaires, merci." exit 0
Comparez ceci avec l'utilisation de documents en lignes pour commenter des blocs de code.
En utilisant la variable d'état de sortie $?, un script peut tester si un paramètre contient seulement des chiffres, ainsi il peut être traité comme un entier.
#!/bin/bash SUCCESS=0 E_BADINPUT=65 test "$1" -ne 0 -o "$1" -eq 0 2>/dev/null # Un entier est soit égal à 0 soit différent de 0. # 2>/dev/null supprime les messages d'erreur. if [ $? -ne "$SUCCESS" ] then echo "Usage: `basename $0` integer-input" exit $E_BADINPUT fi let "sum = $1 + 25" # Donnera une erreur si $1 n'est pas un entier. echo "Sum = $sum" # Toute variable, pas simplement un paramètre de ligne de commande, peut être #+ testé de cette façon. exit 0
L'échelle 0 - 255 des valeurs de retour des fonctions est une limitation importante. Les variables globales et autres moyens de contourner ce problème sont souvent des problèmes eux-même. Une autre méthode, pour que la fonction communique une valeur de retour au corps principal du script, est que la fonction écrive sur stdout la « valeur de sortie » (habituellement avec un echo) et de l'affecter à une variable. C'est une variante de la substitution de commandes.
Exemple 33.15. Astuce de valeur de retour
#!/bin/bash # multiplication.sh multiplie () # Multiplie les paramètres passés. { # Acceptera un nombre variable d'arguments. local produit=1 until [ -z "$1" ] # Jusqu'à la fin de tous les arguments... do let "produit *= $1" shift done echo $produit # N'affichera pas sur stdout } #+ car cela va être affecté à une variable. mult1=15383; mult2=25211 val1=`multiplie $mult1 $mult2` echo "$mult1 X $mult2 = $val1" # 387820813 mult1=25; mult2=5; mult3=20 val2=`multiplie $mult1 $mult2 $mult3` echo "$mult1 X $mult2 X $mult3 = $val2" # 2500 mult1=188; mult2=37; mult3=25; mult4=47 val3=`multiplie $mult1 $mult2 $mult3 $mult4` echo "$mult1 X $mult2 X $mult3 X $mult4 = $val3" # 8173300 exit 0
La même technique fonctionne aussi pour les chaînes de caractères alphanumériques. Ceci signifie qu'une fonction peut « renvoyer » une valeur non-numérique.
capitaliser_ichar () # Capitaliser le premier caractère { #+ de(s) chaîne(s) de caractères passées. chaine0="$@" # Accepte plusieurs arguments. premiercaractere=${chaine0:0:1} # Premier caractère. chaine1=${chaine0:1} # Reste de(s) chaîne(s) de caractères. PremierCaractere=`echo "$premiercaractere" | tr a-z A-Z` # Capitalise le premier caractère. echo "$PremierCaractere$chaine1" # Sortie vers stdout. } nouvellechaine=`capitalize_ichar "toute phrase doit commencer avec une lettre majuscule."` echo "$nouvellechaine" # Toute phrase doit commencer avec une lettre majuscule.
Il est même possible pour une fonction de « renvoyer » plusieurs valeurs avec cette méthode.
Exemple 33.16. Une astuce permettant de renvoyer plus d'une valeur de retour
#!/bin/bash # sum-product.sh # Une fonction peut "renvoyer" plus d'une valeur. somme_et_produit () # Calcule à la fois la somme et le produit des arguments. { echo $(( $1 + $2 )) $(( $1 * $2 )) # Envoie sur stdout chaque valeur calculée, séparée par une espace. } echo echo "Entrez le premier nombre " read premier echo echo "Entrez le deuxième nombre " read second echo valretour=`somme_et_produit $premier $second` # Affecte à la variable la sortie #+ de la fonction. somme=`echo "$valretour" | awk '{print $1}'` # Affecte le premier champ. produit=`echo "$valretour" | awk '{print $2}'`# Affecte le deuxième champ. echo "$premier + $second = $somme" echo "$premier * $second = $produit" echo exit 0
Ensuite dans notre liste d'astuces se trouvent les techniques permettant de passer un tableau à une fonction, « renvoyant » alors un tableau en retour à la fonction principale du script.
Le passage d'un tableau nécessite de charger des éléments séparés par une espace d'un tableau dans une variable avec la substitution de commandes. Récupérer un tableau comme « valeur de retour » à partir d'une fonction utilise le stratagème mentionné précédemment de la sortie (echo) du tableau dans la fonction, puis d'invoquer la substitution de commande et l'opérateur ( ... ) pour l'assigner dans un tableau.
Exemple 33.17. Passer et renvoyer un tableau
#!/bin/bash # array-function.sh : Passer un tableau à une fonction et... # "renvoyer" un tableau à partir d'une fonction Passe_Tableau () { local tableau_passe # Variable locale. tableau_passe=( `echo "$1"` ) echo "${tableau_passe[@]}" # Liste tous les éléments du nouveau tableau déclaré #+ et initialisé dans la fonction. } tableau_original=( element1 element2 element3 element4 element5 ) echo echo "tableau_original = ${tableau_original[@]}" # Liste tous les éléments du tableau original. # Voici une astuce qui permet de passer un tableau à une fonction. # ********************************** argument=`echo ${tableau_original[@]}` # ********************************** # Emballer une variable #+ avec tous les éléments du tableau original séparés avec une espace. # # Notez que d'essayer de passer un tableau en lui-même ne fonctionnera pas. # Voici une astuce qui permet de récupérer un tableau comme "valeur de retour". # ***************************************** tableau_renvoye=( `Passe_Tableau "$argument"` ) # ***************************************** # Affecte une sortie de la fonction à une variable de type tableau. echo "tableau_renvoye = ${tableau_renvoye[@]}" echo "=============================================================" # Maintenant, essayez encore d'accèder au tableau en dehors de la #+ fonction. Passe_Tableau "$argument" # La fonction liste elle-même le tableau, mais... #+ accèder au tableau de l'extérieur de la fonction est interdit. echo "Tableau passé (de l'intérieur de la fonction) = ${tableau_passe[@]}" # Valeur NULL comme il s'agit d'une variable locale. echo exit 0
Pour un exemple plus élaboré du passage d'un tableau dans les fonctions, voir l'Exemple A.10, « « life : Jeu de la Vie » ».
En utilisant la construction en double parenthèses, il est possible d'utiliser la syntaxe style C pour initialiser et incrémenter des variables ainsi que dans des boucles for et while. Voir l'Exemple 10.12, « Une boucle for à la C » et l'Exemple 10.17, « Syntaxe à la C pour une boucle while ».
Initialiser path et umask au début d'un script le rend plus « portable » -- il est plus probable qu'il fonctionne avec des machines « étrangères » dont l'utilisateur a pu modifier $PATH et umask.
#!/bin/bash PATH=/bin:/usr/bin:/usr/local/bin ; export PATH umask 022 # Les fichiers que le script crée auront les droits 755. # Merci à Ian D. Allen pour ce conseil.
Une technique de scripts utiles est d'envoyer de manière répétée la sortie d'un filtre (par un tuyau) vers le même filtre, mais avec un ensemble différent d'arguments et/ou options. Ceci est spécialement intéressant pour tr et grep.
# De l'exemple "wstrings.sh". wlist=`strings "$1" | tr A-Z a-z | tr '[:space:]' Z | \ tr -cs '[:alpha:]' Z | tr -s '\173-\377' Z | tr Z ' '`
Exemple 33.18. Un peu de fun avec des anagrammes
#!/bin/bash # agram.sh: Jouer avec des anagrammes. # Trouver les anagrammes de... LETTRES=etaoinshrdlu FILTRE='.......' # Combien de lettres au minimum ? # 1234567 anagram "$LETTRES" | # Trouver tous les anagrammes de cet ensemble de lettres... grep '$FILTRE' | # Avec au moins sept lettres, grep '^is' | # commençant par 'is', grep -v 's$' | # sans les puriels, grep -v 'ed$' # sans verbe au passé ("ed" en anglais) # Il est possible d'ajouter beaucoup de combinaisons #+ dans les conditions et les filtres. # Utilise l'utilitaire "anagram" #+ qui fait partie du paquetage de liste de mots "yawl" de l'auteur. # http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz # http://personal.riverusers.com/~thegrendel/yawl-0.3.2.tar.gz exit 0 # Fin du code. bash$ sh agram.sh islander isolate isolead isotheral # Exercices : # ---------- # Modifiez ce script pour configurer LETTRES via la ligne de commande. # Transformez les filtres en paramètres dans les lignes 11 à 13 #+ (comme ce qui a été fait pour $FILTRE), #+ de façon à ce qu'ils puissent être indiqués en passant les arguments #+ à une fonction. # Pour une approche légèrement différente de la construction d'anagrammes, #+ voir le script agram2.sh.
Voir aussi l'Exemple 27.3, « État de la connexion », l'Exemple 15.23, « Générer des énigmes « Crypto-Citations » » et l'Exemple A.9, « soundex : Conversion phonétique ».
Utiliser des « documents anonymes » pour mettre en commentaire des blocs de code, pour ne pas avoir à mettre en commentaire chaque ligne avec un #. Voir Exemple 18.11, « Décommenter un bloc de code ».
Lancer sur une machine un script dépendant de la présence d'une commande qui peut être absente est dangereux. Utilisez whatis pour éviter des problèmes potentiels avec ceci.
CMD=commande1 # Premier choix. PlanB=commande2 # Option en cas de problème. commande_test=$(whatis "$CMD" | grep 'nothing appropriate') # Si 'commande1' n'est pas trouvé sur ce système, 'whatis' renverra #+ "commande1: nothing appropriate." # # Une alternative plus saine est : # commande_test=$(whereis "$CMD" | grep \/) # Mais, du coup, le sens du test suivant devrait être inversé #+ car la variable $commande_test détient le contenu si et seulement si #+ $CMD existe sur le système. # (Merci bojster.) if [[ -z "$command_test" ]] # Vérifie si la commande est présente. then $CMD option1 option2 # Lancez commande1 avec ses options. else # Sinon, $PlanB #+ lancez commande2. fi
Un test if-grep pourrait ne pas renvoyer les résultats attendus dans un cas d'erreur lorsque le texte est affiché sur stderr plutôt que sur stdout.
if ls -l fichier_inexistant | grep -q 'No such file or directory' then echo "Le fichier \"fichier_inexistant\" n'existe pas." fi
Rediriger stderr sur stdout corrige ceci.
if ls -l fichier_inexistant 2>&1 | grep -q 'No such file or directory' # ^^^^ then echo "Le \"fichier_inexistant\" n'existe pas." fi # Merci à Chris Martin de nous l'avoir indiqué.
Si vous devez vraiment accéder à une variable d'un sous-shell en dehors de ce sous-shell, voici une façon de le faire.
FICHIERTEMP=fichiertemp # Crée un fichier temporaire pour stocker la variable. ( # À l'intérieur du sous-shell... variable_interne=interne echo $variable_interne echo $variable_interne >>$FICHIERTEMP # Ajout dans le fichier temporaire. ) # En dehors du sous-shell... echo; echo "-----"; echo echo $variable_interne # Null, comme on s'y attendait. echo "-----"; echo # Maintenant... read variable_interne <$FICHIERTEMP # Lecture de variable shell. rm -f "$FICHIERTEMP" # Supprime le fichier temporaire. echo "$variable_interne" # C'est un hack assez sale mais fonctionnel.
La commande run-parts est utile pour exécuter un ensemble de scripts dans l'ordre, particulièrement en combinaison avec cron ou at.
Il serait bien d'être capable d'invoquer les objets X-Windows à partir d'un script shell. Il existe plusieurs paquets qui disent le faire, à savoir Xscript, Xmenu et widtools. Les deux premiers ne semblent plus maintenus. Heureusement, il est toujours possible d'obtenir widtools ici.
Le paquet widtools (widget tools, outils pour objets) nécessite que la bibliothèque XForms soit installée. De plus, le Makefile a besoin d'être édité de façon judicieuse avant que le paquet ne soit construit sur un système Linux typique. Finalement, trois des six objets offerts ne fonctionnent pas (en fait, ils génèrent un défaut de segmentation).
La famille d'outils dialog offre une méthode d'appel des widgets « dialog » à partir d'un script shell. L'utilitaire original dialog fonctionne dans une console texte mais ses successeurs, gdialog, Xdialog et kdialog utilisent des ensembles de widgets basés sur X-Windows.
Exemple 33.19. Widgets appelés à partir d'un script shell
#!/bin/bash # dialog.sh : Utiliser les composants graphiques de 'gdialog'. # Vous devez avoir installé 'gdialog' sur votre système pour lancer ce script. # Version 1.1 (corrigée le 04/05/05). # Ce script s'inspire de l'article suivant. # "Scripting for X Productivity" de Marco Fioretti, # LINUX JOURNAL, numéro 113, septembre 2003, pp. 86-9. # Merci à toutes ces braves âmes chez LJ. # Erreur d'entrée dans la boîte de saisie. E_ENTREE=65 # Dimensions de l'affichage des composants graphiques de saisie. HAUTEUR=50 LARGEUR=60 # Nom du fichier de sortie (construit à partir du nom du script). FICHIER_SORTIE=$0.sortie # Affiche ce script dans un composant texte. gdialog --title "Affichage : $0" --textbox $0 $HAUTEUR $LARGEUR # Maintenant, nous allons essayer de sauvegarder l'entrée dans un fichier. echo -n "VARIABLE=" > $FICHIER_SORTIE gdialog --title "Entrée utilisateur" \ --inputbox "Entrez une variable, s'il-vous-plaît :" \ $HAUTEUR $LARGEUR 2>> $FICHIER_SORTIE if [ "$?" -eq 0 ] # Une bonne pratique consiste à vérifier le code de sortie. then echo "Exécution de \"dialog box\" sans erreurs." else echo "Erreur(s) lors de l'exécution de \"dialog box\"." # Ou clic sur "Annuler" au lieu du bouton "OK". rm $FICHIER_SORTIE exit $E_ENTREE fi # Maintenant, nous allons retrouver et afficher la variable sauvée. . $FICHIER_SORTIE # 'Source'r le fichier sauvé. echo "La variable d'entrée dans \"input box\" était : "$VARIABLE"" rm $FICHIER_SORTIE # Nettoyage avec la suppression du fichier temporaire. # Quelques applications pourraient avoir besoin de réclamer ce fichier. exit $?
Pour d'autres méthodes d'écriture des scripts utilisant des widgets, essayez Tk ou wish (des dérivés de Tcl), PerlTk (Perl avec des extensions Tk), tksh (ksh avec des extensions Tk), XForms4Perl (Perl avec des extensions XForms), Gtk-Perl (Perl avec des extensions Gtk) ou PyQt (Python avec des extensions Qt).
Pour réaliser de multiple révisions d'un script complexe, utilisez le paquet contenant le système de contrôle de révision nommé rcs.
Entre autres bénéfices de celui-ci se trouve la mise à jour automatique de balises d'en-tête. La commande co de rcs effectue un remplacement de certains termes réservés comme, par exemple, remplacer #$Id: abs-part5.xml,v 1.5 2008-01-02 18:10:18 gleu Exp $ dans un script avec quelque chose comme :
#$Id: abs-part5.xml,v 1.5 2008-01-02 18:10:18 gleu Exp $