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 19.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 15.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"
Dotan Barak nous envoie un fichier modèle pour afficher une barre de progression dans un script.
Exemple 36.15. Barre de progression
#!/bin/bash # progress-bar.sh # Author: Dotan Barak (very minor revisions by ABS Guide author). # Used in ABS Guide with permission (thanks!). BAR_WIDTH=50 BAR_CHAR_START="[" BAR_CHAR_END="]" BAR_CHAR_EMPTY="." BAR_CHAR_FULL="=" BRACKET_CHARS=2 LIMIT=100 print_progress_bar() { # Calculate how many characters will be full. let "full_limit = ((($1 - $BRACKET_CHARS) * $2) / $LIMIT)" # Calculate how many characters will be empty. let "empty_limit = ($1 - $BRACKET_CHARS) - ${full_limit}" # Prepare the bar. bar_line="${BAR_CHAR_START}" for ((j=0; j<full_limit; j++)); do bar_line="${bar_line}${BAR_CHAR_FULL}" done for ((j=0; j<empty_limit; j++)); do bar_line="${bar_line}${BAR_CHAR_EMPTY}" done bar_line="${bar_line}${BAR_CHAR_END}" printf "%3d%% %s" $2 ${bar_line} } # Here is a sample of code that uses it. MAX_PERCENT=100 for ((i=0; i<=MAX_PERCENT; i++)); do # usleep 10000 # ... Or run some other commands ... # print_progress_bar ${BAR_WIDTH} ${i} echo -en "\r" done echo "" exit
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 intégrés 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=85 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 » (généralement avec un echo) et de l'affecter à une variable. C'est une variante de la substitution de commandes.
Exemple 36.16. 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 36.17. 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
Il ne peut y avoir qu' une seule instruction echo dans le fonction pour que cela fonctionne. Si vous modifiez l'exemple précédent :
somme_et_produit () { echo "Ceci est la fonction somme_et_produit." # Ceci casse tout ! echo $(( $1 + $2 )) $(( $1 * $2 )) } ... retval=`somme_et_produit $first $second` # Affecte la sortie de la fonction. # Maintenant, cela ne fonctionnera plus correctement.
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 36.18. 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, « Le Jeu de la Vie ».
En utilisant une expression avec doubles parenthèses, il est possible d'utiliser la syntaxe du style du langage C pour initialiser et incrémenter des variables ainsi que dans des boucles for et while. Voir l'Exemple 11.12, « Une boucle for comme en C » et l'Exemple 11.17, « Syntaxe à la C pour une boucle while ».
Initialiser le path et le umask au début d'un script le rend plus portable -- il est plus probable qu'il fonctionne sur des machines « étrangères » dont l'utilisateur a peut-être modifié $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 36.19. 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 29.4, « État de la connexion », l'Exemple 16.25, « 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 19.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.
Pour avoir plusieurs révisions d'un script complexe, utilisez le package de contrôle de version appelé rcs.
Un des avantages de ce programme est qu'il modifie automatiquement les balises d'en-tête. La commande co dans rcs fait un remplacement de paramètres pour certains mots clés réservés, par exemple # $Id$ dans un script est remplacé par une expression du style :
# $Id: bonjour-lesgens.sh,v 1.8 2008-06-12 17:11:55 gleu Exp $
Ce serait bien de pouvoir invoquer des 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 36.20. Widgets appelés à partir d'un script shell
#!/bin/bash # dialog.sh: Using 'gdialog' widgets. # Must have 'gdialog' installed on your system to run this script. # Or, you can replace all instance of 'gdialog' below with 'kdialog' ... # Version 1.1 (corrected 04/05/05) # This script was inspired by the following article. # "Scripting for X Productivity," by Marco Fioretti, # LINUX JOURNAL, Issue 113, September 2003, pp. 86-9. # Thank you, all you good people at LJ. # Input error in dialog box. E_INPUT=65 # Dimensions of display, input widgets. HEIGHT=50 WIDTH=60 # Output file name (constructed out of script name). OUTFILE=$0.output # Display this script in a text widget. gdialog --title "Displaying: $0" --textbox $0 $HEIGHT $WIDTH # Now, we'll try saving input in a file. echo -n "VARIABLE=" > $OUTFILE gdialog --title "User Input" --inputbox "Enter variable, please:" \ $HEIGHT $WIDTH 2>> $OUTFILE if [ "$?" -eq 0 ] # It's good practice to check exit status. then echo "Executed \"dialog box\" without errors." else echo "Error(s) in \"dialog box\" execution." # Or, clicked on "Cancel", instead of "OK" button. rm $OUTFILE exit $E_INPUT fi # Now, we'll retrieve and display the saved variable. . $OUTFILE # 'Source' the saved file. echo "The variable input in the \"input box\" was: "$VARIABLE"" rm $OUTFILE # Clean up by removing the temp file. # Some applications may need to retain this file. exit $? # Exercise: Rewrite this script using the 'zenity' widget set.
La commande xmessage est une méthode simple d'affichage d'une fenêtre contenant un message. Par exemple :
xmessage Erreur fatale dans le script! -button exit
Le dernier venu dans ce domaine est zenity. Cet outil affiche un dialogue GTK+ et fonctionne très bien dans un script.
get_info () { zenity --entry # Montre une fenêtre de saisie #+ et affiche la saisie de l'utilisateur en sortie. # Essayez aussi les options --calendar et --scale. } reponse=$( get_info ) # Capture stdout dans la variable $reponse. echo "L'utilisateur a saisi : "$answer""
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).