A. Contribution de scripts

Ces scripts, bien que ne rentrant pas dans le texte de ce document, illustrent quelques techniques intéressantes de programmation shell. Ils sont aussi utiles. Amusez-vous à les analyser et à les lancer.

Exemple A.1. mailformat : Formater un courrier électronique

#!/bin/bash
#  mail-format.sh (ver. 1.1) : Formate les courriers électroniques.

#  Supprime les caractères '>', les tabulations et coupe aussi les lignes
#+ excessivement longues.

# =================================================================
#                 Vérification standard des argument(s) du script
ARGS=1
E_MAUVAISARGS=65
E_PASDEFICHIER=66

if [ $# -ne $ARGS ]  # Le bon nombre d'arguments a-t'il été passé au script?
then
  echo "Usage: `basename $0` nomfichier"
  exit $E_MAUVAISARGS
fi

if [ -f "$1" ]       # Vérifie si le fichier existe.
then
    nomfichier=$1
else
    echo "Le fichier \"$1\" n'existe pas."
    exit $E_PASDEFICHIER
fi
# =================================================================

LONGUEUR_MAX=70
# Longueur à partir de laquelle on coupe les lignes excessivement longues.

# ---------------------------------
# Une variable peut contenir un script sed.
scriptsed='s/^>//
s/^  *>//
s/^  *//
s/              *//'
# ---------------------------------

#  Supprime les caractères '>' et tabulations en début de lignes,
#+ puis coupe les lignes à $LONGUEUR_MAX caractères.
sed "$scriptsed" $1 | fold -s --width=$LONGUEUR_MAX
#  option -s pour couper les lignes à une espace blanche, si possible.


#  Ce script a été inspiré par un article d'un journal bien connu
#+ proposant un utilitaire Windows de 164Ko pour les mêmes fonctionnalités.
#
#  Un joli ensemble d'utilitaires de manipulation de texte et un langage de
#+ scripts efficace apportent une alternative à des exécutables gonflés.

exit 0

Exemple A.2. rn : Un utilitaire simple pour renommer des fichiers

Ce script est une modification de l'Exemple 15.20, « lowercase : Change tous les noms de fichier du répertoire courant en minuscule. ».

#! /bin/bash
#
# Un très simplifié "renommeur" de fichiers (basé sur "lowercase.sh").
#
#  L'utilitaire "ren", par Vladimir Lanin (lanin@csd2.nyu.edu),
#+ fait un bien meilleur travail que ceci.


ARGS=2
E_MAUVAISARGS=65
UN=1                   # Pour avoir correctement singulier ou pluriel
                       # (voir plus bas.)

if [ $# -ne "$ARGS" ]
then
  echo "Usage: `basename $0` ancien-modele nouveau-modele"
  #  Comme avec "rn gif jpg", qui renomme tous les fichiers gif du répertoire
  #+ courant en jpg.
  exit $E_MAUVAISARGS
fi

nombre=0               # Garde la trace du nombre de fichiers renommés.


for fichier in *$1*    # Vérifie tous les fichiers correspondants du répertoire.
do
   if [ -f "$fichier" ]  # S'il y a correspondance...
   then
     fname=`basename $fichier`             # Supprime le chemin.
     n=`echo $fname | sed -e "s/$1/$2/"`   # Substitue ancien par nouveau dans
                                           # le fichier.
     mv $fname $n                          # Renomme.
     let "nombre += 1"
   fi
done   

if [ "$nombre" -eq "$UN" ]                # Pour une bonne grammaire.
then
  echo "$nombre fichier renommé."
else 
  echo "$nombre fichiers renommés."
fi 

exit 0


# Exercices:
# ---------
# Avec quel type de fichiers cela ne fonctionnera pas?
# Comment corriger cela?
#
#  Réécrire ce script pour travailler sur tous les fichiers d'un répertoire,
#+ contenant des espaces dans leur noms, et en les renommant après avoir
#+ substitué chaque espace par un tiret bas.

Exemple A.3. blank-rename : Renommer les fichiers dont le nom contient des espaces

C'est une version encore plus simple du script précédent.

#! /bin/bash
# blank-rename.sh
#
#  Substitue les tirets soulignés par des blancs dans tous les fichiers d'un
#+ répertoire.

UN=1                     # Pour obtenir le singulier/pluriel correctement (voir
                         # plus bas).
nombre=0                 # Garde trace du nombre de fichiers renommés.
TROUVE=0                 # Valeur de retour en cas de succès.

for fichier in *         #Traverse tous les fichiers du répertoire.
do
     echo "$fichier" | grep -q " "         #  Vérifie si le nom du fichier
     if [ $? -eq $TROUVE ]                 #+ contient un (des) espace(s).
     then
       nomf=$fichier                       # Oui, ce nom de fichier doit être travaillé.
       n=`echo $nomf | sed -e "s/ /_/g"`   # Remplace l'espace par un tiret.
       mv "$nomf" "$n"                     # Réalise le renommage.
       let "nombre += 1"
     fi
done   

if [ "$nombre" -eq "$UN" ]                 # Pour une bonne grammaire.
then
  echo "$nombre fichier renommé."
else 
  echo "$nombre fichiers renommés."
fi 

exit 0

Exemple A.4. encryptedpw : Charger un fichier sur un site ftp, en utilisant un mot de passe crypté en local

#!/bin/bash

# Exemple "ex72.sh" modifié pour utiliser les mots de passe cryptés.

#  Notez que c'est toujours moyennement sécurisé, car le mot de passe décrypté
#+ est envoyé en clair.
#  Utilisez quelque chose comme "ssh" si cela vous préoccupe.

E_MAUVAISARGS=65

if [ -z "$1" ]
then
  echo "Usage: `basename $0` nomfichier"
  exit $E_MAUVAISARGS
fi  

NomUtilisateur=bozo      # Changez suivant vos besoins.
motpasse=/home/bozo/secret/fichier_avec_mot_de_passe_crypte
# Le fichier contient un mot de passe crypté.

Nomfichier=`basename $1` # Supprime le chemin du fichier

Serveur="XXX"            #  Changez le nom du serveur et du répertoire suivant
Repertoire="YYY"         #+ vos besoins.


MotDePasse=`cruft <$motpasse`          # Décrypte le mot de passe.
#  Utilise le paquetage de cryptage de fichier de l'auteur,
#+ basé sur l'algorithme classique "onetime pad",
#+ et disponible à partir de:
#+ Site primaire:  ftp://ibiblio.org/pub/Linux/utils/file
#+                 cruft-0.2.tar.gz [16k]


ftp -n $Serveur <<Fin-de-Session
user $NomUtilisateur $MotDePasse
binary
bell
cd $Repertoire
put $Nomfichier
bye
Fin-de-Session
# L'option -n de "ftp" désactive la connexion automatique.
# Notez que "bell" fait sonner une cloche après chaque transfert.

exit 0

Exemple A.5. copy-cd : Copier un CD de données

#!/bin/bash
# copy-cd.sh: copier un CD de données

CDROM=/dev/cdrom                           # périphérique CD ROM
OF=/home/bozo/projects/cdimage.iso         # fichier de sortie
#       /xxxx/xxxxxxx/                     A modifier suivant votre système.
TAILLEBLOC=2048
VITESSE=2                                  # Utiliser une vitesse supèrieure
                                           #+ si elle est supportée.
PERIPHERIQUE=cdrom
#PERIPHERIQUE="0,0" pour les anciennes versions de cdrecord

echo; echo "Insérez le CD source, mais ne le montez *pas*."
echo "Appuyez sur ENTER lorsque vous êtes prêt. "
read pret                                  # Attendre une entrée, $pret n'est
                                           # pas utilisé.

echo; echo "Copie du CD source vers $OF."
echo "Ceci peut prendre du temps. Soyez patient."

dd if=$CDROM of=$OF bs=$TAILLEBLOC         # Copie brute du périphérique.


echo; echo "Retirez le CD de données."
echo "Insérez un CDR vierge."
echo "Appuyez sur ENTER lorsque vous êtes prêt. "
read pret                                  # Attendre une entrée, $pret n'est
                                           # pas utilisé.

echo "Copie de $OF vers CDR."

cdrecord -v -isosize speed=$VITESSE dev=$PERIPHERIQUE $OF
# Utilise le paquetage "cdrecord" de Joerg Schilling's (voir sa doc).
# http://www.fokus.gmd.de/nthp/employees/schilling/cdrecord.html


echo; echo "Copie terminée de $OF vers un CDR du périphérique $CDROM."

echo "Voulez-vous écraser le fichier image (o/n)? "  # Probablement un fichier
                                                     # immense.
read reponse

case "$reponse" in
[oO]) rm -f $OF
      echo "$OF supprimé."
      ;;
*)    echo "$OF non supprimé.";;
esac

echo

#  Exercice:
#  Modifiez l'instruction "case" pour aussi accepter "oui" et "Oui" comme
#+ entrée.

exit 0

Exemple A.6. collatz : Séries de Collatz

#!/bin/bash
# collatz.sh

#  Le célèbre "hailstone" ou la série de Collatz.
#  ----------------------------------------------
#  1) Obtenir un entier "de recherche" à partir de la ligne de commande.
#  2) NOMBRE &lt;--- seed
#  3) Afficher NOMBRE.
#  4)  Si NOMBRE est pair, divisez par 2, ou
#  5)+ si impair, multiplier par 3 et ajouter 1.
#  6) NOMBRE &lt;--- résultat
#  7) Boucler à l'étape 3 (pour un nombre spécifié d'itérations).
#
#  La théorie est que chaque séquence, quelle soit la valeur initiale,
#+ se stabilisera éventuellement en répétant des cycles "4,2,1...",
#+ même après avoir fluctuée à travers un grand nombre de valeurs.
#
#  C'est une instance d'une "itération", une opération qui remplit son
#+ entrée par sa sortie.
#  Quelque fois, le résultat est une série "chaotique".


MAX_ITERATIONS=200
# Pour une grande échelle de nombre (&gt;32000), augmenter MAX_ITERATIONS.

h=${1:-$$}                      #  Nombre de recherche
                                #  Utiliser $PID comme nombre de recherche,
                                #+ si il n'est pas spécifié en argument de la
                                #+ ligne de commande.

echo
echo "C($h) --- $MAX_ITERATIONS Iterations"
echo

for ((i=1; i<=MAX_ITERATIONS; i++))
do

echo -n "$h     "
#          ^^^^^
#           tab

  let "reste = h % 2"
  if [ "$reste" -eq 0 ]       # Pair?
  then
    let "h /= 2"              # Divise par 2.
  else
    let "h = h*3 + 1"         # Multiplie par 3 et ajoute 1.
  fi


COLONNES=10                   # Sortie avec 10 valeurs par ligne.
let "retour_ligne = i % $COLONNES"
if [ "$retour_ligne" -eq 0 ]
then
  echo
fi  

done

echo

#  Pour plus d'informations sur cette fonction mathématique,
#+ voir _Computers, Pattern, Chaos, and Beauty_, par Pickover, p. 185 ff.,
#+ comme listé dans la bibliographie.

exit 0

Exemple A.7. days-between : Calculer le nombre de jours entre deux dates

#!/bin/bash
# days-between.sh:    Nombre de jours entre deux dates.
# Usage: ./days-between.sh [M]M/[D]D/AAAA [M]M/[D]D/AAAA
#
# Note: Script modifié pour tenir compte des changements dans Bash 2.05b +
#+      qui ont fermé la "fonctionnalité" permettant de renvoyer des valeurs
#+      entières négatives grandes.

ARGS=2                # Deux arguments attendus en ligne de commande.
E_PARAM_ERR=65        # Erreur de paramètres.

ANNEEREF=1600         # Année de référence.
SIECLE=100
JEA=365
AJUST_DIY=367         # Ajusté pour l'année bissextile + fraction.
MEA=12
JEM=31
CYCLE=4

MAXRETVAL=256         #  Valeur de retour positive la plus grande possible
                      #+ renvoyée par une fonction.

diff=                 #  Déclaration d'une variable globale pour la différence
                      #+ de date.
value=                #  Déclaration d'une variable globale pour la valeur
                      #+ absolue.
jour=                 #  Déclaration de globales pour jour, mois, année.
mois=
annee=


Erreur_Param ()        # Mauvais paramètres en ligne de commande.
{
  echo "Usage: `basename $0` [M]M/[D]D/YYYY [M]M/[D]D/YYYY"
  echo "       (la date doit être supérieure au 1/3/1600)"
  exit $E_PARAM_ERR
}  


Analyse_Date ()                #  Analyse la date à partir des paramètres en
{                              #+ ligne de commande.
  mois=${1%%/**}
  jm=${1%/**}                  # Jour et mois.
  jour=${dm#*/}
  let "annee = `basename $1`"  #  Pas un nom de fichier mais fonctionne de la
                               #+ même façon.
}  


verifie_date ()                 # Vérifie la validité d'une date.
{
  [ "$jour" -gt "$JEM" ] || [ "$mois" -gt "$MEA" ] ||
  [ "$annee" -lt "$ANNEEREF" ] && Erreur_Param
  # Sort du script si mauvaise(s) valeur(s).
  # Utilise une liste-ou ou une liste-et.
  #
  # Exercice: Implémenter une vérification de date plus rigoureuse.
}


supprime_zero_devant () #  Il est préférable de supprimer les zéros possibles
{                       #+ du jour et/ou du mois sinon Bash va les
  val=${1#0}            #+ interpréter comme des valeurs octales
  return $val           #+ (POSIX.2, sect 2.9.2.1).
}


index_jour ()         # Formule de Gauss:
{                     # Nombre de jours du 1er mars 1600 jusqu'à la date passée
                      # en arguments.

  jour=$1
  mois=$2
  annee=$3

  let "mois = $mois - 2"
  if [ "$mois" -le 0 ]
  then
    let "mois += 12"
    let "annee -= 1"
  fi  

  let "annee -= $ANNEEREF"
  let "indexyr = $annee / $SIECLE"


  let "Jours = $JEA*$annee + $annee/$CYCLE - $indexyr \
               + $indexyr/$CYCLE + $AJUST_DIY*$mois/$MEA + $jour - $JEM"
  # Pour une explication en détails de cet algorithme, voir
  #+   http://weblogs.asp.net/pgreborio/archive/2005/01/06/347968.aspx

  echo $Days

}  


calcule_difference ()              # Différence entre les indices de deux jours.
{
  let "diff = $1 - $2"             # Variable globale.
}  


abs ()                             #  Valeur absolue.
{                                  #  Utilise une variable globale "valeur".
  if [ "$1" -lt 0 ]                #  Si négatif
  then                             #+ alors
    let "value = 0 - $1"           #+ change de signe,
  else                             #+ sinon
    let "value = $1"               #+ on le laisse.
  fi
}



if [ $# -ne "$ARGS" ]            # Requiert deux arguments en ligne de commande.
then
  Erreur_Param
fi  

Analyse_Date $1
verifie_date $jour $mois $annee      #  Vérifie si la date est valide.

supprime_zero_devant $jour           #  Supprime tout zéro débutant
jour=$?                              #+ sur le jour et/ou le mois.
supprime_zero_devant $mois
mois=$?

let "date1 = `day_index $jour $mois $annee`"

Analyse_Date $2
verifie_date $jour $mois $annee

supprime_zero_devant $jour
jour=$?
supprime_zero_devant $mois
mois=$?

date2 = $(day_index $jour $mois $annee)  # Substitution de commande


calcule_difference $date1 $date2

abs $diff                          # S'assure que c'est positif.
diff=$value

echo $diff

exit 0
#  Comparez ce script avec l'implémentation de la formule de Gauss en C sur
#+ http://buschencrew.hypermart.net/software/datedif

Exemple A.8. makedict : Créer un « dictionnaire »

#!/bin/bash
# makedict.sh  [make dictionary]

# Modification du script /usr/sbin/mkdict.
# Script original copyright 1993, par Alec Muffett.
#
#  Ce script modifié inclus dans ce document d'une manière consistente avec le
#+ document "LICENSE" du paquetage "Crack" dont fait partie le script original.

#  Ce script manipule des fichiers texte pour produire une liste triée de mots
#+ trouvés dans les fichiers.
#  Ceci pourrait être utile pour compiler les dictionnaires et pour des
#+ recherches lexicographiques.


E_MAUVAISARGS=65

if [ ! -r "$1" ]                     #  Au moins un argument, qui doit être
then                                 #+ un fichier valide.
        echo "Usage: $0 fichiers-à-manipuler"
  exit $E_MAUVAISARGS
fi  


# SORT="sort"                        #  Plus nécessaire de définir des options
                                               #+ pour sort. Modification du script
                                                               #+ original.

cat $* |                            # Contenu des fichiers spécifiés vers stdout.
        tr A-Z a-z |               # Convertion en minuscule.
        tr ' ' '\012' |              #  Nouveau: modification des espaces en
                                        #+ retours chariot.
#       tr -cd '\012[a-z][0-9]' |    #  Suppression de tout ce qui n'est pas
                                                 #  alphanumérique
                                                 #+ (script original).
        tr -c '\012a-z'  '\012' |    #  Plutôt que de supprimer,
                                              #+ modification des non alpha en retours
                                                              #+ chariot.
        sort |                       #  Les options $SORT ne sont plus
                                           #+ nécessaires maintenant.
        uniq |                       # Suppression des mots dupliqués.
        grep -v '^#' |               #  Suppression des lignes commençant avec
                                                #+ le symbole '#'.
        grep -v '^$'                 # Suppression des lignes blanches.

exit 0  

Exemple A.9. soundex : Conversion phonétique

#!/bin/bash
# soundex.sh: Calcule le code "soundex" pour des noms

# =======================================================
#        Script soundex
#              par
#         Mendel Cooper
#     thegrendel@theriver.com
#       23 Janvier 2002
#
#   Placé dans le domaine public.
#
#  Une version légèrement différente de ce script est apparu dans
#+ la colonne "Shell Corner" d'Ed Schaefer en juillet 2002
#+ du magazine en ligne "Unix Review",
#+ http://www.unixreview.com/documents/uni1026336632258/
# =======================================================


NBARGS=1                     # A besoin du nom comme argument.
E_MAUVAISARGS=70

if [ $# -ne "$NBARGS" ]
then
  echo "Usage: `basenom $0` nom"
  exit $E_MAUVAISARGS
fi  


affecte_valeur ()              #  Affecte une valeur numérique
{                              #+ aux lettres du nom.

  val1=bfpv                    # 'b,f,p,v' = 1
  val2=cgjkqsxz                # 'c,g,j,k,q,s,x,z' = 2
  val3=dt                      #  etc.
  val4=l
  val5=mn
  val6=r

# Une utilisation particulièrement intelligente de 'tr' suit.
# Essayez de comprendre ce qui se passe ici.

valeur=$( echo "$1" \
| tr -d wh \
| tr $val1 1 | tr $val2 2 | tr $val3 3 \
| tr $val4 4 | tr $val5 5 | tr $val6 6 \
| tr -s 123456 \
| tr -d aeiouy )

# Affecte des valeurs aux lettres.
# Supprime les numéros dupliqués, sauf s'ils sont séparés par des voyelles.
# Ignore les voyelles, sauf en tant que séparateurs, donc les supprime à la fin.
# Ignore 'w' et 'h', même en tant que séparateurs, donc les supprime au début.
#
# La substitution de commande ci-dessus utilise plus de tube qu'un plombier
# <g>.

}  


nom_en_entree="$1"
echo
echo "Nom = $nom_en_entree"


# Change tous les caractères en entrée par des minuscules.
# ------------------------------------------------
nom=$( echo $nom_en_entree | tr A-Z a-z )
# ------------------------------------------------
# Au cas où cet argument est un mélange de majuscules et de minuscules.


# Préfixe des codes soundex: première lettre du nom.
# --------------------------------------------


pos_caract=0                     # Initialise la position du caractère.
prefixe0=${nom:$pos_caract:1}
prefixe=`echo $prefixe0 | tr a-z A-Z`
                                 # Met en majuscule la première lettre de soundex.

let "pos_caract += 1"            # Aller directement au deuxième caractères.
nom1=${nom:$pos_caract}


# ++++++++++++++++++++++++++ Correctif Exception +++++++++++++++++++++++++++++++++
#  Maintenant, nous lançons à la fois le nom en entrée et le nom décalé d'un
#+ caractère vers la droite au travers de la fonction d'affectation de valeur.
#  Si nous obtenons la même valeur, cela signifie que les deux premiers
#+ caractères du nom ont la même valeur et que l'une d'elles doit être annulée.
#  Néanmoins, nous avons aussi besoin de tester si la première lettre du nom est
#+ une voyelle ou 'w' ou 'h', parce que sinon cela va poser problème.

caract1=`echo $prefixe | tr A-Z a-z`    # Première lettre du nom en minuscule.

affecte_valeur $nom
s1=$valeur
affecte_valeur $nom1
s2=$valeur
affecte_valeur $caract1
s3=$valeur
s3=9$s3                              #  Si la première lettre du nom est une
                                     #+ voyelle ou 'w' ou 'h',
                                     #+ alors sa "valeur" sera nulle (non
                                     #+ initialisée).
                                     #+ Donc, positionnons-la à 9, une autre
                                     #+ valeur non utilisée, qui peut être
                                     #+ vérifiée.


if [[ "$s1" -ne "$s2" || "$s3" -eq 9 ]]
then
  suffixe=$s2
else  
  suffixe=${s2:$pos_caract}
fi  
# ++++++++++++++++++++++ fin Correctif Exception +++++++++++++++++++++++++++++++++


fin=000                    # Utilisez au moins 3 zéro pour terminer.


soun=$prefixe$suffixe$fin  # Terminez avec des zéro.

LONGUEURMAX=4              # Tronquer un maximum de 4 caractères
soundex=${soun:0:$LONGUEURMAX}

echo "Soundex = $soundex"

echo

#  Le code soundex est une méthode d'indexage et de classification de noms
#+ en les groupant avec ceux qui sonnent de le même façon.
#  Le code soundex pour un nom donné est la première lettre de ce nom, suivi par
#+ un code calculé sur trois chiffres.
#  Des noms similaires devraient avoir les mêmes codes soundex

#   Exemples:
#   Smith et Smythe ont tous les deux le soundex "S-530"
#   Harrison = H-625
#   Hargison = H-622
#   Harriman = H-655

#  Ceci fonctionne assez bien en pratique mais il existe quelques anomalies.
#
#
#  Certaines agences du gouvernement U.S. utilisent soundex, comme le font les
#  généalogistes.
#
#  Pour plus d'informations, voir
#+ "National Archives and Records Administration home page",
#+ http://www.nara.gov/genealogy/soundex/soundex.html



# Exercice:
# --------
# Simplifier la section "Correctif Exception" de ce script.

exit 0

Exemple A.10. « life : Jeu de la Vie »

#!/bin/bash
# life.sh: "Life in the Slow Lane"
# Version 2: Corrigé par Daniel Albers
#+           pour permettre d'avoir en entrée des grilles non carrées.

# ############################################################################### #
# Ce script est la version Bash du "Jeu de la vie" de John Conway.                #
# "Life" est une implémentation simple d'automatisme cellulaire.                  #
# ------------------------------------------------------------------------------- #
# Sur un tableau rectangulaire, chaque "cellule" sera soit "vivante"              #
# soit "morte". On désignera une cellule vivante avec un point et une             #
# cellule morte avec une espace.                                                   #
#  Nous commençons avec un tableau composé aléatoirement de points et             #
#+ d'espaces. Ce sera la génération de départ, "génération 0".                    #
# Déterminez chaque génération successive avec les règles suivantes :             #
# 1) Chaque cellule a huit voisins, les cellules voisines (gauche,                #
#+   droite, haut, bas ainsi que les quatre diagonales.                           #
#                       123                                                       #
#                       4*5                                                       #
#                       678                                                       #
#                                                                                 #
# 2) Une cellule vivante avec deux ou trois voisins vivants reste                 #
#+   vivante.                                                                     #
# 3) Une cellule morte avec trois cellules vivantes devient vivante               #
#+   (une "naissance").                                                           #
SURVIE=2                                                                          #
NAISSANCE=3                                                                       #
# 4) Tous les autres cas concerne une cellule morte pour la prochaine génération. #
# ############################################################################### #


fichier_de_depart=gen0   # Lit la génération de départ à partir du fichier "gen0".
                         #  Par défaut, si aucun autre fichier n'est spécifié à
                         #+ l'appel de ce script.
                         #
if [ -n "$1" ]           # Spécifie un autre fichier "génération 0".
then
  fichier_de_depart="$1"
fi


######################################################
#  Annule le script si fichier_de_depart non spécifié
#+ ET
#+ gen0 non présent.

E_PASDEFICHIERDEPART=68

if [ ! -e "$fichier_de_depart" ]
then
  echo "Fichier de départ \""$fichier_de_depart"\" manquant !"
  exit $E_PASDEFICHIERDEPART
fi
######################################################

VIVANT1=.
MORT1=_
        # Représente des cellules vivantes et "mortes" dans le fichier de départ.

#  ---------------------------------------------------------- #
#  Ce script utilise un tableau 10 sur 10 (pourrait être augmenté
#+ mais une grande grille ralentirait de beaucoup l'exécution).
LIGNES=10
COLONNES=10
#  Modifiez ces deux variables pour correspondre à la taille
#+ de la grille, si nécessaire.
#  ---------------------------------------------------------- #

GENERATIONS=10          #  Nombre de générations pour le cycle.
                        #  Ajustez-le en l'augmentant si vous en avez le temps.

AUCUNE_VIVANTE=80       #  Code de sortie en cas de sortie prématurée,
                        #+ si aucune cellule n'est vivante.
VRAI=0
FAUX=1
VIVANTE=0
MORTE=1

avar=                   #  Global; détient la génération actuelle.
generation=0            # Initialise le compteur des générations.

# =================================================================


let "cellules = $LIGNES * $COLONNES"
                        # Nombre de cellules.

declare -a initial      # Tableaux contenant les "cellules".
declare -a current

affiche ()
{

alive=0                 # Nombre de cellules "vivantes" à un moment donné.
                        # Initialement à zéro.

declare -a tab
tab=( `echo "$1"` )     # Argument convertit en tableau.

nombre_element=${#tab[*]}

local i
local verifligne

for ((i=0; i<$nombre_element; i++))
do

# Insère un saut de ligne à la fin de chaque ligne.
  let "verifligne = $i % COLONNES"
  if [ "$verifligne" -eq 0 ]
  then
    echo                # Saut de ligne.
    echo -n "      "    # Indentation.
  fi  

  cellule=${tab[i]}

  if [ "$cellule" = . ]
  then
    let "vivante += 1"
  fi  

  echo -n "$cellule" | sed -e 's/_/ /g'
  # Affiche le tableau et modifie les tirets bas en espaces.
done  

return

}

EstValide ()                            # Teste si les coordonnées sont valides.
{

  if [ -z "$1"  -o -z "$2" ]          # Manque-t'il des arguments requis ?
  then
    return $FAUX
  fi

local ligne
local limite_basse=0                   # Désactive les coordonnées négatives.
local limite_haute
local gauche
local droite

let "limite_haute = $LIGNES * $COLONNES - 1" # Nombre total de cellules.


if [ "$1" -lt "$limite_basse" -o "$1" -gt "$limite_haute" ]
then
  return $FAUX                       # En dehors des limites.
fi  

ligne=$2
let "gauche = $ligne * $COLONNES"            # Limite gauche.
let "droite = $gauche + $COLONNES - 1"       # Limite droite.

if [ "$1" -lt "$gauche" -o "$1" -gt "$droite" ]
then
  return $FAUX                       # En dehors des limites.
fi  

return $VRAI                          # Coordonnées valides.

}  


EstVivante ()           # Teste si la cellule est vivante.
                        #  Prend un tableau, un numéro de cellule et un état de
                        #+ cellule comme arguments.
{
  ObtientNombre "$1" $2 # Récupère le nombre de cellules vivantes dans le voisinage.
  local voisinage=$?


  if [ "$voisinage" -eq "$NAISSANCE" ]  # Vivante dans tous les cas.
  then
    return $VIVANTE
  fi

  if [ "$3" = "." -a "$voisinage" -eq "$SURVIE" ]
  then                  # Vivante uniquement si précédemment vivante.
    return $VIVANTE
  fi  

  return $MORTE          # Par défaut.

}  


ObtientNombre ()        # Compte le nombre de cellules vivantes dans le
                        # voisinage de la cellule passée en argument.
                        # Deux arguments nécessaires :
                        # $1) tableau contenant les variables
                        # $2) numéro de cellule
{
  local numero_cellule=$2
  local tableau
  local haut
  local centre
  local bas
  local l
  local ligne
  local i
  local t_hau
  local t_cen
  local t_bas
  local total=0
  local LIGNE_NHBD=3

  tableau=( `echo "$1"` )

  let "haut = $numero_cellule - $COLONNES - 1"  #  Initialise le voisinage de la
                                                #+ cellule.
  let "centre = $numero_cellule - 1"
  let "bas = $numero_cellule + $COLONNES - 1"
  let "l = $numero_cellule / $COLONNES"

  for ((i=0; i<$LIGNE_NHBD; i++))     # Parcours de gauche à droite.
  do
    let "t_hau = $haut + $i"
    let "t_cen = $centre + $i"
    let "t_bas = $bas + $i"


    let "ligne = $l"                  # Calcule la ligne centrée du voisinage.
    EstValide $t_cen $ligne           # Position de la cellule valide ?
    if [ $? -eq "$VRAI" ]
    then
      if [ ${tableau[$t_cen]} = "$VIVANT1" ] # Est-elle vivante ?
      then                            # Oui ?
        let "total += 1"              # Incrémenter le total.
      fi        
    fi  

    let "ligne = $l - 1"              # Compte la ligne du haut.
    EstValide $t_haut $haut
    if [ $? -eq "$VRAI" ]
    then
      if [ ${tableau[$t_haut]} = "$VIVANT1" ] 
      then
        let "total += 1"
      fi        
    fi  

    let "ligne = $l + 1"              # Compte la ligne du bas.
    EstValide $t_bas $ligne
    if [ $? -eq "$VRAI" ]
    then
      if [ ${tableau[$t_bas]} = "$VIVANT1" ] 
      then
        let "total += 1"
      fi        
    fi  

  done  


  if [ ${tableau[$numero_cellule]} = "$VIVANT1" ]
  then
    let "total -= 1"        #  S'assurer que la valeur de la cellule testée
  fi                        #+ n'est pas elle-même comptée.


  return $total
  
}

prochaine_gen ()               # Mise à jour du tableau des générations.
{

local tableau
local i=0

tableau=( `echo "$1"` )        # Argument passé converti en tableau.

while [ "$i" -lt "$cellules" ]
do
  EstVivante "$1" $i ${tableau[$i]}  # La cellule est-elle vivante ?
  if [ $? -eq "$VIVANTE" ]
  then                         #  Si elle l'est, alors
    tableau[$i]=.              #+ représente la cellule avec un point.
  else  
    tableau[$i]="_"            #  Sinon, avec un tiret bas.
   fi                          #+ (qui sera transformé plus tard en espace).
  let "i += 1" 
done   


# let "generation += 1"   # Incrémente le nombre de générations.
# Pourquoi cette ligne a-t'elle été mise en commentaire ?

# Initialise la variable à passer en tant que paramètre à la fonction
# "affiche".
une_var=`echo ${tableau[@]}` # Convertit un tableau en une variable de type chaîne.
affiche "$une_var"           # L'affiche.
echo; echo
echo "Génération $generation  -  $vivante vivante"

if [ "$alive" -eq 0 ]
then
  echo
  echo "Sortie prématurée : aucune cellule encore vivante !"
  exit $AUCUNE_VIVANTE    #  Aucun intérêt à continuer
fi                        #+ si aucune cellule n'est vivante.

}


# =========================================================

# main ()

# Charge un tableau initial avec un fichier de départ.
initial=( `cat "$fichier_de_depart" | sed -e '/#/d' | tr -d '\n' |\
sed -e 's/\./\. /g' -e 's/_/_ /g'` )
# Supprime les lignes contenant le symbole de commentaires '#'.
# Supprime les retours chariot et insère des espaces entre les éléments.

clear          # Efface l'écran.

echo #         Titre
echo "======================="
echo "    $GENERATIONS générations"
echo "           du"
echo "   \"Jeu de la Vie\""
echo "======================="


# -------- Affiche la première génération. --------
Gen0=`echo ${initial[@]}`
affiche "$Gen0"           # Affiche seulement.
echo; echo
echo "Génération $generation  -  $alive vivante"
# -------------------------------------------


let "generation += 1"     # Incrémente le compteur de générations.
echo

# ------- Affiche la deuxième génération. -------
Actuelle=`echo ${initial[@]}`
prochaine_gen "$Actuelle"          # Mise à jour & affichage.
# ------------------------------------------

let "generation += 1"     # Incrémente le compteur de générations.

# ------ Boucle principale pour afficher les générations conséquentes ------
while [ "$generation" -le "$GENERATIONS" ]
do
  Actuelle="$une_var"
  prochaine_gen "$Actuelle"
  let "generation += 1"
done
# ==============================================================

echo

exit 0 # FIN

# Le tableau dans ce script a un "problème de bordures".
# Les bordures haute, basse et des côtés avoisinent une absence de cellules mortes.
# Exercice: Modifiez le script pour avoir la grille
# +         de façon à ce que les côtés gauche et droit se touchent,
# +         comme le haut et le bas.
#
# Exercice: Créez un nouveau fichier "gen0" pour ce script.
#           Utilisez une grille 12 x 16, au lieu du 10 x 10 original.
#           Faites les modifications nécessaires dans le script,
#+          de façon à ce qu'il s'exécute avec le fichier modifié.
#
# Exercice: Modifiez ce script de façon à ce qu'il puisse déterminer la taille
#+          de la grille à partir du fichier "gen0" et initialiser toute variable
#+          nécessaire au bon fonctionnement du script.
#           Ceci rend inutile la modification des variables dans le script
#+          suite à un modification de la taille de la grille.

Exemple A.11. Fichier de données pour le « Jeu de la Vie »

# gen0
#
# 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 Moraes-COPYRIGHT pour les droits. Ce fichier est inclus dans l'archive tar HTML/source du guide ABS.

Exemple A.12. 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 une espace blanche.
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éfère à l'exception des arguments
# ==> passés.
# ==> Est-il intéressant de l'embarquer dans une fonction? Pourquoi?

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

#! /bin/sh 
# $Id: ftpget.sh,v 1.7 2006/08/09 09:07:52 gleu 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.
# ==> Ce script n'est rien de plus qu'un emballage shell autour de "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 distant est 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 et les placera dans 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 
#


# ==> 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

E_MAUVAISARGS=65

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 hotedistant] [-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 $E_MAUVAISARGS
fi
shift
trap 'rm -f ${FICHIER_TEMPORAIRE} ; exit' 0 1    2            3     15
# ==>                          Signaux:     HUP  INT (Ctl-C)  QUIT  TERM
# ==> Supprimer FICHIER_TEMPORAIRE dans le cas d'une sortie anormale du script.
echo "user anonymous ${USER-gnu}@${SITE} > ${FICHIER_TEMPORAIRE}"
# ==> Ajout des guillemets (recommandé 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
# ==> 'lcd' et 'mget' sont des commandes ftp. Voir "man ftp"...
done
if [ $# -ne 0 ]; then
        echo $usage
        exit $E_MAUVAISARGS
        # ==> Modifié de l'"exit 2" pour se conformer avec le standard du style.
fi
if [ x${verbflag} != x ]; then
        optionsftp="${optionsftp} -v"
fi
if [ x${hotedistant} = x ]; then
        hotedistant=prep.ai.mit.edu
        # ==> À modifier 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, « Substitution de paramètres ».

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

#!/bin/bash
#  Pourrait nécessiter 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 du guide ABS de l'utiliser ici.
#
# ==> Commentaires ajoutés par l'auteur du document ==>


MATRICE="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
# ==> Les mots de passe seront constitués de caractères alphanumériques.
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'initialiser à 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.15. 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 distante 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?

  # ==>  Est-il nécessaire de supprimer le tube avant de sortir du script ?
  # ==>  Comment le faire ?

  exit 0

+

Stéphane Chazelas a contribué avec le script suivant pour démontrer que générer des nombres premiers ne requiert pas de tableaux.

Exemple A.16. primes: Générer des nombres premiers 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 du crible d'Ératosthène,
#+ mais utilise à la place 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écommentez les lignes 16 et 24 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.

+

C'est la version de Rick Boivie du script de Jordi Sanfeliu, qui a donné sa permission pour utiliser son script élégant sur les arborescences.

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

#!/bin/sh
# tree.sh

#  Écrit par Rick Boivie.
#  Utilisé avec sa permission.
#  Ceci est une version revue et simplifiée d'un script
#+ par Jordi Sanfeliu (et corrigée par Ian Kjos).
#  Ce script remplace la version précédente utilisée dans
#+ les précédentes versions du Guide d'écriture avancé de scripts Bash.

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


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 du
                # ==> répertoire.
         while [ $zz != $1 ]    # Conserve la trace de la boucle interne.
         do
            echo -n "|   "    # ==> Affiche le symbole 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 des longues listes.
         else
            echo "+---$dir"      # ==> Affiche le symbole du connecteur
                                 # ==> horizontal et le nom du répertoire.
            numdirs=`expr $numdirs + 1` # ==> Incrémente le compteur de répertoire.
            if cd "$dir" ; then # ==> S'il peut se déplacer dans le sous-répertoire...
              search `expr $1 + 1` # avec la récursivité ;-)
              # ==> La fonction s'appelle elle-même.
              cd ..
            fi
         fi
      fi
   done
}

if [ $# != 0 ] ; then
  cd $1 # se déplace au répertoire indiqué.
  #else # reste dans le répertoire actuel.
fi

echo "Répertoire initial = `pwd`"
numdirs=0

search 0
echo "Nombre total de répertoires = $numdirs"

exit 0

+

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.18. 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 &lt;friedman@prep.ai.mit.edu&gt;
# ==>     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

L'exemple de tableaux complexes par Michael Zick utilise la commande de vérification de sommes md5sum pour coder les informations sur le répertoire.

Exemple A.19. Informations sur un répertoire

#! /bin/bash
# directory-info.sh
# Analyse et affiche des informations sur le répertoire.

# NOTE: Modification des lignes 273 et 353 suivant le fichier "README".

# Michael Zick est l'auteur de ce script.
# Utilisé ici avec son autorisation.

# Contrôles
# Si outrepassé par les arguments de la commande, ils doivent être dans l'ordre:
#   Arg1: "Descripteur du répertoire"
#   Arg2: "Chemins à exclure"
#   Arg3: "Répertoires à exclure"
#
# Les variables d'environnement outrepassent les valeurs par défaut.
# Les arguments de la commande outrepassent les variables d'environnement.

# Emplacement par défaut du contenu des descripteurs de fichiers.
MD5UCFS=${1:-${MD5UCFS:-'/tmpfs/ucfs'}}

# Répertoires à exclure
declare -a \
  CHEMINS_A_EXCLURE=${2:-${CHEMINS_A_EXCLURE:-'(/proc /dev /devfs /tmpfs)'}}

# Répertoires à exclure
declare -a \
  REPERTOIRES_A_EXCLURE=${3:-${REPERTOIRES_A_EXCLURE:-'(ucfs lost+found tmp wtmp)'}}

# Fichiers à exclure
declare -a \
  FICHIERS_A_EXCLURE=${3:-${FICHIERS_A_EXCLURE:-'(core "Nom avec des espaces")'}}


# Document intégré utilisé comme bloc de commentaires.
: &lt;&lt;LSfieldsDoc
# # # Affiche les informations sur les répertoires du système de fichiers # # #
#
#       AfficheRepertoire "FileGlob" "Field-Array-Name"
# ou
#       AfficheRepertoire -of "FileGlob" "Field-Array-Filename"
#       '-of' signifiant 'sortie vers fichier'
# # # # #

Description du format de la chaîne : ls (GNU fileutils) version 4.0.36

Produit une ligne (ou plus) formattée :
inode droits    liens propriétaire groupe ...
32736 -rw-------    1 mszick   mszick

taille jour mois date hh:mm:ss année chemin
2756608 Sun Apr 20 08:53:06 2003 /home/mszick/core

Sauf, s'il est formatté :
inode  droits    liens propriétaire groupe ...
266705 crw-rw----    1    root  uucp

majeur mineur jour mois date hh:mm:ss année chemin
4,  68 Sun Apr 20 09:27:33 2003 /dev/ttyS4
NOTE: cette virgule bizarre après le nombre majeur

NOTE: le 'chemin' pourrait avoir plusieurs champs :
/home/mszick/core
/proc/982/fd/0 -> /dev/null
/proc/982/fd/1 -> /home/mszick/.xsession-errors
/proc/982/fd/13 -> /tmp/tmpfZVVOCs (deleted)
/proc/982/fd/7 -> /tmp/kde-mszick/ksycoca
/proc/982/fd/8 -> socket:[11586]
/proc/982/fd/9 -> pipe:[11588]

Si ce n'est pas suffisant pour que votre analyseur continue à deviner,
soit une soit les deux parties du chemin peuvent être relatives :
../Built-Shared -> Built-Static
../linux-2.4.20.tar.bz2 -> ../../../SRCS/linux-2.4.20.tar.bz2

Le premier caractère du champ des droits (sur 11 (10 ?) caractères) :
's' Socket
'd' Répertoire
'b' Périphérique bloc
'c' Périphérique caractère
'l' Lien symbolique
NOTE: Les liens non symboliques ne sont pas identifiés - testés pour des numéros
d'inodes identiques sur le même système de fichiers.
Toutes les informations sur les fichiers liés sont partagées sauf le nom et
l'emplacement.
NOTE: Un "lien" est connu comme un "alias" sur certains systèmes.
'-' fichier sans distinction.

Suivi par trois groupes de lettres pour l'utilisateur, le groupe et les autres.
Caractère 1: '-' non lisible; 'r' lisible
Caractère 2: '-' pas d'écriture; 'w' écriture (writable)
Caractère 3, utilisateur et groupe: Combine l'éxécution et un spécial
'-' non exécutable, non spécial
'x' exécutable, non spécial
's' exécutable, spécial
'S' non exécutable, spécial
Caractère 3, autres: Combine l'éxécution et le sticky (tacky?)
'-' non éxécutable, non tacky
'x' exécutable, non tacky
't' exécutable, tacky
'T' non exécutable, tacky

Suivi par un indicateur d'accès
Non testé, il pourrait être le onzième caractère
ou il pourrait générer un autre champ
' ' Pas d'accès autre
'+' Accès autre
LSfieldsDoc


AfficheRepertoire()
{
        local -a T
        local -i of=0           # Valeur par défaut
#       OLD_IFS=$IFS            # Utilise la variable BASH par défaut ' \t\n'

        case "$#" in
        3)      case "$1" in
                -of)    of=1 ; shift ;;
                 * )    return 1 ;;
                esac ;;
        2)      : ;;            # L'instruction "continue" du pauvre
        *)      return 1 ;;
        esac

        # NOTE: la commande (ls) N'est PAS entre guillemets (")
        T=( $(ls --inode --ignore-backups --almost-all --directory \
        --full-time --color=none --time=status --sort=none \
        --format=long $1) )

        case $of in
        #  Affecte T en retour pour le tableau dont le nom a été passé
        #+ à $2
                0) eval $2=\( \"\$\{T\[@\]\}\" \) ;;
        # Ecrit T dans le nom du fichier passé à $2
                1) echo "${T[@]}" > "$2" ;;
        esac
        return 0
   }

# # # # # Est-ce que cette chaîne est un nombre légal ? # # # # #
#
#       EstNombre "Var"
# # # # # Il doit y avoir un meilleur moyen, hum...

EstNombre()
{
        local -i int
        if [ $# -eq 0 ]
        then
                return 1
        else
                (let int=$1)  2>/dev/null
                return $?       # Code de sortie du thread créé pour let
        fi
}

# # # Informations sur l'index des répertoires du système de fichiers # # #
#
#       AfficheIndex "Field-Array-Name" "Index-Array-Name"
# ou
#       AfficheIndex -if Field-Array-Filename Index-Array-Name
#       AfficheIndex -of Field-Array-Name Index-Array-Filename
#       AfficheIndex -if -of Field-Array-Filename Index-Array-Filename
# # # # #

: &lt;&lt;AfficheIndexDoc
Parcourt un tableau de champs répertoire créé par AfficheRepertoire

Ayant supprimé les retours chariots dans un rapport habituellement ligne par
ligne, construit un index vers l'élement du tableau commençant à chaque ligne.

Chaque ligne obtient deux entrées de l'index, le premier élément de chaque ligne
(inode) et l'élément qui contient le chemin du fichier.

La première paire d'entrée de l'index (Numero-Ligne==0) apporte une
information :
Nom-Tableau-Index[0] : Nombre de "lignes" indexé
Nom-Tableau-Index[1] : Pointeur de la "ligne courante" vers Nom-Tableau-Index

Les paires d'index suivantes (si elles existent) contiennent les index des
éléments dans Nom-Tableau-Champ avec :
Nom-Tableau-Index[Numero-Ligne * 2] : L'élément champ "inode".
NOTE: La distance peut être de +11 ou +12 éléments.
Nom-Tableau-Index[(Numero-Ligne * 2) + 1] : L'élément "chemin".
NOTE: La distance est un nombre variable d'éléments.
La prochaine paire de lignes d'index pour Numero-Ligne+1.
AfficheIndexDoc



AfficheIndex()
{
        local -a LISTE                  # Variable locale du nom de liste
        local -a -i INDEX=( 0 0 )       # Variable locale de l'index à renvoyer
        local -i Lidx Lcpt
        local -i if=0 of=0              # Par défaut

        case "$#" in                    # Test simpliste des options
                0) return 1 ;;
                1) return 1 ;;
                2) : ;;                 # Instruction "continue" du pauvre
                3) case "$1" in
                        -if) if=1 ;;
                        -of) of=1 ;;
                         * ) return 1 ;;
                   esac ; shift ;;
                4) if=1 ; of=1 ; shift ; shift ;;
                *) return 1
        esac

        # Fait une copie locale de liste
        case "$if" in
                0) eval LISTE=\( \"\$\{$1\[@\]\}\" \) ;;
                1) LISTE=( $(cat $1) ) ;;
        esac

        # "Grok (grope?)" le tableau
        Lcpt=${#LISTE[@]}
        Lidx=0
        until (( Lidx >= Lcpt ))
        do
        if EstNombre ${LISTE[$Lidx]}
        then
                local -i inode nom
                local ft
                inode=Lidx
                local m=${LISTE[$Lidx+2]}       # Champ des liens
                ft=${LISTE[$Lidx+1]:0:1}        # Stats rapides
                case $ft in
                b)      ((Lidx+=12)) ;;         # Périphérique bloc
                c)      ((Lidx+=12)) ;;         # Périphérique caractère
                *)      ((Lidx+=11)) ;;         # Le reste
                esac
                nom=Lidx
                case $ft in
                -)      ((Lidx+=1)) ;;          # Le plus simple
                b)      ((Lidx+=1)) ;;          # Périphérique bloc
                c)      ((Lidx+=1)) ;;          # Périphérique caractère
                d)      ((Lidx+=1)) ;;          # Encore un autre
                l)      ((Lidx+=3)) ;;          # Au MOINS deux autres champs
#  Un peu plus d'élégance ici permettrait de gérer des tubes, des sockets,
#+ des fichiers supprimés - plus tard.
                *)      until EstNombre ${LISTE[$Lidx]} || ((Lidx >= Lcpt))
                        do
                                ((Lidx+=1))
                        done
                        ;;                      # Non requis.
                esac
                INDEX[${#INDEX[*]}]=$inode
                INDEX[${#INDEX[*]}]=$nom
                INDEX[0]=${INDEX[0]}+1          # Une "ligne" de plus
# echo "Ligne: ${INDEX[0]} Type: $ft Liens: $m Inode: \
# ${LIST[$inode]} Nom: ${LIST[$name]}"

        else
                ((Lidx+=1))
        fi
        done
        case "$of" in
                0) eval $2=\( \"\$\{INDEX\[@\]\}\" \) ;;
                1) echo "${INDEX[@]}" > "$2" ;;
        esac
        return 0                        # Que pourrait'il arriver de mal ?
}

# # # # # Fichier identifié par son contenu # # # # #
#
#       DigestFile Nom-Tableau-Entree Nom-Tableau-Digest
# ou
#       DigestFile -if NomFichier-EnEntree Nom-Tableau-Digest
# # # # #

# Document intégré utilisé comme bloc de commentaires.
: &lt;&lt;DigestFilesDoc

La clé (no pun intended) vers un Système de Fichiers au Contenu Unifié (UCFS)
permet de distinguer les fichiers du système basés sur leur contenu.
Distinguer des fichiers par leur nom est tellement 20è siècle.

Le contenu se distingue en calculant une somme de contrôle de ce contenu.
Cette version utilise le programme md5sum pour générer une représentation de la
somme de contrôle 128 bit du contenu.
Il existe une chance pour que deux fichiers ayant des contenus différents
génèrent la même somme de contrôle utilisant md5sum (ou tout autre outil de
calcul de somme de contrôle). Si cela devait devenir un problème, alors
l'utilisation de md5sum peut être remplacée par une signature cryptographique.
Mais jusque là...

La documentation de md5sum précise que la sortie de cette commande affiche
trois champs mais, à la lecture, il apparaît comme deux champs (éléments du
tableau). Ceci se fait par le manque d'espaces blancs entre le second et le
troisième champ. Donc, cette fonction groupe la sortie du md5sum et renvoit :
        [0]     Somme de contrôle sur 32 caractères en héxidecimal (nom du
                fichier UCFS)
        [1]     Caractère seul : ' ' fichier texte, '*' fichier binaire
        [2]     Nom système de fichiers (style 20è siècle)
        Note: Ce nom pourrait être le caractère '-' indiquant la lecture de
        STDIN

DigestFilesDoc



DigestFile()
{
        local if=0              # Par défaut.
        local -a T1 T2

        case "$#" in
        3)      case "$1" in
                -if)    if=1 ; shift ;;
                 * )    return 1 ;;
                esac ;;
        2)      : ;;            # Instruction "continue" du pauvre
        *)      return 1 ;;
        esac

        case $if in
        0) eval T1=\( \"\$\{$1\[@\]\}\" \)
           T2=( $(echo ${T1[@]} | md5sum -) )
           ;;
        1) T2=( $(md5sum $1) )
           ;;
        esac

        case ${#T2[@]} in
        0) return 1 ;;
        1) return 1 ;;
        2) case ${T2[1]:0:1} in         # SanScrit-2.0.5
           \*) T2[${#T2[@]}]=${T2[1]:1}
               T2[1]=\*
               ;;
            *) T2[${#T2[@]}]=${T2[1]}
               T2[1]=" "
               ;;
           esac
           ;;
        3) : ;; # Suppose qu'il fonctionne
        *) return 1 ;;
        esac

        local -i len=${#T2[0]}
        if [ $len -ne 32 ] ; then return 1 ; fi
        eval $2=\( \"\$\{T2\[@\]\}\" \)
}

# # # # # Trouve l'emplacement du fichier # # # # #
#
#       LocateFile [-l] NomFichier Nom-Tableau-Emplacement
# ou
#       LocateFile [-l] -of NomFichier NomFichier-Tableau-Emplacement
# # # # #

#  L'emplacement d'un fichier correspond à l'identifiant du système de fichiers
#+ et du numéro de l'inode.

# Document intégré comme bloc de commentaire.
: &lt;&lt;StatFieldsDoc
        Basé sur stat, version 2.2
        champs de stat -t et stat -lt
        [0]     nom
        [1]     Taille totale
                Fichier - nombre d'octets
                Lien symbolique - longueur de la chaîne représentant le chemin
        [2]     Nombre de blocs (de 512 octets) alloués
        [3]     Type de fichier et droits d'accès (hex)
        [4]     ID utilisateur du propriétaire
        [5]     ID groupe du propriétaire
        [6]     Numéro de périphérique
        [7]     Numéro de l'inode
        [8]     Nombre de liens
        [9]     Type de périphérique (si périphérique d'inode) Majeur
        [10]    Type de périphérique (si périphérique d'inode) Mineur
        [11]    Heure du dernier accès
                Pourrait être désactivé dans 'mount' avec noatime
                atime des fichiers changés par exec, read, pipe, utime, mknod
                (mmap?)
                atime des répertoires changés par ajout/suppression des fichiers
        [12]    Heure de dernière modification
                mtime des fichiers changés par write, truncate, utime, mknod
                mtime des répertoires changés par ajout/suppression des fichiers
        [13]    Heure de dernier changement
                ctime reflète le temps de la dernière modification de l'inode
                (propriétaire, groupe, droits, nombre de liens)
-*-*- Pour :
        Code de sortie: 0
        Taille du tableau: 14
        Contenu du tableau
        Elément 0: /home/mszick
        Elément 1: 4096
        Elément 2: 8
        Elément 3: 41e8
        Elément 4: 500
        Elément 5: 500
        Elément 6: 303
        Elément 7: 32385
        Elément 8: 22
        Elément 9: 0
        Elément 10: 0
        Elément 11: 1051221030
        Elément 12: 1051214068
        Elément 13: 1051214068

        Pour un lien de la forme nom_lien -> vrai_nom
        stat -t  nom_lien renvoit des informations sur le lien
        stat -lt nom_lien renvoit des informations sur le vrai fichier

        Champs stat -tf et stat -ltf
        [0]     nom
        [1]     ID-0?           # Peut-être un jour, mais la structure stat de
        [2]     ID-0?           # Linux n'a ni le champ LABEL ni UUID,
                                # actuellement l'information doit provenir
                                # d'utilitaires système spécifiques
        Ceci sera transformé en :
        [1]     UUID si possible
        [2]     Label du volume si possible
        Note: 'mount -l' renvoit le label et pourrait renvoyer le UUID

        [3]     Longueur maximum des noms de fichier
        [4]     Type de système de fichiers
        [5]     Nombre total de blocs dans le système de fichiers
        [6]     Blocs libres
        [7]     Blocs libres pour l'utilisateur non root
        [8]     Taille du bloc du système de fichiers
        [9]     Nombre total d'inodes
        [10]    Inodes libres

-*-*- Per:
        Code de sortie: 0
        Taille du tableau : 11
        Contenu du tableau
        Elément 0: /home/mszick
        Elément 1: 0
        Elément 2: 0
        Elément 3: 255
        Elément 4: ef53
        Elément 5: 2581445
        Elément 6: 2277180
        Elément 7: 2146050
        Elément 8: 4096
        Elément 9: 1311552
        Elément 10: 1276425

StatFieldsDoc


#       LocateFile [-l] NomFichier Nom-Tableau-Emplacement
#       LocateFile [-l] -of NomFichier Nom-Tableau-Emplacement

LocateFile()
{
        local -a LOC LOC1 LOC2
        local lk="" of=0

        case "$#" in
        0) return 1 ;;
        1) return 1 ;;
        2) : ;;
        *) while (( "$#" > 2 ))
           do
              case "$1" in
               -l) lk=-1 ;;
              -of) of=1 ;;
                *) return 1 ;;
              esac
           shift
           done ;;
        esac

# Plus de Sanscrit-2.0.5
      # LOC1=( $(stat -t $lk $1) )
      # LOC2=( $(stat -tf $lk $1) )
      #  Supprimez le commentaire des deux lignes ci-dessus si le système
      #+ dispose de la commande "stat" installée.
        LOC=( ${LOC1[@]:0:1} ${LOC1[@]:3:11}
              ${LOC2[@]:1:2} ${LOC2[@]:4:1} )

        case "$of" in
                0) eval $2=\( \"\$\{LOC\[@\]\}\" \) ;;
                1) echo "${LOC[@]}" > "$2" ;;
        esac
        return 0
# Ce qui rend comme résultat (si vous êtes chanceux et avez installé "stat")
# -*-*- Descripteur de l'emplacement -*-*-
#       Code de sortie : 0
#       Taille du tableau : 15
#       Contenu du tableau
#       Elément 0: /home/mszick         Nom du 20è siècle
#       Elément 1: 41e8                 Type et droits
#       Elément 2: 500                  Utilisateur
#       Elément 3: 500                  Groupe
#       Elément 4: 303                  Périphérique
#       Elément 5: 32385                Inode
#       Elément 6: 22                   Nombre de liens
#       Elément 7: 0                    Numéro majeur
#       Elément 8: 0                    Numéro mineur
#       Elément 9: 1051224608           Dernier accès
#       Elément 10: 1051214068          Dernière modification
#       Elément 11: 1051214068          Dernier statut
#       Elément 12: 0                   UUID (à faire)
#       Elément 13: 0                   Volume Label (à faire)
#       Elément 14: ef53                Type de système de fichiers
}



# Et enfin, voici un code de test

AfficheTableau() # AfficheTableau Nom
{
        local -a Ta

        eval Ta=\( \"\$\{$1\[@\]\}\" \)
        echo
        echo "-*-*- Liste de tableaux -*-*-"
        echo "Taille du tableau $1: ${#Ta[*]}"
        echo "Contenu du tableau $1:"
        for (( i=0 ; i<${#Ta[*]} ; i++ ))
        do
            echo -e "\tElément $i: ${Ta[$i]}"
        done
        return 0
}

declare -a CUR_DIR
# Pour de petits tableaux
AfficheRepertoire "${PWD}" CUR_DIR
AfficheTableau CUR_DIR

declare -a DIR_DIG
DigestFile CUR_DIR DIR_DIG
echo "Le nouveau \"nom\" (somme de contrôle) pour ${CUR_DIR[9]} est ${DIR_DIG[0]}"

declare -a DIR_ENT
# BIG_DIR # Pour de réellement gros tableaux - utilise un fichier temporaire en
          # disque RAM
# BIG-DIR # AfficheRepertoire -of "${CUR_DIR[11]}/*" "/tmpfs/junk2"
AfficheRepertoire "${CUR_DIR[11]}/*" DIR_ENT

declare -a DIR_IDX
# BIG-DIR # AfficheIndex -if "/tmpfs/junk2" DIR_IDX
AfficheIndex DIR_ENT DIR_IDX

declare -a IDX_DIG
# BIG-DIR # DIR_ENT=( $(cat /tmpfs/junk2) )
# BIG-DIR # DigestFile -if /tmpfs/junk2 IDX_DIG
DigestFile DIR_ENT IDX_DIG
# Les petits (devraient) être capable de paralléliser AfficheIndex & DigestFile
# Les grands (devraient) être capable de paralléliser AfficheIndex & DigestFile
# & l'affectation
echo "Le \"nom\" (somme de contrôle) pour le contenu de ${PWD} est ${IDX_DIG[0]}"

declare -a FILE_LOC
LocateFile ${PWD} FILE_LOC
AfficheTableau FILE_LOC

exit 0

Stéphane 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ée objet dans un script shell.
# Script par Stephane Chazelas.

#  Note Importante :
#  ---- ----------
#  Si vous exécutez ce script avec une version 3 ou ultérieure de Bash,
#+ remplacez tous les points dans les noms de fonctions avec un
#+ caractère légal, par exemple un tiret bas.


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

Mariusz Gniazdowski a contribué avec une bibliothèque de hachage à utiliser dans des scripts.

Exemple A.21. Bibliothèque de fonctions de hachage

# Hash:
# Bibliothèque de fonctions de hachage
# Auteur : Mariusz Gniazdowski &lt;mgniazd-at-gmail.com&gt;
# Date : 2005-04-07

# Fonctions rendant l'émulation de hâchage en Bash un peu moins pénible.


#    Limitations:
#  * Seules les variables globales sont supportées.
#  * Chaque instance de hâchage génère une variable globale par valeur.
#  * Les collisions de noms de variables sont possibles
#+   si vous définissez des variables comme __hash__hashname_key
#  * Les clés doivent utilisant des caractères faisant partie du nom d'une variable Bash
#+   (pas de tirets, points, etc.).
#  * Le hâchage est créé comme une variable :
#    ... hashname_keyname
#    Donc si quelqu'un crée des hâchages ainsi :
#      myhash_ + mykey = myhash__mykey
#      myhash + _mykey = myhash__mykey
#    Alors, il y aura collision.
#    (Ceci ne devrait pas poser un problème majeur.)


Hash_config_varname_prefix=__hash__


# Émule:  hash[key]=value
#
# Paramètres:
# 1 - hash  (hachage)
# 2 - key   (clé)
# 3 - value (valeur
function hash_set {
        eval "${Hash_config_varname_prefix}${1}_${2}=\"${3}\""
}


# Émule:  value=hash[key]
#
# Paramètres:
# 1 - hash
# 2 - key
# 3 - value (nom d'une variable globale à initialiser)
function hash_get_into {
        eval "$3=\"\$${Hash_config_varname_prefix}${1}_${2}\""
}


# Émule:  echo hash[key]
#
# Paramètres:
# 1 - hash
# 2 - key
# 3 - echo params (like -n, for example)
function hash_echo {
        eval "echo $3 \"\$${Hash_config_varname_prefix}${1}_${2}\""
}


# Émule:  hash1[key1]=hash2[key2]
#
# Paramètres:
# 1 - hash1
# 2 - key1
# 3 - hash2
# 4 - key2
function hash_copy {
        eval "${Hash_config_varname_prefix}${1}_${2}\
=\"\$${Hash_config_varname_prefix}${3}_${4}\""
}


# Émule:  hash[keyN-1]=hash[key2]=...hash[key1]
#
# Copie la première clé au reste des clés.
#
# Paramètres:
# 1 - hash1
# 2 - key1
# 3 - key2
# . . .
# N - keyN
function hash_dup {
        local hashName="$1" keyName="$2"
        shift 2
        until [ ${#} -le 0 ]; do
                eval "${Hash_config_varname_prefix}${hashName}_${1}\
=\"\$${Hash_config_varname_prefix}${hashName}_${keyName}\""
                shift;
        done;
}


# Émule:  unset hash[key]
#
# Paramètres:
# 1 - hash
# 2 - key
function hash_unset {
        eval "unset ${Hash_config_varname_prefix}${1}_${2}"
}


# Emulates something similar to:  ref=&amp;hash[key]
#
# The reference is name of the variable in which value is held.
#
# Paramètres:
# 1 - hash
# 2 - key
# 3 - ref - Nom d'une variable globale à initialiser.
function hash_get_ref_into {
        eval "$3=\"${Hash_config_varname_prefix}${1}_${2}\""
}


# Émule quelque chose de similaire à:  echo &amp;hash[key]
#
# Cette référence est le nom d'une variable dans laquelle est contenue la valeur.
#
# Paramètres:
# 1 - hash
# 2 - key
# 3 - echo params (comme -n par exemple)
function hash_echo_ref {
        eval "echo $3 \"${Hash_config_varname_prefix}${1}_${2}\""
}



# Émule quelque chose de similaire à:  $$hash[key](param1, param2, ...)
#
# Paramètres:
# 1 - hash
# 2 - key
# 3,4, ... - Paramètres de fonction.
function hash_call {
        local hash key
        hash=$1
        key=$2
        shift 2
        eval "eval \"\$${Hash_config_varname_prefix}${hash}_${key} \\\"\\\$@\\\"\""
}


# Émule quelque chose de similaire à:  isset(hash[key]) ou hash[key]==NULL
#
# Paramètres:
# 1 - hash
# 2 - key
# Renvoit:
# 0 - cette clé existe
# 1 - cette clé n'existe pas
function hash_is_set {
        eval "if [[ \"\${${Hash_config_varname_prefix}${1}_${2}-a}\" = \"a\" && 
                        \"\${${Hash_config_varname_prefix}${1}_${2}-b}\" = \"b\" ]]; then return 1; else return 0; fi"
}


# Émule quelque chose de similaire à:
#   foreach($hash as $key => $value) { fun($key,$value); }
#
# Il est possible d'écrite plusieurs variations de cette fonction.
# Ici, nous utilisons un appel de fonction pour la rendre aussi "générique" que possible.
#
# Paramètres:
# 1 - hash
# 2 - function name
function hash_foreach {
        local keyname oldIFS="$IFS"
        IFS=' '
        for i in $(eval "echo \${!${Hash_config_varname_prefix}${1}_*}"); do
                keyname=$(eval "echo \${i##${Hash_config_varname_prefix}${1}_}")
                eval "$2 $keyname \"\$$i\""
        done
        IFS="$oldIFS"
}

# NOTE : Sur les lignes 103 et 116, modification de l'arobase.
#        Mais, cela n'a pas d'importance parce qu'il s'agit de lignes de commentaires.

Voici un exemple de script utilisant cette bibliothèque de hachage.

Exemple A.22. Coloriser du texte en utilisant les fonctions de hachage

#!/bin/bash
# hash-example.sh: Colorisation de texte.
# Auteur : Mariusz Gniazdowski <mgniazd-at-gmail.com>

. Hash.lib      # Chargement de la bibliothèque des fonctions.

hash_set couleurs rouge        "\033[0;31m"
hash_set couleurs bleu         "\033[0;34m"
hash_set couleurs bleu_leger   "\033[1;34m"
hash_set couleurs rouge_leger  "\033[1;31m"
hash_set couleurs cyan         "\033[0;36m"
hash_set couleurs vert_leger   "\033[1;32m"
hash_set couleurs gris_leger   "\033[0;37m"
hash_set couleurs vert         "\033[0;32m"
hash_set couleurs jaune        "\033[1;33m"
hash_set couleurs violet_leger "\033[1;35m"
hash_set couleurs violet       "\033[0;35m"
hash_set couleurs reset_couleur "\033[0;00m"


# $1 - nom de la clé
# $2 - valeur
essaie_couleurs() {
        echo -en "$2"
        echo "Cette ligne est $1."
}
hash_foreach couleurs essaie_couleurs
hash_echo couleurs reset_couleur -en

echo -e '\nSurchargeons quelques couleurs avec du jaune.\n'
# Il est difficile de lire du texte jaune sur certains terminaux.
hash_dup couleurs jaune rouge vert_leger bleu vert gris_leger cyan
hash_foreach couleurs essaie_couleurs
hash_echo couleurs reset_color -en

echo -e '\nSupprimons-les et essayons couleurs une fois encore...\n'

for i in rouge vert_leger bleu vert gris_leger cyan; do
        hash_unset couleurs $i
done
hash_foreach couleurs essaie_couleurs
hash_echo couleurs reset_couleur -en

hash_set autre texte "Autres exemples..."
hash_echo autre texte
hash_get_into autre txt texte
echo $texte

hash_set autre my_fun essaie_couleurs
hash_call autre my_fun   purple "`hash_echo couleurs violet`"
hash_echo couleurs reset_couleur -en

echo; echo "Retour à la normale ?"; echo

exit $?

#  Sur certains terminaux, les couleurs "légères" sont affichées en gras
#  et finissent par sembler plus sombres que les normales.
#  Pourquoi ?


Un exemple illustrant les mécanismes de hachage à partir d'un autre point de vue.

Exemple A.23. Encore plus sur les fonctions de hachage

#!/bin/bash
# $Id: ha.sh,v 1.2 2007/01/08 23:58:45 gleu Exp $
# Copyright 2005 Oliver Beckstein
# Sous licence GNU Public
# L'auteur du script a donné le droit de l'inclure dans le guide ABS.
# (Merci !)

#----------------------------------------------------------------
# pseudo hachage basé sur l'expansion des paramètres indirects
# API : accès par les fonctions :
# 
# création du hachage :
#  
#      newhash Lovers
#
# ajout des entrées (notez les guillemets pour les espaces)
#    
#      addhash Lovers Tristan Isolde
#      addhash Lovers 'Romeo Montague' 'Juliet Capulet'
#
# accès à la valeur par la clé
#
#      gethash Lovers Tristan   ---->  Isolde
#
# affichage de toutes les valeurs
#
#      keyshash Lovers         ----> 'Tristan'  'Romeo Montague'
#
#
# Convention : au lieu de la syntaxe perls foo{bar} = boing',
# utiliser
#       '_foo_bar=boing' (deux tirets bas, pas d'espaces)
#
# 1) stocke la clé    dans _NAME_keys[]
# 2) stocke la valeur dans _NAME_values[] en utilisant le même index
# L'index de la dernière entrée est _NAME_ptr
#
# NOTE : pas de vérification d'erreurs.


function _inihash () {
    # fonction privée
    # appelée au début de chaque procédure
    # définit : _keys _values _ptr
    #
    # usage : _inihash NAME
    local name=$1
    _keys=_${name}_keys
    _values=_${name}_values
    _ptr=_${name}_ptr
}

function newhash () {
    # usage : newhash NAME
    #        NAME ne devrait pas contenir d'espaces ou de '.'.
    #        En fait, il doit être un nom syntaxiquement correct pour une variable Bash.
    # Nous nous reposons sur Bash pour reconnaître automatiquement des tableaux.
    local name=$1 
    local _keys _values _ptr
    _inihash ${name}
    eval ${_ptr}=0
}


function addhash () {
    # usage : addhash NAME KEY 'VALUE with spaces'
    #        les arguments avec espaces doivent être mis entre guillemets ''
    local name=$1 k="$2" v="$3" 
    local _keys _values _ptr
    _inihash ${name}

    #echo "DEBUG(addhash): ${_ptr}=${!_ptr}"

    eval let ${_ptr}=${_ptr}+1
    eval "$_keys[${!_ptr}]=\"${k}\""
    eval "$_values[${!_ptr}]=\"${v}\""
}

function gethash () {
    # usage: gethash NAME KEY
    #        returns boing
    #        ERR=0 si le nom est trouvée, 1 sinon
    # Ce n'est pas un bon hachage -- nous cherchons simplement dans les clés
    local name=$1 key="$2" 
    local _keys _values _ptr 
    local k v i found h
    _inihash ${name}
    
    # _ptr contient l'index le plus haut dans le hachage
    found=0

    for i in $(seq 1 ${!_ptr}); do
        h="\${${_keys}[${i}]}"  # plus propre de le faire en deux étapes
        eval k=${h}             # (tout spécialement avec les guillemets pour les espaces)
        if [ "${k}" = "${key}" ]; then found=1; break; fi
    done;

    [ ${found} = 0 ] && return 1;
    # sinon i est l'index qui correspond à la clé
    h="\${${_values}[${i}]}"
    eval echo "${h}"
    return 0;   
}

function keyshash () {
    # usage : keyshash NAME
    # renvoie la liste de toutes les clés définies pour le nom du hachage
    local name=$1 key="$2" 
    local _keys _values _ptr 
    local k i h
    _inihash ${name}
    
    # _ptr contient l'index le plus haut du hachage
    for i in $(seq 1 ${!_ptr}); do
        h="\${${_keys}[${i}]}"   # plus propre de le faire en deux étapes
        eval k=${h}              # (tout spécialement avec les guillemets pour les espaces)
        echo -n "'${k}' "
    done;
}


# --------------------------------------------------------------------

# Maintenant, testons-le.
# (d'après les commentaires au début du script).
newhash Lovers
addhash Lovers Tristan Isolde
addhash Lovers 'Romeo Montague' 'Juliet Capulet'

# Résultats en sortie.
echo
gethash Lovers Tristan      # Isolde
echo
keyshash Lovers             # 'Tristan' 'Romeo Montague'
echo; echo


exit 0

# Exercice :
#----------
#
# Ajouter des vérifications d'erreur aux fonctions.

Maintenant, un script qui installe et monte ces jolies clés USB, version « disques durs ».

Exemple A.24. Monter des périphériques de stockage USB

#!/bin/bash
# ==> usb.sh
# ==> Script pour monter et installer les périphériques de stockage d'une clé USB.
# ==> Lancer en tant que root au démarrage du système (voir ci-dessous).
# ==>
# ==> Les nouvelles distributions Linux (2004 ou ultérieures) détectent
# ==> automatiquement et installent les clés USB.
# ==> Elles n'ont donc pas besoin de ce script.
# ==> Mais c'est toujours instructif.

#  This code is free software covered by GNU GPL license version 2 or above.
#  Please refer to http://www.gnu.org/ for the full license text.
#
#  Ce code est un logiciel libre couvert par la licence GNU GPL version 2 et
#+ ultérieure. Référez-vous à http://www.gnu.org/ pour le texte complet.
#
#  Une partie du code provient de usb-mount écrit par Michael Hamilton (LGPL)
#+ voir http://users.actrix.co.nz/michael/usbmount.html
#
#  INSTALLATION
#  ------------
#  Placez ceci dans /etc/hotplug/usb/clefusb.
#  Puis regardez dans /etc/hotplug/usb.distmap, copiez toutes les entrées de
#+ stockage USB dans /etc/hotplug/usb.usermap, en substituant "usb-storage" par
#+ "diskonkey".
#  Sinon, ce code est seulement lancé lors de l'appel/suppression du module du
#+ noyau (au moins lors de mes tests), ce qui annule le but.
#
#  A FAIRE
#  -------
#  Gère plus d'un périphérique "diskonkey" en même temps (c'est-à-dire
#+ /dev/diskonkey1 et /mnt/clefusb1), etc. Le plus gros problème ici concerne
#+ la gestion par devlabel, que je n'ai pas essayé.
#
#  AUTEUR et SUPPORT
#  -----------------
#  Konstantin Riabitsev, &lt;icon linux duke edu&gt;.
#  Envoyez tout problème via mon adresse de courrier électronique.
#
# ==> Commentaires ajoutés par l'auteur du guide ABS.



PERIPH_LIENSYMBOLIQUE=/dev/diskonkey
POINT_MONTAGE=/mnt/clefusb
LABEL_PERIPH=/sbin/devlabel
CONFIG_LABEL_PERIPH=/etc/sysconfig/devlabel
JE_SUIS=$0

##
# Fonctions pratiquement récupérées du code d'usb-mount.
#
function tousUsbScsiAttaches {
    find /proc/scsi/ -path '/proc/scsi/usb-storage*' -type f |
    xargs grep -l 'Attaché: Oui'
}
function periphScsiAPartirScsiUsb {
    echo $1 | awk -F"[-/]" '{ n=$(NF-1);
    print "/dev/sd" substr("abcdefghijklmnopqrstuvwxyz", n+1, 1) }'
}

if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]; then
    ##
    # Récupéré du code d'usbcam.
    #
    if [ -f /var/run/console.lock ]; then
        PROPRIETAIRE_CONSOLE=`cat /var/run/console.lock`
    elif [ -f /var/lock/console.lock ]; then
        PROPRIETAIRE_CONSOLE=`cat /var/lock/console.lock`
    else
        PROPRIETAIRE_CONSOLE=
    fi
    for entreeProc in $(tousUsbScsiAttaches); do
        scsiDev=$(periphScsiAPartirScsiUsb $entreeProc)
        #  Quelques bogues avec usb-storage?
        #  Les partitions ne sont pas dans /proc/partitions jusqu'à ce qu'elles
        #+ soient utilisées.
        /sbin/fdisk -l $scsiDev >/dev/null
        ##
        #  La plupart des périphériques ont des informations de partitionnement,
        #+ donc les données sont sur /dev/sd?1. Néanmois, quelques-uns plus
        #+ stupides n'ont pas du tout de partitions et utilisent le périphérique
        #+ complet pour du stockage de données. Il essaie de deviner si vous
        #+ avez un /dev/sd?1 et si non, il utilise le périphérique entier.
        #
        if grep -q `basename $scsiDev`1 /proc/partitions; then
            part="$scsiDev""1"
        else
            part=$scsiDev
        fi
        ##
        #  Modifie le propriétaire de la partition par l'utilisateur de la
        #+ console pour qu'ils puissent le monter.
        #
        if [ ! -z "$PROPRIETAIRE_CONSOLE" ]; then
            chown $PROPRIETAIRE_CONSOLE:disk $part
        fi
        ##
        # Ceci vérifie si nous avons déjà cet UID défini avec devlabel. Sinon,
        # il ajoute alors le périphérique à la liste.
        #
        prodid=`$LABEL_PERIPH printid -d $part`
        if ! grep -q $prodid $CONFIG_LABEL_PERIPH; then
                # croisez les doigts et espérez que cela fonctionne
            $LABEL_PERIPH add -d $part -s $PERIPH_LIENSYMBOLIQUE 2>/dev/null
        fi
        ##
        # Vérifie si le point de montage existe et le crée dans le cas contraire.
        #
        if [ ! -e $POINT_MONTAGE ]; then
            mkdir -p $POINT_MONTAGE
        fi
        ##
        # S'occupe de /etc/fstab pour faciliter le montage.
        #
        if ! grep -q "^$PERIPH_LIENSYMBOLIQUE" /etc/fstab; then
            # Ajoute une entrée fstab
            echo -e \
                "$PERIPH_LIENSYMBOLIQUE\t\t$POINT_MONTAGE\t\tauto\tnoauto,owner,kudzu 0 0" \
                >> /etc/fstab
        fi
    done
    if [ ! -z "$REMOVER" ]; then
        ##
        #  Assurez-vous que ce script est appelé lors de la suppression du
        #+ périphérique.
        #
        mkdir -p `dirname $REMOVER`
        ln -s $JE_SUIS $REMOVER
    fi
elif [ "${ACTION}" = "remove" ]; then
    ##
    # Si le périphérique est monté, le démonte proprement.
    #
    if grep -q "$POINT_MONTAGE" /etc/mtab; then
        # Démonte proprement.
        umount -l $POINT_MONTAGE
    fi
    ##
    # Le supprime à partir de /etc/fstab s'il existe.
    #
    if grep -q "^$PERIPH_LIENSYMBOLIQUE" /etc/fstab; then
        grep -v "^$PERIPH_LIENSYMBOLIQUE" /etc/fstab > /etc/.fstab.new
        mv -f /etc/.fstab.new /etc/fstab
    fi
fi

exit 0

Un script qui réalise une conversion texte vers HTML.

Exemple A.25. Convertir en HTML

#!/bin/bash
# tohtml.sh

# Convertit un fichier texte au format HTML.
# Auteur :      Mendel Cooper
# Licence :     GPL3
# Utilisation : sh tohtml.sh < fichiertexte > fichierhtml
#  Ce script est facilement modifiable pour accepter
#+ des noms de fichier source et destination.

#     Suppositions :
# 1) Les paragraphes du fichier texte (cible) sont séparés par une ligne blanche.
# 2) Les images JPEG (*.jpg) sont situées dans le sous-répertoire "images".
# 3) Les phrases importantes (en italique) commencent avec un espace suivi d'un
#+   tiret bas ou sont le premier caractère sur la ligne et finissent avec un
#+   tiret bas suivi d'un espace ou d'une fin de ligne.


# Paramétrages
TAILLEPOLICE=2        # Taille de police.
REPIMG="images"  # Répertoire images.
# En-têtes
ENT01='&lt;!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN"&gt;'
ENT02='&lt;!-- Convertit en HTML par le script ***tohtml.sh*** --&gt;'
ENT03='&lt;!-- auteur du script : M. Leo Cooper &lt;thegrendel@theriver.com&gt; --&gt;'
ENT10='&lt;html&gt;'
ENT11='&lt;head&gt;'
ENT11a='&lt;/head&gt;'
ENT12a='&lt;title&gt;'
ENT12b='&lt;/title&gt;'
ENT121='&lt;META NAME="GENERATOR" CONTENT="tohtml.sh script"&gt;'
ENT13='&lt;body bgcolor="#dddddd"&gt;'   # Modifie la couleur du fond.
ENT14a='&lt;font size='
ENT14b='&gt;'
# Bas de page
FTR10='&lt;/body&gt;'
FTR11='&lt;/html&gt;'
# Balises
GRAS="&lt;b&gt;"
CENTRE="&lt;center&gt;"
FIN_CENTRE="&lt;/center&gt;"
LF="&lt;br&gt;"


ecrire_entetes ()
  {
  echo "$ENT01"
  echo
  echo "$ENT02"
  echo "$ENT03"
  echo
  echo
  echo "$ENT10"
  echo "$ENT11"
  echo "$ENT121"
  echo "$ENT11a"
  echo "$ENT13"
  echo
  echo -n "$ENT14a"
  echo -n "$TAILLEPOLICE"
  echo "$ENT14b"
  echo
  echo "$GRAS"        # Tout en gras (plus facile à lire).
  }


traitement_texte ()
  {
  while read ligne    # Lire une ligne à la fois.
  do
    {
    if [ ! "$ligne" ] # Ligne vide ?
    then              # Alors un nouveau paragraphe doit suivre.
      echo
      echo "$LF"      # Insérer deux balises &lt;br&gt;.
      echo "$LF"
      echo
      continue        # Ignorer le test du tiret bas.
    else              # Sinon...

      if [[ "$ligne" =~ "\[*jpg\]" ]] # Une image ?
      then                            # Supprimer les crochets.
        temp=$( echo "$ligne" | sed -e 's/\[//' -e 's/\]//' )
        line=""$CENTRE" &lt;img src="\"$REPIMG"/$temp\"&gt; "$FIN_CENTRE" "
                                      # Ajouter la balise de l'image
                                      # et la centrer.
      fi

    fi


    echo "$ligne" | grep -q _
    if [ "$?" -eq 0 ]    # Si la ligne contient un tiret bas..
    then
      # ============================================================
      # Placer en italique une phrase entre tiret bas.
      temp=$( echo "$ligne" |
              sed -e 's/ _/ &lt;i&gt;/' -e 's/_ /&lt;\/i&gt; /' |
              sed -e 's/^_/&lt;i&gt;/'  -e 's/_$/&lt;\/i&gt;/' )
      #  Traiter seulement les tirets bas préfixés par un espace,
      #+ suivi par un espace ou en fin ou en début de ligne.
      #  Ne pas convertir les tirets bas contenus dans un mot !
      line="$temp"
      # Ralentit l'exécution du script. Cela peut-il être optimisé ?
      # ============================================================
    fi


   
    echo
    echo "$ligne"
    echo
    } # Fin while
  done
  }   # Fin traitement_texte ()


ecrire_basdepage ()  # Fin des balises.
  {
  echo "$FTR10"
  echo "$FTR11"
  }


# main () {
# =========
ecrire_entetes
traitement_texte
ecrire_basdepage
# =========
#         }

exit $?

#  Exercices :
#  ----------
#  1) Correction : vérifiez le tiret bas de fermeture avant une virgule ou
#+    un point.
#  2) Ajoutez un test sur le présence d'un tiret bas de fin dans les phrases
#+    à mettre en italique.

Voici quelque chose qui va réchauffer le coeur des webmasters : un script qui sauvegarde les traces du serveur web.

Exemple A.26. Préserver les weblogs

#!/bin/bash
# archiveweblogs.sh v1.0

# Troy Engel <tengel@fluid.com>
# Légèrement modifié par l'auteur du document
# Utilisé avec sa permission.
#
#  Ce script préservera les traces web habituellement supprimées à partir d'une
#+ installation RedHat/Apache par défaut.
#  Il sauvegardera les fichiers en indiquant la date et l'heure dans le nom du
#+ fichier, compressé avec bzip, dans un répertoire donné.
#
#  Lancez ceci avec crontab la nuit car bzip2 avale la puissance du CPU sur des
#+ journaux particulièrement gros.
#  0 2 * * * /opt/sbin/archiveweblogs.sh


PROBLEME=66

# Modifiez-le par votre répertoire de sauvegarde.
REP_SAUVEGARDE=/opt/sauvegardes/journaux_web

# Apache/RedHat par défaut
JOURS_DE_SAUVEGARDE="4 3 2 1"
REP_JOURNAUX=/var/log/httpd
JOURNAUX="access_log error_log"

# Emplacement par défaut des programmes RedHat
LS=/bin/ls
MV=/bin/mv
ID=/usr/bin/id
CUT=/bin/cut
COL=/usr/bin/column
BZ2=/usr/bin/bzip2

# Sommes-nous root?
USER=`$ID -u`
if [ "X$USER" != "X0" ]; then
  echo "PANIQUE : Seul root peut lancer ce script !"
  exit $PROBLEME
fi

# Le répertoire de sauvegarde existe-t'il ? est-il modifiable ?
if [ ! -x $REP_SAUVEGARDE ]; then
  echo "PANIQUE : $REP_SAUVEGARDE n'existe pas ou n'est pas modifiable !"
  exit $PROBLEME
fi

# Déplace, renomme et compresse avec bzip2 les journaux
for jour in $JOURS_DE_SAUVEGARDE; do
  for journal in $JOURNAUX; do
    MONFICHIER="$REP_JOURNAUX/$journal.$jour"
    if [ -w $MONFICHIER ]; then
      DTS=`$LS -lgo --time-style=+%Y%m%d $MONFICHIER | $COL -t | $CUT -d ' ' -f7`
      $MV $MONFICHIER $REP_SAUVEGARDE/$journal.$DTS
      $BZ2 $REP_SAUVEGARDE/$journal.$DTS
    else
            # Affiche une erreur seulement si le fichier existe (ne peut
            # s'écrire sur lui-même).
      if [ -f $MONFICHIER ]; then
        echo "ERREUR : $MONFICHIER n'est pas modifiable. Je passe au suivant."
      fi
    fi
  done
done

exit 0

Comment empêcher le shell d'étendre et de réinterpréter les chaînes ?

Exemple A.27. Protéger les chaînes littérales

#! /bin/bash
# protect_literal.sh

# set -vx

:<<-'_Protect_Literal_String_Doc'

    Copyright (c) Michael S. Zick, 2003; All Rights Reserved
    License: Unrestricted reuse in any form, for any purpose.
    Warranty: None
    Revision: $ID$

    Copyright (c) Michael S. Zick, 2003; Tous droits réservés
    Licence: Utilisation non restreinte quelque soit sa forme, quelque soit le
    but.
    Garantie : Aucune
    Revision: $ID$

    Documentation redirigée vers no-operation sous Bash. Bash enverra ce bloc
    vers '/dev/null' lorsque le script sera lu la première fois.
    (Supprimez le commentaire de la commande ci-dessus pour voir cette action.)

    Supprimez la première ligne (Sha-Bang, #!) lors de l'utilisation de ce
    script en tant que procédure d'une bibliothèque. Décommentez aussi 
    le code d'exemple utilisé dans les deux places indiquées.


    Usage:
        _protect_literal_str 'une chaine quelconque qui correspond à votre
        ${fantaisie}'
        Affiche simplement l'argument sur la sortie standard, les guillemets étant
        restaurés.

        $(_protect_literal_str 'une chaine quelconque qui correspond à votre
        ${fantaisie}')
        sur le côté droit d'une instruction d'affectation.

    Fait:
        Utilisé sur le côté droit d'une affectation, préserve les guillemets
        protégeant le contenu d'un littéral lors de son affectation.

    Notes:
        Les noms étranges (_*) sont utilisé pour éviter de rencontrer ceux
        choisis par l'utilisateur lorsqu'il l'utilise en tant que bibliothèque.


_Protect_Literal_String_Doc

# La fonction 'pour illustration'

_protect_literal_str() {

# Récupére un caractère inutilisé, non affichable comme IFS local.
# Non requis, mais montre ce que nous ignorons.
    local IFS=$'\x1B'               # caractère \ESC

# Entoure tous_elements_de entre guillemets lors de l'affectation.
    local tmp=$'\x27'$@$'\x27'
#    local tmp=$'\''$@$'\''         # Encore plus sale.

    local len=${#tmp}               # Info seulement.
    echo $tmp a une longueur de $len.         # Sortie ET information.
}

# Ceci est la version nom-court.
_pls() {
    local IFS=$'x1B'                # caractère \ESC (non requis)
    echo $'\x27'$@$'\x27'           # Paramètre global codé en dur
}

# :<<-'_Protect_Literal_String_Test'
# # # Supprimez le "# " ci-dessus pour désactiver ce code. # # #

# Voir à quoi ressemble ceci une fois affiché.
echo
echo "- - Test Un - -"
_protect_literal_str 'Bonjour $utilisateur'
_protect_literal_str 'Bonjour "${nom_utilisateur}"'
echo

# Ce qui donne :
# - - Test Un - -
# 'Bonjour $utilisateur' fait 13 caractères de long.
# 'Bonjour "${nom_utilisateur}"' a une taille de 21 caractères.

#  Cela ressemble à notre attente, donc pourquoi tout ceci ?
#  La différence est cachée à l'intérieur de l'ordonnancement interne des opérations
#+ de Bash.
#  Ce qui s'affiche lorsque vous l'utilisez sur le côté droit de l'affectation.

# Déclarez un tableau pour les valeurs de tests.
declare -a tableauZ

#  Affecte les éléments comprenant différents types de guillemets et de caractères
#+ d'échappement.
tableauZ=( zero "$(_pls 'Bonjour ${Moi}')" 'Bonjour ${Toi}' "\'Passe: ${pw}\'" )

# Maintenant, affiche ce tableau.
echo "- - Test Deux - -"
for (( i=0 ; i<${#tableauZ[*]} ; i++ ))
do
    echo  Elément $i: ${tableauZ[$i]} fait  ${#tableauZ[$i]} caractères de long.
done
echo

# Ce qui nous donne :
# - - Test Deux - -
# Elément 0: zero fait 4 caractères de long.           # Notre élément marqueur
# Elément 1: 'Bonjour ${Moi}' fait 13 caractères de long.# Notre "$(_pls '...' )"
# Elément 2: Bonjour ${Toi} fait 12 caractères de long.  # Les guillemets manquent
# Elément 3: \'Passe: \' fait 10 caractères de long.    # ${pw} n'affiche rien

# Maintenant, affectez ce résultat.
declare -a tableau2=( ${tableauZ[@]} )

# Et affiche ce qui s'est passé.
echo "- - Test Trois - -"
for (( i=0 ; i<${#tableau2[*]} ; i++ ))
do
    echo  Elément $i: ${tableau2[$i]} fait ${#tableau2[$i]} caractères de long.
done
echo

# Ce qui nous donne :
# - - Test Trois - -
# Elément 0: zero fait 4 caractères de long.         # Notre élément marqueur.
# Elément 1: Hello ${Moi} fait 11 caractères de long.# Résultat attendu.
# Elément 2: Hello fait 5 caractères de long.        # ${Toi} n'affiche rien.
# Elément 3: 'Passe: fait 6 caractères de long.      # Se coupe sur les espaces.
# Elément 4: ' fait 1 caractères de long.            # Le guillemet final est ici
                                                     # maintenant.

#  Les guillemets de début et de fin de notre élément 1 sont supprimés.
#  Bien que non affiché, les espaces blancs de début et de fin sont aussi supprimés.
#  Maintenant que le contenu des chaînes est initialisé, Bash placera toujours, en interne,
#+ entre guillemets les contenus comme requis lors de ses opérations.

#  Pourquoi?
#  En considérant notre construction "$(_pls 'Hello ${Moi}')" :
#  " ... " -> Supprime les guillemets.
#  $( ... ) -> Remplace avec le resultat de ..., supprime ceci.
#  _pls ' ... ' -> appelé avec des arguments littérales, supprime les guillemets.
#  Le résultat renvoyé inclut les guillemets ; MAIS le processus ci-dessus a déjà
#+ été réalisé, donc il devient une partie de la valeur affectée.
#
#  De manière identique, lors d'une utilisation plus poussée de la variable de type
#+ chaînes de caractères, le ${Moi} fait partie du contenu (résultat) et survit à
#+ toutes les opérations.
#  (Jusqu'à une indication explicite pour évaluer la chaîne).

#  Astuce : Voir ce qui arrive lorsque les guillemets ($'\x27') sont remplacés par
#+ des caractères ($'\x22') pour les procédures ci-dessus.
#  Intéressant aussi pour supprimer l'ajout de guillemets.

# _Protect_Literal_String_Test
# # # Supprimez le caractère "# " ci-dessus pour désactiver ce code. # # #

exit 0

Et si vous voulez que le shell étende et réinterprète les chaînes ?

Exemple A.28. Ne pas protéger les chaînes littérales

#! /bin/bash
# unprotect_literal.sh

# set -vx

:<<-'_UnProtect_Literal_String_Doc'

    Copyright (c) Michael S. Zick, 2003; All Rights Reserved
    License: Unrestricted reuse in any form, for any purpose.
    Warranty: None
    Revision: $ID$

    Copyright (c) Michael S. Zick, 2003; Tous droits réservés
    Licence: Utilisation non restreinte quelque soit sa forme, quelque soit le
    but.
    Garantie : Aucune
    Revision: $ID$

    Documentation redirigée vers no-operation sous Bash. Bash enverra ce bloc
    vers '/dev/null' lorsque le script est lu la première fois.
    (Supprimez le commentaire de la commande ci-dessus pour voir cette action.)

    Supprimez la première ligne (Sha-Bang, #!) lors de l'utilisation de ce
    script en tant que procédure d'une bibliothèque. Dé-commentez aussi 
    le code d'exemple utilisé dans les deux places indiquées.


    Utilisation:
        Complément de la fonction "$(_pls 'Chaine litterale')".
        (Voir l'exemple protect_literal.sh.)

        VarChaine=$(_upls VariableChaineProtege)

    Fait:
        Lorsqu'utilisé sur le côté droit d'une instruction d'affectation ;
        fait que la substition est intégré à la chaîne protégée.

    Notes:
        Les noms étranges (_*) sont utilisé pour éviter de rencontrer ceux
        choisis par l'utilisateur lorsqu'il l'utilise en tant que bibliothèque.


_UnProtect_Literal_String_Doc

_upls() {
    local IFS=$'x1B'                # Caractère \ESC (non requis)
    eval echo $@                    # Substitution on the glob.
}

# :<<-'_UnProtect_Literal_String_Test'
# # # Supprimez le "# " ci-dessus pour désactiver ce code. # # #


_pls() {
    local IFS=$'x1B'                # Caractère \ESC (non requis)
    echo $'\x27'$@$'\x27'           # Paramètre global codé en dur.
}

# Déclare un tableau pour les valeurs de tests.
declare -a tableauZ

# Affecte les éléments avec des types différents de guillements et échappements.
tableauZ=( zero "$(_pls 'Bonjour ${Moi}')" 'Bonjour ${Toi}' "\'Passe: ${pw}\'" )

# Maintenant, faire une affectation avec ce résultat.
declare -a tableau2=( ${tableauZ[@]} )

# Ce qui fait :
# - - Test trois - -
# Elément 0: zero est d'une longueur 4            # Notre élément marqueur.
# Elément 1: Bonjour ${Moi} est d'une longueur 11 # Résultat attendu.
# Elément 2: Bonjour est d'une longueur 5         # ${Toi} ne renvoit rien.
# Elément 3: 'Passe est d'une longueur 6          # Divisé sur les espaces.
# Elément 4: ' est d'une longueur 1               # La fin du guillemet est ici
                                                  # maintenant.

# set -vx

#  Initialise 'Moi' avec quelque-chose pour la substitution imbriqué ${Moi}.
#  Ceci a besoin d'être fait SEULEMENT avant d'évaluer la chaîne protégée.
#  (C'est pourquoi elle a été protégée.)

Moi="au gars du tableau."

# Initialise une variable de chaînes de caractères pour le résultat.
nouvelleVariable=$(_upls ${tableau2[1]})

# Affiche le contenu.
echo $nouvelleVariable

# Avons-nous réellement besoin d'une fonction pour faire ceci ?
variablePlusRecente=$(eval echo ${tableau2[1]})
echo $variablePlusRecente

#  J'imagine que non mais la fonction _upls nous donne un endroit où placer la
#+ documentation.
#  Ceci aide lorsque nous oublions une construction # comme ce que signifie
#+ $(eval echo ... ).

#  Que se passe-t'il si Moi n'est pas initialisé quand la chaîne protégée est
#+ évaluée ?
unset Moi
variableLaPlusRecente=$(_upls ${tableau2[1]})
echo $variableLaPlusRecente

# Simplement partie, pas d'aide, pas d'exécution, pas d'erreurs.

#  Pourquoi ?
#  Initialiser le contenu d'une variable de type chaîne contenant la séquence de
#+ caractères qui ont une signification dans Bash est un problème général
#+ d'écriture des scripts.
#
#  Ce problème est maintenant résolu en huit lignes de code (et quatre pages de
#+ description).

#  Où cela nous mène-t'il ?
#  Les pages web au contenu dynamique en tant que tableau de chaînes Bash.
#  Le contenu par requête pour une commande Bash 'eval' sur le modèle de page
#+ stocké.
#  Pas prévu pour remplacer PHP, simplement quelque chose d'intéressant à faire.
###
#  Vous n'avez pas une application pour serveur web ?
#  Aucun problème, vérifiez dans le répertoire d'exemples des sources Bash :
#+ il existe aussi un script Bash pour faire ça. 

# _UnProtect_Literal_String_Test
# # # Supprimez le "# " ci-dessus pour désactiver ce code. # # #

exit 0

Ce puissant script chasse les spammers.

Exemple A.29. Identification d'un spammer

#!/bin/bash

# $Id: is_spammer.bash,v 1.6 2007/01/08 23:58:45 gleu Exp $
# L'information ci-dessus est l'ID RCS.

# La dernière version de ce script est disponible sur http://www.morethan.org.
#
# Spammer-identification
# par Michael S. Zick
# Utilisé dans le guide ABS Guide avec sa permission.



#######################################################
# Documentation
# Voir aussi "Quickstart" à la fin du script.
#######################################################

:<<-'__is_spammer_Doc_'

    Copyright (c) Michael S. Zick, 2004
    Licence : Ré-utilisation non restreinte quelque soit la forme et
              le but
    Garantie: Aucune -{C'est un script; l'utilisateur est seul responsable.}-

Impatient?
    Code de l'application : Allez à "# # #  Code 'Chez le spammeur'  # # #"
    Sortie d'exemple      : ":<<-'_is_spammer_outputs_'"
    Comment l'utiliser    : Entrer le nom du script sans arguments.
                Ou allez à "Quickstart" à la fin du script.

Fournit
    Avec un nom de domaine ou une adresse IP(v4) en entrée :

    Lance un ensemble exhaustif de requêtes pour trouver les ressources réseau
    associées (raccourci pour un parcours récursif dans les TLD).

    Vérifie les adresses IP(v4) disponibles sur les serveurs de noms Blacklist.

    S'il se trouve faire partie d'une adresse IP(v4) indiquée, rapporte les
    enregistrements texte de la liste noire.
    (habituellement des liens hypertextes vers le rapport spécifique.)

Requiert
    Une connexion Internet fonctionnelle.
    (Exercice : ajoutez la vérification et/ou annulez l'opération si la
    connexion n'est pas établie lors du lancement du script.)
    Une version de Bash disposant des tableaux (2.05b+).

    Le programme externe 'dig' --
    ou outil fourni avec l'ensemble de programmes 'bind'.
    Spécifiquement, la version qui fait partie de Bind série 9.x
    Voir : http://www.isc.org

    Toutes les utilisations de 'dig' sont limitées à des fonctions d'emballage,
    qui pourraient être ré-écrites si nécessaire.
    Voir : dig_wrappers.bash pour plus de détails.
         ("Documentation supplémentaire" -- ci-dessous)

Usage
    Ce script requiert un seul argument, qui pourrait être:
    1) Un nom de domaine ;
    2) Une adresse IP(v4) ;
    3) Un nom de fichier, avec un nom ou une adresse par ligne.

    Ce script accepte un deuxième argument optionnel, qui pourrait être:
    1) Un serveur de noms Blacklist ;
    2) Un nom de fichier avec un serveur de noms Blacklist par ligne.

    Si le second argument n'est pas fourni, le script utilise un ensemble
    intégré de serveurs Blacklist (libres).

    Voir aussi la section Quickstart à la fin de ce script (après 'exit').

Codes de retour
    0 - Tout est OK
    1 - Échec du script
    2 - Quelque chose fait partie de la liste noire

Variables d'environnement optionnelles
    SPAMMER_TRACE
        S'il comprend le nom d'un fichier sur lequel le script a droit
        d'écriture, le script tracera toute l'exécution.

    SPAMMER_DATA
        S'il comprend le nom d'un fichier sur lequel le script a droit
        d'écriture, le script y enregitrera les données trouvées sous la forme
        d'un fichier GraphViz.
        Voir : http://www.research.att.com/sw/tools/graphviz

    SPAMMER_LIMIT
        Limite la profondeur des recherches de ressources.

        Par défaut à deux niveaux.

        Un paramètrage de 0 (zero) signifie 'illimité' . . .
          Attention : le script pourrait parcourir tout Internet !

        Une limite de 1 ou 2 est plus utile dans le cas d'un fichier de noms de
        domaine et d'adresses.
        Une limite encore plus haute est utile pour chasser les gangs de spam.


Documentation supplémentaire
    Téléchargez l'ensemble archivé de scripts expliquant et illustrant la
    fonction contenue dans ce script.
    http://personal.riverusers.com/mszick_clf.tar.bz2


Notes d'étude
    Ce script utilise un grand nombre de fonctions.
    Pratiquement toutes les fonctions générales ont leur propre script
    d'exemple. Chacun des scripts d'exemples ont leur commentaires (niveau
    tutoriel).

Projets pour ce script
    Ajoutez le support des adresses IP(v6).
    Les adresses IP(v6) sont reconnues mais pas gérées.

Projet avancé
    Ajoutez le détail de la recherche inverse dans les informations découvertes.

    Rapportez la chaîne de délégation et les contacts d'abus.

    Modifiez la sortie du fichier GraphViz pour inclure les informations
    nouvellement découvertes.

__is_spammer_Doc_

#######################################################




#### Configuration spéciale pour l'IFS utilisée pour l'analyse des chaînes. ####

# Espace blanc == :Espace:Tabulation:Retour à la ligne:Retour chariot:
WSP_IFS=$'\x20'$'\x09'$'\x0A'$'\x0D'

# Pas d'espace blanc == Retour à la ligne:Retour chariot
NO_WSP=$'\x0A'$'\x0D'

# Séparateur de champ pour les adresses IP décimales
ADR_IFS=${NO_WSP}'.'

# Tableau de conversions de chaînes
DOT_IFS='.'${WSP_IFS}

# # # Machine à pile pour les opérations restantes # # #
# Cet ensemble de fonctions est décrite dans func_stack.bash.
# (Voir "Documentation supplémentaire" ci-dessus.)
# # #

# Pile globale des opérations restantes.
declare -f -a _pending_
# Sentinelle gloable pour les épuiseurs de pile
declare -i _p_ctrl_
# Déteneur global pour la fonction en cours d'exécution
declare -f _pend_current_

# # # Version de déboguage seulement - à supprimer pour une utilisation normale
# # #
#
# La fonction stockée dans _pend_hook_ est appellée immédiatement avant que
# chaque fonction en cours ne soit évaluée. Pile propre, _pend_current_ configuré.
#
# Ceci est démontré dans pend_hook.bash.
declare -f _pend_hook_
# # #

# La fonction ne faisant rien.
pend_dummy() { : ; }

# Efface et initialise la pile des fonctions.
pend_init() {
    unset _pending_[@]
    pend_func pend_stop_mark
    _pend_hook_='pend_dummy'  # Débogage seulement.
}

# Désactive la fonction du haut de la pile.
pend_pop() {
    if [ ${#_pending_[@]} -gt 0 ]
    then
        local -i _top_
        _top_=${#_pending_[@]}-1
        unset _pending_[$_top_]
    fi
}

# pend_func function_name [$(printf '%q\n' arguments)]
pend_func() {
    local IFS=${NO_WSP}
    set -f
    _pending_[${#_pending_[@]}]=$@
    set +f
}

# La fonction qui arrête la sortie :
pend_stop_mark() {
    _p_ctrl_=0
}

pend_mark() {
    pend_func pend_stop_mark
}

# Exécute les fonctions jusqu'à 'pend_stop_mark' . . .
pend_release() {
    local -i _top_             # Déclare _top_ en tant qu'entier.
    _p_ctrl_=${#_pending_[@]}
    while [ ${_p_ctrl_} -gt 0 ]
    do
       _top_=${#_pending_[@]}-1
       _pend_current_=${_pending_[$_top_]}
       unset _pending_[$_top_]
       $_pend_hook_            # Débogage seulement.
       eval $_pend_current_
    done
}

# Supprime les fonctions jusqu'à 'pend_stop_mark' . . .
pend_drop() {
    local -i _top_
    local _pd_ctrl_=${#_pending_[@]}
    while [ ${_pd_ctrl_} -gt 0 ]
    do
       _top_=$_pd_ctrl_-1
       if [ "${_pending_[$_top_]}" == 'pend_stop_mark' ]
       then
           unset _pending_[$_top_]
           break
       else
           unset _pending_[$_top_]
           _pd_ctrl_=$_top_
       fi
    done
    if [ ${#_pending_[@]} -eq 0 ]
    then
        pend_func pend_stop_mark
    fi
}

#### Éditeurs de tableaux ####

# Cette fonction est décrite dans edit_exact.bash.
# (Voir "Additional documentation", ci-dessus.)
# edit_exact &lt;excludes_array_name&gt; &lt;target_array_name&gt;
edit_exact() {
    [ $# -eq 2 ] ||
    [ $# -eq 3 ] || return 1
    local -a _ee_Excludes
    local -a _ee_Target
    local _ee_x
    local _ee_t
    local IFS=${NO_WSP}
    set -f
    eval _ee_Excludes=\( \$\{$1\[@\]\} \)
    eval _ee_Target=\( \$\{$2\[@\]\} \)
    local _ee_len=${#_ee_Target[@]}     # Longueur originale.
    local _ee_cnt=${#_ee_Excludes[@]}   # Exclut la longueur de la liste.
    [ ${_ee_len} -ne 0 ] || return 0    # Ne peut pas éditer une longueur nulle.
    [ ${_ee_cnt} -ne 0 ] || return 0    # Ne peut pas éditer une longueur nulle.
    for (( x = 0; x < ${_ee_cnt} ; x++ ))
    do
        _ee_x=${_ee_Excludes[$x]}
        for (( n = 0 ; n < ${_ee_len} ; n++ ))
        do
            _ee_t=${_ee_Target[$n]}
            if [ x"${_ee_t}" == x"${_ee_x}" ]
            then
                unset _ee_Target[$n]     # Désactive la correspondance.
                [ $# -eq 2 ] && break    # Si deux arguments, alors terminé.
            fi
        done
    done
    eval $2=\( \$\{_ee_Target\[@\]\} \)
    set +f
    return 0
}

# Cette fonction est décrite dans edit_by_glob.bash.
# edit_by_glob &lt;excludes_array_name&gt; &lt;target_array_name&gt;
edit_by_glob() {
    [ $# -eq 2 ] ||
    [ $# -eq 3 ] || return 1
    local -a _ebg_Excludes
    local -a _ebg_Target
    local _ebg_x