Chapitre 30. Déboguage

Le shell Bash ne contient ni débogueur ni même de commandes ou d'instructions spécifiques pour le déboguage. [1] Les erreurs de syntaxe ou de frappe dans les scripts génèrent des messages d'erreur incompréhensibles n'apportant souvent aucune aide pour déboguer un script non fonctionnel.

Exemple 30-1. Un script bugué

#!/bin/bash
# ex74.sh

# C'est un script buggué.

a=37

if [$a -gt 27 ]
then
  echo $a
fi  

exit 0

Sortie d'un script:
./ex74.sh: [37: command not found
Que se passe-t'il avec ce script (petite aide: après le if)?

Exemple 30-2. Mot clé manquant

#!/bin/bash
# missing-keyword.sh: Quel message d'erreur sera généré?

for a in 1 2 3
do
  echo "$a"
# done     # Requiert le mot clé 'done' mis en commentaire ligne 7.

exit 0  

Sortie d'un script:
missing-keyword.sh: line 10: syntax error: unexpected end of file
	
Notez que le message d'erreur ne fait pas nécessairement référence à la ligne où l'erreur se trouve mais à la ligne où l'interpréteur Bash s'aperçoit de l'erreur.

Les messages d'erreur peuvent ne pas tenir compte des lignes de commentaires d'un script lors de l'affichage du numéro de ligne de l'instruction ayant provoqué une erreur de syntaxe.

Que faire si le script s'exécute mais ne fonctionne pas comme vous vous y attendiez? C'est une erreur de logique trop commune.

Exemple 30-3. test24, un autre script bogué

#!/bin/bash

#  Ceci est supposé supprimer tous les fichiers du répertoire courant contenant
#+ des espaces dans le nom.
#  Cela ne fonctionne pas. Pourquoi?


mauvaisnom=`ls | grep ' '`

# echo "$mauvaisnom"

rm "$mauvaisnom"

exit 0

Essayez de trouver ce qui ne va pas avec Exemple 30-3 en supprimant les caractères de commentaires de la ligne echo "$badname". Les instructions echo sont utiles pour voir si ce que vous attendiez est bien ce que vous obtenez.

Dans ce cas particulier, rm "$badname" ne donnera pas les résultats attendus parce que $badname ne devrait pas être entre guillemets. Le placer entre guillemets nous assure que rm n'a qu'un seul argument (il correspondra à un seul nom de fichier). Une correction partielle est de supprimer les guillemets de $badname et de réinitialiser $IFS pour contenir seulement un retour à la ligne, IFS=$'\n'. Néanmoins, il existe des façons plus simples de faire cela.
# Bonnes méthodes de suppression des fichiers contenant des espaces dans leur nom.
rm *\ *
rm *" "*
rm *' '*
# Merci, S.C.

Résumer les symptômes d'un script bogué,

  1. Il quitte brutalement avec un message d'erreur de syntaxe (<< syntax error >>), ou

  2. Il se lance bien, mais ne fonctionne pas de la façon attendue (erreur logique, logic error).

  3. Il fonctionne comme vous vous y attendiez, mais a de déplaisants effets indésirables (logic bomb).

Il existe des outils pour déboguer des scripts non fonctionnels

  1. des instructions echo aux points critiques du script pour tracer les variables, ou pour donner un état de ce qui se passe.

  2. utiliser le filtre tee pour surveiller les processus ou les données aux points critiques.

  3. initialiser des paramètres optionnelles -n -v -x

    sh -n nomscript vérifie les erreurs de syntaxe sans réellement exécuter le script. C'est l'équivalent de l'insertion de set -n ou set -o noexec dans le script. Notez que certains types d'erreurs de syntaxe peuvent passer à côté de cette vérification.

    sh -v nomscript affiche chaque commande avant de l'exécuter. C'est l'équivalent de l'insertion de set -v ou set -o verbose dans le script.

    Les options -n et -v fonctionnent bien ensemble. sh -nv nomscript permet une vérification verbeuse de la syntaxe.

    sh -x nomscript affiche le résultat de chaque commande, mais d'une façon abrégée. C'est l'équivalent de l'insertion de set -x ou set -o xtrace dans le script.

    Insérer set -u ou set -o nounset dans le script le lance, mais donne un message d'erreur unbound variable à chaque essai d'utilisation d'une variable non déclarée.

  4. Utiliser une fonction << assert >> pour tester une variable ou une condition aux points critiques d'un script. (Cette idée est empruntée du C.)

    Exemple 30-4. Tester une condition avec un << assert >>

    #!/bin/bash
    # assert.sh
    
    assert ()                 #  Si la condition est fausse,
    {                         #+ sort du script avec un message d'erreur.
      E_PARAM_ERR=98
      E_ASSERT_FAILED=99
    
    
      if [ -z "$2" ]          # Pas assez de paramètres passés.
      then
        return $E_PARAM_ERR   # Pas de dommages.
      fi
    
      noligne=$2
    
      if [ ! $1 ] 
      then
        echo "Mauvaise assertion:  \"$1\""
        echo "Fichier \"$0\", ligne $noligne"
        exit $E_ASSERT_FAILED
      # else (sinon)
      #   return (retour)
      #   et continue l'exécution du script.
      fi  
    }    
    
    
    a=5
    b=4
    condition="$a -lt $b"     # Message d'erreur et sortie du script.
                              #  Essayer de configurer la "condition" en autre chose
                              #+ et voir ce qui se passe.
    
    assert "$condition" $LINENO
    # Le reste du script s'exécute si assert n'échoue pas.
    
    
    # Quelques commandes.
    # ...
    echo "Cette instruction s'exécute seulement si \"assert\" n'échoue pas."
    # ...
    # Quelques commandes de plus.
    
    exit 0
  5. piéger la sortie.

    La commande exit d'un script déclenche un signal 0, terminant le processus, c'est-à-dire le script lui-même. [2] Il est souvent utilisé pour récupérer la main lors de exit, en forçant un << affichage >> des variables, par exemple. Le trap doit être la première commande du script.

Récupérer les signaux

trap

Spécifie une action à la réception d'un signal; aussi utile pour le déboguage.

Note

Un signal est un simple message envoyé au processus, soit par le noyau soit par un autre processus lui disant de réaliser quelque action spécifiée (habituellement pour finir son exécution). Par exemple, appuyer sur Control-C, envoit une interruption utilisateur, un signal INT, au programme en cours d'exécution.

trap '' 2
# Ignore l'interruption 2 (Control-C), sans action définie.

trap 'echo "Control-C désactivé."' 2
# Message lorsque Control-C est utilisé.

Exemple 30-5. Récupérer la sortie

#!/bin/bash

trap 'echo Liste de Variables --- a = $a  b = $b' EXIT
# EXIT est le nom du signal généré en sortie d'un script.

a=39

b=36

exit 0
# Notez que mettre en commentaire la commande 'exit' ne fait aucune différence,
# car le script sort dans tous les cas après avoir exécuté les commandes.

Exemple 30-6. Nettoyage après un Control-C

#!/bin/bash
# logon.sh: Un script rapide mais sale pour vérifier si vous êtes déjà connecté.


VRAI=1
JOURNAL=/var/log/messages
# Notez que $JOURNAL doit être lisible (chmod 644 /var/log/messages).
FICHIER_TEMPORAIRE=temp.$$
# Crée un fichier temporaire "unique", en utilisant l'identifiant du processus.
MOTCLE=adresse
# A la connexion, la ligne "remote IP address xxx.xxx.xxx.xxx"
#                     ajoutée à /var/log/messages.
ENLIGNE=22
INTERRUPTION_UTILISATEUR=13
VERIFIE_LIGNES=100
# Nombre de lignes à vérifier dans le journal.

trap 'rm -f $FICHIER_TEMPORAIRE; exit $INTERRUPTION_UTILISATEUR' TERM INT
# Nettoie le fichier temporaire si le script est interrompu avec control-c.

echo

while [ $VRAI ]  # Boucle sans fin.
do
  tail -$VERIFIE_LIGNES $JOURNAL> $FICHIER_TEMPORAIRE
  # Sauve les 100 dernières lignes du journal dans un fichier temporaire.
  # Nécessaire, car les nouveaux noyaux génèrent beaucoup de messages lors de la
  # connexion.
  search=`grep $MOTCLE $FICHIER_TEMPORAIRE`
  # Vérifie la présence de la phrase "IP address",
  # indiquant une connexion réussie.

  if [ ! -z "$search" ] # Guillemets nécessaires à cause des espaces possibles.
  then
     echo "En ligne"
     rm -f $FICHIER_TEMPORAIRE    # Suppression du fichier temporaire.
     exit $ENLIGNE
  else
     echo -n "."        # l'option -n supprime les retours à la ligne de echo,
                        # de façon à obtenir des lignes de points continues.
  fi

  sleep 1  
done  


# Note: Si vous modifiez la variable MOTCLE par "Exit",
# ce script peut être utilisé lors de la connexion pour vérifier une déconnexion
# inattendue.

# Exercice: Modifiez le script, suivant la note ci-dessus, et embellissez-le.

exit 0


# Nick Drage suggère une autre méthode.

while true
  do ifconfig ppp0 | grep UP 1> /dev/null && echo "connecté" && exit 0
  echo -n "."   # Affiche des points (.....) jusqu'au moment de la connexion.
  sleep 2
done

# Problème: Appuyer sur Control-C pour terminer ce processus peut être
# insuffisant (des points pourraient toujours être affichés).
# Exercice: Corrigez ceci.



# Stephane Chazelas a lui-aussi suggéré une autre méthode.

CHECK_INTERVAL=1

while ! tail -1 "$JOURNAL" | grep -q "$MOTCLE"
do echo -n .
   sleep $CHECK_INTERVAL
done
echo "On-line"

# Exercice: Discutez les avantages et inconvénients de chacune des méthodes.

Note

L'argument DEBUG pour trap exécute une action spécifique après chaque commande dans un script. Cela permet de tracer les variables, par exemple.

Exemple 30-7. Tracer une variable

#!/bin/bash

trap 'echo "VARIABLE-TRACE> \$variable = \"$variable\""' DEBUG
# Affiche la valeur de $variable après chaque commande.

variable=29

echo "Initialisation de \"\$variable\" à $variable."

let "variable *= 3"
echo "Multiplication de \"\$variable\" avec 3."

#  La construction "trap 'commandes' DEBUG" serait plus utile dans le contexte
#+ d'un script plus complexe, où placer de nombreuses instructions
#+ "echo $variable" serait difficile et long.

# Merci, Stephane Chazelas pour cette information.

exit 0

Note

trap '' SIGNAL (deux apostrophes adjacentes) désactive SIGNAL pour le reste du script. trap SIGNAL restaure la fonctionnalité de SIGNAL. C'est utile pour protéger une portion critique d'un script d'une interruption indésirable.

	trap '' 2  # Le signal 2 est Control-C, maintenant désactivé.
	command
	command
	command
	trap 2     # Réactive Control-C
	

Notes

[1]

Le débogueur Bash de Rocky Bernstein comble légèrement ce manque.

[2]

Par convention, signal 0 est affecté à exit.

# # # 2) A living cell with either 2 or 3 living neighbors remains alive. # # 3) A dead cell with 3 living neighbors becomes alive (a "birth"). # SURVIVE=2 # BIRTH=3 # # 4) All other cases result in dead cells. # # ##################################################################### # startfile=gen0 # Read the starting generation from the file "gen0". # Default, if no other file specified when invoking script. # if [ -n "$1" ] # Specify another "generation 0" file. then if [ -e "$1" ] # Check for existence. then startfile="$1" fi fi ALIVE1=. DEAD1=_ # Represent living and "dead" cells in the start-up file. # This script uses a 10 x 10 grid (may be increased, #+ but a large grid will will cause very slow execution). ROWS=10 COLS=10 GENERATIONS=10 # How many generations to cycle through. # Adjust this upwards, #+ if you have time on your hands. NONE_ALIVE=80 # Exit status on premature bailout, #+ if no cells left alive. TRUE=0 FALSE=1 ALIVE=0 DEAD=1 avar= # Global; holds current generation. generation=0 # Initialize generation count. # ================================================================= let "cells = $ROWS * $COLS" # How many cells. declare -a initial # Arrays containing "cells". declare -a current display () { alive=0 # How many cells "alive". # Initially zero. declare -a arr arr=( `echo "$1"` ) # Convert passed arg to array. element_count=${#arr[*]} local i local rowcheck for ((i=0; i<$element_count; i++)) do # Insert newline at end of each row. let "rowcheck = $i % ROWS" if [ "$rowcheck" -eq 0 ] then echo # Newline. echo -n " " # Indent. fi cell=${arr[i]} if [ "$cell" = . ] then let "alive += 1" fi echo -n "$cell" | sed -e 's/_/ /g' # Print out array and change underscores to spaces. done return } IsValid () # Test whether cell coordinate valid. { if [ -z "$1" -o -z "$2" ] # Mandatory arguments missing? then return $FALSE fi local row local lower_limit=0 # Disallow negative coordinate. local upper_limit local left local right let "upper_limit = $ROWS * $COLS - 1" # Total number of cells. if [ "$1" -lt "$lower_limit" -o "$1" -gt "$upper_limit" ] then return $FALSE # Out of array bounds. fi row=$2 let "left = $row * $ROWS" # Left limit. let "right = $left + $COLS - 1" # Right limit. if [ "$1" -lt "$left" -o "$1" -gt "$right" ] then return $FALSE # Beyond row boundary. fi return $TRUE # Valid coordinate. } IsAlive () # Test whether cell is alive. # Takes array, cell number, state of cell as arguments. { GetCount "$1" $2 # Get alive cell count in neighborhood. local nhbd=$? if [ "$nhbd" -eq "$BIRTH" ] # Alive in any case. then return $ALIVE fi if [ "$3" = "." -a "$nhbd" -eq "$SURVIVE" ] then # Alive only if previously alive. return $ALIVE fi return $DEAD # Default. } GetCount () # Count live cells in passed cell's neighborhood. # Two arguments needed: # $1) variable holding array # $2) cell number { local cell_number=$2 local array local top local center local bottom local r local row local i local t_top local t_cen local t_bot local count=0 local ROW_NHBD=3 array=( `echo "$1"` ) let "top = $cell_number - $COLS - 1" # Set up cell neighborhood. let "center = $cell_number - 1" let "bottom = $cell_number + $COLS - 1" let "r = $cell_number / $ROWS" for ((i=0; i<$ROW_NHBD; i++)) # Traverse from left to right. do let "t_top = $top + $i" let "t_cen = $center + $i" let "t_bot = $bottom + $i" let "row = $r" # Count center row of neighborhood. IsValid $t_cen $row # Valid cell position? if [ $? -eq "$TRUE" ] then if [ ${array[$t_cen]} = "$ALIVE1" ] # Is it alive? then # Yes? let "count += 1" # Increment count. fi fi let "row = $r - 1" # Count top row. IsValid $t_top $row if [ $? -eq "$TRUE" ] then if [ ${array[$t_top]} = "$ALIVE1" ] then let "count += 1" fi fi let "row = $r + 1" # Count bottom row. IsValid $t_bot $row if [ $? -eq "$TRUE" ] then if [ ${array[$t_bot]} = "$ALIVE1" ] then let "count += 1" fi fi done if [ ${array[$cell_number]} = "$ALIVE1" ] then let "count -= 1" # Make sure value of tested cell itself fi #+ is not counted. return $count } next_gen () # Update generation array. { local array local i=0 array=( `echo "$1"` ) # Convert passed arg to array. while [ "$i" -lt "$cells" ] do IsAlive "$1" $i ${array[$i]} # Is cell alive? if [ $? -eq "$ALIVE" ] then # If alive, then array[$i]=. #+ represent the cell as a period. else array[$i]="_" # Otherwise underscore fi #+ (which will later be converted to space). let "i += 1" done # let "generation += 1" # Increment generation count. # Set variable to pass as parameter to "display" function. avar=`echo ${array[@]}` # Convert array back to string variable. display "$avar" # Display it. echo; echo echo "Generation $generation -- $alive alive" if [ "$alive" -eq 0 ] then echo echo "Premature exit: no more cells alive!" exit $NONE_ALIVE # No point in continuing fi #+ if no live cells. } # ========================================================= # main () # Load initial array with contents of startup file. initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '\n' |\ sed -e 's/\./\. /g' -e 's/_/_ /g'` ) # Delete lines containing '#' comment character. # Remove linefeeds and insert space between elements. clear # Clear screen. echo # Title echo "=======================" echo " $GENERATIONS generations" echo " of" echo "\"Life in the Slow Lane\"" echo "=======================" # -------- Display first generation. -------- Gen0=`echo ${initial[@]}` display "$Gen0" # Display only. echo; echo echo "Generation $generation -- $alive alive" # ------------------------------------------- let "generation += 1" # Increment generation count. echo # ------- Display second generation. ------- Cur=`echo ${initial[@]}` next_gen "$Cur" # Update & display. # ------------------------------------------ let "generation += 1" # Increment generation count. # ------ Main loop for displaying subsequent generations ------ while [ "$generation" -le "$GENERATIONS" ] do Cur="$avar" next_gen "$Cur" let "generation += 1" done # ============================================================== echo exit 0 # -------------------------------------------------------------- # The grid in this script has a "boundary problem". # The the top, bottom, and sides border on a void of dead cells. # Exercise: Change the script to have the grid wrap around, # + so that the left and right sides will "touch", # + as will the top and bottom.

Exemple A-12. Fichier de données pour le << Jeu de la Vie >>

# This is an example "generation 0" start-up file for "life.sh".
# --------------------------------------------------------------
#  The "gen0" file is a 10 x 10 grid using a period (.) for live cells,
#+ and an underscore (_) for dead ones. We cannot simply use spaces
#+ for dead cells in this file because of a peculiarity in Bash arrays.
#  [Exercise for the reader: explain this.]
#
# Lines beginning with a '#' are comments, and the script ignores them.
__.__..___
___._.____
____.___..
_._______.
____._____
..__...___
____._____
___...____
__.._..___
_..___..__

+++

Les deux scripts suivants sont de Mark Moraes de l'Université de Toronto. Voir le fichier joint << Moraes-COPYRIGHT >> pour les permissions et restrictions.

Exemple A-13. behead: Supprimer les en-têtes des courriers électroniques et des nouvelles

#! /bin/sh
# Supprime l'entête d'un message mail/news jusqu'à la première ligne vide.
# Mark Moraes, Université de Toronto

# ==> Ces commentaires sont ajoutés par l'auteur de ce document.

if [ $# -eq 0 ]; then
# ==> Si pas d'arguments en ligne de commande, alors fonctionne avec un
# ==> fichier redirigé vers stdin.
	sed -e '1,/^$/d' -e '/^[ 	]*$/d'
	# --> Supprime les lignes vides et les autres jusqu'à la première
	# --> commençant avec un espace blanc.
else
# ==> Si des arguments sont présents en ligne de commande, alors fonctionne avec
# ==> des fichiers nommés.
	for i do
		sed -e '1,/^$/d' -e '/^[ 	]*$/d' $i
		# --> De même.
	done
fi

# ==> Exercice: Ajouter la vérification d'erreurs et d'autres options.
# ==>
# ==> Notez que le petit script sed se répère à l'exception des arguments
# ==> passés.
# ==> Est-il intéressant de l'embarquer dans une fonction? Pourquoi?

Exemple A-14. ftpget: Télécharger des fichiers via ftp

#! /bin/sh 
# $Id: ftpget.sh,v 1.2 2003/11/02 17:21:35 guillaume Exp $ 
# Script pour réaliser une suite d'actions avec un ftp anonyme. Généralement,
# convertit une liste d'arguments de la ligne de commande en entrée vers ftp.
# Simple et rapide - écrit comme compagnon de ftplist 
# -h spécifie l'hôte distant (par défaut prep.ai.mit.edu) 
# -d spécifie le répertoire distant où se déplacer - vous pouvez spécifier une
# séquence d'options -d - elles seront exécutées chacune leur tour. Si les
# chemins sont relatifs, assurez-vous d'avoir la bonne séquence. Attention aux
# chemins relatifs, il existe bien trop de liens symboliques de nos jours.
# (par défaut, le répertoire au moment de la connexion)
# -v active l'option verbeux de ftp et affiche toutes les réponses du serveur
# ftp
# -f fichierdistant[:fichierlocal] récupère le fichier distant et le renomme en
# localfile 
# -m modele fait un mget suivant le modèle spécifié. Rappelez-vous de mettre
# entre guillemets les caractères shell.
# -c fait un cd local vers le répertoire spécifié
# Par exemple example, 
# 	ftpget -h expo.lcs.mit.edu -d contrib -f xplaces.shar:xplaces.sh \
#		-d ../pub/R3/fixes -c ~/fixes -m 'fix*' 
# récupèrera xplaces.shar à partir de ~ftp/contrib sur expo.lcs.mit.edu et
# l'enregistrera sous xplaces.sh dans le répertoire actuel, puis obtiendra
# tous les correctifs de ~ftp/pub/R3/fixes en les plaçant sous le répertoire
# ~/fixes.
# De façon évidente, la séquence des options est importante, car les commandes
# équivalentes sont exécutées par ftp dans le même ordre.
#
# Mark Moraes (moraes@csri.toronto.edu), Feb 1, 1989 
# ==> Les signes inférieur et supérieur ont été modifiés par des "parens" pour
# ==> éviter des soucis avec DocBook.
#


# ==> Ces commentaires ont été ajoutés par l'auteur de ce document.

# PATH=/local/bin:/usr/ucb:/usr/bin:/bin
# export PATH
# ==> Les deux lignes ci-dessus faisaient parti du script original et étaient
# ==> probablement inutiles

FICHIER_TEMPORAIRE=/tmp/ftp.$$
# ==> Crée un fichier temporaire, en utilisant l'identifiant du processus du
# ==> script ($$) pour construire le nom du fichier.

SITE=`domainname`.toronto.edu
# ==> 'domainname' est similaire à 'hostname'
# ==> Ceci pourrait être réécrit en ajoutant un paramètre ce qui rendrait son
# ==> utilisation plus générale.

usage="Usage: $0 [-h hotedisrant] [-d repertoiredistant]... [-f fichierdistant:fichierlocal]... \
		[-c repertoirelocal] [-m modele] [-v]"
optionsftp="-i -n"
verbflag=
set -f 		# So we can use globbing in -m
set x `getopt vh:d:c:m:f: $*`
if [ $? != 0 ]; then
	echo $usage
	exit 65
fi
shift
trap 'rm -f ${FICHIER_TEMPORAIRE} ; exit' 0 1 2 3 15
echo "user anonymous ${USER-gnu}@${SITE} > ${FICHIER_TEMPORAIRE}"
# ==> Ajout des guillemets (recommendé pour les echo complexes).
echo binary >> ${FICHIER_TEMPORAIRE}
for i in $*   # ==> Analyse les arguments de la ligne de commande.
do
	case $i in
	-v) verbflag=-v; echo hash >> ${FICHIER_TEMPORAIRE}; shift;;
	-h) hotedistant=$2; shift 2;;
	-d) echo cd $2 >> ${FICHIER_TEMPORAIRE}; 
	    if [ x${verbflag} != x ]; then
	        echo pwd >> ${FICHIER_TEMPORAIRE};
	    fi;
	    shift 2;;
	-c) echo lcd $2 >> ${FICHIER_TEMPORAIRE}; shift 2;;
	-m) echo mget "$2" >> ${FICHIER_TEMPORAIRE}; shift 2;;
	-f) f1=`expr "$2" : "\([^:]*\).*"`; f2=`expr "$2" : "[^:]*:\(.*\)"`;
	    echo get ${f1} ${f2} >> ${FICHIER_TEMPORAIRE}; shift 2;;
	--) shift; break;;
	esac
done
if [ $# -ne 0 ]; then
	echo $usage
	exit 65   # ==> Modifier de l'"exit 2" pour se conformer avec le standard.
fi
if [ x${verbflag} != x ]; then
	optionsftp="${optionsftp} -v"
fi
if [ x${hotedistant} = x ]; then
	hotedistant=prep.ai.mit.edu
	# ==> A réécrire pour utiliser votre site ftp favori.
fi
echo quit >> ${FICHIER_TEMPORAIRE}
# ==> Toutes les commandes sont sauvegardées dans fichier_temporaire.

ftp ${optionsftp} ${hotedistant} < ${FICHIER_TEMPORAIRE}
# ==> Maintenant, exécution par ftp de toutes les commandes contenues dans le
# ==> fichier fichier_temporaire.

rm -f ${FICHIER_TEMPORAIRE}
# ==> Enfin, fichier_temporaire est supprimé (vous pouvez souhaiter le copier
# ==> dans un journal).


# ==> Exercices:
# ==> ---------
# ==> 1) Ajouter une vérification d'erreurs.
# ==> 2) Ajouter des tas de trucs.

+

Antek Sawicki a contribué avec le script suivant, qui fait une utilisation très intelligente des opérateurs de substitution de paramètres discutés dans la Section 9.3.

Exemple A-15. password: Générer des mots de passe aléatoires de 8 caractères

#!/bin/bash
#  Pourrait avoir besoin d'être appelé avec un #!/bin/bash2 sur les anciennes
#+ machines.
#
#  Générateur de mots de passe aléatoires pour bash 2.x
#+ par Antek Sawicki <tenox@tenox.tc>,
#  qui a généreusement permis à l'auteur de ce document de l'utiliser ici.
#
# ==> Commentaires ajoutés par l'auteur du document ==>


MATRICE="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
LONGUEUR="8"
# ==> Modification possible de 'LONGUEUR' pour des mots de passe plus longs.


while [ "${n:=1}" -le "$LONGUEUR" ]
# ==> Rappelez-vous que := est l'opérateur de "substitution par défaut".
# ==> Donc, si 'n' n'a pas été initialisé, l'initialisez à 1.
do
	PASS="$PASS${MATRICE:$(($RANDOM%${#MATRICE})):1}"
	# ==> Très intelligent, pratiquement trop astucieux.

	# ==> Commençons par le plus intégré...
	# ==> ${#MATRICE} renvoie la longueur du tableau MATRICE.

	# ==> $RANDOM%${#MATRICE} renvoie un nombre aléatoire entre 1 et la
	# ==> longueur de MATRICE - 1.

	# ==> ${MATRICE:$(($RANDOM%${#MATRICE})):1}
	# ==> renvoie l'expansion de MATRICE à une position aléatoire, par
	# ==> longueur 1. 
	# ==> Voir la substitution de paramètres {var:pos:len}, section 3.3.1
	# ==> et les exemples suivants.

	# ==> PASS=... copie simplement ce résultat dans PASS (concaténation).

	# ==> Pour mieux visualiser ceci, décommentez la ligne suivante
	# ==>             echo "$PASS"
	# ==> pour voir la construction de PASS, un caractère à la fois,
	# ==> à chaque itération de la boucle.

	let n+=1
	# ==> Incrémentez 'n' pour le prochain tour.
done

echo "$PASS"      # ==> Ou, redirigez le fichier, comme voulu.

exit 0

+

James R. Van Zandt a contribué avec ce script, qui utilise les tubes nommés et, ce sont ses mots, << really exercises quoting and escaping >>.

Exemple A-16. fifo: Faire des sauvegardes journalières, en utilisant des tubes nommés

#!/bin/bash
# ==> Script de James R. Van Zandt, et utilisé ici avec sa permission.

# ==> Commentaires ajoutés par l'auteur de ce document.

  
  ICI=`uname -n`    # ==> nom d'hôte
  LA_BAS=bilbo
  echo "début de la sauvegarde distabte vers $LA_BAS à `date +%r`"
  # ==> `date +%r` renvoie l'heure en un format sur 12 heures, par exempe
  # ==> "08:08:34 PM".
  
  #  Assurez-vous que /pipe est réellement un tube et non pas un fichier
  #+ standard.
  rm -rf /tube
  mkfifo /tube       # ==> Crée un fichier "tube nommé", nommé "/tube".
  
  # ==> 'su xyz' lance les commandes en tant qu'utilisateur "xyz".
  # ==> 'ssh' appele le shell sécurisé (client de connexion à distance).
  su xyz -c "ssh $LA_BAS \"cat >/home/xyz/sauve/${ICI}-jour.tar.gz\" < /tube"&
  cd /
  tar -czf - bin boot dev etc home info lib man root sbin share usr var >/tube
  # ==> Utilise un tube nommé, /tube, pour communiquer entre processus:
  # ==> 'tar/gzip' écrit dans le tube et 'ssh' lit /tube.

  #  ==> Le résultat final est que cela sauvegarde les répertoires principaux;
  #+ ==> à partir de /.

  # ==> Quels sont les avantages d'un "tube nommé" dans cette situation,
  # ==> en opposition avec le "tube anonyme", avec |?
  # ==> Est-ce qu'un tube anonyme pourrait fonctionner ici?


  exit 0

+

Stephane Chazelas a contribué avec le script suivant pour démontrer que générer des nombres aléatoires ne requiert pas de tableaux.

Exemple A-17. primes: Générer des nombres aléatoires en utilisant l'opérateur modulo

#!/bin/bash
# primes.sh: Génère des nombres premiers, sans utiliser des tableaux.
# Script contribué par Stephane Chazelas.

#  Il n'utilise *pas* l'algorithme classique "Sieve of Eratosthenes",
#+ mais utilise à la palce la méthode plus intuitive de test de chaque nombre
#+ candidat pour les facteurs (diviseurs), en utilisant l'opérateur modulo "%".


LIMITE=1000                    # Premiers de 2 à 1000

Premiers()
{
 (( n = $1 + 1 ))             # Va au prochain entier.
 shift                        # Prochain paramètre dans la liste.
#  echo "_n=$n i=$i_"
 
 if (( n == LIMITE ))
 then echo $*
 return
 fi

 for i; do                    #  "i" est initialisé à "@", les précédentes
                              #+ valeurs de $n.
#   echo "-n=$n i=$i-"
   (( i * i > n )) && break   # Optimisation.
   (( n % i )) && continue    #  Passe les non premiers en utilisant l'opérateur
                              #+ modulo.
   Premiers $n $@             # Récursion à l'intérieur de la boucle.
   return
   done

   Premiers $n $@ $n          # Récursion à l'extérieur de la boucle.
                              #  Accumule successivement les paramètres de
			      #+ position.
                              # "$@" est la liste des premiers accumulés.
}

Premiers 1

exit 0

# Décommenter les lignes 17 et 25 pour vous aider à comprendre ce qui se passe.

# Comparez la vitesse de cet algorithme de génération des nombres premiers avec
# celui de "Sieve of Eratosthenes" (ex68.sh).

# Exercice: Réécrivez ce script sans récursion, pour une exécution plus rapide.

+

Jordi Sanfeliu a donné sa permission pour utiliser son élégant script sur les arborescences.

Exemple A-18. tree: Afficher l'arborescence d'un répertoire

#!/bin/sh
#         @(#) tree      1.1  30/11/95       by Jordi Sanfeliu
#                                         email: mikaku@arrakis.es
#
#     Version initiale :  1.0  30/11/95
#     Prochaine version:  1.1  24/02/97   Maintenant avec des liens symboliques
#     Corrigé par      :  Ian Kjos, pour supporter les répertoires non dispo
#                         email: beth13@mail.utexas.edu
#
#         Tree est un outil pour visualiser la hiérarchie d'un répertoire
#

# ==> Le script 'Tree' utilisé ici avec la permission de son auteur, Jordi Sanfeliu.
# ==> Commentaires ajoutés par l'auteur de ce document.
# ==> Ajout des guillemets pour les arguments.


search () {
   for dir in `echo *`
   # ==> `echo *` affiche tous les fichiers du répertoire actuel sans retour à
   # ==> la ligne.
   # ==> Même effet que     for dir in *
   # ==> mais "dir in `echo *`" ne gère pas les noms de fichiers comprenant des
   # ==> espaces blancs.
   do
      if [ -d "$dir" ] ; then   # ==> S'il s'agit d'un répertoire (-d)...
         zz=0   # ==> Variable temporaire, pour garder trace du niveau de
                # ==> répertoire.
         while [ $zz != $deep ]    # Conserve la trace de la boucle interne.
         do
            echo -n "|   "    # ==> Affiche le symbôle du connecteur vertical
	                      # ==> avec 2 espaces mais pas de retour à la ligne
                              # ==> pour l'indentation.
            zz=`expr $zz + 1` # ==> Incrémente zz.
         done
         if [ -L "$dir" ] ; then   # ==> Si le répertoire est un lien symbolique...
            echo "+---$dir" `ls -l $dir | sed 's/^.*'$dir' //'`
	    # ==> Affiche le connecteur horizontal et affiche le nom du
            # ==> répertoire mais...
	    # ==> supprime la partie date/heure.
         else
            echo "+---$dir"      # ==> Affiche le symbole du connecteur
                                 # ==> horizontal et le nom du répertoire.
            if cd "$dir" ; then  # ==> S'il peut se déplacer dans le sous-répertoire...
               deep=`expr $deep + 1`   # ==> Incrémente la profondeur.
               search     # avec la récursivité ;-)
	                  # ==> La fonction s'appelle elle-même.
               numdirs=`expr $numdirs + 1`   # ==> Incrémente le compteur de
	                                     # ==> répertoires.
            fi
         fi
      fi
   done
   cd ..   # ==> Se placer un niveau au-dessus.
   if [ "$deep" ] ; then  # ==> Si la profondeur est nulle (renvoie TRUE)...
      swfi=1              # ==> initialiser l'indicateur indiquant que la
                          # ==> recherche est terminée.
   fi
   deep=`expr $deep - 1`  # ==> Décrémente la profondeur.
}

# - Principal -
if [ $# = 0 ] ; then
   cd `pwd` # ==> Pas d'arguments au script, alors utilise le répertoire actuel.
else
   cd $1    # ==> Sinon, va dans le répertoire indiqué.
fi
echo "Répertoire initial = `pwd`"
swfi=0      # ==> Indicateur de terminaison.
deep=0      # ==> Profondeur de la liste.
numdirs=0
zz=0

while [ "$swfi" != 1 ]   # Tant que l'indicateur n'est pas initialisé
do
   search   # ==> Appelle la fonctione après avoir initialisé les variables.
done
echo "Nombre total de répertoires = $numdirs"

exit 0
# ==> Challenge: essayez de comprendre comment fonctionne ce script

+

Noah Friedman a donné sa permission pour utiliser son script contenant des fonctions sur les chaînes de caractères, qui reproduit les fonctions de manipulations de la bibliothèque C string.

Exemple A-19. string: Manipuler les chaînes de caractères comme en C

#!/bin/bash

# string.bash --- bash emulation of string(3) library routines
# Author: Noah Friedman <friedman@prep.ai.mit.edu>
# ==>     Used with his kind permission in this document.
# Created: 1992-07-01
# Last modified: 1993-09-29
# Public domain

# Conversion to bash v2 syntax done by Chet Ramey

# Commentary:
# Code:

#:docstring strcat:
# Usage: strcat s1 s2
#
# Strcat appends the value of variable s2 to variable s1. 
#
# Example:
#    a="foo"
#    b="bar"
#    strcat a b
#    echo $a
#    => foobar
#
#:end docstring:

###;;;autoload   ==> Autoloading of function commented out.
function strcat ()
{
    local s1_val s2_val

    s1_val=${!1}                        # indirect variable expansion
    s2_val=${!2}
    eval "$1"=\'"${s1_val}${s2_val}"\'
    # ==> eval $1='${s1_val}${s2_val}' avoids problems,
    # ==> if one of the variables contains a single quote.
}

#:docstring strncat:
# Usage: strncat s1 s2 $n
# 
# Line strcat, but strncat appends a maximum of n characters from the value
# of variable s2.  It copies fewer if the value of variabl s2 is shorter
# than n characters.  Echoes result on stdout.
#
# Example:
#    a=foo
#    b=barbaz
#    strncat a b 3
#    echo $a
#    => foobar
#
#:end docstring:

###;;;autoload
function strncat ()
{
    local s1="$1"
    local s2="$2"
    local -i n="$3"
    local s1_val s2_val

    s1_val=${!s1}                       # ==> indirect variable expansion
    s2_val=${!s2}

    if [ ${#s2_val} -gt ${n} ]; then
       s2_val=${s2_val:0:$n}            # ==> substring extraction
    fi

    eval "$s1"=\'"${s1_val}${s2_val}"\'
    # ==> eval $1='${s1_val}${s2_val}' avoids problems,
    # ==> if one of the variables contains a single quote.
}

#:docstring strcmp:
# Usage: strcmp $s1 $s2
#
# Strcmp compares its arguments and returns an integer less than, equal to,
# or greater than zero, depending on whether string s1 is lexicographically
# less than, equal to, or greater than string s2.
#:end docstring:

###;;;autoload
function strcmp ()
{
    [ "$1" = "$2" ] && return 0

    [ "${1}" '<' "${2}" ] > /dev/null && return -1

    return 1
}

#:docstring strncmp:
# Usage: strncmp $s1 $s2 $n
# 
# Like strcmp, but makes the comparison by examining a maximum of n
# characters (n less than or equal to zero yields equality).
#:end docstring:

###;;;autoload
function strncmp ()
{
    if [ -z "${3}" -o "${3}" -le "0" ]; then
       return 0
    fi
   
    if [ ${3} -ge ${#1} -a ${3} -ge ${#2} ]; then
       strcmp "$1" "$2"
       return $?
    else
       s1=${1:0:$3}
       s2=${2:0:$3}
       strcmp $s1 $s2
       return $?
    fi
}

#:docstring strlen:
# Usage: strlen s
#
# Strlen returns the number of characters in string literal s.
#:end docstring:

###;;;autoload
function strlen ()
{
    eval echo "\${#${1}}"
    # ==> Returns the length of the value of the variable
    # ==> whose name is passed as an argument.
}

#:docstring strspn:
# Usage: strspn $s1 $s2
# 
# Strspn returns the length of the maximum initial segment of string s1,
# which consists entirely of characters from string s2.
#:end docstring:

###;;;autoload
function strspn ()
{
    # Unsetting IFS allows whitespace to be handled as normal chars. 
    local IFS=
    local result="${1%%[!${2}]*}"
 
    echo ${#result}
}

#:docstring strcspn:
# Usage: strcspn $s1 $s2
#
# Strcspn returns the length of the maximum initial segment of string s1,
# which consists entirely of characters not from string s2.
#:end docstring:

###;;;autoload
function strcspn ()
{
    # Unsetting IFS allows whitspace to be handled as normal chars. 
    local IFS=
    local result="${1%%[${2}]*}"
 
    echo ${#result}
}

#:docstring strstr:
# Usage: strstr s1 s2
# 
# Strstr echoes a substring starting at the first occurrence of string s2 in
# string s1, or nothing if s2 does not occur in the string.  If s2 points to
# a string of zero length, strstr echoes s1.
#:end docstring:

###;;;autoload
function strstr ()
{
    # if s2 points to a string of zero length, strstr echoes s1
    [ ${#2} -eq 0 ] && { echo "$1" ; return 0; }

    # strstr echoes nothing if s2 does not occur in s1
    case "$1" in
    *$2*) ;;
    *) return 1;;
    esac

    # use the pattern matching code to strip off the match and everything
    # following it
    first=${1/$2*/}

    # then strip off the first unmatched portion of the string
    echo "${1##$first}"
}

#:docstring strtok:
# Usage: strtok s1 s2
#
# Strtok considers the string s1 to consist of a sequence of zero or more
# text tokens separated by spans of one or more characters from the
# separator string s2.  The first call (with a non-empty string s1
# specified) echoes a string consisting of the first token on stdout. The
# function keeps track of its position in the string s1 between separate
# calls, so that subsequent calls made with the first argument an empty
# string will work through the string immediately following that token.  In
# this way subsequent calls will work through the string s1 until no tokens
# remain.  The separator string s2 may be different from call to call.
# When no token remains in s1, an empty value is echoed on stdout.
#:end docstring:

###;;;autoload
function strtok ()
{
 :
}

#:docstring strtrunc:
# Usage: strtrunc $n $s1 {$s2} {$...}
#
# Used by many functions like strncmp to truncate arguments for comparison.
# Echoes the first n characters of each string s1 s2 ... on stdout. 
#:end docstring:

###;;;autoload
function strtrunc ()
{
    n=$1 ; shift
    for z; do
        echo "${z:0:$n}"
    done
}

# provide string

# string.bash ends here


# ========================================================================== #
# ==> Everything below here added by the document author.

# ==> Suggested use of this script is to delete everything below here,
# ==> and "source" this file into your own scripts.

# strcat
string0=one
string1=two
echo
echo "Testing \"strcat\" function:"
echo "Original \"string0\" = $string0"
echo "\"string1\" = $string1"
strcat string0 string1
echo "New \"string0\" = $string0"
echo

# strlen
echo
echo "Testing \"strlen\" function:"
str=123456789
echo "\"str\" = $str"
echo -n "Length of \"str\" = "
strlen str
echo



# Exercise:
# --------
# Add code to test all the other string functions above.


exit 0

+

+

Stephane Chazelas montre la programmation objet dans un script Bash.

Exemple A-20. obj-oriented: Bases de données orientées objet

#!/bin/bash
# obj-oriented.sh: programmation orienté objet dans un script shell.
# Script par Stephane Chazelas.


person.new()        # Ressemble à la déclaration d'une classe en C++.
{
  local nom_objet=$1 nom=$2 prenom=$3 datenaissance=$4

  eval "$nom_objet.set_nom() {
          eval \"$nom_objet.get_nom() {
                   echo \$1
                 }\"
        }"

  eval "$nom_objet.set_prenom() {
          eval \"$nom_objet.get_prenom() {
                   echo \$1
                 }\"
        }"

  eval "$nom_objet.set_datenaissance() {
          eval \"$nom_objet.get_datenaissance() {
            echo \$1
          }\"
          eval \"$nom_objet.show_datenaissance() {
            echo \$(date -d \"1/1/1970 0:0:\$1 GMT\")
          }\"
          eval \"$nom_objet.get_age() {
            echo \$(( (\$(date +%s) - \$1) / 3600 / 24 / 365 ))
          }\"
        }"

  $nom_objet.set_nom $nom
  $nom_objet.set_prenom $prenom
  $nom_objet.set_datenaissance $datenaissance
}

echo

person.new self Bozeman Bozo 101272413
#  Crée une instance de "person.new" (en fait, passe les arguments à la
#+ fonction).

self.get_prenom              #   Bozo
self.get_nom                 #   Bozeman
self.get_age                 #   28
self.get_datenaissance       #   101272413
self.show_datenaissance      #   Sat Mar 17 20:13:33 MST 1973

echo

# typeset -f
# pour voir les fonctions créées (attention, cela fait défiler la page).

exit 0