A. Scripts supplémentaires

Bien que n'entrant pas dans le texte de ce document, ces scripts 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 16.22, « 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-motif nouveau-motif"
  #  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

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 chiffré 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 : Recopier 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. 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 <--- seed
#  3) Afficher NOMBRE.
#  4)  Si NOMBRE est pair, divisez par 2, ou
#  5)+ si impair, multiplier par 3 et ajouter 1.
#  6) NOMBRE <--- 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 (>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 (/usr/sbin/cracklib-forman).
# 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 d'autres
#+ buts 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
                                  #+ (dans le script original).
        tr -c '\012a-z'  '\012' | #  Plutôt que de supprimer les caractères
                                  #+ autres qu'alphanumériques,
                                  #+ les modifie 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 tubes que ne
# pourrait le faire 'un plombier <sourire>.
}  


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. Le 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       L'étoile est la cellule en question.       #
#                       678                                                  #
#                                                                            #
# 2) Une cellule vivante avec deux ou trois voisins vivants reste            #
#+   vivante.                                                                #
SURVIE=2                                                                     #
# 3) Une cellule morte avec trois cellules vivantes devient vivante          #
#+   (une "naissance").                                                      #
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" ]  # Redondance.
      then                                     # Cela peut-il être optimisé ?
        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                          #+ (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.
#
# Exercice : Optimisez ce script.
#            Le code est répétitif et redondant,
#+           par exemple aux lignes 335-336.

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.
__.__..___
___._.____
____.___..
_._______.
____._____
..__...___
____._____
___...____
__.._..___
_..___..__

+++

Le script suivant est 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?

+

Antek Sawicki nous a adressé le script suivant, qui fait une utilisation très intelligente des opérateurs de substitution de paramètres discutés dans la Section 10.2, « Remplacement de paramètres ».

Exemple A.13. 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 nous a envoyé ce script utilisant des tubes nommés et qui, d'après lui, « entraîne réellement aux citations et aux échappements ».

Exemple A.14. fifo: Faire des sauvegardes journalières, avec 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 utilisé le script suivant pour démontrer la génération de nombres premiers sans tableaux.

Exemple A.15. 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  $?  # Envoyer la sortie du script à 'fmt' pour un affichage plus joli.

# 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.16. 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

+

La version de Patsie du script tree.

Exemple A.17. tree2 : un autre script d'arborescence de répertoires

#!/bin/bash
# tree2.sh

# Lightly modified/reformatted by ABS Guide author.
# Included in ABS Guide with permission of script author (thanks!).

## Recursive file/dirsize checking script, by Patsie
##
## This script builds a list of files/directories and their size (du -akx)
## and processes this list to a human readable tree shape
## The 'du -akx' is only as good as the permissions the owner has.
## So preferably run as root* to get the best results, or use only on
## directories for which you have read permissions. Anything you can't
## read is not in the list.

#* ABS Guide author advises caution when running scripts as root!


##########  THIS IS CONFIGURABLE  ##########

TOP=5                   # Top 5 biggest (sub)directories.
MAXRECURS=5             # Max 5 subdirectories/recursions deep.
E_BL=80                 # Blank line already returned.
E_DIR=81                # Directory not specified.


##########  DON'T CHANGE ANYTHING BELOW THIS LINE  ##########

PID=$$                            # Our own process ID.
SELF=`basename $0`                # Our own program name.
TMP="/tmp/${SELF}.${PID}.tmp"     # Temporary 'du' result.

# Convert number to dotted thousand.
function dot { echo "            $*" |
               sed -e :a -e 's/\(.*[0-9]\)\([0-9]\{3\}\)/\1,\2/;ta' |
               tail -c 12; }

# Usage: tree <recursion> <indent prefix> <min size> <directory>
function tree {
  recurs="$1"           # How deep nested are we?
  prefix="$2"           # What do we display before file/dirname?
  minsize="$3"          # What is the minumum file/dirsize?
  dirname="$4"          # Which directory are we checking?

# Get ($TOP) biggest subdirs/subfiles from TMP file.
  LIST=`egrep "[[:space:]]${dirname}/[^/]*$" "$TMP" |
        awk '{if($1>'$minsize') print;}' | sort -nr | head -$TOP`
  [ -z "$LIST" ] && return        # Empty list, then go back.

  cnt=0
  num=`echo "$LIST" | wc -l`      # How many entries in the list.

  ## Main loop
  echo "$LIST" | while read size name; do
    ((cnt+=1))                    # Count entry number.
    bname=`basename "$name"`      # We only need a basename of the entry.
    [ -d "$name" ] && bname="$bname/"
                                  # If it's a directory, append a slash.
    echo "`dot $size`$prefix +-$bname"
                                  # Display the result.
    #  Call ourself recursively if it's a directory
    #+ and we're not nested too deep ($MAXRECURS).
    #  The recursion goes up: $((recurs+1))
    #  The prefix gets a space if it's the last entry,
    #+ or a pipe if there are more entries.
    #  The minimum file/dirsize becomes
    #+ a tenth of his parent: $((size/10)).
    # Last argument is the full directory name to check.
    if [ -d "$name" -a $recurs -lt $MAXRECURS ]; then
      [ $cnt -lt $num ] \
        || (tree $((recurs+1)) "$prefix  " $((size/10)) "$name") \
        && (tree $((recurs+1)) "$prefix |" $((size/10)) "$name")
    fi
  done

  [ $? -eq 0 ] && echo "           $prefix"
  # Every time we jump back add a 'blank' line.
  return $E_BL
  # We return 80 to tell we added a blank line already.
}

###                ###
###  main program  ###
###                ###

rootdir="$@"
[ -d "$rootdir" ] ||
  { echo "$SELF: Usage: $SELF <directory>" >&2; exit $E_DIR; }
  # We should be called with a directory name.

echo "Building inventory list, please wait ..."
     # Show "please wait" message.
du -akx "$rootdir" 1>"$TMP" 2>/dev/null
     # Build a temporary list of all files/dirs and their size.
size=`tail -1 "$TMP" | awk '{print $1}'`
     # What is our rootdirectory's size?
echo "`dot $size` $rootdir"
     # Display rootdirectory's entry.
tree 0 "" 0 "$rootdir"
     # Display the tree below our rootdirectory.

rm "$TMP" 2>/dev/null
     # Clean up TMP file.

exit $?

Noah Friedman nous a permis d'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 <friedman@prep.ai.mit.edu>
# ==>     Used with his kind permission in this document.
# Created: 1992-07-01
# Last modified: 1993-09-29
# Public domain

# Conversion to bash v2 syntax done by Chet Ramey

# Commentary:
# Code:

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

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

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

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

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

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

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

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

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

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

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

    return 1
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# provide string

# string.bash ends here


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

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

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

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



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


exit 0

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 les répertoires

#! /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.
: <<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
# # # # #

: <<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.
: <<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.
: <<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.

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

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

# Hash:
# Bibliothèque de fonctions de hachage
# Auteur : Mariusz Gniazdowski <mgniazd-at-gmail.com>
# 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=&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 &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.21. Coloriser du texte en utilisant les fonctions de hachage

#!/bin/bash
# hash-example.sh: Colorizing text.
# Author: Mariusz Gniazdowski <mariusz.gn-at-gmail.com>

. Hash.lib      # Load the library of functions.

hash_set colors red          "\033[0;31m"
hash_set colors blue         "\033[0;34m"
hash_set colors light_blue   "\033[1;34m"
hash_set colors light_red    "\033[1;31m"
hash_set colors cyan         "\033[0;36m"
hash_set colors light_green  "\033[1;32m"
hash_set colors light_gray   "\033[0;37m"
hash_set colors green        "\033[0;32m"
hash_set colors yellow       "\033[1;33m"
hash_set colors light_purple "\033[1;35m"
hash_set colors purple       "\033[0;35m"
hash_set colors reset_color  "\033[0;00m"


# $1 - keyname
# $2 - value
try_colors() {
        echo -en "$2"
        echo "This line is $1."
}
hash_foreach colors try_colors
hash_echo colors reset_color -en

echo -e '\nLet us overwrite some colors with yellow.\n'
# It's hard to read yellow text on some terminals.
hash_dup colors yellow   red light_green blue green light_gray cyan
hash_foreach colors try_colors
hash_echo colors reset_color -en

echo -e '\nLet us delete them and try colors once more . . .\n'

for i in red light_green blue green light_gray cyan; do
        hash_unset colors $i
done
hash_foreach colors try_colors
hash_echo colors reset_color -en

hash_set other txt "Other examples . . ."
hash_echo other txt
hash_get_into other txt text
echo $text

hash_set other my_fun try_colors
hash_call other my_fun   purple "`hash_echo colors purple`"
hash_echo colors reset_color -en

echo; echo "Back to normal?"; echo

exit $?

#  On some terminals, the "light" colors print in bold,
#  and end up looking darker than the normal ones.
#  Why is this?


Un exemple pour illustrer les mécanismes de hachage, d'un autre point de vue différent.

Exemple A.22. Pour en savoir plus sur les fonctions de hachage

#!/bin/bash
# $Id: ha.sh,v 1.2 2005/04/21 23:24:26 oliver Exp $
# Copyright 2005 Oliver Beckstein
# Released under the GNU Public License
# Author of script granted permission for inclusion in ABS Guide.
# (Thank you!)

#----------------------------------------------------------------
# pseudo hash based on indirect parameter expansion
# API: access through functions:
# 
# create the hash:
#  
#      newhash Lovers
#
# add entries (note single quotes for spaces)
#    
#      addhash Lovers Tristan Isolde
#      addhash Lovers 'Romeo Montague' 'Juliet Capulet'
#
# access value by key
#
#      gethash Lovers Tristan   ---->  Isolde
#
# show all keys
#
#      keyshash Lovers         ----> 'Tristan'  'Romeo Montague'
#
#
# Convention: instead of perls' foo{bar} = boing' syntax,
# use
#       '_foo_bar=boing' (two underscores, no spaces)
#
# 1) store key   in _NAME_keys[]
# 2) store value in _NAME_values[] using the same integer index
# The integer index for the last entry is _NAME_ptr
#
# NOTE: No error or sanity checks, just bare bones.


function _inihash () {
    # private function
    # call at the beginning of each procedure
    # defines: _keys _values _ptr
    #
    # Usage: _inihash NAME
    local name=$1
    _keys=_${name}_keys
    _values=_${name}_values
    _ptr=_${name}_ptr
}

function newhash () {
    # Usage: newhash NAME
    #        NAME should not contain spaces or dots.
    #        Actually: it must be a legal name for a Bash variable.
    # We rely on Bash automatically recognising arrays.
    local name=$1 
    local _keys _values _ptr
    _inihash ${name}
    eval ${_ptr}=0
}


function addhash () {
    # Usage: addhash NAME KEY 'VALUE with spaces'
    #        arguments with spaces need to be quoted with single quotes ''
    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 if entry found, 1 otherwise
    #  That's not a proper hash --
    #+ we simply linearly search through the keys.
    local name=$1 key="$2" 
    local _keys _values _ptr 
    local k v i found h
    _inihash ${name}
    
    # _ptr holds the highest index in the hash
    found=0

    for i in $(seq 1 ${!_ptr}); do
        h="\${${_keys}[${i}]}"  #  Safer to do it in two steps,
        eval k=${h}             #+ especially when quoting for spaces.
        if [ "${k}" = "${key}" ]; then found=1; break; fi
    done;

    [ ${found} = 0 ] && return 1;
    # else: i is the index that matches the key
    h="\${${_values}[${i}]}"
    eval echo "${h}"
    return 0;   
}

function keyshash () {
    # Usage: keyshash NAME
    # Returns list of all keys defined for hash name.
    local name=$1 key="$2" 
    local _keys _values _ptr 
    local k i h
    _inihash ${name}
    
    # _ptr holds the highest index in the hash
    for i in $(seq 1 ${!_ptr}); do
        h="\${${_keys}[${i}]}"   #  Safer to do it in two steps,
        eval k=${h}              #+ especially when quoting for spaces.
        echo -n "'${k}' "
    done;
}


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

# Now, let's test it.
# (Per comments at the beginning of the script.)
newhash Lovers
addhash Lovers Tristan Isolde
addhash Lovers 'Romeo Montague' 'Juliet Capulet'

# Output results.
echo
gethash Lovers Tristan      # Isolde
echo
keyshash Lovers             # 'Tristan' 'Romeo Montague'
echo; echo


exit 0

# Exercise:
# --------

# Add error checks to the functions.

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

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

#!/bin/bash
# ==> usb.sh
# ==> Script for mounting and installing pen/keychain USB storage devices.
# ==> Runs as root at system startup (see below).
# ==>
# ==> Newer Linux distros (2004 or later) autodetect
# ==> and install USB pen drives, and therefore don't need this script.
# ==> But, it's still instructive.
 
#  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.
#
#  Some code lifted from usb-mount by Michael Hamilton's usb-mount (LGPL)
#+ see http://users.actrix.co.nz/michael/usbmount.html
#
#  INSTALL
#  -------
#  Put this in /etc/hotplug/usb/diskonkey.
#  Then look in /etc/hotplug/usb.distmap, and copy all usb-storage entries
#+ into /etc/hotplug/usb.usermap, substituting "usb-storage" for "diskonkey".
#  Otherwise this code is only run during the kernel module invocation/removal
#+ (at least in my tests), which defeats the purpose.
#
#  TODO
#  ----
#  Handle more than one diskonkey device at one time (e.g. /dev/diskonkey1
#+ and /mnt/diskonkey1), etc. The biggest problem here is the handling in
#+ devlabel, which I haven't yet tried.
#
#  AUTHOR and SUPPORT
#  ------------------
#  Konstantin Riabitsev, <icon linux duke edu>.
#  Send any problem reports to my email address at the moment.
#
# ==> Comments added by ABS Guide author.



SYMLINKDEV=/dev/diskonkey
MOUNTPOINT=/mnt/diskonkey
DEVLABEL=/sbin/devlabel
DEVLABELCONFIG=/etc/sysconfig/devlabel
IAM=$0

##
# Functions lifted near-verbatim from usb-mount code.
#
function allAttachedScsiUsb {
  find /proc/scsi/ -path '/proc/scsi/usb-storage*' -type f |
  xargs grep -l 'Attached: Yes'
}
function scsiDevFromScsiUsb {
  echo $1 | awk -F"[-/]" '{ n=$(NF-1);
  print "/dev/sd" substr("abcdefghijklmnopqrstuvwxyz", n+1, 1) }'
}

if [ "${ACTION}" = "add" ] && [ -f "${DEVICE}" ]; then
    ##
    # lifted from usbcam code.
    #
    if [ -f /var/run/console.lock ]; then
        CONSOLEOWNER=`cat /var/run/console.lock`
    elif [ -f /var/lock/console.lock ]; then
        CONSOLEOWNER=`cat /var/lock/console.lock`
    else
        CONSOLEOWNER=
    fi
    for procEntry in $(allAttachedScsiUsb); do
        scsiDev=$(scsiDevFromScsiUsb $procEntry)
        #  Some bug with usb-storage?
        #  Partitions are not in /proc/partitions until they are accessed
        #+ somehow.
        /sbin/fdisk -l $scsiDev >/dev/null
        ##
        #  Most devices have partitioning info, so the data would be on
        #+ /dev/sd?1. However, some stupider ones don't have any partitioning
        #+ and use the entire device for data storage. This tries to
        #+ guess semi-intelligently if we have a /dev/sd?1 and if not, then
        #+ it uses the entire device and hopes for the better.
        #
        if grep -q `basename $scsiDev`1 /proc/partitions; then
            part="$scsiDev""1"
        else
            part=$scsiDev
        fi
        ##
        #  Change ownership of the partition to the console user so they can
        #+ mount it.
        #
        if [ ! -z "$CONSOLEOWNER" ]; then
            chown $CONSOLEOWNER:disk $part
        fi
        ##
        # This checks if we already have this UUID defined with devlabel.
        # If not, it then adds the device to the list.
        #
        prodid=`$DEVLABEL printid -d $part`
        if ! grep -q $prodid $DEVLABELCONFIG; then
            # cross our fingers and hope it works
            $DEVLABEL add -d $part -s $SYMLINKDEV 2>/dev/null
        fi
        ##
        # Check if the mount point exists and create if it doesn't.
        #
        if [ ! -e $MOUNTPOINT ]; then
            mkdir -p $MOUNTPOINT
        fi
        ##
        # Take care of /etc/fstab so mounting is easy.
        #
        if ! grep -q "^$SYMLINKDEV" /etc/fstab; then
            # Add an fstab entry
            echo -e \
                "$SYMLINKDEV\t\t$MOUNTPOINT\t\tauto\tnoauto,owner,kudzu 0 0" \
                >> /etc/fstab
        fi
    done
    if [ ! -z "$REMOVER" ]; then
        ##
        # Make sure this script is triggered on device removal.
        #
        mkdir -p `dirname $REMOVER`
        ln -s $IAM $REMOVER
    fi
elif [ "${ACTION}" = "remove" ]; then
    ##
    # If the device is mounted, unmount it cleanly.
    #
    if grep -q "$MOUNTPOINT" /etc/mtab; then
        # unmount cleanly
        umount -l $MOUNTPOINT
    fi
    ##
    # Remove it from /etc/fstab if it's there.
    #
    if grep -q "^$SYMLINKDEV" /etc/fstab; then
        grep -v "^$SYMLINKDEV" /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.24. Convertir en HTML

#!/bin/bash
# tohtml.sh [v. 0.2, reldate: 06/26/08, still buggy]

# Convert a text file to HTML format.
# Author: Mendel Cooper
# License: GPL3
# Usage: sh tohtml.sh < textfile > htmlfile
# Script can easily be modified to accept source and target filenames.

#    Assumptions:
# 1) Paragraphs in (target) text file are separated by a blank line.
# 2) Jpeg images (*.jpg) are located in "images" subdirectory.
#    In the target file, the image names are enclosed in square brackets,
#    for example, [image01.jpg].
# 3) Emphasized (italic) phrases begin with a space+underscore
#+   or the first character on the line is an underscore,
#+   and end with an underscore+space or underscore+end-of-line.


# Settings
FNTSIZE=2        # Small-medium font size
IMGDIR="images"  # Image directory
# Headers
HDR01='<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN">'
HDR02='<!-- Converted to HTML by ***tohtml.sh*** script -->'
HDR03='<!-- script author: M. Leo Cooper <thegrendel.abs@gmail.com> -->'
HDR10='<html>'
HDR11='<head>'
HDR11a='</head>'
HDR12a='<title>'
HDR12b='</title>'
HDR121='<META NAME="GENERATOR" CONTENT="tohtml.sh script">'
HDR13='<body bgcolor="#dddddd">'   # Change background color to suit.
HDR14a='<font size='
HDR14b='>'
# Footers
FTR10='</body>'
FTR11='</html>'
# Tags
BOLD="<b>"
CENTER="<center>"
END_CENTER="</center>"
LF="<br>"


write_headers ()
  {
  echo "$HDR01"
  echo
  echo "$HDR02"
  echo "$HDR03"
  echo
  echo
  echo "$HDR10"
  echo "$HDR11"
  echo "$HDR121"
  echo "$HDR11a"
  echo "$HDR13"
  echo
  echo -n "$HDR14a"
  echo -n "$FNTSIZE"
  echo "$HDR14b"
  echo
  echo "$BOLD"        # Everything in bold (more easily readable).
  }


process_text ()
  {
  while read line     # Read one line at a time.
  do
    {
    if [ ! "$line" ]  # Blank line?
    then              # Then new paragraph must follow.
      echo
      echo "$LF"      # Insert two <br> tags.
      echo "$LF"
      echo
      continue        # Skip the underscore test.
    else              # Otherwise . . .

      if [[ "$line" =~ "\[*jpg\]" ]]  # Is a graphic?
      then                            # Strip away brackets.
        temp=$( echo "$line" | sed -e 's/\[//' -e 's/\]//' )
        line=""$CENTER" <img src="\"$IMGDIR"/$temp\"> "$END_CENTER" "
                                      # Add image tag.
                                      # And, center it.
      fi

    fi


    echo "$line" | grep -q _
    if [ "$?" -eq 0 ]    # If line contains underscore ...
    then
      # ===================================================
      # Convert underscored phrase to italics.
      temp=$( echo "$line" |
              sed -e 's/ _/ <i>/' -e 's/_/<\/i> /' |
              sed -e 's/^_/<i>/'  -e 's/_/<\/i>/' )
      #  Process only underscores prefixed by space,
      #+ or at beginning or end of line.
      #  Do not convert underscores embedded within a word!
      line="$temp"
      # Slows script execution. Can be optimized?
      # ===================================================
    fi


   
    echo
    echo "$line"
    echo
    } # End while
  done
  }   # End process_text ()


write_footers ()  # Termination tags.
  {
  echo "$FTR10"
  echo "$FTR11"
  }


# main () {
# =========
write_headers
process_text
write_footers
# =========
#         }

exit $?

#  Exercises:
#  ---------
#  1) Fixup: Check for closing underscore before a comma or period.
#  2) Add a test for the presence of a closing underscore
#+    in phrases to be italicized.

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

Exemple A.25. Préserver les weblogs

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

# Troy Engel <tengel@fluid.com>
# Slightly modified by document author.
# Used with permission.
#
#  This script will preserve the normally rotated and
#+ thrown away weblogs from a default RedHat/Apache installation.
#  It will save the files with a date/time stamp in the filename,
#+ bzipped, to a given directory.
#
#  Run this from crontab nightly at an off hour,
#+ as bzip2 can suck up some serious CPU on huge logs:
#  0 2 * * * /opt/sbin/archiveweblogs.sh


PROBLEM=66

# Set this to your backup dir.
BKP_DIR=/opt/backups/weblogs

# Default Apache/RedHat stuff
LOG_DAYS="4 3 2 1"
LOG_DIR=/var/log/httpd
LOG_FILES="access_log error_log"

# Default RedHat program locations
LS=/bin/ls
MV=/bin/mv
ID=/usr/bin/id
CUT=/bin/cut
COL=/usr/bin/column
BZ2=/usr/bin/bzip2

# Are we root?
USER=`$ID -u`
if [ "X$USER" != "X0" ]; then
  echo "PANIC: Only root can run this script!"
  exit $PROBLEM
fi

# Backup dir exists/writable?
if [ ! -x $BKP_DIR ]; then
  echo "PANIC: $BKP_DIR doesn't exist or isn't writable!"
  exit $PROBLEM
fi

# Move, rename and bzip2 the logs
for logday in $LOG_DAYS; do
  for logfile in $LOG_FILES; do
    MYFILE="$LOG_DIR/$logfile.$logday"
    if [ -w $MYFILE ]; then
      DTS=`$LS -lgo --time-style=+%Y%m%d $MYFILE | $COL -t | $CUT -d ' ' -f7`
      $MV $MYFILE $BKP_DIR/$logfile.$DTS
      $BZ2 $BKP_DIR/$logfile.$DTS
    else
      # Only spew an error if the file exits (ergo non-writable).
      if [ -f $MYFILE ]; then
        echo "ERROR: $MYFILE not writable. Skipping."
      fi
    fi
  done
done

exit 0

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

Exemple A.26. 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$

    Documentation redirected to the Bash no-operation.
    Bash will '/dev/null' this block when the script is first read.
    (Uncomment the above set command to see this action.)

    Remove the first (Sha-Bang) line when sourcing this as a library
    procedure.  Also comment out the example use code in the two
    places where shown.


    Usage:
        _protect_literal_str 'Whatever string meets your ${fancy}'
        Just echos the argument to standard out, hard quotes
        restored.

        $(_protect_literal_str 'Whatever string meets your ${fancy}')
        as the right-hand-side of an assignment statement.

    Does:
        As the right-hand-side of an assignment, preserves the
        hard quotes protecting the contents of the literal during
        assignment.

    Notes:
        The strange names (_*) are used to avoid trampling on
        the user's chosen names when this is sourced as a
        library.

_Protect_Literal_String_Doc

# The 'for illustration' function form

_protect_literal_str() {

# Pick an un-used, non-printing character as local IFS.
# Not required, but shows that we are ignoring it.
    local IFS=$'\x1B'               # \ESC character

# Enclose the All-Elements-Of in hard quotes during assignment.
    local tmp=$'\x27'$@$'\x27'
#    local tmp=$'\''$@$'\''         # Even uglier.

    local len=${#tmp}               # Info only.
    echo $tmp is $len long.         # Output AND information.
}

# This is the short-named version.
_pls() {
    local IFS=$'x1B'                # \ESC character (not required)
    echo $'\x27'$@$'\x27'           # Hard quoted parameter glob
}

# :<<-'_Protect_Literal_String_Test'
# # # Remove the above "# " to disable this code. # # #

# See how that looks when printed.
echo
echo "- - Test One - -"
_protect_literal_str 'Hello $user'
_protect_literal_str 'Hello "${username}"'
echo

# Which yields:
# - - Test One - -
# 'Hello $user' is 13 long.
# 'Hello "${username}"' is 21 long.

#  Looks as expected, but why all of the trouble?
#  The difference is hidden inside the Bash internal order
#+ of operations.
#  Which shows when you use it on the RHS of an assignment.

# Declare an array for test values.
declare -a arrayZ

# Assign elements with various types of quotes and escapes.
arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" )

# Now list that array and see what is there.
echo "- - Test Two - -"
for (( i=0 ; i<${#arrayZ[*]} ; i++ ))
do
    echo  Element $i: ${arrayZ[$i]} is: ${#arrayZ[$i]} long.
done
echo

# Which yields:
# - - Test Two - -
# Element 0: zero is: 4 long.           # Our marker element
# Element 1: 'Hello ${Me}' is: 13 long. # Our "$(_pls '...' )"
# Element 2: Hello ${You} is: 12 long.  # Quotes are missing
# Element 3: \'Pass: \' is: 10 long.    # ${pw} expanded to nothing

# Now make an assignment with that result.
declare -a array2=( ${arrayZ[@]} )

# And print what happened.
echo "- - Test Three - -"
for (( i=0 ; i<${#array2[*]} ; i++ ))
do
    echo  Element $i: ${array2[$i]} is: ${#array2[$i]} long.
done
echo

# Which yields:
# - - Test Three - -
# Element 0: zero is: 4 long.           # Our marker element.
# Element 1: Hello ${Me} is: 11 long.   # Intended result.
# Element 2: Hello is: 5 long.          # ${You} expanded to nothing.
# Element 3: 'Pass: is: 6 long.         # Split on the whitespace.
# Element 4: ' is: 1 long.              # The end quote is here now.

#  Our Element 1 has had its leading and trailing hard quotes stripped.
#  Although not shown, leading and trailing whitespace is also stripped.
#  Now that the string contents are set, Bash will always, internally,
#+ hard quote the contents as required during its operations.

#  Why?
#  Considering our "$(_pls 'Hello ${Me}')" construction:
#  " ... " -> Expansion required, strip the quotes.
#  $( ... ) -> Replace with the result of..., strip this.
#  _pls ' ... ' -> called with literal arguments, strip the quotes.
#  The result returned includes hard quotes; BUT the above processing
#+ has already been done, so they become part of the value assigned.
#
#  Similarly, during further usage of the string variable, the ${Me}
#+ is part of the contents (result) and survives any operations
#  (Until explicitly told to evaluate the string).

#  Hint: See what happens when the hard quotes ($'\x27') are replaced
#+ with soft quotes ($'\x22') in the above procedures.
#  Interesting also is to remove the addition of any quoting.

# _Protect_Literal_String_Test
# # # Remove the above "# " to disable this code. # # #

exit 0

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

Exemple A.27. 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$

    Documentation redirected to the Bash no-operation. Bash will
    '/dev/null' this block when the script is first read.
    (Uncomment the above set command to see this action.)

    Remove the first (Sha-Bang) line when sourcing this as a library
    procedure.  Also comment out the example use code in the two
    places where shown.


    Usage:
        Complement of the "$(_pls 'Literal String')" function.
        (See the protect_literal.sh example.)

        StringVar=$(_upls ProtectedSringVariable)

    Does:
        When used on the right-hand-side of an assignment statement;
        makes the substitions embedded in the protected string.

    Notes:
        The strange names (_*) are used to avoid trampling on
        the user's chosen names when this is sourced as a
        library.


_UnProtect_Literal_String_Doc

_upls() {
    local IFS=$'x1B'                # \ESC character (not required)
    eval echo $@                    # Substitution on the glob.
}

# :<<-'_UnProtect_Literal_String_Test'
# # # Remove the above "# " to disable this code. # # #


_pls() {
    local IFS=$'x1B'                # \ESC character (not required)
    echo $'\x27'$@$'\x27'           # Hard quoted parameter glob
}

# Declare an array for test values.
declare -a arrayZ

# Assign elements with various types of quotes and escapes.
arrayZ=( zero "$(_pls 'Hello ${Me}')" 'Hello ${You}' "\'Pass: ${pw}\'" )

# Now make an assignment with that result.
declare -a array2=( ${arrayZ[@]} )

# Which yielded:
# - - Test Three - -
# Element 0: zero is: 4 long            # Our marker element.
# Element 1: Hello ${Me} is: 11 long    # Intended result.
# Element 2: Hello is: 5 long           # ${You} expanded to nothing.
# Element 3: 'Pass: is: 6 long          # Split on the whitespace.
# Element 4: ' is: 1 long               # The end quote is here now.

# set -vx

#  Initialize 'Me' to something for the embedded ${Me} substitution.
#  This needs to be done ONLY just prior to evaluating the
#+ protected string.
#  (This is why it was protected to begin with.)

Me="to the array guy."

# Set a string variable destination to the result.
newVar=$(_upls ${array2[1]})

# Show what the contents are.
echo $newVar

# Do we really need a function to do this?
newerVar=$(eval echo ${array2[1]})
echo $newerVar

#  I guess not, but the _upls function gives us a place to hang
#+ the documentation on.
#  This helps when we forget what a # construction like:
#+ $(eval echo ... ) means.

# What if Me isn't set when the protected string is evaluated?
unset Me
newestVar=$(_upls ${array2[1]})
echo $newestVar

# Just gone, no hints, no runs, no errors.

#  Why in the world?
#  Setting the contents of a string variable containing character
#+ sequences that have a meaning in Bash is a general problem in
#+ script programming.
#
#  This problem is now solved in eight lines of code
#+ (and four pages of description).

#  Where is all this going?
#  Dynamic content Web pages as an array of Bash strings.
#  Content set per request by a Bash 'eval' command
#+ on the stored page template.
#  Not intended to replace PHP, just an interesting thing to do.
###
#  Don't have a webserver application?
#  No problem, check the example directory of the Bash source;
#+ there is a Bash script for that also.

# _UnProtect_Literal_String_Test
# # # Remove the above "# " to disable this code. # # #

exit 0

Ce script intéressant aide à pourchasser les spammers.

Exemple A.28. Identification d'un spammer

#!/bin/bash

# $Id: is_spammer.bash,v 1.7 2008-05-10 08:36:14 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 <excludes_array_name> <target_array_name>
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 <excludes_array_name> <target_array_name>
edit_by_glob() {
    [ $# -eq 2 ] ||
    [ $# -eq 3 ] || return 1
    local -a _ebg_Excludes
    local -a _ebg_Target
    local _ebg_x
    local _ebg_t
    local IFS=${NO_WSP}
    set -f
    eval _ebg_Excludes=\( \$\{$1\[@\]\} \)
    eval _ebg_Target=\( \$\{$2\[@\]\} \)
    local _ebg_len=${#_ebg_Target[@]}
    local _ebg_cnt=${#_ebg_Excludes[@]}
    [ ${_ebg_len} -ne 0 ] || return 0
    [ ${_ebg_cnt} -ne 0 ] || return 0
    for (( x = 0; x < ${_ebg_cnt} ; x++ ))
    do
        _ebg_x=${_ebg_Excludes[$x]}
        for (( n = 0 ; n < ${_ebg_len} ; n++ ))
        do
            [ $# -eq 3 ] && _ebg_x=${_ebg_x}'*'  #  Do prefix edit
            if [ ${_ebg_Target[$n]:=} ]          #+ if defined & set.
            then
                _ebg_t=${_ebg_Target[$n]/#${_ebg_x}/}
                [ ${#_ebg_t} -eq 0 ] && unset _ebg_Target[$n]
            fi
        done
    done
    eval $2=\( \$\{_ebg_Target\[@\]\} \)
    set +f
    return 0
}

# Cette fonction est décrite par unique_lines.bash.
# unique_lines <in_name> <out_name>
unique_lines() {
    [ $# -eq 2 ] || return 1
    local -a _ul_in
    local -a _ul_out
    local -i _ul_cnt
    local -i _ul_pos
    local _ul_tmp
    local IFS=${NO_WSP}
    set -f
    eval _ul_in=\( \$\{$1\[@\]\} \)
    _ul_cnt=${#_ul_in[@]}
    for (( _ul_pos = 0 ; _ul_pos < ${_ul_cnt} ; _ul_pos++ ))
    do
        if [ ${_ul_in[${_ul_pos}]:=} ]      # Si définie et non vide
        then
            _ul_tmp=${_ul_in[${_ul_pos}]}
            _ul_out[${#_ul_out[@]}]=${_ul_tmp}
            for (( zap = _ul_pos ; zap < ${_ul_cnt} ; zap++ ))
            do
                [ ${_ul_in[${zap}]:=} ] &&
                [ 'x'${_ul_in[${zap}]} == 'x'${_ul_tmp} ] &&
                    unset _ul_in[${zap}]
            done
        fi
    done
    eval $2=\( \$\{_ul_out\[@\]\} \)
    set +f
    return 0
}

# Cette fonction est décrite par char_convert.bash.
# to_lower <string>
to_lower() {
    [ $# -eq 1 ] || return 1
    local _tl_out
    _tl_out=${1//A/a}
    _tl_out=${_tl_out//B/b}
    _tl_out=${_tl_out//C/c}
    _tl_out=${_tl_out//D/d}
    _tl_out=${_tl_out//E/e}
    _tl_out=${_tl_out//F/f}
    _tl_out=${_tl_out//G/g}
    _tl_out=${_tl_out//H/h}
    _tl_out=${_tl_out//I/i}
    _tl_out=${_tl_out//J/j}
    _tl_out=${_tl_out//K/k}
    _tl_out=${_tl_out//L/l}
    _tl_out=${_tl_out//M/m}
    _tl_out=${_tl_out//N/n}
    _tl_out=${_tl_out//O/o}
    _tl_out=${_tl_out//P/p}
    _tl_out=${_tl_out//Q/q}
    _tl_out=${_tl_out//R/r}
    _tl_out=${_tl_out//S/s}
    _tl_out=${_tl_out//T/t}
    _tl_out=${_tl_out//U/u}
    _tl_out=${_tl_out//V/v}
    _tl_out=${_tl_out//W/w}
    _tl_out=${_tl_out//X/x}
    _tl_out=${_tl_out//Y/y}
    _tl_out=${_tl_out//Z/z}
    echo ${_tl_out}
    return 0
}

#### Fonctions d'aide de l'application ####

# Tout le monde n'utilise pas de points comme séparateur (APNIC, par exemple).
# Cette fonction est décrite par to_dot.bash
# to_dot <string>
to_dot() {
    [ $# -eq 1 ] || return 1
    echo ${1//[#|@|%]/.}
    return 0
}

# Cette fonction est décrite par is_number.bash.
# is_number <input>
is_number() {
    [ "$#" -eq 1 ]    || return 1  # est-ce blanc ?
    [ x"$1" == 'x0' ] && return 0  # est-ce zéro  ?
    local -i tst
    let tst=$1 2>/dev/null         # sinon, c'est numérique !
    return $?
}

# Cette fonction est décrite par is_address.bash.
# is_address <input>
is_address() {
    [ $# -eq 1 ] || return 1    # Blanc ==> faux
    local -a _ia_input
    local IFS=${ADR_IFS}
    _ia_input=( $1 )
    if  [ ${#_ia_input[@]} -eq 4 ]  &&
        is_number ${_ia_input[0]}   &&
        is_number ${_ia_input[1]}   &&
        is_number ${_ia_input[2]}   &&
        is_number ${_ia_input[3]}   &&
        [ ${_ia_input[0]} -lt 256 ] &&
        [ ${_ia_input[1]} -lt 256 ] &&
        [ ${_ia_input[2]} -lt 256 ] &&
        [ ${_ia_input[3]} -lt 256 ]
    then
        return 0
    else
        return 1
    fi
}

# Cette fonction est décrite par split_ip.bash.
# split_ip <IP_address> <array_name_norm> [<array_name_rev>]
split_ip() {
    [ $# -eq 3 ] ||              #  Soit trois
    [ $# -eq 2 ] || return 1     #+ soit deux arguments
    local -a _si_input
    local IFS=${ADR_IFS}
    _si_input=( $1 )
    IFS=${WSP_IFS}
    eval $2=\(\ \$\{_si_input\[@\]\}\ \)
    if [ $# -eq 3 ]
    then
        # Construit le tableau de l'ordre des requêtes.
        local -a _dns_ip
        _dns_ip[0]=${_si_input[3]}
        _dns_ip[1]=${_si_input[2]}
        _dns_ip[2]=${_si_input[1]}
        _dns_ip[3]=${_si_input[0]}
        eval $3=\(\ \$\{_dns_ip\[@\]\}\ \)
    fi
    return 0
}

# Cette fonction est décrite par dot_array.bash.
# dot_array <array_name>
dot_array() {
    [ $# -eq 1 ] || return 1     # Un seul argument requis.
    local -a _da_input
    eval _da_input=\(\ \$\{$1\[@\]\}\ \)
    local IFS=${DOT_IFS}
    local _da_output=${_da_input[@]}
    IFS=${WSP_IFS}
    echo ${_da_output}
    return 0
}

# Cette fonction est décrite par file_to_array.bash
# file_to_array <file_name> <line_array_name>
file_to_array() {
    [ $# -eq 2 ] || return 1  # Deux arguments requis.
    local IFS=${NO_WSP}
    local -a _fta_tmp_
    _fta_tmp_=( $(cat $1) )
    eval $2=\( \$\{_fta_tmp_\[@\]\} \)
    return 0
}

# Columnized print of an array of multi-field strings.
# col_print <array_name> <min_space> <tab_stop [tab_stops]>
col_print() {
    [ $# -gt 2 ] || return 0
    local -a _cp_inp
    local -a _cp_spc
    local -a _cp_line
    local _cp_min
    local _cp_mcnt
    local _cp_pos
    local _cp_cnt
    local _cp_tab
    local -i _cp
    local -i _cpf
    local _cp_fld
    #  ATTENTION : LIGNE SUIVANTE NON BLANCHE -- CE SONT DES ESPACES ENTRE
    #+             GUILLEMET.
    local _cp_max='                                                            '
    set -f
    local IFS=${NO_WSP}
    eval _cp_inp=\(\ \$\{$1\[@\]\}\ \)
    [ ${#_cp_inp[@]} -gt 0 ] || return 0 # Le cas vide est simple.
    _cp_mcnt=$2
    _cp_min=${_cp_max:1:${_cp_mcnt}}
    shift
    shift
    _cp_cnt=$#
    for (( _cp = 0 ; _cp < _cp_cnt ; _cp++ ))
    do
        _cp_spc[${#_cp_spc[@]}]="${_cp_max:2:$1}" #"
        shift
    done
    _cp_cnt=${#_cp_inp[@]}
    for (( _cp = 0 ; _cp < _cp_cnt ; _cp++ ))
    do
        _cp_pos=1
        IFS=${NO_WSP}$'\x20'
        _cp_line=( ${_cp_inp[${_cp}]} )
        IFS=${NO_WSP}
        for (( _cpf = 0 ; _cpf < ${#_cp_line[@]} ; _cpf++ ))
        do
            _cp_tab=${_cp_spc[${_cpf}]:${_cp_pos}}
            if [ ${#_cp_tab} -lt ${_cp_mcnt} ]
            then
                _cp_tab="${_cp_min}"
            fi
            echo -n "${_cp_tab}"
            (( _cp_pos = ${_cp_pos} + ${#_cp_tab} ))
            _cp_fld="${_cp_line[${_cpf}]}"
            echo -n ${_cp_fld}
            (( _cp_pos = ${_cp_pos} + ${#_cp_fld} ))
        done
        echo
    done
    set +f
    return 0
}

# # # # Flux de données 'Chassez le spammeur' # # # #

# Code de retour de l'application
declare -i _hs_RC

# Entrée originale, à partir de laquelle les adresses IP sont supprimées
# Après cela, les noms de domaine à vérifier
declare -a uc_name

# Les adresses IP de l'entrée originale sont déplacées ici
# Après cela, les adresses IP à vérifier
declare -a uc_address

# Noms contre lesquels l'expansion d'adresses est lancée
# Prêt pour la recherche des détails des noms
declare -a chk_name

# Noms contre lesquelles l'expansion de noms est lancée
# Prêt pour la recherche des détails des adresses
declare -a chk_address

#  La récursion est depth-first-by-name.
#  expand_input_address maintient cette liste pour prohiber
#+ deux fois les adresses à rechercher durant la récursion
#+ des noms de domaine.
declare -a been_there_addr
been_there_addr=( '127.0.0.1' ) # Liste blanche pour localhost

# Noms que nous avons vérifié (ou abandonné)
declare -a known_name

# Adresses que nous avons vérifié (ou abandonné)
declare -a known_address

#  Liste de zéro ou plus de serveurs Blacklist pour la vérification.
#  Chaque 'known_address' vérifiera chaque serveur,
#+ avec des réponses négatives et des échecs supprimés.
declare -a list_server

# limite d'indirection - initialisée à zéro == pas de limite
indirect=${SPAMMER_LIMIT:=2}

# # # # données de sortie d'informations 'Chassez le
spammeur' # # # #

# Tout nom de domaine pourrait avoir de nombreuses adresses IP.
# Toute adresse IP pourrait avoir de multiples noms de domaines.
# Du coup, trace des paires uniques adresse-nom.
declare -a known_pair
declare -a reverse_pair

#  En plus des variables de flux de données ; known_address
#+ known_name et list_server, ce qui suit est sorti vers le fichier d'interface
#+ graphique externe.

# Chaîne d'autorité, parent -> champs SOA.
declare -a auth_chain

# Référence la chaîne, nom du parent -> nom du fils
declare -a ref_chain

# Chaîne DNS - nom de domaine -> adresse
declare -a name_address

# Paires de nom et service - nom de domaine -> service
declare -a name_srvc

# Paires de nom et ressource - nom de domaine -> enregistrement de ressource
declare -a name_resource

# Paires de parent et fils - nom de parent -> nom du fils
# Ceci POURRAIT NE PAS être identique au ref_chain qui suit !
declare -a parent_child

# Paires des correspondances d'adresses et des listes noires - adresse->serveur
declare -a address_hits

# Liste les données du fichier d'interface
declare -f _dot_dump
_dot_dump=pend_dummy   # Initialement un no-op

#  Les traces des données sont activées en initialisant la variable
#+ d'environnement SPAMMER_DATA avec le nom d'un fichier sur lequel le script
#+ peut écrire.
declare _dot_file

# Fonction d'aide pour la fonction dump-to-dot-file
# dump_to_dot <array_name> <prefix>
dump_to_dot() {
    local -a _dda_tmp
    local -i _dda_cnt
    local _dda_form='    '${2}'%04u %s\n'
    local IFS=${NO_WSP}
    eval _dda_tmp=\(\ \$\{$1\[@\]\}\ \)
    _dda_cnt=${#_dda_tmp[@]}
    if [ ${_dda_cnt} -gt 0 ]
    then
        for (( _dda = 0 ; _dda < _dda_cnt ; _dda++ ))
        do
            printf "${_dda_form}" \
                   "${_dda}" "${_dda_tmp[${_dda}]}" >>${_dot_file}
        done
    fi
}

# Qui initialise aussi _dot_dump par cette fonction . . .
dump_dot() {
    local -i _dd_cnt
    echo '# Data vintage: '$(date -R) >${_dot_file}
    echo '# ABS Guide: is_spammer.bash; v2, 2004-msz' >>${_dot_file}
    echo >>${_dot_file}
    echo 'digraph G {' >>${_dot_file}

    if [ ${#known_name[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Known domain name nodes' >>${_dot_file}
        _dd_cnt=${#known_name[@]}
        for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ ))
        do
            printf '    N%04u [label="%s"] ;\n' \
                   "${_dd}" "${known_name[${_dd}]}" >>${_dot_file}
        done
    fi

    if [ ${#known_address[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Known address nodes' >>${_dot_file}
        _dd_cnt=${#known_address[@]}
        for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ ))
        do
            printf '    A%04u [label="%s"] ;\n' \
                   "${_dd}" "${known_address[${_dd}]}" >>${_dot_file}
        done
    fi

    echo                                   >>${_dot_file}
    echo '/*'                              >>${_dot_file}
    echo ' * Known relationships :: User conversion to'  >>${_dot_file}
    echo ' * graphic form by hand or program required.'  >>${_dot_file}
    echo ' *'                              >>${_dot_file}

    if [ ${#auth_chain[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Authority reference edges followed and field source.'  >>${_dot_file}
        dump_to_dot auth_chain AC
    fi

    if [ ${#ref_chain[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Name reference edges followed and field source.'  >>${_dot_file}
        dump_to_dot ref_chain RC
    fi

    if [ ${#name_address[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Known name->address edges' >>${_dot_file}
        dump_to_dot name_address NA
    fi

    if [ ${#name_srvc[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Known name->service edges' >>${_dot_file}
        dump_to_dot name_srvc NS
    fi

    if [ ${#name_resource[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Known name->resource edges' >>${_dot_file}
        dump_to_dot name_resource NR
    fi

    if [ ${#parent_child[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Known parent->child edges' >>${_dot_file}
        dump_to_dot parent_child PC
    fi

    if [ ${#list_server[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Known Blacklist nodes' >>${_dot_file}
        _dd_cnt=${#list_server[@]}
        for (( _dd = 0 ; _dd < _dd_cnt ; _dd++ ))
        do
            printf '    LS%04u [label="%s"] ;\n' \
                   "${_dd}" "${list_server[${_dd}]}" >>${_dot_file}
        done
    fi

    unique_lines address_hits address_hits
    if [ ${#address_hits[@]} -gt 0 ]
    then
        echo >>${_dot_file}
        echo '# Known address->Blacklist_hit edges' >>${_dot_file}
        echo '# CAUTION: dig warnings can trigger false hits.' >>${_dot_file}
        dump_to_dot address_hits AH
    fi
    echo          >>${_dot_file}
    echo ' *'     >>${_dot_file}
    echo ' * That is a lot of relationships. Happy graphing.' >>${_dot_file}
    echo ' */'    >>${_dot_file}
    echo '}'      >>${_dot_file}
    return 0
}

# # # # Flux d'exécution 'Chassez le spammeur' # # # #

#  La trace d'exécution est activée en initialisant la variable d'environnement
#+ SPAMMER_TRACE avec le nom d'un fichier sur lequel le script peut écrire.
declare -a _trace_log
declare _log_file

# Fonction pour remplir le journal de traces
trace_logger() {
    _trace_log[${#_trace_log[@]}]=${_pend_current_}
}

# Enregistre le journal des traces vers la variable fichier.
declare -f _log_dump
_log_dump=pend_dummy   # Initialement un no-op.

# Enregistre le journal des traces vers un fichier.
dump_log() {
    local -i _dl_cnt
    _dl_cnt=${#_trace_log[@]}
    for (( _dl = 0 ; _dl < _dl_cnt ; _dl++ ))
    do
        echo ${_trace_log[${_dl}]} >> ${_log_file}
    done
    _dl_cnt=${#_pending_[@]}
    if [ ${_dl_cnt} -gt 0 ]
    then
        _dl_cnt=${_dl_cnt}-1
        echo '# # # Operations stack not empty # # #' >> ${_log_file}
        for (( _dl = ${_dl_cnt} ; _dl >= 0 ; _dl-- ))
        do
            echo ${_pending_[${_dl}]} >> ${_log_file}
        done
    fi
}

# # # Emballages de l'outil 'dig' # # #
#
#  Ces emballages sont dérivées des exemples affichés dans
#+ dig_wrappers.bash.
#
#  La différence majeur est que ceux-ci retournent leur résultat comme une liste
#+ dans un tableau.
#
#  Voir dig_wrappers.bash pour les détails et utiliser ce script pour développer
#+ toute modification.
#
# # #

# Réponse courte : 'dig' analyse la réponse.

# Recherche avant :: Nom -> Adresse
# short_fwd <domain_name> <array_name>
short_fwd() {
    local -a _sf_reply
    local -i _sf_rc
    local -i _sf_cnt
    IFS=${NO_WSP}
echo -n '.'
# echo 'sfwd: '${1}
    _sf_reply=( $(dig +short ${1} -c in -t a 2>/dev/null) )
    _sf_rc=$?
    if [ ${_sf_rc} -ne 0 ]
    then
        _trace_log[${#_trace_log[@]}]='## Lookup error '${_sf_rc}' on '${1}' ##'
# [ ${_sf_rc} -ne 9 ] && pend_drop
        return ${_sf_rc}
    else
        # Quelques versions de 'dig' renvoient des avertissements sur stdout.
        _sf_cnt=${#_sf_reply[@]}
        for (( _sf = 0 ; _sf < ${_sf_cnt} ; _sf++ ))
        do
            [ 'x'${_sf_reply[${_sf}]:0:2} == 'x;;' ] &&
                unset _sf_reply[${_sf}]
        done
        eval $2=\( \$\{_sf_reply\[@\]\} \)
    fi
    return 0
}

# Recherche inverse :: Adresse -> Nom
# short_rev <ip_address> <array_name>
short_rev() {
    local -a _sr_reply
    local -i _sr_rc
    local -i _sr_cnt
    IFS=${NO_WSP}
echo -n '.'
# echo 'srev: '${1}
    _sr_reply=( $(dig +short -x ${1} 2>/dev/null) )
    _sr_rc=$?
    if [ ${_sr_rc} -ne 0 ]
    then
        _trace_log[${#_trace_log[@]}]='## Lookup error '${_sr_rc}' on '${1}'
##'
# [ ${_sr_rc} -ne 9 ] && pend_drop
        return ${_sr_rc}
    else
        # Quelques versions de 'dig' renvoient des avertissements sur stdout.
        _sr_cnt=${#_sr_reply[@]}
        for (( _sr = 0 ; _sr < ${_sr_cnt} ; _sr++ ))
        do
            [ 'x'${_sr_reply[${_sr}]:0:2} == 'x;;' ] &&
                unset _sr_reply[${_sr}]
        done
        eval $2=\( \$\{_sr_reply\[@\]\} \)
    fi
    return 0
}

#  Recherche du format spécial utilisé pour lancer des requêtes sur les serveurs
#+ de listes noires (blacklist).
# short_text <ip_address> <array_name>
short_text() {
    local -a _st_reply
    local -i _st_rc
    local -i _st_cnt
    IFS=${NO_WSP}
# echo 'stxt: '${1}
    _st_reply=( $(dig +short ${1} -c in -t txt 2>/dev/null) )
    _st_rc=$?
    if [ ${_st_rc} -ne 0 ]
    then
        _trace_log[${#_trace_log[@]}]='## Text lookup error '${_st_rc}' on '${1}' ##'
# [ ${_st_rc} -ne 9 ] && pend_drop
        return ${_st_rc}
    else
        # Quelques versions de 'dig' renvoient des avertissements sur stdout.
        _st_cnt=${#_st_reply[@]}
        for (( _st = 0 ; _st < ${#_st_cnt} ; _st++ ))
        do
            [ 'x'${_st_reply[${_st}]:0:2} == 'x;;' ] &&
                unset _st_reply[${_st}]
        done
        eval $2=\( \$\{_st_reply\[@\]\} \)
    fi
    return 0
}

# Les formes longues, aussi connues sous le nom de versions "Analyse toi-même"

# RFC 2782   Recherche de service
# dig +noall +nofail +answer _ldap._tcp.openldap.org -t srv
# _<service>._<protocol>.<domain_name>
# _ldap._tcp.openldap.org. 3600   IN      SRV     0 0 389 ldap.openldap.org.
# domain TTL Class SRV Priority Weight Port Target

# Recherche avant :: Nom -> transfert de zone du pauvre
# long_fwd <domain_name> <array_name>
long_fwd() {
    local -a _lf_reply
    local -i _lf_rc
    local -i _lf_cnt
    IFS=${NO_WSP}
echo -n ':'
# echo 'lfwd: '${1}
    _lf_reply=( $(
        dig +noall +nofail +answer +authority +additional \
            ${1} -t soa ${1} -t mx ${1} -t any 2>/dev/null) )
    _lf_rc=$?
    if [ ${_lf_rc} -ne 0 ]
    then
        _trace_log[${#_trace_log[@]}]='## Zone lookup error '${_lf_rc}' on
'${1}' ##'
# [ ${_lf_rc} -ne 9 ] && pend_drop
        return ${_lf_rc}
    else
        # Quelques versions de 'dig' renvoient des avertissements sur stdout.
        _lf_cnt=${#_lf_reply[@]}
        for (( _lf = 0 ; _lf < ${_lf_cnt} ; _lf++ ))
        do
            [ 'x'${_lf_reply[${_lf}]:0:2} == 'x;;' ] &&
                unset _lf_reply[${_lf}]
        done
        eval $2=\( \$\{_lf_reply\[@\]\} \)
    fi
    return 0
}
#   La recherche inverse de nom de domaine correspondant à l'adresse IPv6:
#       4321:0:1:2:3:4:567:89ab
#   pourrait donnée (en hexadécimal) :
#   b.a.9.8.7.6.5.0.4.0.0.0.3.0.0.0.2.0.0.0.1.0.0.0.0.0.0.0.1.2.3.4.IP6.ARPA.

# Recherche inverse :: Adresse -> chaîne de délégation du pauvre
# long_rev <rev_ip_address> <array_name>
long_rev() {
    local -a _lr_reply
    local -i _lr_rc
    local -i _lr_cnt
    local _lr_dns
    _lr_dns=${1}'.in-addr.arpa.'
    IFS=${NO_WSP}
echo -n ':'
# echo 'lrev: '${1}
    _lr_reply=( $(
         dig +noall +nofail +answer +authority +additional \
             ${_lr_dns} -t soa ${_lr_dns} -t any 2>/dev/null) )
    _lr_rc=$?
    if [ ${_lr_rc} -ne 0 ]
    then
        _trace_log[${#_trace_log[@]}]='## Delegation lookup error '${_lr_rc}' on '${1}' ##'
# [ ${_lr_rc} -ne 9 ] && pend_drop
        return ${_lr_rc}
    else
        # Quelques versions de 'dig' renvoient des avertissements sur stdout.
        _lr_cnt=${#_lr_reply[@]}
        for (( _lr = 0 ; _lr < ${_lr_cnt} ; _lr++ ))
        do
            [ 'x'${_lr_reply[${_lr}]:0:2} == 'x;;' ] &&
                unset _lr_reply[${_lr}]
        done
        eval $2=\( \$\{_lr_reply\[@\]\} \)
    fi
    return 0
}

## Fonctions spécifiques à l'application ##

# Récupère un nom possible ; supprime root et TLD.
# name_fixup <string>
name_fixup(){
    local -a _nf_tmp
    local -i _nf_end
    local _nf_str
    local IFS
    _nf_str=$(to_lower ${1})
    _nf_str=$(to_dot ${_nf_str})
    _nf_end=${#_nf_str}-1
    [ ${_nf_str:${_nf_end}} != '.' ] &&
        _nf_str=${_nf_str}'.'
    IFS=${ADR_IFS}
    _nf_tmp=( ${_nf_str} )
    IFS=${WSP_IFS}
    _nf_end=${#_nf_tmp[@]}
    case ${_nf_end} in
    0) # Pas de point, seulement des points
        echo
        return 1
    ;;
    1) # Seulement un TLD.
        echo
        return 1
    ;;
    2) # Pourrait être bon.
       echo ${_nf_str}
       return 0
       # Besoin d'une table de recherche ?
       if [ ${#_nf_tmp[1]} -eq 2 ]
       then # TLD codé suivant le pays.
           echo
           return 1
       else
           echo ${_nf_str}
           return 0
       fi
    ;;
    esac
    echo ${_nf_str}
    return 0
}

# Récupère le(s) entrée(s) originale(s).
split_input() {
    [ ${#uc_name[@]} -gt 0 ] || return 0
    local -i _si_cnt
    local -i _si_len
    local _si_str
    unique_lines uc_name uc_name
    _si_cnt=${#uc_name[@]}
    for (( _si = 0 ; _si < _si_cnt ; _si++ ))
    do
        _si_str=${uc_name[$_si]}
        if is_address ${_si_str}
        then
            uc_address[${#uc_address[@]}]=${_si_str}
            unset uc_name[$_si]
        else
            if ! uc_name[$_si]=$(name_fixup ${_si_str})
            then
                unset ucname[$_si]
            fi
        fi
    done
    uc_name=( ${uc_name[@]} )
    _si_cnt=${#uc_name[@]}
    _trace_log[${#_trace_log[@]}]='## Input '${_si_cnt}' unchecked name input(s). ##'
    _si_cnt=${#uc_address[@]}
    _trace_log[${#_trace_log[@]}]='## Input '${_si_cnt}' unchecked address input(s). ##'
    return 0
}

## Fonctions de découverte -- verrouillage récursif par des données externes ##
## Le début 'si la liste est vide; renvoyer 0' de chacun est requis. ##

# Limiteur de récursion
# limit_chk() <next_level>
limit_chk() {
    local -i _lc_lmt
    # Vérifiez la limite d'indirection.
    if [ ${indirect} -eq 0 ] || [ $# -eq 0 ]
    then
        # Le choix 'faites-à-chaque-fois'
        echo 1                 # Toute valeur le fera.
        return 0               # OK pour continuer.
    else
        # La limite est effective.
        if [ ${indirect} -lt ${1} ]
        then
            echo ${1}          # Quoi que ce soit.
            return 1           # Arrêter ici.
        else
            _lc_lmt=${1}+1     # Augmenter la limite donnée.
            echo ${_lc_lmt}    # L'afficher.
            return 0           # OK pour continuer.
        fi
    fi
}

# Pour chaque nom dans uc_name:
#     Déplacez le nom dans chk_name.
#     Ajoutez les adresses à uc_address.
#     Lancez expand_input_address.
#     Répétez jusqu'à ce que rien de nouveau ne soit trouvé.
# expand_input_name <indirection_limit>
expand_input_name() {
    [ ${#uc_name[@]} -gt 0 ] || return 0
    local -a _ein_addr
    local -a _ein_new
    local -i _ucn_cnt
    local -i _ein_cnt
    local _ein_tst
    _ucn_cnt=${#uc_name[@]}

    if  ! _ein_cnt=$(limit_chk ${1})
    then
        return 0
    fi

    for (( _ein = 0 ; _ein < _ucn_cnt ; _ein++ ))
    do
        if short_fwd ${uc_name[${_ein}]} _ein_new
        then
            for (( _ein_cnt = 0 ; _ein_cnt < ${#_ein_new[@]}; _ein_cnt++ ))
            do
                _ein_tst=${_ein_new[${_ein_cnt}]}
                if is_address ${_ein_tst}
                then
                    _ein_addr[${#_ein_addr[@]}]=${_ein_tst}
                fi
           done
        fi
    done
    unique_lines _ein_addr _ein_addr     # Scrub duplicates.
    edit_exact chk_address _ein_addr     # Scrub pending detail.
    edit_exact known_address _ein_addr   # Scrub already detailed.
    if [ ${#_ein_addr[@]} -gt 0 ]        # Anything new?
    then
        uc_address=( ${uc_address[@]} ${_ein_addr[@]} )
        pend_func expand_input_address ${1}
        _trace_log[${#_trace_log[@]}]='## Added '${#_ein_addr[@]}' unchecked address input(s). ##'
    fi
    edit_exact chk_name uc_name          # Scrub pending detail.
    edit_exact known_name uc_name        # Scrub already detailed.
    if [ ${#uc_name[@]} -gt 0 ]
    then
        chk_name=( ${chk_name[@]} ${uc_name[@]}  )
        pend_func detail_each_name ${1}
    fi
    unset uc_name[@]
    return 0
}

# Pour chaque adresse dans uc_address:
#     Déplacez l'adresse vers chk_address.
#     Ajoutez les noms à uc_name.
#     Lancez expand_input_name.
#     Répétez jusqu'à ce que rien de nouveau ne soit trouvé.
# expand_input_address <indirection_limit>
expand_input_address() {
    [ ${#uc_address[@]} -gt 0 ] || return 0
    local -a _eia_addr
    local -a _eia_name
    local -a _eia_new
    local -i _uca_cnt
    local -i _eia_cnt
    local _eia_tst
    unique_lines uc_address _eia_addr
    unset uc_address[@]
    edit_exact been_there_addr _eia_addr
    _uca_cnt=${#_eia_addr[@]}
    [ ${_uca_cnt} -gt 0 ] &&
        been_there_addr=( ${been_there_addr[@]} ${_eia_addr[@]} )

    for (( _eia = 0 ; _eia < _uca_cnt ; _eia++ ))
    do
            if short_rev ${_eia_addr[${_eia}]} _eia_new
            then
                for (( _eia_cnt = 0 ; _eia_cnt < ${#_eia_new[@]} ; _eia_cnt++ ))
                do
                    _eia_tst=${_eia_new[${_eia_cnt}]}
                    if _eia_tst=$(name_fixup ${_eia_tst})
                    then
                        _eia_name[${#_eia_name[@]}]=${_eia_tst}
                    fi
                done
            fi
    done
    unique_lines _eia_name _eia_name     # Scrub duplicates.
    edit_exact chk_name _eia_name        # Scrub pending detail.
    edit_exact known_name _eia_name      # Scrub already detailed.
    if [ ${#_eia_name[@]} -gt 0 ]        # Anything new?
    then
        uc_name=( ${uc_name[@]} ${_eia_name[@]} )
        pend_func expand_input_name ${1}
        _trace_log[${#_trace_log[@]}]='## Added '${#_eia_name[@]}' unchecked name input(s). ##'
    fi
    edit_exact chk_address _eia_addr     # Scrub pending detail.
    edit_exact known_address _eia_addr   # Scrub already detailed.
    if [ ${#_eia_addr[@]} -gt 0 ]        # Anything new?
    then
        chk_address=( ${chk_address[@]} ${_eia_addr[@]} )
        pend_func detail_each_address ${1}
    fi
    return 0
}

# La réponse de la zone analysez-le-vous-même.
# L'entrée est la liste chk_name.
# detail_each_name <indirection_limit>
detail_each_name() {
    [ ${#chk_name[@]} -gt 0 ] || return 0
    local -a _den_chk       # Noms à vérifier
    local -a _den_name      # Noms trouvés ici
    local -a _den_address   # Adresses trouvées ici
    local -a _den_pair      # Paires trouvés ici
    local -a _den_rev       # Paires inverses trouvées ici
    local -a _den_tmp       # Ligne en cours d'analyse
    local -a _den_auth      # Contact SOA en cours d'analyse
    local -a _den_new       # La réponse de la zone
    local -a _den_pc        # Parent-Fils devient très rapide
    local -a _den_ref       # Ainsi que la chaîne de référence
    local -a _den_nr        # Nom-Ressource peut être gros
    local -a _den_na        # Nom-Adresse
    local -a _den_ns        # Nom-Service
    local -a _den_achn      # Chaîne d'autorité
    local -i _den_cnt       # Nombre de noms à détailler
    local -i _den_lmt       # Limite d'indirection
    local _den_who          # Named en cours d'exécution
    local _den_rec          # Type d'enregistrement en cours d'exécution
    local _den_cont         # Domaine du contact
    local _den_str          # Correction du nom
    local _den_str2         # Correction inverse
    local IFS=${WSP_IFS}

    # Copie locale, unique de noms à vérifier
    unique_lines chk_name _den_chk
    unset chk_name[@]       # Fait avec des globales.

    # Moins de noms déjà connus
    edit_exact known_name _den_chk
    _den_cnt=${#_den_chk[@]}

    # S'il reste quelque chose, ajoutez à known_name.
    [ ${_den_cnt} -gt 0 ] &&
        known_name=( ${known_name[@]} ${_den_chk[@]} )

    # pour la liste des (précédents) noms inconnus . . .
    for (( _den = 0 ; _den < _den_cnt ; _den++ ))
    do
        _den_who=${_den_chk[${_den}]}
        if long_fwd ${_den_who} _den_new
        then
            unique_lines _den_new _den_new
            if [ ${#_den_new[@]} -eq 0 ]
            then
                _den_pair[${#_den_pair[@]}]='0.0.0.0 '${_den_who}
            fi

            # Analyser chaque ligne de la réponse.
            for (( _line = 0 ; _line < ${#_den_new[@]} ; _line++ ))
            do
                IFS=${NO_WSP}$'\x09'$'\x20'
                _den_tmp=( ${_den_new[${_line}]} )
                IFS=${WSP_IFS}
                #  Si l'enregistrement est utilisable et n'est pas un message
                #+ d'avertissement . . .
                if [ ${#_den_tmp[@]} -gt 4 ] && [ 'x'${_den_tmp[0]} != 'x;;' ]
                then
                    _den_rec=${_den_tmp[3]}
                    _den_nr[${#_den_nr[@]}]=${_den_who}' '${_den_rec}
                    # Début de RFC1033 (+++)
                    case ${_den_rec} in

                         #<name>  [<ttl>]  [<class>]  SOA  <origin>  <person>
                    SOA) # Début de l'autorité
                        if _den_str=$(name_fixup ${_den_tmp[0]})
                        then
                            _den_name[${#_den_name[@]}]=${_den_str}
                            _den_achn[${#_den_achn[@]}]=${_den_who}' '${_den_str}' SOA'
                            #  origine SOA -- nom de domaine de l'enregistrement
                            #+ de la zone maître
                            if _den_str2=$(name_fixup ${_den_tmp[4]})
                            then
                                _den_name[${#_den_name[@]}]=${_den_str2}
                                _den_achn[${#_den_achn[@]}]=${_den_who}' '${_den_str2}' SOA.O'
                            fi
                            # Adresse mail responsable (peut-être boguée).
                            # Possibilité d'un premier.dernier@domaine.nom
                            # ignoré.
                            set -f
                            if _den_str2=$(name_fixup ${_den_tmp[5]})
                            then
                                IFS=${ADR_IFS}
                                _den_auth=( ${_den_str2} )
                                IFS=${WSP_IFS}
                                if [ ${#_den_auth[@]} -gt 2 ]
                                then
                                     _den_cont=${_den_auth[1]}
                                     for (( _auth = 2 ; _auth < ${#_den_auth[@]}
; _auth++ ))
                                     do
                                      
_den_cont=${_den_cont}'.'${_den_auth[${_auth}]}
                                     done
                                     _den_name[${#_den_name[@]}]=${_den_cont}'.'
                                     _den_achn[${#_den_achn[@]}]=${_den_who}'
'${_den_cont}'. SOA.C'
                                fi
                            fi
                            set +f
                        fi
                    ;;


                    A) # Enregistrement d'adresse IP(v4)
                        if _den_str=$(name_fixup ${_den_tmp[0]})
                        then
                            _den_name[${#_den_name[@]}]=${_den_str}
                            _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' '${_den_str}
                            _den_na[${#_den_na[@]}]=${_den_str}' '${_den_tmp[4]}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' A'
                        else
                            _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' unknown.domain'
                            _den_na[${#_den_na[@]}]='unknown.domain '${_den_tmp[4]}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' unknown.domain A'
                        fi
                        _den_address[${#_den_address[@]}]=${_den_tmp[4]}
                        _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_tmp[4]}
                    ;;

                    NS) #  Enregistrement du nom de serveur
                        #  Nom de domaine en cours de service (peut être autre
                        #+ chose que l'actuel)
                        if _den_str=$(name_fixup ${_den_tmp[0]})
                        then
                            _den_name[${#_den_name[@]}]=${_den_str}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' NS'

                            # Nom du domaine du fournisseur de services
                            if _den_str2=$(name_fixup ${_den_tmp[4]})
                            then
                                _den_name[${#_den_name[@]}]=${_den_str2}
                                _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str2}' NSH'
                                _den_ns[${#_den_ns[@]}]=${_den_str2}' NS'
                                _den_pc[${#_den_pc[@]}]=${_den_str}' '${_den_str2}
                            fi
                        fi
                    ;;

                    MX) # Enregistrement du serveur de mails
                        # Nom de domaine en service (jokers non gérés ici)
                        if _den_str=$(name_fixup ${_den_tmp[0]})
                        then
                            _den_name[${#_den_name[@]}]=${_den_str}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' MX'
                        fi
                        # Nom du domaine du fournisseur de service
                        if _den_str=$(name_fixup ${_den_tmp[5]})
                        then
                            _den_name[${#_den_name[@]}]=${_den_str}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' MXH'
                            _den_ns[${#_den_ns[@]}]=${_den_str}' MX'
                            _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str}
                        fi
                    ;;

                    PTR) # Enregistrement de l'adresse inverse
                         # Nom spécial
                        if _den_str=$(name_fixup ${_den_tmp[0]})
                        then
                            _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' PTR'
                            # Nom d'hôte (pas un CNAME)
                            if _den_str2=$(name_fixup ${_den_tmp[4]})
                            then
                                _den_rev[${#_den_rev[@]}]=${_den_str}' '${_den_str2}
                                _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str2}' PTRH'
                                _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str}
                            fi
                        fi
                    ;;

                    AAAA) # Enregistrement de l'adresse IP(v6)
                        if _den_str=$(name_fixup ${_den_tmp[0]})
                        then
                            _den_name[${#_den_name[@]}]=${_den_str}
                            _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' '${_den_str}
                            _den_na[${#_den_na[@]}]=${_den_str}' '${_den_tmp[4]}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' AAAA'
                        else
                            _den_pair[${#_den_pair[@]}]=${_den_tmp[4]}' unknown.domain'
                            _den_na[${#_den_na[@]}]='unknown.domain '${_den_tmp[4]}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' unknown.domain'
                        fi
                        # Aucun travaux sur les adresses IPv6
                            _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_tmp[4]}
                    ;;

                    CNAME) # Enregistrement du nom de l'alias
                           # Pseudo
                        if _den_str=$(name_fixup ${_den_tmp[0]})
                        then
                            _den_name[${#_den_name[@]}]=${_den_str}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' CNAME'
                            _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str}
                        fi
                        # Nom d'hôte
                        if _den_str=$(name_fixup ${_den_tmp[4]})
                        then
                            _den_name[${#_den_name[@]}]=${_den_str}
                            _den_ref[${#_den_ref[@]}]=${_den_who}' '${_den_str}' CHOST'
                            _den_pc[${#_den_pc[@]}]=${_den_who}' '${_den_str}
                        fi
                    ;;
#                   TXT)
#                   ;;
                    esac
                fi
            done
        else # Erreur de recherche == enregistrement 'A' 'adresse inconnue'
            _den_pair[${#_den_pair[@]}]='0.0.0.0 '${_den_who}
        fi
    done

    # Tableau des points de contrôle grandit.
    unique_lines _den_achn _den_achn      # Fonctionne mieux, tout identique.
    edit_exact auth_chain _den_achn       # Fonctionne mieux, éléments uniques.
    if [ ${#_den_achn[@]} -gt 0 ]
    then
        IFS=${NO_WSP}
        auth_chain=( ${auth_chain[@]} ${_den_achn[@]} )
        IFS=${WSP_IFS}
    fi

    unique_lines _den_ref _den_ref      # Fonctionne mieux, tout identique.
    edit_exact ref_chain _den_ref       # Fonctionne mieux, éléments uniques.
    if [ ${#_den_ref[@]} -gt 0 ]
    then
        IFS=${NO_WSP}
        ref_chain=( ${ref_chain[@]} ${_den_ref[@]} )
        IFS=${WSP_IFS}
    fi

    unique_lines _den_na _den_na
    edit_exact name_address _den_na
    if [ ${#_den_na[@]} -gt 0 ]
    then
        IFS=${NO_WSP}
        name_address=( ${name_address[@]} ${_den_na[@]} )
        IFS=${WSP_IFS}
    fi

    unique_lines _den_ns _den_ns
    edit_exact name_srvc _den_ns
    if [ ${#_den_ns[@]} -gt 0 ]
    then
        IFS=${NO_WSP}
        name_srvc=( ${name_srvc[@]} ${_den_ns[@]} )
        IFS=${WSP_IFS}
    fi

    unique_lines _den_nr _den_nr
    edit_exact name_resource _den_nr
    if [ ${#_den_nr[@]} -gt 0 ]
    then
        IFS=${NO_WSP}
        name_resource=( ${name_resource[@]} ${_den_nr[@]} )
        IFS=${WSP_IFS}
    fi

    unique_lines _den_pc _den_pc
    edit_exact parent_child _den_pc
    if [ ${#_den_pc[@]} -gt 0 ]
    then
        IFS=${NO_WSP}
        parent_child=( ${parent_child[@]} ${_den_pc[@]} )
        IFS=${WSP_IFS}
    fi

    # Mise à jour de la liste known_pair (adresse et nom).
    unique_lines _den_pair _den_pair
    edit_exact known_pair _den_pair
    if [ ${#_den_pair[@]} -gt 0 ]  # Rien de nouveau?
    then
        IFS=${NO_WSP}
        known_pair=( ${known_pair[@]} ${_den_pair[@]} )
        IFS=${WSP_IFS}
    fi

    # Mise à jour de la liste des pairs inversés.
    unique_lines _den_rev _den_rev
    edit_exact reverse_pair _den_rev
    if [ ${#_den_rev[@]} -gt 0 ]   # Rien de nouveau ?
    then
        IFS=${NO_WSP}
        reverse_pair=( ${reverse_pair[@]} ${_den_rev[@]} )
        IFS=${WSP_IFS}
    fi

    # Vérification de la limite d'indirection -- abandon si elle est atteinte.
    if ! _den_lmt=$(limit_chk ${1})
    then
        return 0
    fi

    #  Le moteur d'exécution est LIFO. L'ordre des opérations en attente est
    #+ important.
    # Avons-nous défini de nouvelles adresses ?
    unique_lines _den_address _den_address    # Scrub duplicates.
    edit_exact known_address _den_address     # Scrub already processed.
    edit_exact un_address _den_address        # Scrub already waiting.
    if [ ${#_den_address[@]} -gt 0 ]          # Anything new?
    then
        uc_address=( ${uc_address[@]} ${_den_address[@]} )
        pend_func expand_input_address ${_den_lmt}
        _trace_log[${#_trace_log[@]}]='## Added '${#_den_address[@]}' unchecked address(s). ##'
    fi

    # Avons-nous trouvé de nouveaux noms ?
    unique_lines _den_name _den_name          # Scrub duplicates.
    edit_exact known_name _den_name           # Scrub already processed.
    edit_exact uc_name _den_name              # Scrub already waiting.
    if [ ${#_den_name[@]} -gt 0 ]             # Anything new?
    then
        uc_name=( ${uc_name[@]} ${_den_name[@]} )
        pend_func expand_input_name ${_den_lmt}
        _trace_log[${#_trace_log[@]}]='## Added '${#_den_name[@]}' unchecked name(s). ##'
    fi
    return 0
}

# Réponse de délégation analysez-le-vous-même
# L'entrée est la liste chk_address.
# detail_each_address <indirection_limit>
detail_each_address() {
    [ ${#chk_address[@]} -gt 0 ] || return 0
    unique_lines chk_address chk_address
    edit_exact known_address chk_address
    if [ ${#chk_address[@]} -gt 0 ]
    then
        known_address=( ${known_address[@]} ${chk_address[@]} )
        unset chk_address[@]
    fi
    return 0
}

## Fonctions de sortie spécifiques à l'application ##

# Affiche joliment les pairs connues.
report_pairs() {
    echo
    echo 'Known network pairs.'
    col_print known_pair 2 5 30

    if [ ${#auth_chain[@]} -gt 0 ]
    then
        echo
        echo 'Known chain of authority.'
        col_print auth_chain 2 5 30 55
    fi

    if [ ${#reverse_pair[@]} -gt 0 ]
    then
        echo
        echo 'Known reverse pairs.'
        col_print reverse_pair 2 5 55
    fi
    return 0
}

#  Vérifie une adresse contre la liste des serveurs
#+ faisant partie de la liste noire.
# Un bon endroit pour capturer avec GraphViz :
# address->status(server(reports))
# check_lists <ip_address>
check_lists() {
    [ $# -eq 1 ] || return 1
    local -a _cl_fwd_addr
    local -a _cl_rev_addr
    local -a _cl_reply
    local -i _cl_rc
    local -i _ls_cnt
    local _cl_dns_addr
    local _cl_lkup

    split_ip ${1} _cl_fwd_addr _cl_rev_addr
    _cl_dns_addr=$(dot_array _cl_rev_addr)'.'
    _ls_cnt=${#list_server[@]}
    echo '    Checking address '${1}
    for (( _cl = 0 ; _cl < _ls_cnt ; _cl++ ))
    do
        _cl_lkup=${_cl_dns_addr}${list_server[${_cl}]}
        if short_text ${_cl_lkup} _cl_reply
        then
            if [ ${#_cl_reply[@]} -gt 0 ]
            then
                echo '        Records from '${list_server[${_cl}]}
                address_hits[${#address_hits[@]}]=${1}' '${list_server[${_cl}]}
                _hs_RC=2
                for (( _clr = 0 ; _clr < ${#_cl_reply[@]} ; _clr++ ))
                do
                    echo '            '${_cl_reply[${_clr}]}
                done
            fi
        fi
    done
    return 0
}

## La colle habituelle de l'application ##

# Qui l'a fait ?
credits() {
   echo
   echo "Guide d'écriture avancée des scripts Bash : is_spammer.bash, v2,
2004-msz"
}

# Comment l'utiliser ?
# (Voir aussi, "Quickstart" à la fin de ce script.)
usage() {
    cat <<-'_usage_statement_'
    Le script is_spammer.bash requiert un ou deux arguments.

    arg 1) Pourrait être :
        a) Un nom de domaine
        b) Une adresse IPv4
        c) Le nom d'un fichier avec des noms et adresses mélangés, un par ligne.

    arg 2) Pourrait être :
        a) Un nom de domaine d'un serveur Blacklist
        b) Le nom d'un fichier contenant une liste de noms de domaine Blacklist,
           un domaine par ligne.
        c) Si non présent, une liste par défaut de serveurs Blacklist (libres)
           est utilisée.
        d) Si un fichier vide, lisible, est donné, la recherche de serveurs
           Blacklist est désactivée.

    Toutes les sorties du script sont écrites sur stdout.

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

    Requiert le programme externe 'dig' provenant des programmes DNS de 'bind-9'
    Voir http://www.isc.org

    La limite de la profondeur de recherche du nom de domaine est par défaut de
    deux niveaux.
    Initialisez la variable d'environnement SPAMMER_LIMIT pour modifier ceci.
    SPAMMER_LIMIT=0 signifie 'illimité'

    La limite peut aussi être initialisée sur la ligne de commande.
    Si arg#1 est un entier, la limite utilise cette valeur
    puis les règles d'arguments ci-dessus sont appliquées.

    Initialiser la variable d'environnemnt 'SPAMMER_DATA' à un nom de fichier
    demandera au script d'écrire un fichier graphique GraphViz.

    Pour la version de développement ;
    Initialiser la variable d'environnement 'SPAMMER_TRACE' avec un nom de
    fichier demandera au moteur d'exécution de tracer tous les appels de
    fonction.

_usage_statement_
}

# La liste par défaut des serveurs Blacklist :
# Plusieurs choix, voir : http://www.spews.org/lists.html

declare -a default_servers
# Voir : http://www.spamhaus.org (Conservateur, bien maintenu)
default_servers[0]='sbl-xbl.spamhaus.org'
# Voir : http://ordb.org (Relais mail ouverts)
default_servers[1]='relays.ordb.org'
# Voir : http://www.spamcop.net/ (Vous pouvez rapporter les spammeurs ici)
default_servers[2]='bl.spamcop.net'
# Voir : http://www.spews.org (Un système de détection rapide)
default_servers[3]='l2.spews.dnsbl.sorbs.net'
# Voir : http://www.dnsbl.us.sorbs.net/using.shtml
default_servers[4]='dnsbl.sorbs.net'
# Voir : http://dsbl.org/usage (Différentes listes de relai de mail)
default_servers[5]='list.dsbl.org'
default_servers[6]='multihop.dsbl.org'
default_servers[7]='unconfirmed.dsbl.org'

# Argument utilisateur #1
setup_input() {
    if [ -e ${1} ] && [ -r ${1} ]  # Nom d'un fichier lisible
    then
        file_to_array ${1} uc_name
        echo 'Using filename >'${1}'< as input.'
    else
        if is_address ${1}          # Adresse IP ?
        then
            uc_address=( ${1} )
            echo 'Starting with address >'${1}'<'
        else                       # Doit être un nom.
            uc_name=( ${1} )
            echo 'Starting with domain name >'${1}'<'
        fi
    fi
    return 0
}

# Argument utilisateur #2
setup_servers() {
    if [ -e ${1} ] && [ -r ${1} ]  # Nom d'un fichier lisible
    then
        file_to_array ${1} list_server
        echo 'Using filename >'${1}'< as blacklist server list.'
    else
        list_server=( ${1} )
        echo 'Using blacklist server >'${1}'<'
    fi
    return 0
}

# Variable d'environnement utilisateur SPAMMER_TRACE
live_log_die() {
    if [ ${SPAMMER_TRACE:=} ]    # Journal de trace ?
    then
        if [ ! -e ${SPAMMER_TRACE} ]
        then
            if ! touch ${SPAMMER_TRACE} 2>/dev/null
            then
                pend_func echo $(printf '%q\n' \
                'Unable to create log file >'${SPAMMER_TRACE}'<')
                pend_release
                exit 1
            fi
            _log_file=${SPAMMER_TRACE}
            _pend_hook_=trace_logger
            _log_dump=dump_log
        else
            if [ ! -w ${SPAMMER_TRACE} ]
            then
                pend_func echo $(printf '%q\n' \
                'Unable to write log file >'${SPAMMER_TRACE}'<')
                pend_release
                exit 1
            fi
            _log_file=${SPAMMER_TRACE}
            echo '' > ${_log_file}
            _pend_hook_=trace_logger
            _log_dump=dump_log
        fi
    fi
    return 0
}

# Variable d'environnement utilisateur SPAMMER_DATA
data_capture() {
    if [ ${SPAMMER_DATA:=} ]    # Tracer les données ?
    then
        if [ ! -e ${SPAMMER_DATA} ]
        then
            if ! touch ${SPAMMER_DATA} 2>/dev/null
            then
                pend_func echo $(printf '%q]n' \
                'Unable to create data output file >'${SPAMMER_DATA}'<')
                pend_release
                exit 1
            fi
            _dot_file=${SPAMMER_DATA}
            _dot_dump=dump_dot
        else
            if [ ! -w ${SPAMMER_DATA} ]
            then
                pend_func echo $(printf '%q\n' \
                'Unable to write data output file >'${SPAMMER_DATA}'<')
                pend_release
                exit 1
            fi
            _dot_file=${SPAMMER_DATA}
            _dot_dump=dump_dot
        fi
    fi
    return 0
}

# Réunir les arguments spécifiés par l'utilisateur.
do_user_args() {
    if [ $# -gt 0 ] && is_number $1
    then
        indirect=$1
        shift
    fi

    case $# in                 # L'utilisateur nous traite-t'il correctement?
        1)
            if ! setup_input $1    # Vérification des erreurs.
            then
                pend_release
                $_log_dump
                exit 1
            fi
            list_server=( ${default_servers[@]} )
            _list_cnt=${#list_server[@]}
            echo 'Using default blacklist server list.'
            echo 'Search depth limit: '${indirect}
            ;;
        2)
            if ! setup_input $1    # Vérification des erreurs.
            then
                pend_release
                $_log_dump
                exit 1
            fi
            if ! setup_servers $2  # Vérification des erreurs.
            then
                pend_release
                $_log_dump
                exit 1
            fi
            echo 'Search depth limit: '${indirect}
            ;;
        *)
            pend_func usage
            pend_release
            $_log_dump
            exit 1
            ;;
    esac
    return 0
}

# Un outil à but général de déboguage.
# list_array <array_name>
list_array() {
    [ $# -eq 1 ] || return 1  # Un argument requis.

    local -a _la_lines
    set -f
    local IFS=${NO_WSP}
    eval _la_lines=\(\ \$\{$1\[@\]\}\ \)
    echo
    echo "Element count "${#_la_lines[@]}" array "${1}
    local _ln_cnt=${#_la_lines[@]}

    for (( _i = 0; _i < ${_ln_cnt}; _i++ ))
    do
        echo 'Element '$_i' >'${_la_lines[$_i]}'<'
    done
    set +f
    return 0
}

## Code 'Chez le spammeur' ##
pend_init                               # Initialisation du moteur à pile.
pend_func credits                       # Dernière chose à afficher.

## Gérer l'utilisateur ##
live_log_die                            #  Initialiser le journal de trace de
                                        #+ déboguage.
data_capture                            #  Initialiser le fichier de capture de
                                        #+ données.
echo
do_user_args $@

## N'a pas encore quitté - Il y a donc un peu d'espoir ##
# Groupe de découverte - Le moteur d'exécution est LIFO - queue en ordre
# inverse d'exécution.
_hs_RC=0                                # Code de retour de Chassez le spammeur
pend_mark
    pend_func report_pairs              # Paires nom-adresse rapportées.

    # Les deux detail_* sont des fonctions mutuellement récursives.
    # Elles mettent en queue les fonctions expand_* functions si nécessaire.
    # Ces deux (les dernières de ???) sortent de la récursion.
    pend_func detail_each_address       #  Obtient toutes les ressources
                                        #+ des adresses.
    pend_func detail_each_name          #  Obtient toutes les ressources
                                        #+ des noms.

    #  Les deux expand_* sont des fonctions mutuellement récursives,
    #+ qui mettent en queue les fonctions detail_* supplémentaires si
    #+ nécessaire.
    pend_func expand_input_address 1    #  Étend les noms en entrées par des
                                        #+ adresses.
    pend_func expand_input_name 1       #  Étend les adresses en entrées par des
                                        #+ noms.

    # Commence avec un ensemble unique de noms et d'adresses.
    pend_func unique_lines uc_address uc_address
    pend_func unique_lines uc_name uc_name

    # Entrée mixe séparée de noms et d'adresses.
    pend_func split_input
pend_release

## Paires rapportées -- Liste unique d'adresses IP trouvées
echo
_ip_cnt=${#known_address[@]}
if [ ${#list_server[@]} -eq 0 ]
then
    echo 'Blacklist server list empty, none checked.'
else
    if [ ${_ip_cnt} -eq 0 ]
    then
        echo 'Known address list empty, none checked.'
    else
        _ip_cnt=${_ip_cnt}-1   # Start at top.
        echo 'Checking Blacklist servers.'
        for (( _ip = _ip_cnt ; _ip >= 0 ; _ip-- ))
        do
            pend_func check_lists $( printf '%q\n' ${known_address[$_ip]} )
        done
    fi
fi
pend_release
$_dot_dump                   # Fichier graphique
$_log_dump                   # Trace d'exécution
echo


#########################################
# Exemple de sortie provenant du script #
#########################################
:<<-'_is_spammer_outputs_'

./is_spammer.bash 0 web4.alojamentos7.com

Starting with domain name >web4.alojamentos7.com<
Using default blacklist server list.
Search depth limit: 0
.:....::::...:::...:::.......::..::...:::.......::
Known network pairs.
    66.98.208.97             web4.alojamentos7.com.
    66.98.208.97             ns1.alojamentos7.com.
    69.56.202.147            ns2.alojamentos.ws.
    66.98.208.97             alojamentos7.com.
    66.98.208.97             web.alojamentos7.com.
    69.56.202.146            ns1.alojamentos.ws.
    69.56.202.146            alojamentos.ws.
    66.235.180.113           ns1.alojamentos.org.
    66.235.181.192           ns2.alojamentos.org.
    66.235.180.113           alojamentos.org.
    66.235.180.113           web6.alojamentos.org.
    216.234.234.30           ns1.theplanet.com.
    12.96.160.115            ns2.theplanet.com.
    216.185.111.52           mail1.theplanet.com.
    69.56.141.4              spooling.theplanet.com.
    216.185.111.40           theplanet.com.
    216.185.111.40           www.theplanet.com.
    216.185.111.52           mail.theplanet.com.

Checking Blacklist servers.
    Checking address 66.98.208.97
        Records from dnsbl.sorbs.net
            "Spam Received See: http://www.dnsbl.sorbs.net/lookup.shtml?66.98.208.97"
    Checking address 69.56.202.147
    Checking address 69.56.202.146
    Checking address 66.235.180.113
    Checking address 66.235.181.192
    Checking address 216.185.111.40
    Checking address 216.234.234.30
    Checking address 12.96.160.115
    Checking address 216.185.111.52
    Checking address 69.56.141.4

Advanced Bash Scripting Guide: is_spammer.bash, v2, 2004-msz

_is_spammer_outputs_

exit ${_hs_RC}

###############################################################
#  Le script ignore tout ce qui se trouve entre ici et la fin #
#+ à cause de la commande 'exit' ci-dessus.                   #
###############################################################



Quickstart
==========

 Prérequis

  Bash version 2.05b ou 3.00 (bash --version)
  Une version de Bash supportant les tableaux. Le support des tableaux est
  inclus dans les configurations par défaut de Bash.

  'dig,' version 9.x.x (dig $HOSTNAME, voir la première ligne en sortie)
  Une version de dig supportant les options +short.
  Voir dig_wrappers.bash pour les détails.


 Prérequis optionnels

  'named', un programme de cache DNS local. N'importe lequel conviendra.
  Faites deux fois : dig $HOSTNAME 
  Vérifier près de la fin de la sortie si vous voyez:
    SERVER: 127.0.0.1#53
  Ceci signifie qu'il fonctionne.


 Support optionnel des graphiques

  'date', un outil standard *nix. (date -R)

  dot un programme pour convertir le fichier de description graphique en
  un diagramme. (dot -V)
  Fait partie de l'ensemble des programmes Graph-Viz.
  Voir [http://www.research.att.com/sw/tools/graphviz||GraphViz]

  'dotty', un éditeur visuel pour les fichiers de description graphique.
  Fait aussi partie de l'ensemble des programmes Graph-Viz.




 Quick Start

Dans le même répertoire que le script is_spammer.bash; 
Lancez : ./is_spammer.bash

 Détails d'utilisation

1. Choix de serveurs Blacklist.

  (a) Pour utiliser les serveurs par défaut, liste intégrée : ne rien faire.

  (b) Pour utiliser votre propre liste : 

    i. Créez un fichier avec un seul serveru Blacklist par ligne.

    ii. Indiquez ce fichier en dernier argument du script.

  (c) Pour utiliser un seul serveur Blacklist : Dernier argument de ce script.

  (d) Pour désactiver les recherches Blacklist :

    i. Créez un fichier vide (touch spammer.nul)
       Le nom du fichier n'a pas d'importance.

    ii. Indiquez ce nom en dernier argument du script.

2. Limite de la profondeur de recherche.

  (a) Pour utiliser la valeur par défaut de 2 : ne rien faire.

  (b) Pour configurer une limite différente : 
      Une limite de 0 signifie illimitée.

    i. export SPAMMER_LIMIT=1
       ou tout autre limite que vous désirez.

    ii. OU indiquez la limite désirée en premier argument de ce script.

3. Journal de trace de l'exécution (optionnel).

  (a) Pour utiliser la configuration par défaut (sans traces) : ne rien faire.

  (b) Pour écrire dans un journal de trace :
      export SPAMMER_TRACE=spammer.log
      ou tout autre nom de fichier que vous voulez.

4. Fichier de description graphique optionnel.

  (a) Pour utiliser la configuration par défaut (sans graphique) : ne rien
      faire.

  (b) Pour écrire un fichier de description graphique Graph-Viz :
      export SPAMMER_DATA=spammer.dot
      ou tout autre nom de fichier que vous voulez.

5. Où commencer la recherche.

  (a) Commencer avec un simple nom de domaine :

    i. Sans limite de recherche sur la ligne de commande : Premier
       argument du script.

    ii. Avec une limite de recherche sur la ligne de commande : Second
        argument du script.

  (b) Commencer avec une simple adresse IP :

    i. Sans limite de recherche sur la ligne de commande : Premier
       argument du script.

    ii. Avec une limite de recherche sur la ligne de commande : Second
        argument du script.

  (c) Commencer avec de nombreux noms et/ou adresses :
      Créer un fichier avec un nom ou une adresse par ligne.
      Le nom du fichier n'a pas d'importance.

    i. Sans limite de recherche sur la ligne de commande : Fichier comme premier
       argument du script.

    ii. Avec une limite de recherche sur la ligne de commande : Fichier comme
        second argument du script.

6. Que faire pour l'affichage en sortie.

  (a) Pour visualiser la sortie à l'écran : ne rien faire.

  (b) Pour sauvegarder la sortie dans un fichier : rediriger stdout vers un
      fichier.

  (c) Pour désactiver la sortie : rediriger stdout vers /dev/null.

7. Fin temporaire de la phase de décision.
   appuyez sur RETURN 
   attendez (sinon, regardez les points et les virgules).

8. De façon optionnelle, vérifiez le code de retour.

  (a) Code de retour 0: Tout est OK

  (b) Code de retour 1: Échec du script de configuration

  (c) Code de retour 2: Quelque chose était sur la liste noire.

9. Où est mon graphe (diagramme) ?

Le script ne produit pas directement un graphe (diagramme).
Il produit seulement un fichier de description graphique. Vous pouvez utiliser
ce fichier qui a été créé par le programme 'dot'.

Jusqu'à l'édition du fichier de description pour décrire les relations que vous
souhaitez montrer, tout ce que vous obtenez est un ensemble de noms et de noms
d'adresses.

Toutes les relations découvertes par le script font partie d'un bloc en
commentaires dans le fichier de description graphique, chacun ayant un en-tête
descriptif.

L'édition requise pour tracer une ligne entre une paire de noeuds peut se faire
avec un éditeur de texte à partir des informations du fichier descripteur. 

Avec ces lignes quelque part dans le fichier descripteur :

# Known domain name nodes

N0000 [label="guardproof.info."] ;

N0002 [label="third.guardproof.info."] ;



# Known address nodes

A0000 [label="61.141.32.197"] ;



/*

# Known name->address edges

NA0000 third.guardproof.info. 61.141.32.197



# Known parent->child edges

PC0000 guardproof.info. third.guardproof.info.

 */

Modifiez ceci en les lignes suivantes après avoir substitué les identifiants de
noeuds avec les relations :

# Known domain name nodes

N0000 [label="guardproof.info."] ;

N0002 [label="third.guardproof.info."] ;



# Known address nodes

A0000 [label="61.141.32.197"] ;



# PC0000 guardproof.info. third.guardproof.info.

N0000->N0002 ;



# NA0000 third.guardproof.info. 61.141.32.197

N0002->A0000 ;



/*

# Known name->address edges

NA0000 third.guardproof.info. 61.141.32.197



# Known parent->child edges

PC0000 guardproof.info. third.guardproof.info.

 */

Lancez le programme 'dot' et vous avez votre premier diagramme réseau.

En plus des formes graphiques habituelles, le fichier de description inclut des
paires/données de format similaires, décrivant les services, les enregistrements
de zones (sous-graphe ?), des adresses sur liste noire et d'autres choses
pouvant être intéressante à inclure dans votre graphe. Cette information
supplémentaire pourrait être affichée comme différentes formes de noeuds,
couleurs, tailles de lignes, etc.

Le fichier de description peut aussi être lu et édité par un script Bash (bien
sûr). Vous devez être capable de trouver la plupart des fonctions requises à
l'intérieur du script "is_spammer.bash".

# Fin de Quickstart.



Note Supplémentaire
==== ==============

Michael Zick indique qu'il existe un "makeviz.bash" interactif sur
le site Web rediris.es. Impossible de donner le lien complet car
ce n'est pas un site accessible publiquement.

Un autre script anti-spam.

Exemple A.29. Chasse aux spammeurs

#!/bin/bash
# whx.sh: "whois" spammer lookup
# Author: Walter Dnes
# Slight revisions (first section) by ABS Guide author.
# Used in ABS Guide with permission.

# Needs version 3.x or greater of Bash to run (because of =~ operator).
# Commented by script author and ABS Guide author.



E_BADARGS=65        # Missing command-line arg.
E_NOHOST=66         # Host not found.
E_TIMEOUT=67        # Host lookup timed out.
E_UNDEF=68          # Some other (undefined) error.
HOSTWAIT=10         # Specify up to 10 seconds for host query reply.
                    # The actual wait may be a bit longer.
OUTFILE=whois.txt   # Output file.
PORT=4321


if [ -z "$1" ]      # Check for (required) command-line arg.
then
  echo "Usage: $0 domain name or IP address"
  exit $E_BADARGS
fi


if [[ "$1" =~ "[a-zA-Z][a-zA-Z]$" ]]  # Ends in two alpha chars?
then                                  # It's a domain name && must do host lookup.
  IPADDR=$(host -W $HOSTWAIT $1 | awk '{print $4}')
                                      # Doing host lookup to get IP address.
                                      # Extract final field.
else
  IPADDR="$1"                         # Command-line arg was IP address.
fi

echo; echo "IP Address is: "$IPADDR""; echo

if [ -e "$OUTFILE" ]
then
  rm -f "$OUTFILE"
  echo "Stale output file \"$OUTFILE\" removed."; echo
fi


#  Sanity checks.
#  (This section needs more work.)
#  ===============================
if [ -z "$IPADDR" ]
# No response.
then
  echo "Host not found!"
  exit $E_NOHOST    # Bail out.
fi

if [[ "$IPADDR" =~ "^[;;]" ]]
#  ;; connection timed out; no servers could be reached
then
  echo "Host lookup timed out!"
  exit $E_TIMEOUT   # Bail out.
fi

if [[ "$IPADDR" =~ "[(NXDOMAIN)]$" ]]
#  Host xxxxxxxxx.xxx not found: 3(NXDOMAIN)
then
  echo "Host not found!"
  exit $E_NOHOST    # Bail out.
fi

if [[ "$IPADDR" =~ "[(SERVFAIL)]$" ]]
#  Host xxxxxxxxx.xxx not found: 2(SERVFAIL)
then
  echo "Host not found!"
  exit $E_NOHOST    # Bail out.
fi




# ======================== Main body of script ========================

AFRINICquery() {
#  Define the function that queries AFRINIC. Echo a notification to the
#+ screen, and then run the actual query, redirecting output to $OUTFILE.

  echo "Searching for $IPADDR in whois.afrinic.net"
  whois -h whois.afrinic.net "$IPADDR" > $OUTFILE

#  Check for presence of reference to an rwhois.
#  Warn about non-functional rwhois.infosat.net server
#+ and attempt rwhois query.
  if grep -e "^remarks: .*rwhois\.[^ ]\+" "$OUTFILE"
  then
    echo " " >> $OUTFILE
    echo "***" >> $OUTFILE
    echo "***" >> $OUTFILE
    echo "Warning: rwhois.infosat.net was not working as of 2005/02/02" >> $OUTFILE
    echo "         when this script was written." >> $OUTFILE
    echo "***" >> $OUTFILE
    echo "***" >> $OUTFILE
    echo " " >> $OUTFILE
    RWHOIS=`grep "^remarks: .*rwhois\.[^ ]\+" "$OUTFILE" | tail -n 1 |\
    sed "s/\(^.*\)\(rwhois\..*\)\(:4.*\)/\2/"`
    whois -h ${RWHOIS}:${PORT} "$IPADDR" >> $OUTFILE
  fi
}

APNICquery() {
  echo "Searching for $IPADDR in whois.apnic.net"
  whois -h whois.apnic.net "$IPADDR" > $OUTFILE

#  Just  about  every  country has its own internet registrar.
#  I don't normally bother consulting them, because the regional registry
#+ usually supplies sufficient information.
#  There are a few exceptions, where the regional registry simply
#+ refers to the national registry for direct data.
#  These are Japan and South Korea in APNIC, and Brasil in LACNIC.
#  The following if statement checks $OUTFILE (whois.txt) for the presence
#+ of "KR" (South Korea) or "JP" (Japan) in the country field.
#  If either is found, the query is re-run against the appropriate
#+ national registry.

  if grep -E "^country:[ ]+KR$" "$OUTFILE"
  then
    echo "Searching for $IPADDR in whois.krnic.net"
    whois -h whois.krnic.net "$IPADDR" >> $OUTFILE
  elif grep -E "^country:[ ]+JP$" "$OUTFILE"
  then
    echo "Searching for $IPADDR in whois.nic.ad.jp"
    whois -h whois.nic.ad.jp "$IPADDR"/e >> $OUTFILE
  fi
}

ARINquery() {
  echo "Searching for $IPADDR in whois.arin.net"
  whois -h whois.arin.net "$IPADDR" > $OUTFILE

#  Several large internet providers listed by ARIN have their own
#+ internal whois service, referred to as "rwhois".
#  A large block of IP addresses is listed with the provider
#+ under the ARIN registry.
#  To get the IP addresses of 2nd-level ISPs or other large customers,
#+ one has to refer to the rwhois server on port 4321.
#  I originally started with a bunch of "if" statements checking for
#+ the larger providers.
#  This approach is unwieldy, and there's always another rwhois server
#+ that I didn't know about.
#  A more elegant approach is to check $OUTFILE for a reference
#+ to a whois server, parse that server name out of the comment section,
#+ and re-run the query against the appropriate rwhois server.
#  The parsing looks a bit ugly, with a long continued line inside
#+ backticks.
#  But it only has to be done once, and will work as new servers are added.
#@   ABS Guide author comment: it isn't all that ugly, and is, in fact,
#@+  an instructive use of Regular Expressions.

  if grep -E "^Comment: .*rwhois.[^ ]+" "$OUTFILE"
  then
    RWHOIS=`grep -e "^Comment:.*rwhois\.[^ ]\+" "$OUTFILE" | tail -n 1 |\
    sed "s/^\(.*\)\(rwhois\.[^ ]\+\)\(.*$\)/\2/"`
    echo "Searching for $IPADDR in ${RWHOIS}"
    whois -h ${RWHOIS}:${PORT} "$IPADDR" >> $OUTFILE
  fi
}

LACNICquery() {
  echo "Searching for $IPADDR in whois.lacnic.net"
  whois -h whois.lacnic.net "$IPADDR" > $OUTFILE

#  The  following if statement checks $OUTFILE (whois.txt) for the presence of
#+ "BR" (Brasil) in the country field.
#  If it is found, the query is re-run against whois.registro.br.

  if grep -E "^country:[ ]+BR$" "$OUTFILE"
  then
    echo "Searching for $IPADDR in whois.registro.br"
    whois -h whois.registro.br "$IPADDR" >> $OUTFILE
  fi
}

RIPEquery() {
  echo "Searching for $IPADDR in whois.ripe.net"
  whois -h whois.ripe.net "$IPADDR" > $OUTFILE
}

#  Initialize a few variables.
#  * slash8 is the most significant octet
#  * slash16 consists of the two most significant octets
#  * octet2 is the second most significant octet




slash8=`echo $IPADDR | cut -d. -f 1`
  if [ -z "$slash8" ]  # Yet another sanity check.
  then
    echo "Undefined error!"
    exit $E_UNDEF
  fi
slash16=`echo $IPADDR | cut -d. -f 1-2`
#                             ^ Period specified as 'cut" delimiter.
  if [ -z "$slash16" ]
  then
    echo "Undefined error!"
    exit $E_UNDEF
  fi
octet2=`echo $slash16 | cut -d. -f 2`
  if [ -z "$octet2" ]
  then
    echo "Undefined error!"
    exit $E_UNDEF
  fi


#  Check for various odds and ends of reserved space.
#  There is no point in querying for those addresses.

if [ $slash8 == 0 ]; then
  echo $IPADDR is '"This Network"' space\; Not querying
elif [ $slash8 == 10 ]; then
  echo $IPADDR is RFC1918 space\; Not querying
elif [ $slash8 == 14 ]; then
  echo $IPADDR is '"Public Data Network"' space\; Not querying
elif [ $slash8 == 127 ]; then
  echo $IPADDR is loopback space\; Not querying
elif [ $slash16 == 169.254 ]; then
  echo $IPADDR is link-local space\; Not querying
elif [ $slash8 == 172 ] && [ $octet2 -ge 16 ] && [ $octet2 -le 31 ];then
  echo $IPADDR is RFC1918 space\; Not querying
elif [ $slash16 == 192.168 ]; then
  echo $IPADDR is RFC1918 space\; Not querying
elif [ $slash8 -ge 224 ]; then
  echo $IPADDR is either Multicast or reserved space\; Not querying
elif [ $slash8 -ge 200 ] && [ $slash8 -le 201 ]; then LACNICquery "$IPADDR"
elif [ $slash8 -ge 202 ] && [ $slash8 -le 203 ]; then APNICquery "$IPADDR"
elif [ $slash8 -ge 210 ] && [ $slash8 -le 211 ]; then APNICquery "$IPADDR"
elif [ $slash8 -ge 218 ] && [ $slash8 -le 223 ]; then APNICquery "$IPADDR"

#  If we got this far without making a decision, query ARIN.
#  If a reference is found in $OUTFILE to APNIC, AFRINIC, LACNIC, or RIPE,
#+ query the appropriate whois server.

else
  ARINquery "$IPADDR"
  if grep "whois.afrinic.net" "$OUTFILE"; then
    AFRINICquery "$IPADDR"
  elif grep -E "^OrgID:[ ]+RIPE$" "$OUTFILE"; then
    RIPEquery "$IPADDR"
  elif grep -E "^OrgID:[ ]+APNIC$" "$OUTFILE"; then
    APNICquery "$IPADDR"
  elif grep -E "^OrgID:[ ]+LACNIC$" "$OUTFILE"; then
    LACNICquery "$IPADDR"
  fi
fi

#@  ---------------------------------------------------------------
#   Try also:
#   wget http://logi.cc/nw/whois.php3?ACTION=doQuery&DOMAIN=$IPADDR
#@  ---------------------------------------------------------------

#  We've  now  finished  the querying.
#  Echo a copy of the final result to the screen.

cat $OUTFILE
# Or "less $OUTFILE" . . .


exit 0

#@  ABS Guide author comments:
#@  Nothing fancy here, but still a very useful tool for hunting spammers.
#@  Sure, the script can be cleaned up some, and it's still a bit buggy,
#@+ (exercise for reader), but all the same, it's a nice piece of coding
#@+ by Walter Dnes.
#@  Thank you!

L'interface de « Little Monster » pour wget.

Exemple A.30. Rendre wget plus facile à utiliser

#!/bin/bash
# wgetter2.bash

# Auteur : Little Monster [monster@monstruum.co.uk]
# ==> Utilisé dans le guide ABS avec la permission de l'auteur du script.
# ==> Ce script a toujours besoin de débogage et de corrections (exercice
# ==> laissé au lecteur).
# ==> Il pourrait aussi bénéficier de meilleurs commentaires.


#  Ceci est wgetter2 --
#+ un script Bash rendant wget un peu plus facile à utiliser
#+ et évitant de la frappe clavier.

#  Écrit avec attention par Little Monster.
#  Plus ou moins complet le 02/02/2005.
#  Si vous pensez que ce script est améliorable,
#+ envoyez-moi un courrier électronique à : monster@monstruum.co.uk
# ==> et mettez en copie l'auteur du guide ABS.
#  Ce script est sous licence GPL.
#  Vous êtes libre de le copier, modifier, ré-utiliser,
#+ mais, s'il-vous-plait, ne dites pas que vous l'avez écrit.
#  À la place, indiquez vos changements ici.

# =======================================================================
# journal des modifications :

# 07/02/2005.  Corrections par Little Monster.
# 02/02/2005.  Petits ajouts de Little Monster.
#              (Voir après # +++++++++++ )
# 29/01/2005.  Quelques petites modifications de style et nettoyage de l'auteur
#              du guide ABS.
#              Ajout des codes d'erreur.
# 22/11/2004.  Fin de la version initiale de la seconde version de wgetter :
#              wgetter2 est né.
# 01/12/2004.  Modification de la fonction 'runn' de façon à ce qu'il
#              fonctionne de deux façons --
#              soit en demandant le nom d'un fichier soit en le récupérant sur
#              la ligne de commande.
# 01/12/2004.  Gestion sensible si aucune URL n'est fournie.
# 01/12/2004.  Boucle des options principales, de façon à ne pas avoir à
#              rappeller wgetter 2 tout le temps.
#              À la place, fonctionne comme une session.
# 01/12/2004.  Ajout d'une boucle dans la fonction 'runn'.
#              Simplifié et amélioré.
# 01/12/2004.  Ajout de state au paramètrage de récursion.
#              Active la ré-utilisation de la valeur précédente.
# 05/12/2004.  Modification de la routine de détection de fichiers dans la
#              fonction 'runn' de façon à ce qu'il ne soit pas gêné par des
#              valeurs vides et pour qu'il soit plus propre.
# 01/02/2004.  Ajout de la routine de récupération du cookie à partir de 
#              la dernière version (qui n'est pas encore prête), de façon à ne
#              pas avoir à codé en dur les chemins.
# =======================================================================

# Codes d'erreur pour une sortie anormale.
E_USAGE=67                   # Message d'usage, puis quitte.
E_SANS_OPTS=68               # Aucun argument en ligne de commande.
E_SANS_URLS=69               # Aucune URL passée au script.
E_SANS_FICHIERSAUVEGARDE=70  # Aucun nom de fichier de sortie passé au script.
E_SORTIE_UTILISATEUR=71      # L'utilisateur a décidé de quitter.


#  Commande wget par défaut que nous voulons utiliser.
#  C'est l'endroit où la changer, si nécessaire.
#  NB: si vous utilisez un proxy, indiquez http_proxy = yourproxy dans .wgetrc.
#  Sinon, supprimez --proxy=on, ci-dessous.
# ====================================================================
CommandeA="wget -nc -c -t 5 --progress=bar --random-wait --proxy=on -r"
# ====================================================================



# --------------------------------------------------------------------
# Initialisation de quelques autres variables avec leur explications.

pattern=" -A .jpg,.JPG,.jpeg,.JPEG,.gif,.GIF,.htm,.html,.shtml,.php"
                    #  Options de wget pour ne récupérer que certain types de
                    #+ fichiers. Mettre en commentaire si inutile
today=`date +%F`    # Utilisé pour un nom de fichier.
home=$HOME          # Utilise HOME pour configurer une variable interne.
                    #  Au cas où d'autres chemins sont utilisés, modifiez cette
                    #+ variable.
depthDefault=3      # Configure un niveau de récursion sensible.
Depth=$depthDefault # Sinon, le retour de l'utilisateur ne sera pas intégré.
RefA=""             # Configure la page blanche de référence.
Flag=""             #  Par défaut, ne sauvegarde rien,
                    #+ ou tout ce qui pourrait être voulu dans le futur.
lister=""           # Utilisé pour passer une liste d'url directement à wget.
Woptions=""         # Utilisé pour passer quelques options à wget.
inFile=""           # Utilisé pour la fonction run.
newFile=""          # Utilisé pour la fonction run.
savePath="$home/w-save"
Config="$home/.wgetter2rc"
                    #  Quelques variables peuvent être stockées, 
                    #+ si elles sont modifiées en permanence à l'intérieur de ce
                    #+ script.
Cookie_List="$home/.cookielist"
                    # Pour que nous sachions où sont conservés les cookies...
cFlag=""            # Une partie de la routine de sélection du cookie.

#  Définissez les options disponibles. Lettres faciles à modifier ici si
#+ nécessaire.
#  Ce sont les options optionnelles ; vous n'avez pas besoin d'attendre
#+ qu'elles vous soient demandées.

save=s   # Sauvegarde la commande au lieu de l'exécuter.
cook=c   # Modifie le cookie pour cette session.
help=h   # Guide d'usage.
list=l   # Passe à wget l'option -i et la liste d'URL.
runn=r   # Lance les commandes sauvegardées comme argument de l'option.
inpu=i   # Lance les commandes sauvegardées de façon interactive.
wopt=w   # Autorise la saisie d'options à passer directement à wget.
# --------------------------------------------------------------------


if [ -z "$1" ]; then   # Soyons sûr de donner quelque chose à manger à wget.
   echo "Vous devez entrer au moins une RLS ou une option!"
   echo "-$help pour l'utilisation."
   exit $E_SANS_OPTS
fi



# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
# ajout ajout ajout ajout ajout ajout ajout ajout ajout ajout ajout ajout

if [ ! -e "$Config" ]; then   #  Vérification de l'existence du fichier de
                              #+ configuration.
   echo "Création du fichier de configuration, $Config"
   echo "# Ceci est le fichier de configuration pour wgetter2" > "$Config"
   echo "# Vos paramètres personnalisés seront sauvegardés dans ce fichier" \
     >> "$Config"
else
   source $Config             #  Import des variables que nous avons initialisé
                              #+ en dehors de ce script.
fi

if [ ! -e "$Cookie_List" ]; then
   # Configure une liste de cookie, si elle n'existe pas.
   echo "Recherche des cookies..."
   find -name cookies.txt >> $Cookie_List   # Crée une liste des cookies.
fi #  Isole ceci dans sa propre instruction 'if',
   #+ au cas où nous serions interrompu durant la recherche.

if [ -z "$cFlag" ]; then # Si nous n'avons pas encore fait ceci...
   echo                  # Ajoute un espacement après l'invite de la commande.
   echo "Il semble que vous n'avez pas encore configuré votre source de cookies."
   n=0                   # S'assure que le compteur ne contient pas de valeurs.
   while read; do
      Cookies[$n]=$REPLY #  Place les cookies que nous avons trouvé dans un
                         #+ tableau.
      echo "$n) ${Cookies[$n]}"  # Crée un menu.
      n=$(( n + 1 ))     # Incrémente le comteur.
   done < $Cookie_List   # Remplit l'instruction read.
   echo "Saisissez le nombre de cookies que vous souhaitez utiliser."
   echo "Si vous ne voulez pas utiliser de cookie, faites simplement RETURN."
   echo
   echo "Je ne vous demanderais plus ceci. Éditez $Config"
   echo "si vous décidez de le changer ultérieurement"
   echo "ou utilisez l'option -${cook} pour des modifications sur une session."
   read
   if [ ! -z $REPLY ]; then   # L'utilisateur n'a pas seulement faire ENTER.
      Cookie=" --load-cookies ${Cookies[$REPLY]}"
      # Initialise la variable ici ainsi que dans le fichier de configuration.

      echo "Cookie=\" --load-cookies ${Cookies[$REPLY]}\"" >> $Config
   fi
   echo "cFlag=1" >> $Config  #  Pour que nous nous rappelions de ne pas le
                              #+ demander de nouveau.
fi

# fin section ajoutée fin section ajoutée fin section ajoutée 
# +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++



# Une autre variable.
# Celle-ci pourrait être ou pas sujet à variation.
# Un peu comme le petit affichage.
CookiesON=$Cookie
# echo "cookie file is $CookiesON" # Pour débogage.
# echo "home is ${home}"           # Pour débogage. Faites attention à celui-ci!


wopts()
{
echo "Entrer les options à fournir à wget."
echo "Il est supposé que vous savez ce que vous faites."
echo
echo "Vous pouvez passer leurs arguments ici aussi."
# C'est-à-dire que tout ce qui est saisi ici sera passé à wget.

read Wopts
# Lire les options à donner à wget.

Woptions=" $Wopts"
#         ^  Pourquoi cet espace initial ?
# Affecter à une autre variable.
# Pour le plaisir, ou pour tout autre chose...

echo "options ${Wopts} fournies à wget"
# Principalement pour du débogage.
# Est joli.

return
}


save_func()
{
echo "Les paramètres vont être sauvegardés."
if [ ! -d $savePath ]; then  #  Vérifie si le répertoire existe.
   mkdir $savePath           #  Crée le répertoire pour la sauvegarde
                             #+ si ce dernier n'existe pas.
fi

Flag=S
# Indique au dernier bout de code ce qu'il faut faire.
# Positionne un drapeau car le boulot est effectué dans la partie principale.

return
}


usage() # Indique comment cela fonctionne.
{
    echo "Bienvenue dans wgetter. C'est une interface pour wget."
    echo "Il lancera en permanence wget avec ces options :"
    echo "$CommandeA"
    echo "et le motif de correspondance: $motif (que vous pouvez changer en"
    echo "haut du script)."
    echo "Il vous demandera aussi une profondeur de récursion depth et si vous"
    echo "souhaitez utiliser une page de référence."
    echo "Wgetter accepte les options suivantes :"
    echo ""
    echo "-$help : Affiche cette aide."
    echo "-$save : Sauvegarde la commande dans un fichier"
    echo "$savePath/wget-($today) au lieu de l'exécuter."
    echo "-$runn : Exécute les commandes wget sauvegardées au lieu d'en"
    echo "commencer une nouvelle --"
    echo "Saisissez le nom du fichier comme argument de cette option."
    echo "-$inpu : Exécute les commandes wget sauvegardées, de façon"
    echo "interactive -- "
    echo "Le script vous demandera le nom du fichier."
    echo "-$cook : Modifie le fichier des cookies pour cette session."
    echo "-$list : Indique à wget d'utiliser les URL à partir d'une liste"
    echo "plutôt que sur la ligne de commande."
    echo "-$wopt : Passe toute autre option directement à wget."
    echo ""
    echo "Voir la page man de wget pour les options supplémentaires que vous"
    echo "pouvez lui passer."
    echo ""

    exit $E_USAGE  # Fin ici. Ne rien exécuter d'autre.
}



list_func() #  Donne à l'utilisateur l'option pour utiliser l'option -i de wget,
            #+ et une liste d'URL.
{
while [ 1 ]; do
   echo "Saisissez le nom du fichier contenant les URL (appuyez sur q si vous"
   echo "avez changé d'idée)."
   read urlfile
   if [ ! -e "$urlfile" ] && [ "$urlfile" != q ]; then
       # Recherche un fichier ou l'option de sortie.
       echo "Ce fichier n'existe pas!"
   elif [ "$urlfile" = q ]; then   # Vérifie l'option de sortie.
       echo "N'utilise pas de liste d'URL."
       return
   else
      echo "Utilisation de $urlfile."
      echo "Si vous m'avez fourni des URL sur la ligne de commandes,"
      echo "je les utiliserais en premier."
                    # Indique le comportement standard de wget à l'utilisateur.
      lister=" -i $urlfile" # C'est ce que nous voulons fournir à wget.
      return
   fi
done
}


cookie_func()  #  Donne à l'utilisateur l'option d'utiliser un fichier
               #+ cookie différent.
{
while [ 1 ]; do
   echo "Modification du fichier cookie. Appuyez sur return si vous ne voulez "
   echo "pas le changer."
   read Cookies
   # NB: Ceci n'est pas la même chose que Cookie, un peu plus tôt.
   # Il y a un 's' à la fin.
   if [ -z "$Cookies" ]; then                   # Clause d'échappement.
      return
   elif [ ! -e "$Cookies" ]; then
      echo "Le fichier n'existe pas. Essayez de nouveau." # On continue...
   else
       CookiesON=" --load-cookies $Cookies"  # Le fichier est bon -- utilisons-le!
       return
   fi
done
}


run_func()
{
if [ -z "$OPTARG" ]; then
# Teste pour voir si nous utilisons les options en ligne ou la requête.
   if [ ! -d "$savePath" ]; then  # Au cas où le répertoire n'existe pas...
      echo "$savePath ne semble pas exister."
      echo "Merci de fournir un chemin et un nom de fichiers pour les commandes"
      echo "wget sauvegardées :"
      read newFile
         until [ -f "$newFile" ]; do  #  Continue jusqu'à ce que nous obtenions
                                      #+ quelque chose.
            echo "Désolé, ce fichier n'existe pas. Essayez de nouveau."
            # Essaie réellement d'avoir quelque chose.
            read newFile
         done

# -------------------------------------------------------------------------
#         if [ -z ( grep wget ${newfile} ) ]; then
          # Suppose qu'ils n'ont pas encore le bon fichier.
#         echo "Désolé, ce fichier ne contient pas de commandes wget.
#         echo "Annulation."
#         exit
#         fi
#
# Ce code est bogué.
# Il ne fonctionne réellement pas.
# Si vous voulez le corriger, n'hésitez pas !
# -------------------------------------------------------------------------

      filePath="${newFile}"
   else
   echo "Le chemin de sauvegarde est $savePath"
      echo "Merci de saisir le nom du fichier que vous souhaitez utiliser."
      echo "Vous avez le choix entre :"
      ls $savePath                                # Leur donne un choix.
      read inFile
         until [ -f "$savePath/$inFile" ]; do     # Continuez jusqu'à obtention.
            if [ ! -f "${savePath}/${inFile}" ]; then
                                                  # Si le fichier n'existe pas.
               echo "Désolé, ce fichier n'existe pas."
               echo " Faites votre choix à partir de :"
               ls $savePath                           # Si une erreur est faite.
               read inFile
            fi
         done
      filePath="${savePath}/${inFile}"  # En faire une variable...
   fi
else filePath="${savePath}/${OPTARG}"   # qui peut être beaucoup de choses...
fi

if [ ! -f "$filePath" ]; then           # Si nous obtenons un fichier bogué.
   echo "Vous n'avez pas spécifié un fichier convenable."
   echo "Lancez tout d'abord ce script avec l'option -${save}."
   echo "Annulation."
   exit $E_SANS_FICHIERSAUVEGARDE
fi
echo "Utilisation de : $filePath"
while read; do
    eval $REPLY
    echo "Fin : $REPLY"
done < $filePath  # Remplit le fichier que nous utilisons avec une boucle while.

exit
}



# Récupération de toute option que nous utilisons pour ce script.
# Ceci est basé sur la démo de "Learning The Bash Shell" (O'Reilly).
while getopts ":$save$cook$help$list$runn:$inpu$wopt" opt
do
  case $opt in
     $save) save_func;;   #  Sauvegarde de quelques sessions wgetter pour plus
                          #  tard.
     $cook) cookie_func;; #  Modifie le fichier cookie.
     $help) usage;;       #  Obtient de l'aide.
     $list) list_func;;   #  Autorise wget à utiliser une liste d'URL.
     $runn) run_func;;    #  Utile si vous appelez wgetter à partir d'un script
                          #+ cron par exemple.
     $inpu) run_func;;    #  Lorsque vous ne connaissez pas le nom des fichiers.
     $wopt) wopts;;       #  Passe les options directement à wget.
        \?) echo "Option invalide."
            echo "Utilisez -${wopt} si vous voulez passer les options "
            echo "directement à to wget,"
            echo "ou -${help} pour de l'aide";;      # Récupère quelque chose.
  esac
done
shift $((OPTIND - 1))     # Opérations magiques avec $#.


if [ -z "$1" ] && [ -z "$lister" ]; then 
                          #  Nous devrions laisser au moins une URL sur la
                          #+ ligne de commande à moins qu'une liste ne soit
                          #+ utilisée - récupère les lignes de commandes vides.
   echo "Aucune URL fournie ! Vous devez les saisir sur la même ligne "
   echo "que wgetter2."
   echo "Par exemple,  wgetter2 http://somesite http://anothersite."
   echo "Utilisez l'option $help pour plus d'informations."
   exit $E_SANS_URLS        # Quitte avec le bon code d'erreur.
fi

URLS=" $@"
#  Utilise ceci pour que la liste d'URL puisse être modifié si nous restons dans
#+ la boucle d'option.

while [ 1 ]; do
   # C'est ici que nous demandons les options les plus utilisées.
   # (Pratiquement pas changées depuis la version 1 de wgetter)
   if [ -z $curDepth ]; then
      Current=""
   else Current=" La valeur courante est $curDepth"
   fi
       echo "A quelle profondeur dois-je aller ? "
       echo "(entier: valeur par défaut $depthDefault.$Current)"
       read Depth   # Récursion -- A quelle profondeur allons-nous ?
       inputB=""    # Réinitialise ceci à rien sur chaque passe de la boucle.
       echo "Saisissez le nom de la page de référence (par défaut, aucune)."
       read inputB  # Nécessaire pour certains sites.

       echo "Voulez-vous que la sortie soit tracée sur le terminal"
       echo "(o/n, par défaut, oui) ?"
       read noHide  # Sinon, wget le tracera simplement dans un fichier.

       case $noHide in
          # Maintenant, vous me voyez, maintenant, vous ne me voyez plus.
          o|O ) hide="";;
          n|N ) hide=" -b";;
            * ) hide="";;
       esac

       if [ -z ${Depth} ]; then       #  L'utilisateur a accepté la valeur par
                                      #+ défaut ou la valeur courante,
                                      #+ auquel cas Depth est maintenant vide.
          if [ -z ${curDepth} ]; then #  Vérifie si Depth a été configuré
                                      #+ sur une précédente itération.
             Depth="$depthDefault"    #  Configure la profondeur de récursion
                                      #+ par défaut si rien de défini
                                      #+ sinon, l'utilise.
          else Depth="$curDepth"      #  Sinon, utilisez celui configuré
                                      #+ précédemment.
          fi
       fi
   Recurse=" -l $Depth"               #  Initialise la profondeur.
   curDepth=$Depth                    #  Se rappeler de ce paramètrage la
                                      #+ prochaine fois.

       if [ ! -z $inputB ]; then
          RefA=" --referer=$inputB"   #  Option à utiliser pour la page de
                                      #+ référence.
       fi

  
WGETTER="${CommandeA}${motif}${hide}${RefA}${Recurse}${CookiesON}${lister}${Woptions}${URLS}"
   #  Crée une chaîne contenant le lot complet...
   #  NB: pas d'espace imbriqués.
   #  Ils sont dans les éléments individuels si aucun n'est vide,
   #+ nous n'obtenons pas d'espace supplémentaire.

   if [ -z "${CookiesON}" ] && [ "$cFlag" = "1" ] ; then
       echo "Attention -- impossible de trouver le fichier cookie."
       #  Ceci pourrait changer, au cas où l'utilisateur aurait choisi de ne 
       #+ pas utiliser les cookies.
   fi

   if [ "$Flag" = "S" ]; then
      echo "$WGETTER" >> $savePath/wget-${today}
      #  Crée un nom de fichier unique pour aujourd'hui
      #+ ou y ajoute les informations s'il existe déjà.
      echo "$inputB" >> $savePath/site-list-${today}
      #  Crée une liste pour qu'il soit plus simple de s'y référer plus tard,
      #+ car la commande complète est un peu confuse.
      echo "Commande sauvegardée dans le fichier $savePath/wget-${today}"
           # Indication pour l'utilisateur.
      echo "URL de la page de référence sauvegardé dans le fichier "
      echo "$savePath/site-list-${today}"
           # Indication pour l'utilisateur.
      Saver=" avec les options sauvegardées"
      #  Sauvegarde ceci quelque part, de façon à ce qu'il apparaisse dans la
      #+ boucle si nécessaire.
   else
       echo "**********************"
       echo "*****Récupération*****"
       echo "**********************"
       echo ""
       echo "$WGETTER"
       echo ""
       echo "**********************"
       eval "$WGETTER"
   fi

       echo ""
       echo "Continue avec$Saver."
       echo "Si vous voulez stopper, appuyez sur q."
       echo "Sinon, saisissez des URL :"
       # Laissons-les continuer. Indication sur les options sauvegardées.

       read
       case $REPLY in        # Nécessaire de changer ceci par une clause 'trap'.
          q|Q ) exit $E_SORTIE_UTILISATEUR;;  # Exercice pour le lecteur ?
            * ) URLS=" $REPLY";;
       esac

       echo ""
done


exit 0

Exemple A.31. Un script de podcasting

#!/bin/bash

#  bashpodder.sh:
#  By Linc 10/1/2004
#  Find the latest script at
#+ http://linc.homeunix.org:8080/scripts/bashpodder
#  Last revision 12/14/2004 - Many Contributors!
#  If you use this and have made improvements or have comments
#+ drop me an email at linc dot fessenden at gmail dot com
#  I'd appreciate it!

# ==>  ABS Guide extra comments.

# ==>  Author of this script has kindly granted permission
# ==>+ for inclusion in ABS Guide.


# ==> ################################################################
# 
# ==> What is "podcasting"?

# ==> It's broadcasting "radio shows" over the Internet.
# ==> These shows can be played on iPods and other music file players.

# ==> This script makes it possible.
# ==> See documentation at the script author's site, above.

# ==> ################################################################


# Make script crontab friendly:
cd $(dirname $0)
# ==> Change to directory where this script lives.

# datadir is the directory you want podcasts saved to:
datadir=$(date +%Y-%m-%d)
# ==> Will create a date-labeled directory, named: YYYY-MM-DD

# Check for and create datadir if necessary:
if test ! -d $datadir
        then
        mkdir $datadir
fi

# Delete any temp file:
rm -f temp.log

#  Read the bp.conf file and wget any url not already
#+ in the podcast.log file:
while read podcast
  do # ==> Main action follows.
  file=$(wget -q $podcast -O - | tr '\r' '\n' | tr \' \" | \
sed -n 's/.*url="\([^"]*\)".*/\1/p')
  for url in $file
                do
                echo $url >> temp.log
                if ! grep "$url" podcast.log > /dev/null
                        then
                        wget -q -P $datadir "$url"
                fi
                done
    done < bp.conf

# Move dynamically created log file to permanent log file:
cat podcast.log >> temp.log
sort temp.log | uniq > podcast.log
rm temp.log
# Create an m3u playlist:
ls $datadir | grep -v m3u > $datadir/podcast.m3u


exit 0

#################################################
For a different scripting approach to Podcasting,
see Phil Salkie's article, 
"Internet Radio to Podcast with Shell Tools"
in the September, 2005 issue of LINUX JOURNAL,
http://www.linuxjournal.com/article/8171
#################################################

Exemple A.32. Sauvegarde de nuit pour un disque firewire

#!/bin/bash
# nightly-backup.sh
# http://www.richardneill.org/source.php#nightly-backup-rsync
# Copyright (c) 2005 Richard Neill <backup@richardneill.org>.
# This is Free Software licensed under the GNU GPL.
# ==> Included in ABS Guide with script author's kind permission.
# ==> (Thanks!)

#  This does a backup from the host computer to a locally connected
#+ firewire HDD using rsync and ssh.
#  It then rotates the backups.
#  Run it via cron every night at 5am.
#  This only backs up the home directory.
#  If ownerships (other than the user's) should be preserved,
#+ then run the rsync process as root (and re-instate the -o).
#  We save every day for 7 days, then every week for 4 weeks,
#+ then every month for 3 months.

#  See: http://www.mikerubel.org/computers/rsync_snapshots/
#+ for more explanation of the theory.
#  Save as: $HOME/bin/nightly-backup_firewire-hdd.sh

#  Known bugs:
#  ----------
#  i)  Ideally, we want to exclude ~/.tmp and the browser caches.

#  ii) If the user is sitting at the computer at 5am,
#+     and files are modified while the rsync is occurring,
#+     then the BACKUP_JUSTINCASE branch gets triggered.
#      To some extent, this is a 
#+     feature, but it also causes a "disk-space leak".





##### BEGIN CONFIGURATION SECTION ############################################
LOCAL_USER=rjn                # User whose home directory should be backed up.
MOUNT_POINT=/backup           # Mountpoint of backup drive.
                              # NO trailing slash!
                              # This must be unique (eg using a udev symlink)
SOURCE_DIR=/home/$LOCAL_USER  # NO trailing slash - it DOES matter to rsync.
BACKUP_DEST_DIR=$MOUNT_POINT/backup/`hostname -s`.${LOCAL_USER}.nightly_backup
DRY_RUN=false                 #If true, invoke rsync with -n, to do a dry run.
                              # Comment out or set to false for normal use.
VERBOSE=false                 # If true, make rsync verbose.
                              # Comment out or set to false otherwise.
COMPRESS=false                # If true, compress.
                              # Good for internet, bad on LAN.
                              # Comment out or set to false otherwise.

### Exit Codes ###
E_VARS_NOT_SET=64
E_COMMANDLINE=65
E_MOUNT_FAIL=70
E_NOSOURCEDIR=71
E_UNMOUNTED=72
E_BACKUP=73
##### END CONFIGURATION SECTION ##############################################


# Check that all the important variables have been set:
if [ -z "$LOCAL_USER" ] ||
   [ -z "$SOURCE_DIR" ] ||
   [ -z "$MOUNT_POINT" ]  ||
   [ -z "$BACKUP_DEST_DIR" ]
then
   echo 'One of the variables is not set! Edit the file: $0. BACKUP FAILED.'
   exit $E_VARS_NOT_SET
fi

if [ "$#" != 0 ]  # If command-line param(s) . . .
then              # Here document(ation).
  cat <<-ENDOFTEXT
    Automatic Nightly backup run from cron.
    Read the source for more details: $0
    The backup directory is $BACKUP_DEST_DIR .
    It will be created if necessary; initialisation is no longer required.

    WARNING: Contents of $BACKUP_DEST_DIR are rotated.
    Directories named 'backup.\$i' will eventually be DELETED.
    We keep backups from every day for 7 days (1-8),
    then every week for 4 weeks (9-12),
    then every month for 3 months (13-15).

    You may wish to add this to your crontab using 'crontab -e'
    #  Back up files: $SOURCE_DIR to $BACKUP_DEST_DIR
    #+ every night at 3:15 am
         15 03 * * * /home/$LOCAL_USER/bin/nightly-backup_firewire-hdd.sh

    Don't forget to verify the backups are working,
    especially if you don't read cron's mail!"
        ENDOFTEXT
   exit $E_COMMANDLINE
fi


# Parse the options.
# ==================

if [ "$DRY_RUN" == "true" ]; then
  DRY_RUN="-n"
  echo "WARNING:"
  echo "THIS IS A 'DRY RUN'!"
  echo "No data will actually be transferred!"
else
  DRY_RUN=""
fi

if [ "$VERBOSE" == "true" ]; then
  VERBOSE="-v"
else
  VERBOSE=""
fi

if [ "$COMPRESS" == "true" ]; then
  COMPRESS="-z"
else
  COMPRESS=""
fi


#  Every week (actually of 8 days) and every month,
#+ extra backups are preserved.
DAY_OF_MONTH=`date +%d`            # Day of month (01..31).
if [ $DAY_OF_MONTH = 01 ]; then    # First of month.
  MONTHSTART=true
elif [ $DAY_OF_MONTH = 08 \
    -o $DAY_OF_MONTH = 16 \
    -o $DAY_OF_MONTH = 24 ]; then
    # Day 8,16,24  (use 8, not 7 to better handle 31-day months)
      WEEKSTART=true
fi



#  Check that the HDD is mounted.
#  At least, check that *something* is mounted here!
#  We can use something unique to the device, rather than just guessing
#+ the scsi-id by having an appropriate udev rule in
#+ /etc/udev/rules.d/10-rules.local
#+ and by putting a relevant entry in /etc/fstab.
#  Eg: this udev rule:
# BUS="scsi", KERNEL="sd*", SYSFS{vendor}="WDC WD16",
# SYSFS{model}="00JB-00GVA0     ", NAME="%k", SYMLINK="lacie_1394d%n"

if mount | grep $MOUNT_POINT >/dev/null; then
  echo "Mount point $MOUNT_POINT is indeed mounted. OK"
else
  echo -n "Attempting to mount $MOUNT_POINT..." 
           # If it isn't mounted, try to mount it.
  sudo mount $MOUNT_POINT 2>/dev/null

  if mount | grep $MOUNT_POINT >/dev/null; then
    UNMOUNT_LATER=TRUE
    echo "OK"
    #  Note: Ensure that this is also unmounted
    #+ if we exit prematurely with failure.
  else
    echo "FAILED"
    echo -e "Nothing is mounted at $MOUNT_POINT. BACKUP FAILED!"
    exit $E_MOUNT_FAIL
  fi
fi


# Check that source dir exists and is readable.
if [ ! -r  $SOURCE_DIR ] ; then
  echo "$SOURCE_DIR does not exist, or cannot be read. BACKUP FAILED."
  exit $E_NOSOURCEDIR
fi


# Check that the backup directory structure is as it should be.
# If not, create it.
# Create the subdirectories.
# Note that backup.0 will be created as needed by rsync.

for ((i=1;i<=15;i++)); do
  if [ ! -d $BACKUP_DEST_DIR/backup.$i ]; then
    if /bin/mkdir -p $BACKUP_DEST_DIR/backup.$i ; then
    #  ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  No [ ] test brackets. Why?
      echo "Warning: directory $BACKUP_DEST_DIR/backup.$i is missing,"
      echo "or was not initialised. (Re-)creating it."
    else
      echo "ERROR: directory $BACKUP_DEST_DIR/backup.$i"
      echo "is missing and could not be created."
    if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
        # Before we exit, unmount the mount point if necessary.
        cd
        sudo umount $MOUNT_POINT &&
        echo "Unmounted $MOUNT_POINT again. Giving up."
    fi
      exit $E_UNMOUNTED
  fi
fi
done


#  Set the permission to 700 for security
#+ on an otherwise permissive multi-user system.
if ! /bin/chmod 700 $BACKUP_DEST_DIR ; then
  echo "ERROR: Could not set permissions on $BACKUP_DEST_DIR to 700."

  if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
  # Before we exit, unmount the mount point if necessary.
     cd ; sudo umount $MOUNT_POINT \
     && echo "Unmounted $MOUNT_POINT again. Giving up."
  fi

  exit $E_UNMOUNTED
fi

# Create the symlink: current -> backup.1 if required.
# A failure here is not critical.
cd $BACKUP_DEST_DIR
if [ ! -h current ] ; then
  if ! /bin/ln -s backup.1 current ; then
    echo "WARNING: could not create symlink current -> backup.1"
  fi
fi


# Now, do the rsync.
echo "Now doing backup with rsync..."
echo "Source dir: $SOURCE_DIR"
echo -e "Backup destination dir: $BACKUP_DEST_DIR\n"


/usr/bin/rsync $DRY_RUN $VERBOSE -a -S --delete --modify-window=60 \
--link-dest=../backup.1 $SOURCE_DIR $BACKUP_DEST_DIR/backup.0/

#  Only warn, rather than exit if the rsync failed,
#+ since it may only be a minor problem.
#  E.g., if one file is not readable, rsync will fail.
#  This shouldn't prevent the rotation.
#  Not using, e.g., `date +%a`  since these directories
#+ are just full of links and don't consume *that much* space.

if [ $? != 0 ]; then
  BACKUP_JUSTINCASE=backup.`date +%F_%T`.justincase
  echo "WARNING: the rsync process did not entirely succeed."
  echo "Something might be wrong."
  echo "Saving an extra copy at: $BACKUP_JUSTINCASE"
  echo "WARNING: if this occurs regularly, a LOT of space will be consumed,"
  echo "even though these are just hard-links!"
fi

# Save a readme in the backup parent directory.
# Save another one in the recent subdirectory.
echo "Backup of $SOURCE_DIR on `hostname` was last run on \
`date`" > $BACKUP_DEST_DIR/README.txt
echo "This backup of $SOURCE_DIR on `hostname` was created on \
`date`" > $BACKUP_DEST_DIR/backup.0/README.txt

# If we are not in a dry run, rotate the backups.
[ -z "$DRY_RUN" ] &&

  #  Check how full the backup disk is.
  #  Warn if 90%. if 98% or more, we'll probably fail, so give up.
  #  (Note: df can output to more than one line.)
  #  We test this here, rather than before
  #+ so that rsync may possibly have a chance.
  DISK_FULL_PERCENT=`/bin/df $BACKUP_DEST_DIR |
  tr "\n" ' ' | awk '{print $12}' | grep -oE [0-9]+ `
  echo "Disk space check on backup partition \
  $MOUNT_POINT $DISK_FULL_PERCENT% full."
  if [ $DISK_FULL_PERCENT -gt 90 ]; then
    echo "Warning: Disk is greater than 90% full."
  fi
  if [ $DISK_FULL_PERCENT -gt 98 ]; then
    echo "Error: Disk is full! Giving up."
      if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
        # Before we exit, unmount the mount point if necessary.
        cd; sudo umount $MOUNT_POINT &&
        echo "Unmounted $MOUNT_POINT again. Giving up."
      fi
    exit $E_UNMOUNTED
  fi


 # Create an extra backup.
 # If this copy fails, give up.
 if [ -n "$BACKUP_JUSTINCASE" ]; then
   if ! /bin/cp -al $BACKUP_DEST_DIR/backup.0 \
      $BACKUP_DEST_DIR/$BACKUP_JUSTINCASE
   then
     echo "ERROR: Failed to create extra copy \
     $BACKUP_DEST_DIR/$BACKUP_JUSTINCASE"
     if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
       # Before we exit, unmount the mount point if necessary.
       cd ;sudo umount $MOUNT_POINT &&
       echo "Unmounted $MOUNT_POINT again. Giving up."
     fi
     exit $E_UNMOUNTED
   fi
 fi


 # At start of month, rotate the oldest 8.
 if [ "$MONTHSTART" == "true" ]; then
   echo -e "\nStart of month. \
   Removing oldest backup: $BACKUP_DEST_DIR/backup.15"  &&
   /bin/rm -rf  $BACKUP_DEST_DIR/backup.15  &&
   echo "Rotating monthly,weekly backups: \
   $BACKUP_DEST_DIR/backup.[8-14] -> $BACKUP_DEST_DIR/backup.[9-15]"  &&
     /bin/mv $BACKUP_DEST_DIR/backup.14 $BACKUP_DEST_DIR/backup.15  &&
     /bin/mv $BACKUP_DEST_DIR/backup.13 $BACKUP_DEST_DIR/backup.14  &&
     /bin/mv $BACKUP_DEST_DIR/backup.12 $BACKUP_DEST_DIR/backup.13  &&
     /bin/mv $BACKUP_DEST_DIR/backup.11 $BACKUP_DEST_DIR/backup.12  &&
     /bin/mv $BACKUP_DEST_DIR/backup.10 $BACKUP_DEST_DIR/backup.11  &&
     /bin/mv $BACKUP_DEST_DIR/backup.9 $BACKUP_DEST_DIR/backup.10  &&
     /bin/mv $BACKUP_DEST_DIR/backup.8 $BACKUP_DEST_DIR/backup.9

 # At start of week, rotate the second-oldest 4.
 elif [ "$WEEKSTART" == "true" ]; then
   echo -e "\nStart of week. \
   Removing oldest weekly backup: $BACKUP_DEST_DIR/backup.12"  &&
   /bin/rm -rf  $BACKUP_DEST_DIR/backup.12  &&

   echo "Rotating weekly backups: \
   $BACKUP_DEST_DIR/backup.[8-11] -> $BACKUP_DEST_DIR/backup.[9-12]"  &&
     /bin/mv $BACKUP_DEST_DIR/backup.11 $BACKUP_DEST_DIR/backup.12  &&
     /bin/mv $BACKUP_DEST_DIR/backup.10 $BACKUP_DEST_DIR/backup.11  &&
     /bin/mv $BACKUP_DEST_DIR/backup.9 $BACKUP_DEST_DIR/backup.10  &&
     /bin/mv $BACKUP_DEST_DIR/backup.8 $BACKUP_DEST_DIR/backup.9

 else
   echo -e "\nRemoving oldest daily backup: $BACKUP_DEST_DIR/backup.8"  &&
     /bin/rm -rf  $BACKUP_DEST_DIR/backup.8

 fi  &&

 # Every day, rotate the newest 8.
 echo "Rotating daily backups: \
 $BACKUP_DEST_DIR/backup.[1-7] -> $BACKUP_DEST_DIR/backup.[2-8]"  &&
     /bin/mv $BACKUP_DEST_DIR/backup.7 $BACKUP_DEST_DIR/backup.8  &&
     /bin/mv $BACKUP_DEST_DIR/backup.6 $BACKUP_DEST_DIR/backup.7  &&
     /bin/mv $BACKUP_DEST_DIR/backup.5 $BACKUP_DEST_DIR/backup.6  &&
     /bin/mv $BACKUP_DEST_DIR/backup.4 $BACKUP_DEST_DIR/backup.5  &&
     /bin/mv $BACKUP_DEST_DIR/backup.3 $BACKUP_DEST_DIR/backup.4  &&
     /bin/mv $BACKUP_DEST_DIR/backup.2 $BACKUP_DEST_DIR/backup.3  &&
     /bin/mv $BACKUP_DEST_DIR/backup.1 $BACKUP_DEST_DIR/backup.2  &&
     /bin/mv $BACKUP_DEST_DIR/backup.0 $BACKUP_DEST_DIR/backup.1  &&

 SUCCESS=true


if  [ "$UNMOUNT_LATER" == "TRUE" ]; then
  # Unmount the mount point if it wasn't mounted to begin with.
  cd ; sudo umount $MOUNT_POINT && echo "Unmounted $MOUNT_POINT again."
fi


if [ "$SUCCESS" == "true" ]; then
  echo 'SUCCESS!'
  exit 0
fi

# Should have already exited if backup worked.
echo 'BACKUP FAILED! Is this just a dry run? Is the disk full?) '
exit $E_BACKUP

Exemple A.33. Une commande cd étendue

############################################################################
#
#       cdll
#       par Phil Braham
#
#       ###########################################################
#       La dernière version de ce script est disponible à partir de
#       http://freshmeat.net/projects/cd/
#       ###########################################################
#
#       .cd_new
#
#       Une amélioration de la commande Unix cd
#
#       Il y a une pile illimitée d'entrées et d'entrées spéciales. Les
#       entrées de la pile conservent les cd_maxhistory derniers répertoires
#       qui ont été utilisés. Les entrées spéciales peuvent être affectées aux
#       répertoires fréquemment utilisés.
#
#       Les entrées spéciales pourraient être préaffectées en configurant les
#       variables d'environnement CDSn ou en utilisant la commande -u ou -U.
#
#       Ce qui suit est une suggestion pour le fichier .profile :
#
#               . cdll              #  Configure la commande cd
#       alias cd='cd_new'           #  Remplace la commande cd
#               cd -U               #  Charge les entrées pré-affectées pour
#                                   #+ la pile et les entrées spéciales
#               cd -D               #  Configure le mode pas par défaut
#               alias @="cd_new @"  #  Autorise l'utilisation de @ pour récupérer
#                                   #+ l'historique
#
#       Pour une aide, saisissez :
#
#               cd -h ou
#               cd -H
#
#
############################################################################
#
#       Version 1.2.1
#
#       Écrit par Phil Braham - Realtime Software Pty Ltd
#       (realtime@mpx.com.au)
#       Merci d'envoyer vos suggestions ou améliorations à l'auteur
#       (phil@braham.net)
#
############################################################################

cd_hm ()
{
        ${PRINTF} "%s" "cd [dir] [0-9] [@[s|h] [-g [<dir>]] [-d] [-D] [-r<n>] [dir|0-9] [-R<n>] [<dir>|0-9]
   [-s<n>] [-S<n>] [-u] [-U] [-f] [-F] [-h] [-H] [-v]
    <dir> Se place sous le répertoire
    0-n         Se place sous le répertoire précedent (0 est le précédent, 1 est l'avant-dernier, etc)
                n va jusqu'au bout de l'historique (par défaut, 50)
    @           Liste les entrées de l'historique et les entrées spéciales
    @h          Liste les entrées de l'historique
    @s          Liste les entrées spéciales
    -g [<dir>]  Se place sous le nom littéral (sans prendre en compte les noms spéciaux)
                Ceci permet l'accès aux répertoires nommés '0','1','-h' etc
    -d          Modifie l'action par défaut - verbeux. (Voir note)
    -D          Modifie l'action par défaut - silencieux. (Voir note)
    -s<n>       Se place sous l'entrée spéciale <n>*
    -S<n>       Se place sous l'entrée spéciale <n> et la remplace avec le répertoire en cours*
    -r<n> [<dir>] Se place sous le répertoire <dir> and then put it on special entry <n>*
    -R<n> [<dir>] Se place sous le répertoire <dir> et place le répertoire en cours dans une entrée spéciale <n>*
    -a<n>       Autre répertoire suggéré. Voir la note ci-dessous.
    -f [<file>] Fichier des entrées <file>.
    -u [<file>] Met à jour les entrées à partir de <file>.
                Si aucun nom de fichier n'est fourni, utilise le fichier par défaut (${CDPath}${2:-"$CDFile"})
                -F et -U sont les versions silencieuses
    -v          Affiche le numéro de version
    -h          Aide
    -H          Aide détaillée

    *Les entrées spéciales (0 - 9) sont conservées jusqu'à la déconnexion, remplacées par une autre entrée
    ou mises à jour avec la commande -u

    Autres répertoires suggérés :
    Si un répertoire est introuvable, alors CD suggèrera des possibilités. Ce sont les répertoires
    commençant avec les mêmes lettres et si des résultats sont disponibles, ils sont affichés avec
    le préfixe -a<n> où <n> est un numéro.
    Il est possible de se placer dans le répertoire en saisissant cd -a<n> sur la ligne de commande.

    Le répertoire pour -r<n> ou -R<n> pourrait être un numéro. Par exemple :
        $ cd -r3 4  Se place dans le répertoire de l'entrée 4 de l'historique et la place
                    sur l'entrée spéciale 3
        $ cd -R3 4  Place le répertoire en cours sur l'entrée spéciale 3 et se déplace dans l'entrée 4
                    de l'historique
        $ cd -s3    Se déplace dans l'entrée spéciale 3

    Notez que les commandes R,r,S et s pourraient être utilisées sans numéro et faire ainsi référence à 0:
        $ cd -s     Se déplace dans l'entrée spéciale 0
        $ cd -S     Se déplace dans l'entrée spéciale 0 et fait de l'entrée spéciale 0 le répertoire courant
        $ cd -r 1   Se déplace dans l'entrée spéciale 1 et la place sur l'entrée spéciale 0
        $ cd -r     Se déplace dans l'entrée spéciale 0 et la place sur l'entrée spéciale 0
    "
        if ${TEST} "$CD_MODE" = "PREV"
        then
                ${PRINTF} "$cd_mnset"
        else
                ${PRINTF} "$cd_mset"
        fi
}

cd_Hm ()
{
        cd_hm
        ${PRINTF} "%s" "
        Les répertoires précédents (0-$cd_maxhistory) sont stockés dans les variables
        d'environnement CD[0] - CD[$cd_maxhistory]
        De façon similaire, les répertoires spéciaux S0 - $cd_maxspecial sont dans la
        variable d'environnement CDS[0] - CDS[$cd_maxspecial]
        et pourraient être accédés à partir de la ligne de commande

        Le chemin par défaut pour les commandes -f et -u est $CDPath
        Le fichier par défaut pour les commandes est -f et -u est $CDFile

        Configurez les variables d'environnement suivantes :
            CDL_PROMPTLEN  - Configuré à la longueur de l'invite que vous demandez.
                La chaîne de l'invite est configurée suivant les caractères de droite du
                répertoire en cours.
                Si non configuré, l'invite n'est pas modifiée.
            CDL_PROMPT_PRE - Configuré avec la chaîne pour préfixer l'invite.
                La valeur par défaut est:
                    standard:  \"\\[\\e[01;34m\\]\"  (couleur bleu).
                    root:      \"\\[\\e[01;31m\\]\"  (couleur rouge).
            CDL_PROMPT_POST    - Configuré avec la chaîne pour suffixer l'invite.
                La valeur par défaut est:
                    standard:  \"\\[\\e[00m\\]$\"   (réinitialise la couleur et affiche $).
                    root:      \"\\[\\e[00m\\]#\"   (réinitialise la couleur et affiche #).
            CDPath - Configure le chemin par défaut des options -f & -u.
                     Par défaut, le répertoire personnel de l'utilisateur
            CDFile - Configure le fichier par défaut pour les options -f & -u.
                     Par défaut, cdfile
        
"
    cd_version

}

cd_version ()
{
    printf "Version: ${VERSION_MAJOR}.${VERSION_MINOR} Date: ${VERSION_DATE}\n"
}

#
# Tronque à droite.
#
# params:
#   p1 - chaîne
#   p2 - longueur à tronquer
#
# renvoit la chaîne dans tcd
#
cd_right_trunc ()
{
    local tlen=${2}
    local plen=${#1}
    local str="${1}"
    local diff
    local filler="<--"
    if ${TEST} ${plen} -le ${tlen}
    then
        tcd="${str}"
    else
        let diff=${plen}-${tlen}
        elen=3
        if ${TEST} ${diff} -le 2
        then
            let elen=${diff}
        fi
        tlen=-${tlen}
        let tlen=${tlen}+${elen}
        tcd=${filler:0:elen}${str:tlen}
    fi
}

#
# Trois versions de l'historique do :
#    cd_dohistory  - empile l'historique et les spéciaux côte à côte
#    cd_dohistoryH - Affiche seulement l'historique
#    cd_dohistoryS - Affiche seulement les spéciaux
#
cd_dohistory ()
{
    cd_getrc
        ${PRINTF} "Historique :\n"
    local -i count=${cd_histcount}
    while ${TEST} ${count} -ge 0
    do
        cd_right_trunc "${CD[count]}" ${cd_lchar}
            ${PRINTF} "%2d %-${cd_lchar}.${cd_lchar}s " ${count} "${tcd}"

        cd_right_trunc "${CDS[count]}" ${cd_rchar}
            ${PRINTF} "S%d %-${cd_rchar}.${cd_rchar}s\n" ${count} "${tcd}"
        count=${count}-1
    done
}

cd_dohistoryH ()
{
    cd_getrc
        ${PRINTF} "Historique :\n"
        local -i count=${cd_maxhistory}
        while ${TEST} ${count} -ge 0
        do
                ${PRINTF} "${count} %-${cd_flchar}.${cd_flchar}s\n" ${CD[$count]}
                count=${count}-1
        done
}

cd_dohistoryS ()
{
    cd_getrc
        ${PRINTF} "Spéciaux :\n"
        local -i count=${cd_maxspecial}
        while ${TEST} ${count} -ge 0
        do
                ${PRINTF} "S${count} %-${cd_flchar}.${cd_flchar}s\n" ${CDS[$count]}
                count=${count}-1
        done
}

cd_getrc ()
{
    cd_flchar=$(stty -a | awk -F \; '/rows/ { print $2 $3 }' | awk -F \  '{ print $4 }')
    if ${TEST} ${cd_flchar} -ne 0
    then
        cd_lchar=${cd_flchar}/2-5
        cd_rchar=${cd_flchar}/2-5
            cd_flchar=${cd_flchar}-5
    else
            cd_flchar=${FLCHAR:=75}  # cd_flchar is used for for the @s & @h history
            cd_lchar=${LCHAR:=35}
            cd_rchar=${RCHAR:=35}
    fi
}

cd_doselection ()
{
        local -i nm=0
        cd_doflag="TRUE"
        if ${TEST} "${CD_MODE}" = "PREV"
        then
                if ${TEST} -z "$cd_npwd"
                then
                        cd_npwd=0
                fi
        fi
        tm=$(echo "${cd_npwd}" | cut -b 1)
    if ${TEST} "${tm}" = "-"
    then
        pm=$(echo "${cd_npwd}" | cut -b 2)
        nm=$(echo "${cd_npwd}" | cut -d $pm -f2)
        case "${pm}" in
                a) cd_npwd=${cd_sugg[$nm]} ;;
                s) cd_npwd="${CDS[$nm]}" ;;
                S) cd_npwd="${CDS[$nm]}" ; CDS[$nm]=`pwd` ;;
                r) cd_npwd="$2" ; cd_specDir=$nm ; cd_doselection "$1" "$2";;
                R) cd_npwd="$2" ; CDS[$nm]=`pwd` ; cd_doselection "$1" "$2";;
        esac
    fi

        if ${TEST} "${cd_npwd}" != "." -a "${cd_npwd}" != ".." -a "${cd_npwd}" -le ${cd_maxhistory} >>/dev/null 2>&1
        then
                cd_npwd=${CD[$cd_npwd]}
        else
                case "$cd_npwd" in
                         @)  cd_dohistory ; cd_doflag="FALSE" ;;
                        @h) cd_dohistoryH ; cd_doflag="FALSE" ;;
                        @s) cd_dohistoryS ; cd_doflag="FALSE" ;;
                        -h) cd_hm ; cd_doflag="FALSE" ;;
                        -H) cd_Hm ; cd_doflag="FALSE" ;;
                        -f) cd_fsave "SHOW" $2 ; cd_doflag="FALSE" ;;
                        -u) cd_upload "SHOW" $2 ; cd_doflag="FALSE" ;;
                        -F) cd_fsave "NOSHOW" $2 ; cd_doflag="FALSE" ;;
                        -U) cd_upload "NOSHOW" $2 ; cd_doflag="FALSE" ;;
                        -g) cd_npwd="$2" ;;
                        -d) cd_chdefm 1; cd_doflag="FALSE" ;;
                        -D) cd_chdefm 0; cd_doflag="FALSE" ;;
                        -r) cd_npwd="$2" ; cd_specDir=0 ; cd_doselection "$1" "$2";;
                        -R) cd_npwd="$2" ; CDS[0]=`pwd` ; cd_doselection "$1" "$2";;
                        -s) cd_npwd="${CDS[0]}" ;;
                        -S) cd_npwd="${CDS[0]}"  ; CDS[0]=`pwd` ;;
                        -v) cd_version ; cd_doflag="FALSE";;
                esac
        fi
}

cd_chdefm ()
{
        if ${TEST} "${CD_MODE}" = "PREV"
        then
                CD_MODE=""
                if ${TEST} $1 -eq 1
                then
                        ${PRINTF} "${cd_mset}"
                fi
        else
                CD_MODE="PREV"
                if ${TEST} $1 -eq 1
                then
                        ${PRINTF} "${cd_mnset}"
                fi
        fi
}

cd_fsave ()
{
        local sfile=${CDPath}${2:-"$CDFile"}
        if ${TEST} "$1" = "SHOW"
        then
                ${PRINTF} "Saved to %s\n" $sfile
        fi
        ${RM} -f ${sfile}
        local -i count=0
        while ${TEST} ${count} -le ${cd_maxhistory}
        do
                echo "CD[$count]=\"${CD[$count]}\"" >> ${sfile}
                count=${count}+1
        done
        count=0
        while ${TEST} ${count} -le ${cd_maxspecial}
        do
                echo "CDS[$count]=\"${CDS[$count]}\"" >> ${sfile}
                count=${count}+1
        done
}

cd_upload ()
{
        local sfile=${CDPath}${2:-"$CDFile"}
        if ${TEST} "${1}" = "SHOW"
        then
                ${PRINTF} "Chargement de %s\n" ${sfile}
        fi
        . ${sfile}
}

cd_new ()
{
    local -i count
    local -i choose=0

        cd_npwd="${1}"
        cd_specDir=-1
        cd_doselection "${1}" "${2}"

        if ${TEST} ${cd_doflag} = "TRUE"
        then
                if ${TEST} "${CD[0]}" != "`pwd`"
                then
                        count=$cd_maxhistory
                        while ${TEST} $count -gt 0
                        do
                                CD[$count]=${CD[$count-1]}
                                count=${count}-1
                        done
                        CD[0]=`pwd`
                fi
                command cd "${cd_npwd}" 2>/dev/null
        if ${TEST} $? -eq 1
        then
            ${PRINTF} "Répertoire inconnu : %s\n" "${cd_npwd}"
            local -i ftflag=0
            for i in "${cd_npwd}"*
            do
                if ${TEST} -d "${i}"
                then
                    if ${TEST} ${ftflag} -eq 0
                    then
                        ${PRINTF} "Suggest:\n"
                        ftflag=1
                fi
                    ${PRINTF} "\t-a${choose} %s\n" "$i"
                                        cd_sugg[$choose]="${i}"
                    choose=${choose}+1
        fi
            done
        fi
        fi

        if ${TEST} ${cd_specDir} -ne -1
        then
                CDS[${cd_specDir}]=`pwd`
        fi

        if ${TEST} ! -z "${CDL_PROMPTLEN}"
        then
        cd_right_trunc "${PWD}" ${CDL_PROMPTLEN}
            cd_rp=${CDL_PROMPT_PRE}${tcd}${CDL_PROMPT_POST}
                export PS1="$(echo -ne ${cd_rp})"
        fi
}
#################################################################################
#                                                                               #
#                            Initialisation ici                                 #
#                                                                               #
#################################################################################
#
VERSION_MAJOR="1"
VERSION_MINOR="2.1"
VERSION_DATE="24 MAI 2003"
#
alias cd=cd_new
#
# Configuration des commandes
RM=/bin/rm
TEST=test
PRINTF=printf              # Utilise le printf interne

#################################################################################
#                                                                               #
# Modifiez ceci pour modifier les chaînes préfixe et suffixe de l'invite.       #
# Elles ne prennent effet que si CDL_PROMPTLEN est configuré.                   #
#                                                                               #
#################################################################################
if ${TEST} ${EUID} -eq 0
then
#   CDL_PROMPT_PRE=${CDL_PROMPT_PRE:="$HOSTNAME@"}
    CDL_PROMPT_PRE=${CDL_PROMPT_PRE:="\\[\\e[01;31m\\]"}    # Root est en rouge
    CDL_PROMPT_POST=${CDL_PROMPT_POST:="\\[\\e[00m\\]#"}
else
    CDL_PROMPT_PRE=${CDL_PROMPT_PRE:="\\[\\e[01;34m\\]"}    # Les utilisateurs sont en bleu
    CDL_PROMPT_POST=${CDL_PROMPT_POST:="\\[\\e[00m\\]$"}
fi
#################################################################################
#
# cd_maxhistory définit le nombre max d'entrées dans l'historique.
typeset -i cd_maxhistory=50

#################################################################################
#
# cd_maxspecial définit le nombre d'entrées spéciales.
typeset -i cd_maxspecial=9
#
#
#################################################################################
#
# cd_histcount définit le nombre d'entrées affichées dans la commande historique.
typeset -i cd_histcount=9
#
#################################################################################
export CDPath=${HOME}/
#  Modifiez-les pour utiliser un chemin et un nom de fichier                    #
#+ différent de la valeur par défaut                                            #
export CDFile=${CDFILE:=cdfile}                   # pour les commandes -u et -f #
#
#################################################################################
                                                                                #
typeset -i cd_lchar cd_rchar cd_flchar
                               #  Ceci est le nombre de caractères pour que                   #
cd_flchar=${FLCHAR:=75}        #+ cd_flchar puisse être autorisé pour l'historique de @s & @h #

typeset -ax CD CDS
#
cd_mset="\n\tLe mode par défaut est maintenant configuré - saisir cd sans paramètre correspond à l'action par défaut\n\tUtilisez cd -d ou -D pour que cd aille au répertoire précédent sans paramètres\n"
cd_mnset="\n\tL'autre mode est maintenant configuré - saisir cd sans paramètres est identique à saisir cd 0\n\tUtilisez cd -d ou -D pour modifier l'action par défaut de cd\n"

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



: <<DOCUMENTATION

Écrit par Phil Braham. Realtime Software Pty Ltd.
Sortie sous licence GNU. Libre à utiliser. Merci de passer toutes modifications
ou commentaires à l'auteur Phil Braham:

realtime@mpx.com.au
===============================================================================

cdll est un remplacement pour cd et incorpore des fonctionnalités similaires
aux commandes pushd et popd de bash mais est indépendent.

Cette version de cdll a été testée sur Linux en utilisant Bash. Il fonctionnera
sur la plupart des versions Linux mais ne fonctionnera probablement pas sur les
autres shells sans modification.

Introduction
============

cdll permet un déplacement facile entre les répertoires. En allant dans un autre
répertoire, celui en cours est placé automatiquement sur une pile. Par défaut,
50 entrées sont conservées mais c'est configurable. Les répertoires spéciaux
peuvent être gardés pour un accès facile - par défaut jusqu'à 10, mais ceci est
configurable. Les entrées les plus récentes de la pile et les entrées spéciales
peuvent être facilement visualisées.

La pile de répertoires et les entrées spéciales peuvent être sauvegardées dans
un fichier ou chargées à partir d'un fichier. Ceci leur permet d'être initialisé
à la connexion, sauvegardé avant la fin de la session ou déplacé en passant de
projet à projet.

En plus, cdll fournit une invite flexible permettant, par exemple, un nom de
répertoire en couleur, tronqué à partir de la gauche s'ilest trop long.


Configurer cdll
===============

Copiez cdll soit dans votre répertoire personnel soit dans un répertoire central
comme /usr/bin (ceci requiert un accès root).

Copiez le fichier cdfile dans votre répertoie personnel. Il requèrera un accès
en lecture et écriture. Ceci est un fichier par défaut contenant une pile de
répertoires et des entrées spéciales.

Pour remplacer la commande cd, vous devez ajouter les commandes à votre script
de connexion. Le script de connexion fait partie de :

    /etc/profile
    ~/.bash_profile
    ~/.bash_login
    ~/.profile
    ~/.bashrc
    /etc/bash.bashrc.local

Pour configurer votre connexion, ~/.bashrc est recommandé, pour la configuration
globale (et de root), ajoutez les commandes à /etc/bash.bashrc.local

Pour configurer la connexion, ajoutez la commande :
    . <dir>/cdll
Par exemple, si cdll est dans votre répertoire personnel :
    . ~/cdll
Si dans /usr/bin, alors :
    . /usr/bin/cdll

Si vous voulez utiliser ceci à la place de la commande cd interne, alors ajoutez :
    alias cd='cd_new'
Nous devrions aussi recommander les commandes suivantes :
    alias @='cd_new @'
    cd -U
    cd -D

Si vous utilisez la capacité de l'invite de cdll, alors ajoutez ce qui suit :
    CDL_PROMPTLEN=nn
Quand nn est un nombre décrit ci-dessous. Initialement, 99 serait un nombre
convenable.

Du coup, le script ressemble à ceci :

    ######################################################################
    # CD Setup
    ######################################################################
    CDL_PROMPTLEN=21        # Autorise une longueur d'invite d'un maximum
                            # de 21 caractères
    . /usr/bin/cdll         # Initialise cdll
    alias cd='cd_new'       # Remplace la commande cd interne
    alias @='cd_new @'      # Autorise @ sur l'invite pour affiche l'historique
    cd -U                   # Recharge le répertoire
    cd -D                   # Configure l'action par défaut en non posix
    ######################################################################

La signification complète de ces commandes deviendra claire plus tard.

Voici quelques astuces. Si un autre programme modifie le répertoire sans appeler
cdll, alors le répertoire ne sera pas placé sur la pile et aussi si la
fonctionnalité de l'invite est utilisée, alors ceci ne sera pas mise à jour.
Deux programmes qui peuvent faire ceci sont pushd et popd. Pour mettre à jour
l'invite et la pile, saisissez simplement :

    cd .

Notez que si l'entrée précédente sur la pile est le répertoire en cours, alors
la pile n'est pas mise à jour.

Usage
=====
cd [dir] [0-9] [@[s|h] [-g <dir>] [-d] [-D] [-r<n>]
   [dir|0-9] [-R<n>] [<dir>|0-9] [-s<n>] [-S<n>]
   [-u] [-U] [-f] [-F] [-h] [-H] [-v]

    <dir> Se place sous le répertoire
    0-n         Se place sous le répertoire précedent (0 est le précédent, 1 est l'avant-dernier, etc)
                n va jusqu'au bout de l'historique (par défaut, 50)
    @           Liste les entrées de l'historique et les entrées spéciales
    @h          Liste les entrées de l'historique
    @s          Liste les entrées spéciales
    -g [<dir>]  Se place sous le nom littéral (sans prendre en compte les noms spéciaux)
                Ceci permet l'accès aux répertoires nommés '0','1','-h' etc
    -d          Modifie l'action par défaut - verbeux. (Voir note)
    -D          Modifie l'action par défaut - silencieux. (Voir note)
    -s<n>       Se place sous l'entrée spéciale <n>*
    -S<n>       Se place sous l'entrée spéciale <n> et la remplace avec le répertoire en cours*
    -r<n> [<dir>] Se place sous le répertoire <dir> and then put it on special entry <n>*
    -R<n> [<dir>] Se place sous le répertoire <dir> et place le répertoire en cours dans une entrée spéciale <n>*
    -a<n>       Autre répertoire suggéré. Voir la note ci-dessous.
    -f [<file>] Fichier des entrées <file>.
    -u [<file>] Met à jour les entrées à partir de <file>.
                Si aucun nom de fichier n'est fourni, utilise le fichier par défaut (${CDPath}${2:-"$CDFile"})
                -F et -U sont les versions silencieuses
    -v          Affiche le numéro de version
    -h          Aide
    -H          Aide détaillée



Exemples
========

Ces exemples supposent que le mode autre que celui par défaut est configuré (qui
est, cd sans paramètres ira sur le répertoire le plus récent de la pile), que
les alias ont été configurés pour cd et @ comme décrits ci-dessus et que la
fonctionnalité de l'invite de cd est active et la longueur de l'invite est de
21 caractères.

    /home/phil$ @                                                   # Liste les entrées avec le @
    History:                                                        # Affiche la commande @
    .....                                                           # Laissé ces entrées pour être bref
    1 /home/phil/ummdev               S1 /home/phil/perl            # Les deux entrées les plus récentes de l'historique
    0 /home/phil/perl/eg              S0 /home/phil/umm/ummdev      # et deux entrées spéciales sont affichées

    /home/phil$ cd /home/phil/utils/Cdll                            # Maintenant, modifie les répertoires
    /home/phil/utils/Cdll$ @                                        # L'invite reflète le répertoire.
    History:                                                        # Nouvel historique
    .....   
    1 /home/phil/perl/eg              S1 /home/phil/perl            # L'entrée 0 de l'historique a été déplacé dans 1
    0 /home/phil                      S0 /home/phil/umm/ummdev      # et la plus récente a été entrée

Pour aller dans une entrée de l'historique :

    /home/phil/utils/Cdll$ cd 1                                     # Va dans l'entrée 1 de l'historique.
    /home/phil/perl/eg$                                             # Le répertoire en cours est maintenant celui du 1

Pour aller dans une entrée spéciale :

    /home/phil/perl/eg$ cd -s1                                      # Va dans l'entrée spéciale 1
    /home/phil/umm/ummdev$                                          # Le répertoire en cours est S1

Pour aller dans un répertoire nommé, par exemple, 1 :

    /home/phil$ cd -g 1                                             # -g ignore la signification spéciale de 1
    /home/phil/1$

Pour placer le répertoire en cours sur la liste spéciale en tant que S1 :
    cd -r1 .        #  OU
    cd -R1 .        #  Elles ont le même effet si le répertoire est
                    #+ . (le répertoire en cours)

Pour aller dans un répertoire et l'ajouter comme entrée spéciale
    Le répertoire pour -r<n> ou -R<n> pourrait être un nombre. Par exemple :
        $ cd -r3 4  Va dans l'entrée 4 de l'historique et placez-la dans l'entrée spéciale 3
        $ cd -R3 4  Placez le répertoire en cours sur l'entrée spéciale 3 et allez dans l'entrée spéciale 4
        $ cd -s3    Allez dans l'entrée spéciale 3

    Notez que les commands R,r,S et s pourraient être utilisées sans un numéro et faire référence à 0 :
        $ cd -s     Va dans l'entrée spéciale 0
        $ cd -S     Va dans l'entrée spéciale 0 et fait de l'entrée spéciale 0 le répertoire en cours
        $ cd -r 1   Va dans l'entrée 1 de l'historique et la place sur l'entrée spéciale 0
        $ cd -r     Va dans l'entrée 0 de l'historique et la place sur l'entrée spéciale 0


    Autres répertoires suggérés :

    Si un répertoire est introuvable, alors CD suggèrera toute possibilité.
    Il s'agit des répertoires commençant avec les mêmes lettres et si des
    correspondances sont trouvées, ils sont affichés préfixés avec -a<n>
    où <n> est un numéro. Il est possible d'aller dans un répertoire
    de saisir cd -a<n> sur la ligne de commande.

        Utilisez cd -d ou -D pour modifier l'action par défaut de cd. cd -H
        affichera l'action en cours.

        Les entrées de l'historique (0-n) sont stockées dans les variables
        d'environnement CD[0] - CD[n]
        De façon similaire, les répertoires spéciaux S0 - 9 sont dans la variable
        d'environnement CDS[0] - CDS[9] et pourraient être accédés à partir de
        la ligne de commande, par exemple :

            ls -l ${CDS[3]}
            cat ${CD[8]}/file.txt

        Le chemin par défaut pour les commandes -f et -u est ~
        Le nom du fichier par défaut pour les commandes -f et -u est cdfile


Configuration
=============

    Les variables d'environnement suivantes peuvent être configurées :

            CDL_PROMPTLEN  - Configuré à la longueur de l'invite que vous demandez.
                La chaîne de l'invite est configurée suivant les caractères de droite du
                répertoire en cours. Si non configuré, l'invite n'est pas modifiée.
                Notez que ceci est le nombre de caractères raccourcissant le répertoire,
                pas le nombre de caractères total dans l'invite.

            CDL_PROMPT_PRE - Configure une chaîne pour préfixer l'invite.
                Default is:
                    non-root:  "\\[\\e[01;34m\\]"  (initialise la couleur à bleu).
                    root:      "\\[\\e[01;31m\\]"  (initialise la couleur à rouge).

            CDL_PROMPT_POST    - Configure une chaîne pour suffixer l'invite.
                Default is:
                    non-root:  "\\[\\e[00m\\]$"    (réinitialise la couleur et affiche $).
                    root:      "\\[\\e[00m\\]#"    (réinitialise la couleur et affiche #).

        Note:
            CDL_PROMPT_PRE & _POST only t

        CDPath - Configure le chemin par défaut pour les options -f & -u.
                 La valeur par défaut est le répertoire personnel
        CDFile - Configure le nom du fichier pour les options -f & -u.
                 La valeur par défaut est cdfile


    Il existe trois variables définies dans le fichier cdll qui contrôle le nombre
    d'entrées stockées ou affichées. Elles sont dans la sectioon labellées
    'Initialisation ici' jusqu'à la fin du fichier.

        cd_maxhistory       - Le nombre d'entrées stockées dans l'historique.
                              Par défaut, 50.
        cd_maxspecial       - Le nombre d'entrées spéciale autorisées.
                              Par défaut, 9.
        cd_histcount        - Le nombre d'entrées de l'historique et d'entrées spéciales
                              affichées. Par défaut, 9.

    Notez que cd_maxspecial devrait être >= cd_histcount pour afficher des entrées
    spéciales qui ne peuvent pas être initialisées.


Version: 1.2.1 Date: 24-MAY-2003

DOCUMENTATION

Exemple A.34. Un script de configuration d'une carte son

#!/bin/bash
# soundcard-on.sh

#  Script author: Mkarcher
#  http://www.thinkwiki.org/wiki  ...
#  /Script_for_configuring_the_CS4239_sound_chip_in_PnP_mode
#  ABS Guide author made minor changes and added comments.
#  Couldn't contact script author to ask for permission to use, but ...
#+ the script was released under the FDL,
#+ so its use here should be both legal and ethical.

#  Sound-via-pnp-script for Thinkpad 600E
#+ and possibly other computers with onboard CS4239/CS4610
#+ that do not work with the PCI driver
#+ and are not recognized by the PnP code of snd-cs4236.
#  Also for some 770-series Thinkpads, such as the 770x.
#  Run as root user, of course.
#
#  These are old and very obsolete laptop computers,
#+ but this particular script is very instructive,
#+ as it shows how to set up and hack device files.



#  Search for sound card pnp device:

for dev in /sys/bus/pnp/devices/*
do
  grep CSC0100 $dev/id > /dev/null && WSSDEV=$dev
  grep CSC0110 $dev/id > /dev/null && CTLDEV=$dev
done
# On 770x:
# WSSDEV = /sys/bus/pnp/devices/00:07
# CTLDEV = /sys/bus/pnp/devices/00:06
# These are symbolic links to /sys/devices/pnp0/ ...


#  Activate devices:
#  Thinkpad boots with devices disabled unless "fast boot" is turned off
#+ (in BIOS).

echo activate > $WSSDEV/resources
echo activate > $CTLDEV/resources


# Parse resource settings.

{ read # Discard "state = active" (see below).
  read bla port1
  read bla port2
  read bla port3
  read bla irq
  read bla dma1
  read bla dma2
 # The "bla's" are labels in the first field: "io," "state," etc.
 # These are discarded.

 #  Hack: with PnPBIOS: ports are: port1: WSS, port2:
 #+ OPL, port3: sb (unneeded)
 #       with ACPI-PnP:ports are: port1: OPL, port2: sb, port3: WSS
 #  (ACPI bios seems to be wrong here, the PnP-card-code in snd-cs4236.c
 #+  uses the PnPBIOS port order)
 #  Detect port order using the fixed OPL port as reference.
  if [ ${port2%%-*} = 0x388 ]
 #            ^^^^  Strip out everything following hyphen in port address.
 #                  So, if port1 is 0x530-0x537
 #+                 we're left with 0x530 -- the start address of the port.
 then
   # PnPBIOS: usual order
   port=${port1%%-*}
   oplport=${port2%%-*}
 else
   # ACPI: mixed-up order
   port=${port3%%-*}
   oplport=${port1%%-*}
 fi
 } < $WSSDEV/resources
# To see what's going on here:
# ---------------------------
#   cat /sys/devices/pnp0/00:07/resources
#
#   state = active
#   io 0x530-0x537
#   io 0x388-0x38b
#   io 0x220-0x233
#   irq 5
#   dma 1
#   dma 0
#   ^^^   "bla" labels in first field (discarded). 


{ read # Discard first line, as above.
  read bla port1
  cport=${port1%%-*}
  #            ^^^^
  # Just want _start_ address of port.
} < $CTLDEV/resources


# Load the module:

modprobe --ignore-install snd-cs4236 port=$port cport=$cport\
fm_port=$oplport irq=$irq dma1=$dma1 dma2=$dma2 isapnp=0 index=0
# See the modprobe manpage.

exit $?

Exemple A.35. Localise les paragraphes divisés dans un fichier texte

#!/bin/bash
# find-splitpara.sh
#  Finds split paragraphs in a text file,
#+ and tags the line numbers.


ARGCOUNT=1       # Expect one arg.
E_WRONGARGS=65

file="$1"        # Target filename.
lineno=1         # Line number. Start at 1.
Flag=0           # Blank line flag.

if [ $# -ne "$ARGCOUNT" ]
then
  echo "Usage: `basename $0` FILENAME"
  exit $E_WRONGARGS
fi  

file_read ()     # Scan file for pattern, then print line.
{
while read line
do

  if [[ "$line" =~ ^[a-z] && $Flag -eq 1 ]]
     then  # Line begins with lc character, following blank line.
     echo -n "$lineno::   "
     echo "$line"
  fi


  if [[ "$line" =~ "^$" ]]
     then     #  If blank line,
     Flag=1   #+ set flag.
  else
     Flag=0
  fi

  ((lineno++))

done
} < $file  # Redirect file into function's stdin.

file_read


exit $?


# ----------------------------------------------------------------
This is line one of an example paragraph, bla, bla, bla.
This is line two, and line three should follow on next line, but

there is a blank line separating the two parts of the paragraph.
# ----------------------------------------------------------------

Running this script on a file containing the above paragraph
yields:

4::   there is a blank line separating the two parts of the paragraph.


There will be additional output for all the other split paragraphs
in the target file.

Exemple A.36. Tri d'insertion

#!/bin/bash
# insertion-sort.bash: Implémentation du tri d'insertion dans Bash
#                      Grosse utilisation des fonctionnalités tableau de Bash :
#+                     découpage (chaîne), assemblage, etc
# URL: http://www.lugmen.org.ar/~jjo/jjotip/insertion-sort.bash.d
#+          /insertion-sort.bash.sh
#
# Auteur : JuanJo Ciarlante <jjo@irrigacion.gov.ar>
# Légèrement reformaté par l'auteur du guide ABS.
# Licence : GPLv2
# Utilisé dans le guide ABS avec la permission de l'auteur (merci !).
#
# Testez avec    ./insertion-sort.bash -t
# Ou  :          bash insertion-sort.bash -t
# Ce qui suit *ne fonctionne pas* :
#              sh insertion-sort.bash -t
#  Pourquoi pas ? Astuce : quelles fonctionnalités spécifiques de Bash sont
#+ désactivées quand un script est exécuté par 'sh script.sh'?
#
: ${DEBUG:=0}  # Debug, surchargé avec :  DEBUG=1 ./nomscript . . .
# Substitution de paramètres -- configurer DEBUG à 0 si non initialisé auparavant.

# Tableau global : "liste"
typeset -a liste
# Chargement de nombres séparés par des espaces blancs à partir de stdin.
if [ "$1" = "-t" ]; then
DEBUG=1
        read -a liste < <( od -Ad -w24 -t u2 /dev/urandom ) # Liste aléatoire.
#                     ^ ^  substitution de processus
else
        read -a liste
fi
numelem=${#liste[*]}

#  Affiche la liste, marquant l'élément dont l'index est $1
#+ en la surchargeant avec les deux caractères passés à $2.
#  La ligne est préfixée par $3.
afficherliste()
  {
  echo "$3"${liste[@]:0:$1} ${2:0:1}${liste[$1]}${2:1:1} ${liste[@]:$1+1};
  }

# Boucle _pivot_ -- à partir du second élément jusqu'à la fin de la liste.
for(( i=1; i<numelem; i++ )) do
        ((DEBUG))&&showlist i "[]" " "
        # À partir du _pivot_ actuel, retour au premier élément.
        for(( j=i; j; j-- )) do
                # Recherche du premier élément inférieur au "pivot" actuel...
                [[ "${list[j-1]}" -le "${list[i]}" ]] && break
        done
        (( i==j )) && continue ## Aucune insertion n'était nécessaire pour cet élément.
        # . . . Déplacer liste[i] (pivot) à la gauche de liste[j] :
        liste=(${liste[@]:0:j} ${liste[i]} ${liste[j]}\
        #         {0,j-1}        {i}        {j}
              ${liste[@]:j+1:i-(j+1)} ${liste[@]:i+1})
        #          {j+1,i-1}               {i+1,last}
        ((DEBUG))&&afficherliste j "<>" "*"
done


echo
echo  "------"
echo $'Résultat :\n'${liste[@]}

exit $?

Exemple A.37. Écart-type

#!/bin/bash
# sd.sh: Standard Deviation

#  The Standard Deviation indicates how consistent a set of data is.
#  It shows to what extent the individual data points deviate from the
#+ arithmetic mean, i.e., how much they "bounce around" (or cluster).
#  It is essentially the average deviation-distance of the
#+ data points from the mean.

# =========================================================== #
#    To calculate the Standard Deviation:
#
# 1  Find the arithmetic mean (average) of all the data points.
# 2  Subtract each data point from the arithmetic mean,
#    and square that difference.
# 3  Add all of the individual difference-squares in # 2.
# 4  Divide the sum in # 3 by the number of data points.
#    This is known as the "variance."
# 5  The square root of # 4 gives the Standard Deviation.
# =========================================================== #

count=0         # Number of data points; global.
SC=9            # Scale to be used by bc. Nine decimal places.
E_DATAFILE=90   # Data file error.

# ----------------- Set data file ---------------------
if [ ! -z "$1" ]  # Specify filename as cmd-line arg?
then
  datafile="$1" #  ASCII text file,
else            #+ one (numerical) data point per line!
  datafile=sample.dat
fi              #  See example data file, below.

if [ ! -e "$datafile" ]
then
  echo "\""$datafile"\" does not exist!"
  exit $E_DATAFILE
fi
# -----------------------------------------------------


arith_mean ()
{
  local rt=0         # Running total.
  local am=0         # Arithmetic mean.
  local ct=0         # Number of data points.

  while read value   # Read one data point at a time.
  do
    rt=$(echo "scale=$SC; $rt + $value" | bc)
    (( ct++ ))
  done

  am=$(echo "scale=$SC; $rt / $ct" | bc)

  echo $am; return $ct   # This function "returns" TWO values!
  #  Caution: This little trick will not work if $ct > 255!
  #  To handle a larger number of data points,
  #+ simply comment out the "return $ct" above.
} <"$datafile"   # Feed in data file.

sd ()
{
  mean1=$1  # Arithmetic mean (passed to function).
  n=$2      # How many data points.
  sum2=0    # Sum of squared differences ("variance").
  avg2=0    # Average of $sum2.
  sdev=0    # Standard Deviation.

  while read value   # Read one line at a time.
  do
    diff=$(echo "scale=$SC; $mean1 - $value" | bc)
    # Difference between arith. mean and data point.
    dif2=$(echo "scale=$SC; $diff * $diff" | bc) # Squared.
    sum2=$(echo "scale=$SC; $sum2 + $dif2" | bc) # Sum of squares.
  done

    avg2=$(echo "scale=$SC; $sum2 / $n" | bc)  # Avg. of sum of squares.
    sdev=$(echo "scale=$SC; sqrt($avg2)" | bc) # Square root =
    echo $sdev                                 # Standard Deviation.

} <"$datafile"   # Rewinds data file.


# ======================================================= #
mean=$(arith_mean); count=$?   # Two returns from function!
std_dev=$(sd $mean $count)

echo
echo "Number of data points in \""$datafile"\" = $count"
echo "Arithmetic mean (average) = $mean"
echo "Standard Deviation = $std_dev"
echo
# ======================================================= #

exit

#  This script could stand some drastic streamlining,
#+ but not at the cost of reduced legibility, please.


# ++++++++++++++++++++++++++++++++++++++++ #
# A sample data file (sample1.dat):

# 18.35
# 19.0
# 18.88
# 18.91
# 18.64


# $ sh sd.sh sample1.dat

# Number of data points in "sample1.dat" = 5
# Arithmetic mean (average) = 18.756000000
# Standard Deviation = .235338054
# ++++++++++++++++++++++++++++++++++++++++ #

Exemple A.38. Générateur de fichiers pad pour les auteurs de shareware

#!/bin/bash
# pad.sh

#######################################################
#               PAD (xml) file creator
#+ Written by Mendel Cooper <thegrendel.abs@gmail.com>.
#+ Released to the Public Domain.
#
#  Generates a "PAD" descriptor file for shareware
#+ packages, according to the specifications
#+ of the ASP.
#  http://www.asp-shareware.org/pad
#######################################################


# Accepts (optional) save filename as a command-line argument.
if [ -n "$1" ]
then
  savefile=$1
else
  savefile=save_file.xml               # Default save_file name.
fi  


# ===== PAD file headers =====
HDR1="<?xml version=\"1.0\" encoding=\"Windows-1252\" ?>"
HDR2="<XML_DIZ_INFO>"
HDR3="<MASTER_PAD_VERSION_INFO>"
HDR4="\t<MASTER_PAD_VERSION>1.15</MASTER_PAD_VERSION>"
HDR5="\t<MASTER_PAD_INFO>Portable Application Description, or PAD
for short, is a data set that is used by shareware authors to
disseminate information to anyone interested in their software products.
To find out more go to http://www.asp-shareware.org/pad</MASTER_PAD_INFO>"
HDR6="</MASTER_PAD_VERSION_INFO>"
# ============================


fill_in ()
{
  if [ -z "$2" ]
  then
    echo -n "$1? "     # Get user input.
  else
    echo -n "$1 $2? "  # Additional query?
  fi  

  read var             # May paste to fill in field.
                       # This shows how flexible "read" can be.

  if [ -z "$var" ]
  then
    echo -e "\t\t<$1 />" >>$savefile    # Indent with 2 tabs.
    return
  else
    echo -e "\t\t<$1>$var</$1>" >>$savefile
    return ${#var}     # Return length of input string.
  fi
}    

check_field_length ()  # Check length of program description fields.
{
  # $1 = maximum field length
  # $2 = actual field length
  if [ "$2" -gt "$1" ]
  then
    echo "Warning: Maximum field length of $1 characters exceeded!"
  fi
}  

clear                  # Clear screen.
echo "PAD File Creator"
echo "--- ---- -------"
echo

# Write File Headers to file.
echo $HDR1 >$savefile
echo $HDR2 >>$savefile
echo $HDR3 >>$savefile
echo -e $HDR4 >>$savefile
echo -e $HDR5 >>$savefile
echo $HDR6 >>$savefile


# Company_Info
echo "COMPANY INFO"
CO_HDR="Company_Info"
echo "<$CO_HDR>" >>$savefile

fill_in Company_Name
fill_in Address_1
fill_in Address_2
fill_in City_Town 
fill_in State_Province
fill_in Zip_Postal_Code
fill_in Country

# If applicable:
# fill_in ASP_Member "[Y/N]"
# fill_in ASP_Member_Number
# fill_in ESC_Member "[Y/N]"

fill_in Company_WebSite_URL

clear   # Clear screen between sections.

   # Contact_Info
echo "CONTACT INFO"
CONTACT_HDR="Contact_Info"
echo "<$CONTACT_HDR>" >>$savefile
fill_in Author_First_Name
fill_in Author_Last_Name
fill_in Author_Email
fill_in Contact_First_Name
fill_in Contact_Last_Name
fill_in Contact_Email
echo -e "\t</$CONTACT_HDR>" >>$savefile
   # END Contact_Info

clear

   # Support_Info
echo "SUPPORT INFO"
SUPPORT_HDR="Support_Info"
echo "<$SUPPORT_HDR>" >>$savefile
fill_in Sales_Email
fill_in Support_Email
fill_in General_Email
fill_in Sales_Phone
fill_in Support_Phone
fill_in General_Phone
fill_in Fax_Phone
echo -e "\t</$SUPPORT_HDR>" >>$savefile
   # END Support_Info

echo "</$CO_HDR>" >>$savefile
# END Company_Info

clear

# Program_Info 
echo "PROGRAM INFO"
PROGRAM_HDR="Program_Info"
echo "<$PROGRAM_HDR>" >>$savefile
fill_in Program_Name
fill_in Program_Version
fill_in Program_Release_Month
fill_in Program_Release_Day
fill_in Program_Release_Year
fill_in Program_Cost_Dollars
fill_in Program_Cost_Other
fill_in Program_Type "[Shareware/Freeware/GPL]"
fill_in Program_Release_Status "[Beta, Major Upgrade, etc.]"
fill_in Program_Install_Support
fill_in Program_OS_Support "[Win9x/Win2k/Linux/etc.]"
fill_in Program_Language "[English/Spanish/etc.]"

echo; echo

  # File_Info 
echo "FILE INFO"
FILEINFO_HDR="File_Info"
echo "<$FILEINFO_HDR>" >>$savefile
fill_in Filename_Versioned
fill_in Filename_Previous
fill_in Filename_Generic
fill_in Filename_Long
fill_in File_Size_Bytes
fill_in File_Size_K
fill_in File_Size_MB
echo -e "\t</$FILEINFO_HDR>" >>$savefile
  # END File_Info 

clear

  # Expire_Info 
echo "EXPIRE INFO"
EXPIRE_HDR="Expire_Info"
echo "<$EXPIRE_HDR>" >>$savefile
fill_in Has_Expire_Info "Y/N"
fill_in Expire_Count
fill_in Expire_Based_On
fill_in Expire_Other_Info
fill_in Expire_Month
fill_in Expire_Day
fill_in Expire_Year
echo -e "\t</$EXPIRE_HDR>" >>$savefile
  # END Expire_Info 

clear

  # More Program_Info
echo "ADDITIONAL PROGRAM INFO"
fill_in Program_Change_Info
fill_in Program_Specific_Category
fill_in Program_Categories
fill_in Includes_JAVA_VM "[Y/N]"
fill_in Includes_VB_Runtime "[Y/N]"
fill_in Includes_DirectX "[Y/N]"
  # END More Program_Info

echo "</$PROGRAM_HDR>" >>$savefile
# END Program_Info 

clear

# Program Description
echo "PROGRAM DESCRIPTIONS"
PROGDESC_HDR="Program_Descriptions"
echo "<$PROGDESC_HDR>" >>$savefile

LANG="English"
echo "<$LANG>" >>$savefile

fill_in Keywords "[comma + space separated]"
echo
echo "45, 80, 250, 450, 2000 word program descriptions"
echo "(may cut and paste into field)"
#  It would be highly appropriate to compose the following
#+ "Char_Desc" fields with a text editor,
#+ then cut-and-paste the text into the answer fields.
echo
echo "              |---------------45 characters---------------|"
fill_in Char_Desc_45
check_field_length 45 "$?"
echo
fill_in Char_Desc_80
check_field_length 80 "$?"

fill_in Char_Desc_250
check_field_length 250 "$?"

fill_in Char_Desc_450
fill_in Char_Desc_2000

echo "</$LANG>" >>$savefile
echo "</$PROGDESC_HDR>" >>$savefile
# END Program Description

clear
echo "Done."; echo; echo
echo "Save file is:  \""$savefile"\""

exit 0

Exemple A.39. Éditeur de man page

#!/bin/bash
# maned.sh
# A rudimentary man page editor

# Version: 0.1 (Alpha, probably buggy)
# Author: Mendel Cooper <thegrendel.abs@gmail.com>
# Reldate: 16 June 2008
# License: GPL3


savefile=      # Global, used in multiple functions.
E_NOINPUT=90   # User input missing (error). May or may not be critical.

# =========== Markup Tags ============ #
TopHeader=".TH"
NameHeader=".SH NAME"
SyntaxHeader=".SH SYNTAX"
SynopsisHeader=".SH SYNOPSIS"
InstallationHeader=".SH INSTALLATION"
DescHeader=".SH DESCRIPTION"
OptHeader=".SH OPTIONS"
FilesHeader=".SH FILES"
EnvHeader=".SH ENVIRONMENT"
AuthHeader=".SH AUTHOR"
BugsHeader=".SH BUGS"
SeeAlsoHeader=".SH SEE ALSO"
BOLD=".B"
# Add more tags, as needed.
# See groff docs for markup meanings.
# ==================================== #

start ()
{
clear                  # Clear screen.
echo "ManEd"
echo "-----"
echo
echo "Simple man page creator"
echo "Author: Mendel Cooper"
echo; echo; echo
}

progname ()
{
  echo -n "Program name? "
  read name

  echo -n "Manpage section? [Hit RETURN for default (\"1\") ]  "
  read section
  if [ -z "$section" ]
  then
    section=1   # Most man pages are in section 1.
  fi

  if [ -n "$name" ]
  then
    savefile=""$name"."$section""       #  Filename suffix = section.
    echo -n "$1 " >>$savefile
    name1=$(echo "$name" | tr a-z A-Z)  #  Change to uppercase,
                                        #+ per man page convention.
    echo -n "$name1" >>$savefile
  else
    echo "Error! No input."             # Mandatory input.
    exit $E_NOINPUT                     # Critical!
  fi

  echo -n "  \"$section\"">>$savefile   # Append, always append.

  echo -n "Version? "
  read ver
  echo -n " \"Version $ver \"">>$savefile
  echo >>$savefile

  echo -n "Short description [0 - 5 words]? "
  read sdesc
  echo "$NameHeader">>$savefile
  echo ""$BOLD" "$name"">>$savefile
  echo "\- "$sdesc"">>$savefile

}

fill_in ()
{ # This function more or less copied from "pad.sh" script.
  echo -n "$2? "       # Get user input.
  read var             # May paste (a single line only!) to fill in field.

  if [ -n "$var" ]
  then
    echo "$1 " >>$savefile
    echo -n "$var" >>$savefile
  else                 # Don't append empty field to file.
    return $E_NOINPUT  # Not critical here.
  fi

  echo >>$savefile

}    


end ()
{
clear
echo -n "Would you like to view the saved man page (y/n)? "
read ans
if [ "$ans" = "n" -o "$ans" = "N" ]; then exit; fi
exec less "$savefile"  #  Exit script and hand off control to "less" ...
                       #+ ... which formats for viewing man page source.
}


# ---------------------------------------- #
start
progname "$TopHeader"
fill_in "$SynopsisHeader" "Synopsis"
fill_in "$DescHeader" "Long description"
# May paste in *single line* of text.
fill_in "$OptHeader" "Options"
fill_in "$FilesHeader" "Files"
fill_in "$AuthHeader" "Author"
fill_in "$BugsHeader" "Bugs"
fill_in "$SeeAlsoHeader" "See also"
# fill_in "$OtherHeader" ... as necessary.
end    # ... exit not needed.
# ---------------------------------------- #

#  Note that the generated man page will usually
#+ require manual fine-tuning with a text editor.
#  However, it's a distinct improvement upon
#+ writing man source from scratch
#+ or even editing a blank man page template.

#  The main deficiency of the script is that it permits
#+ pasting only a single text line into the input fields.
#  This may be a long, cobbled-together line, which groff
#  will automatically wrap and hyphenate.
#  However, if you want multiple (newline-separated) paragraphs,
#+ these must be inserted by manual text editing on the
#+ script-generated man page.
#  Exercise (difficult): Fix this!

#  This script is not nearly as elaborate as the
#+ full-featured "manedit" package (http://wolfpack.twu.net),
#+ but it's much easier to use.

Exemple A.40. Pétales autour d'une rose

#!/bin/bash -i
# petals.sh

#########################################################################
# Petals Around the Rose                                                #
#                                                                       #
# Version 0.1 Created by Serghey Rodin                                  #
# Version 0.2 Modded by ABS Guide Author                                #
#                                                                       #
# License: GPL3                                                         #
# Used in ABS Guide with permission.                                    #
# ##################################################################### #

hits=0      # Correct guesses.
WIN=6       # Mastered the game.
ALMOST=5    # One short of mastery.
EXIT=exit   # Give up early?

RANDOM=$$   # Seeds the random number generator from PID of script.


# Bones (ASCII graphics for dice)
bone1[1]="|         |"
bone1[2]="|       o |"
bone1[3]="|       o |"
bone1[4]="| o     o |"
bone1[5]="| o     o |"
bone1[6]="| o     o |"
bone2[1]="|    o    |"
bone2[2]="|         |"
bone2[3]="|    o    |"
bone2[4]="|         |"
bone2[5]="|    o    |"
bone2[6]="| o     o |"
bone3[1]="|         |"
bone3[2]="| o       |"
bone3[3]="| o       |"
bone3[4]="| o     o |"
bone3[5]="| o     o |"
bone3[6]="| o     o |"
bone="+---------+"



# Functions

instructions () {

  clear
  echo -n "Do you need instructions? (y/n) "; read ans
  if [ "$ans" = "y" -o "$ans" = "Y" ]; then
    clear
    echo -e '\E[34;47m'  # Blue type.

#  "cat document"
    cat <<INSTRUCTIONSZZZ
The name of the game is Petals Around the Rose,
and that name is significant.
Five dice will roll and you must guess the "answer" for each roll.
It will be zero or an even number.
After your guess, you will be told the answer for the roll, but . . .
that's ALL the information you will get.

Six consecutive correct guesses admits you to the
Fellowship of the Rose.
INSTRUCTIONSZZZ

    echo -e "\033[0m"    # Turn off blue.
    else clear
  fi

}


fortune ()
{
  RANGE=7
  FLOOR=0
  number=0
  while [ "$number" -le $FLOOR ]
  do
    number=$RANDOM
    let "number %= $RANGE"   # 1 - 6.
  done

  return $number
}



throw () { # Calculate each individual die.
  fortune; B1=$?
  fortune; B2=$?
  fortune; B3=$?
  fortune; B4=$?
  fortune; B5=$?

  calc () { # Function embedded within a function!
    case "$1" in
       3   ) rose=2;;
       5   ) rose=4;;
       *   ) rose=0;;
    esac    # Simplified algorithm.
            # Doesn't really get to the heart of the matter.
    return $rose
  }

  answer=0
  calc "$B1"; answer=$(expr $answer + $(echo $?))
  calc "$B2"; answer=$(expr $answer + $(echo $?))
  calc "$B3"; answer=$(expr $answer + $(echo $?))
  calc "$B4"; answer=$(expr $answer + $(echo $?))
  calc "$B5"; answer=$(expr $answer + $(echo $?))
}



game ()
{ # Generate graphic display of dice throw.
  throw
    echo -e "\033[1m"    # Bold.
  echo -e "\n"
  echo -e "$bone\t$bone\t$bone\t$bone\t$bone"
  echo -e \
 "${bone1[$B1]}\t${bone1[$B2]}\t${bone1[$B3]}\t${bone1[$B4]}\t${bone1[$B5]}"
  echo -e \
 "${bone2[$B1]}\t${bone2[$B2]}\t${bone2[$B3]}\t${bone2[$B4]}\t${bone2[$B5]}"
  echo -e \
 "${bone3[$B1]}\t${bone3[$B2]}\t${bone3[$B3]}\t${bone3[$B4]}\t${bone3[$B5]}"
  echo -e "$bone\t$bone\t$bone\t$bone\t$bone"
  echo -e "\n\n\t\t"
    echo -e "\033[0m"    # Turn off bold.
  echo -n "There are how many petals around the rose? "
}



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

instructions

while [ "$petal" != "$EXIT" ]    # Main loop.
do
  game
  read petal
  echo "$petal" | grep [0-9] >/dev/null  # Filter response for digit.
                                         # Otherwise just roll dice again.
  if [ "$?" -eq 0 ]   # If-loop #1.
  then
    if [ "$petal" == "$answer" ]; then    # If-loop #2.
        echo -e "\nCorrect. There are $petal petals around the rose.\n"
        (( hits++ ))

        if [ "$hits" -eq "$WIN" ]; then   # If-loop #3.
          echo -e '\E[31;47m'  # Red type.
          echo -e "\033[1m"    # Bold.
          echo "You have unraveled the mystery of the Rose Petals!"
          echo "Welcome to the Fellowship of the Rose!!!"
          echo "(You are herewith sworn to secrecy.)"; echo
          echo -e "\033[0m"    # Turn off red & bold.
          break                # Exit!
        else echo "You have $hits correct so far."; echo

        if [ "$hits" -eq "$ALMOST" ]; then
          echo "Just one more gets you to the heart of the mystery!"; echo
        fi

      fi                                  # Close if-loop #3.

    else
      echo -e "\nWrong. There are $answer petals around the rose.\n"
      hits=0   # Reset number of correct guesses.
    fi                                    # Close if-loop #2.

    echo -n "Hit ENTER for the next roll, or type \"exit\" to end. "
    read
    if [ "$REPLY" = "$EXIT" ]; then exit
    fi

  fi                  # Close if-loop #1.

  clear
done                  # End of main (while) loop.

###

exit $?

# Resources:
# ---------
# 1) http://en.wikipedia.org/wiki/Petals_Around_the_Rose
#    (Wikipedia entry.)
# 2) http://www.borrett.id.au/computing/petals-bg.htm
#    (How Bill Gates coped with the Petals Around the Rose challenge.)

Exemple A.41. Quacky : un jeu de mots de type Perquackey

#!/bin/bash
# qky.sh

##############################################################
# QUACKEY: a somewhat simplified version of Perquackey [TM]. #
#                                                            #
# Author: Mendel Cooper  <thegrendel.abs@gmail.com>          #
# version 0.1.02      03 May, 2008                           #
# License: GPL3                                              #
##############################################################

WLIST=/usr/share/dict/word.lst
#                     ^^^^^^^^  Word list file found here.
#  ASCII word list, one word per line, UNIX format.
#  A suggested list is the script author's "yawl" word list package.
#  http://bash.neuralshortciruit.com/yawl-0.3.2.tar.gz
#    or
#  http://ibiblio.org/pub/Linux/libs/yawl-0.3.2.tar.gz

NONCONS=0     # Word not constructable from letter set.
CONS=1        # Constructable.
SUCCESS=0
NG=1
FAILURE=''
NULL=0        # Zero out value of letter (if found).
MINWLEN=3     # Minimum word length.
MAXCAT=5      # Maximum number of words in a given category.
PENALTY=200   # General-purpose penalty for unacceptable words.
total=
E_DUP=70      # Duplicate word error.

TIMEOUT=10    # Time for word input.

NVLET=10      # 10 letters for non-vulnerable.
VULET=13      # 13 letters for vulnerable (not yet implemented).

declare -a Words
declare -a Status
declare -a Score=( 0 0 0 0 0 0 0 0 0 0 0 )


letters=( a n s r t m l k p r b c i d s i d z e w u e t f
e y e r e f e g t g h h i t r s c i t i d i j a t a o l a
m n a n o v n w o s e l n o s p a q e e r a b r s a o d s
t g t i t l u e u v n e o x y m r k )
#  Letter distribution table shamelessly borrowed from "Wordy" game,
#+ ca. 1992, written by a certain fine fellow named Mendel Cooper.

declare -a LS

numelements=${#letters[@]}
randseed="$1"

instructions ()
{
  clear
  echo "Welcome to QUACKEY, the anagramming word construction game."; echo
  echo -n "Do you need instructions? (y/n) "; read ans

   if [ "$ans" = "y" -o "$ans" = "Y" ]; then
     clear
     echo -e '\E[31;47m'  # Red foreground. '\E[34;47m' for blue.
     cat <<INSTRUCTION1

QUACKEY is a variant of Perquackey [TM].
The rules are the same, but the scoring is simplified
and plurals of previously played words are allowed.
"Vulnerable" play is not yet implemented,
but it is otherwise feature-complete.

As the game begins, the player gets 10 letters.
The object is to construct valid dictionary words
of at least 3-letter-length from the letterset.
Each word-length category
-- 3-letter, 4-letter, 5-letter, ... --
fills up with the fifth word entered,
and no further words in that category are accepted.

The penalty for too-short (two-letter), duplicate, unconstructable,
and invalid (not in dictionary) words is -200. The same penalty applies
to attempts to enter a word in a filled-up category.

INSTRUCTION1

  echo -n "Hit ENTER for next page of instructions. "; read az1

     cat <<INSTRUCTION2

The scoring mostly corresponds to classic Perquackey:
The first 3-letter word scores    60, plus   10 for each additional one.
The first 4-letter word scores   120, plus   20 for each additional one.
The first 5-letter word scores   200, plus   50 for each additional one.
The first 6-letter word scores   300, plus  100 for each additional one.
The first 7-letter word scores   500, plus  150 for each additional one.
The first 8-letter word scores   750, plus  250 for each additional one.
The first 9-letter word scores  1000, plus  500 for each additional one.
The first 10-letter word scores 2000, plus 2000 for each additional one.

Category completion bonuses are:
3-letter words   100
4-letter words   200
5-letter words   400
6-letter words   800
7-letter words  2000
8-letter words 10000
This is a simplification of the absurdly complicated Perquackey bonus
scoring system.

INSTRUCTION2

  echo -n "Hit ENTER for final page of instructions. "; read az1

     cat <<INSTRUCTION3


Hitting just ENTER for a word entry ends the game.

Individual word entry is timed to a maximum of 10 seconds.
*** Timing out on an entry ends the game. ***
Other than that, the game is untimed.

--------------------------------------------------
Game statistics are automatically saved to a file.
--------------------------------------------------

For competitive ("duplicate") play, a previous letterset
may be duplicated by repeating the script's random seed,
command-line parameter \$1.
For example, "qky 7633" specifies the letterset 
c a d i f r h u s k ...
INSTRUCTION3

  echo; echo -n "Hit ENTER to begin game. "; read az1

       echo -e "\033[0m"    # Turn off red.
     else clear
  fi

  clear

}



seed_random ()
{                         #  Seed random number generator.
  if [ -n "$randseed" ]   #  Can specify random seed.
  then                    #+ for play in competitive mode.
#   RANDOM="$randseed"
    echo "RANDOM seed set to "$randseed""
  else
    randseed="$$"         # Or get random seed from process ID.
    echo "RANDOM seed not specified, set to Process ID of script ($$)."
  fi

  RANDOM="$randseed"

  echo
}


get_letset ()
{
  element=0
  echo -n "Letterset:"

  for lset in $(seq $NVLET)
  do  # Pick random letters to fill out letterset.
    LS[element]="${letters[$((RANDOM%numelements))]}"
    ((element++))
  done

  echo
  echo "${LS[@]}"

}


add_word ()
{
  wrd="$1"
  local idx=0

  Status[0]=""
  Status[3]=""
  Status[4]=""

  while [ "${Words[idx]}" != '' ]
  do
    if [ "${Words[idx]}" = "$wrd" ]
    then
      Status[3]="Duplicate-word-PENALTY"
      let "Score[0]= 0 - $PENALTY"
      let "Score[1]-=$PENALTY"
      return $E_DUP
    fi

    ((idx++))
  done

  Words[idx]="$wrd"
  get_score

}

get_score()
{
  local wlen=0
  local score=0
  local bonus=0
  local first_word=0
  local add_word=0
  local numwords=0

  wlen=${#wrd}
  numwords=${Score[wlen]}
  Score[2]=0
  Status[4]=""   # Initialize "bonus" to 0.

  case "$wlen" in
    3) first_word=60
       add_word=10;;
    4) first_word=120
       add_word=20;;
    5) first_word=200
       add_word=50;;
    6) first_word=300
       add_word=100;;
    7) first_word=500
       add_word=150;;
    8) first_word=750
       add_word=250;;
    9) first_word=1000
       add_word=500;;
   10) first_word=2000
       add_word=2000;;   # This category modified from original rules!
      esac

  ((Score[wlen]++))
  if [ ${Score[wlen]} -eq $MAXCAT ]
  then   # Category completion bonus scoring simplified!
    case $wlen in
      3 ) bonus=100;;
      4 ) bonus=200;;
      5 ) bonus=400;;
      6 ) bonus=800;;
      7 ) bonus=2000;;
      8 ) bonus=10000;;
    esac  # Needn't worry about 9's and 10's.
    Status[4]="Category-$wlen-completion***BONUS***"
    Score[2]=$bonus
  else
    Status[4]=""   # Erase it.
  fi


    let "score =  $first_word +   $add_word * $numwords"
    if [ "$numwords" -eq 0 ]
    then
      Score[0]=$score
    else
      Score[0]=$add_word
    fi   #  All this to distinguish last-word score
         #+ from total running score.
  let "Score[1] += ${Score[0]}"
  let "Score[1] += ${Score[2]}"

}



get_word ()
{
  local wrd=''
  read -t $TIMEOUT wrd   # Timed read.
  echo $wrd
}

is_constructable ()
{ # This was the most complex and difficult-to-write function.
  local -a local_LS=( "${LS[@]}" )  # Local copy of letter set.
  local is_found=0
  local idx=0
  local pos
  local strlen
  local local_word=( "$1" )
  strlen=${#local_word}

  while [ "$idx" -lt "$strlen" ]
  do
    is_found=$(expr index "${local_LS[*]}" "${local_word:idx:1}")
    if [ "$is_found" -eq "$NONCONS" ] # Not constructable!
    then
      echo "$FAILURE"; return
    else
      ((pos = ($is_found - 1) / 2))   # Compensate for spaces betw. letters!
      local_LS[pos]=$NULL             # Zero out used letters.
      ((idx++))                       # Bump index.
    fi
  done

  echo "$SUCCESS"
  return
}

is_valid ()
{ # Surprisingly easy to check if word in dictionary ...
  fgrep -qw "$1" "$WLIST"   # ... thanks to 'grep' ...
  echo $?
}

check_word ()
{
  if [ -z "$1" ]
  then
    return
  fi

  Status[1]=""
  Status[2]=""
  Status[3]=""
  Status[4]=""

  iscons=$(is_constructable "$1")
  if [ "$iscons" ]
  then
    Status[1]="constructable" 
    v=$(is_valid "$1")
    if [ "$v" -eq "$SUCCESS" ]
    then
      Status[2]="valid" 
      strlen=${#1}

      if [ ${Score[strlen]} -eq "$MAXCAT" ]   # Category full!
      then
        Status[3]="Category-$strlen-overflow-PENALTY"
        return $NG
      fi

      case "$strlen" in
        1 | 2 )
        Status[3]="Two-letter-word-PENALTY"
        return $NG;;
        * ) 
        Status[3]=""
        return $SUCCESS;;
      esac
    else
      Status[3]="Not-valid-PENALTY"
      return $NG
    fi
  else
    Status[3]="Not-constructable-PENALTY" 
      return $NG
  fi

  ### FIXME: Streamline the above code.

}


display_words ()
{
  local idx=0
  local wlen0

  clear
  echo "Letterset:   ${LS[@]}"
  echo "Threes:    Fours:    Fives:     Sixes:    Sevens:    Eights:"
  echo "------------------------------------------------------------"


   
  while [ "${Words[idx]}" != '' ]
  do
   wlen0=${#Words[idx]}
   case "$wlen0" in
     3) ;;
     4) echo -n "           " ;;
     5) echo -n "                     " ;;
     6) echo -n "                                " ;;
     7) echo -n "                                          " ;;
     8) echo -n "                                                     " ;;
   esac
   echo "${Words[idx]}"
   ((idx++))
  done

  ### FIXME: The word display is pretty crude.
}


play ()
{
  word="Start game"   # Dummy word, to start ...

  while [ "$word" ]   #  If player just hits return (blank word),
  do                  #+ then game ends.
    echo "$word: "${Status[@]}""
    echo -n "Last score: [${Score[0]}]   TOTAL score: [${Score[1]}]:     Next word: "
    total=${Score[1]}
    word=$(get_word)
    check_word "$word"

    if [ "$?" -eq "$SUCCESS" ]
    then
      add_word "$word"
    else
      let "Score[0]= 0 - $PENALTY"
      let "Score[1]-=$PENALTY"
    fi

  display_words
  done   # Exit game.

  ### FIXME: The play () function calls too many other functions.
  ### This is perilously close to "spaghetti code" ...
}

end_of_game ()
{ # Save and display stats.

  #######################Autosave##########################
  savefile=qky.save.$$
  #                 ^^ PID of script
  echo `date` >> $savefile
  echo "Letterset # $randseed  (random seed) ">> $savefile
  echo -n "Letterset: " >> $savefile
  echo "${LS[@]}" >> $savefile
  echo "---------" >> $savefile
  echo "Words constructed:" >> $savefile
  echo "${Words[@]}" >> $savefile
  echo >> $savefile
  echo "Score: $total" >> $savefile

  echo "Statistics for this round saved in \""$savefile"\""
  #########################################################

  echo "Score for this round: $total"
  echo "Words:  ${Words[@]}"
}

# ---------#
instructions
seed_random
get_letset
play
end_of_game
# ---------#

exit $?

# TODO:
#
# 1) Clean up code!
# 2) Prettify the display_words () function (maybe with widgets?).
# 3) Improve the time-out ... maybe change to untimed entry,
#+   but with a time limit for the overall round.   
# 4) An on-screen countdown timer would be nice.
# 5) Implement "vulnerable" mode of play.
# 6) Improve save-to-file capability (and maybe make it optional).
# 7) Fix bugs!!!

# Reference for more info:
# http://bash.neuralshortcircuit.com/qky.README.html

Exemple A.42. Nim

#!/bin/bash
# nim.sh: Game of Nim

# Author: Mendel Cooper
# Reldate: 15 July 2008
# License: GPL3

ROWS=5     # Five rows of pegs (or matchsticks).
WON=91     # Exit codes to keep track of wins/losses.
LOST=92    # Possibly useful if running in batch mode.  
QUIT=99
peg_msg=   # Peg/Pegs?
Rows=( 0 5 4 3 2 1 )   # Array holding play info.
# ${Rows[0]} holds total number of pegs, updated after each turn.
# Other array elements hold number of pegs in corresponding row.

instructions ()
{
  clear
  tput bold
  echo "Welcome to the game of Nim."; echo
  echo -n "Do you need instructions? (y/n) "; read ans

   if [ "$ans" = "y" -o "$ans" = "Y" ]; then
     clear
     echo -e '\E[33;41m'  # Yellow fg., over red bg.; bold.
     cat <<INSTRUCTIONS

Nim is a game with roots in the distant past.
This particular variant starts with five rows of pegs.

1:    | | | | | 
2:     | | | | 
3:      | | | 
4:       | | 
5:        | 

The number at the left identifies the row.

The human player moves first, and alternates turns with the bot.
A turn consists of removing at least one peg from a single row.
It is permissable to remove ALL the pegs from a row.
For example, in row 2, above, the player can remove 1, 2, 3, or 4 pegs.
The player who removes the last peg loses.

The strategy consists of trying to be the one who removes
the next-to-last peg(s), leaving the loser with the final peg.

To exit the game early, hit ENTER during your turn.
INSTRUCTIONS

echo; echo -n "Hit ENTER to begin game. "; read azx

      echo -e "\033[0m"    # Restore display.
      else tput sgr0; clear
  fi

clear

}


tally_up ()
{
  let "Rows[0] = ${Rows[1]} + ${Rows[2]} + ${Rows[3]} + ${Rows[4]} + \
  ${Rows[5]}"    # Add up how many pegs remaining.
}


display ()
{
  index=1   # Start with top row.
  echo

  while [ "$index" -le "$ROWS" ]
  do
    p=${Rows[index]}
    echo -n "$index:   "          # Show row number.

  # ------------------------------------------------
  # Two concurrent inner loops.

      indent=$index
      while [ "$indent" -gt 0 ]
      do
        echo -n " "               # Staggered rows.
        ((indent--))              # Spacing between pegs.
      done

    while [ "$p" -gt 0 ]
    do
      echo -n "| "
      ((p--))
    done
  # -----------------------------------------------

  echo
  ((index++))
  done  

  tally_up

  rp=${Rows[0]}

  if [ "$rp" -eq 1 ]
  then
    peg_msg=peg
    final_msg="Game over."
  else             # Game not yet over . . .
    peg_msg=pegs
    final_msg=""   # . . . So "final message" is blank.
  fi

  echo "      $rp $peg_msg remaining."
  echo "      "$final_msg""


  echo
}

player_move ()
{

  echo "Your move:"

  echo -n "Which row? "
  while read idx
  do                   # Validity check, etc.

    if [ -z "$idx" ]   # Hitting return quits.
    then
        echo "Premature exit."; echo
        tput sgr0      # Restore display.
        exit $QUIT
    fi

    if [ "$idx" -gt "$ROWS" -o "$idx" -lt 1 ]   # Bounds check.
    then
      echo "Invalid row number!"
      echo -n "Which row? "
    else
      break
    fi
    # TODO:
    # Add check for non-numeric input.
    # Also, script crashes on input outside of range of long double.
    # Fix this.

  done

  echo -n "Remove how many? "
  while read num
  do                   # Validity check.

  if [ -z "$num" ]
  then
    echo "Premature exit."; echo
    tput sgr0          # Restore display.
    exit $QUIT
  fi

    if [ "$num" -gt ${Rows[idx]} -o "$num" -lt 1 ]
    then
      echo "Cannot remove $num!"
      echo -n "Remove how many? "
    else
      break
    fi
  done
  # TODO:
  # Add check for non-numeric input.
  # Also, script crashes on input outside of range of long double.
  # Fix this.

  let "Rows[idx] -= $num"

  display
  tally_up

  if [ ${Rows[0]} -eq 1 ]
  then
   echo "      Human wins!"
   echo "      Congratulations!"
   tput sgr0   # Restore display.
   echo
   exit $WON
  fi

  if [ ${Rows[0]} -eq 0 ]
  then          # Snatching defeat from the jaws of victory . . .
    echo "      Fool!"
    echo "      You just removed the last peg!"
    echo "      Bot wins!"
    tput sgr0   # Restore display.
    echo
    exit $LOST
  fi
}


bot_move ()
{

  row_b=0
  while [[ $row_b -eq 0 || ${Rows[row_b]} -eq 0 ]]
  do
    row_b=$RANDOM          # Choose random row.
    let "row_b %= $ROWS"
  done


  num_b=0
  r0=${Rows[row_b]}

  if [ "$r0" -eq 1 ]
  then
    num_b=1
  else
    let "num_b = $r0 - 1"
         #  Leave only a single peg in the row.
  fi     #  Not a very strong strategy,
         #+ but probably a bit better than totally random.

  let "Rows[row_b] -= $num_b"
  echo -n "Bot:  "
  echo "Removing from row $row_b ... "

  if [ "$num_b" -eq 1 ]
  then
    peg_msg=peg
  else
    peg_msg=pegs
  fi

  echo "      $num_b $peg_msg."

  display
  tally_up

  if [ ${Rows[0]} -eq 1 ]
  then
   echo "      Bot wins!"
   tput sgr0   # Restore display.
   exit $WON
  fi

}


# ================================================== #
instructions     # If human player needs them . . .
tput bold        # Bold characters for easier viewing.
display          # Show game board.

while [ true ]   # Main loop.
do               # Alternate human and bot turns.
  player_move
  bot_move
done
# ================================================== #

# Exercise:
# --------
# Improve the bot's strategy.
# There is, in fact, a Nim strategy that can force a win.
# See the Wikipedia article on Nim:  http://en.wikipedia.org/wiki/Nim
# Recode the bot to use this strategy (rather difficult).

#  Curiosities:
#  -----------
#  Nim played a prominent role in Alain Resnais' 1961 New Wave film,
#+ Last Year at Marienbad.
#
#  In 1978, Leo Christopherson wrote an animated version of Nim,
#+ Android Nim, for the TRS-80 Model I.

Exemple A.43. Chronomètre en ligne de commande

#!/bin/sh
# sw.sh
# A command-line Stopwatch

# Author: Pádraig Brady
#    http://www.pixelbeat.org/scripts/sw
#    (Minor reformatting by ABS Guide author.)
#    Used in ABS Guide with script author's permission.
# Notes:
#    This script starts a few processes per lap, in addition to
#    the shell loop processing, so the assumption is made that
#    this takes an insignificant amount of time compared to
#    the response time of humans (~.1s) (or the keyboard
#    interrupt rate (~.05s)).
#    '?' for splits must be entered twice if characters
#    (erroneously) entered before it (on the same line).
#    '?' since not generating a signal may be slightly delayed
#    on heavily loaded systems.
#    Lap timings on ubuntu may be slightly delayed due to:
#    https://bugs.launchpad.net/bugs/62511
# Changes:
#    V1.0, 23 Aug 2005, Initial release
#    V1.1, 26 Jul 2007, Allow both splits and laps from single invocation.
#                       Only start timer after a key is pressed.
#                       Indicate lap number
#                       Cache programs at startup so there is less error
#                       due to startup delays.
#    V1.2, 01 Aug 2007, Work around `date` commands that don't have
#                       nanoseconds.
#                       Use stty to change interrupt keys to space for
#                       laps etc.
#                       Ignore other input as it causes problems.
#    V1.3, 01 Aug 2007, Testing release.
#    V1.4, 02 Aug 2007, Various tweaks to get working under ubuntu
#                       and Mac OS X.
#    V1.5, 27 Jun 2008, set LANG=C as got vague bug report about it.

export LANG=C

ulimit -c 0   # No coredumps from SIGQUIT.
trap '' TSTP  # Ignore Ctrl-Z just in case.
save_tty=`stty -g` && trap "stty $save_tty" EXIT  # Restore tty on exit.
stty quit ' ' # Space for laps rather than Ctrl-\.
stty eof  '?' # ? for splits rather than Ctrl-D.
stty -echo    # Don't echo input.

cache_progs() {
    stty > /dev/null
    date > /dev/null
    grep . < /dev/null
    (echo "import time" | python) 2> /dev/null
    bc < /dev/null
    sed '' < /dev/null
    printf '1' > /dev/null
    /usr/bin/time false 2> /dev/null
    cat < /dev/null
}
cache_progs   # To minimise startup delay.

date +%s.%N | grep -qF 'N' && use_python=1 # If `date` lacks nanoseconds.
now() {
    if [ "$use_python" ]; then
        echo "import time; print time.time()" 2>/dev/null | python
    else
        printf "%.2f" `date +%s.%N`
    fi
}

fmt_seconds() {
    seconds=$1
    mins=`echo $seconds/60 | bc`
    if [ "$mins" != "0" ]; then
        seconds=`echo "$seconds - ($mins*60)" | bc`
        echo "$mins:$seconds"
    else
        echo "$seconds"
    fi
}

total() {
    end=`now`
    total=`echo "$end - $start" | bc`
    fmt_seconds $total
}

stop() {
    [ "$lapped" ] && lap "$laptime" "display"
    total
    exit
}

lap() {
    laptime=`echo "$1" | sed -n 's/.*real[^0-9.]*\(.*\)/\1/p'`
    [ ! "$laptime" -o "$laptime" = "0.00" ] && return
    # Signals too frequent.
    laptotal=`echo $laptime+0$laptotal | bc`
    if [ "$2" = "display" ]; then
        lapcount=`echo 0$lapcount+1 | bc`
        laptime=`fmt_seconds $laptotal`
        echo $laptime "($lapcount)"
        lapped="true"
        laptotal="0"
    fi
}

echo -n "Space for lap | ? for split | Ctrl-C to stop | Space to start...">&2

while true; do
    trap true INT QUIT  # Set signal handlers.
    laptime=`/usr/bin/time -p 2>&1 cat >/dev/null`
    ret=$?
    trap '' INT QUIT    # Ignore signals within this script.
    if [ $ret -eq 1 -o $ret -eq 2 -o $ret -eq 130 ]; then # SIGINT = stop
        [ ! "$start" ] && { echo >&2; exit; }
        stop
    elif [ $ret -eq 3 -o $ret -eq 131 ]; then             # SIGQUIT = lap
        if [ ! "$start" ]; then
            start=`now` || exit 1
            echo >&2
            continue
        fi
        lap "$laptime" "display"
    else                # eof = split
        [ ! "$start" ] && continue
        total
        lap "$laptime"  # Update laptotal.
    fi
done

exit $?

Exemple A.44. Création de devoirs sur la programmation shell en général

#!/bin/bash
#  homework.sh: All-purpose homework assignment solution.
#  Author: M. Leo Cooper
#  If you substitute your own name as author, then it is plagiarism,
#+ possibly a lesser sin than cheating on your homework!
#  License: Public Domain

#  This script may be turned in to your instructor
#+ in fulfillment of ALL shell scripting homework assignments.
#  It's sparsely commented, but you, the student, can easily remedy that.
#  The script author repudiates all responsibility!

DLA=1
P1=2
P2=4
P3=7
PP1=0
PP2=8
MAXL=9
E_LZY=99

declare -a L
L[0]="3 4 0 17 29 8 13 18 19 17 20 2 19 14 17 28"
L[1]="8 29 12 14 18 19 29 4 12 15 7 0 19 8 2 0 11 11 24 29 17 4 6 17 4 19"
L[2]="29 19 7 0 19 29 8 29 7 0 21 4 29 13 4 6 11 4 2 19 4 3"
L[3]="19 14 29 2 14 12 15 11 4 19 4 29 19 7 8 18 29"
L[4]="18 2 7 14 14 11 22 14 17 10 29 0 18 18 8 6 13 12 4 13 19 26"
L[5]="15 11 4 0 18 4 29 0 2 2 4 15 19 29 12 24 29 7 20 12 1 11 4 29"
L[6]="4 23 2 20 18 4 29 14 5 29 4 6 17 4 6 8 14 20 18 29"
L[7]="11 0 25 8 13 4 18 18 27"
L[8]="0 13 3 29 6 17 0 3 4 29 12 4 29 0 2 2 14 17 3 8 13 6 11 24 26"
L[9]="19 7 0 13 10 29 24 14 20 26"

declare -a \
alph=( A B C D E F G H I J K L M N O P Q R S T U V W X Y Z . , : ' ' )


pt_lt ()
{
  echo -n "${alph[$1]}"
  echo -n -e "\a"
  sleep $DLA
}

b_r ()
{
 echo -e '\E[31;48m\033[1m'
}

cr ()
{
 echo -e "\a"
 sleep $DLA
}

restore ()
{
  echo -e '\033[0m'            # Bold off.
  tput sgr0                    # Normal.
}


p_l ()
{
  for ltr in $1
  do
    pt_lt "$ltr"
  done
}

# ----------------------
b_r

for i in $(seq 0 $MAXL)
do
  p_l "${L[i]}"
  if [[ "$i" -eq "$P1" || "$i" -eq "$P2" || "$i" -eq "$P3" ]]
  then
    cr
  elif [[ "$i" -eq "$PP1" || "$i" -eq "$PP2" ]]
  then
    cr; cr
  fi
done

restore
# ----------------------

echo

exit $E_LZY

#  A typical example of an obfuscated script that is difficult
#+ to understand, and frustrating to maintain.
#  In your career as a sysadmin, you'll run into these critters
#+ all too often.

Exemple A.45. Le Circuit du Chevalier

#!/bin/bash
# ktour.sh

# author: mendel cooper
# reldate: 12 Jan 2009
# license: public domain
# (Not much sense GPLing something that's pretty much in the common
#+ domain anyhow.)

###################################################################
#             The Knight's Tour, a classic problem.               #
#             =====================================               #
#  The knight must move onto every square of the chess board,     #
#  but cannot revisit any square he has already visited.          #
#                                                                 #
#  And just why is Sir Knight unwelcome for a return visit?       #
#  Could it be that he has a habit of partying into the wee hours #
#+ of the morning?                                                #
#  Possibly he leaves pizza crusts in the bed, empty beer bottles #
#+ all over the floor, and clogs the plumbing. . . .              #
#                                                                 #
#  -------------------------------------------------------------  #
#                                                                 #
#  Usage: ktour.sh [start-square] [stupid]                        #
#                                                                 #
#  Note that start-square can be a square number                  #
#+ in the range 0 - 63 ... or                                     #
#  a square designator in conventional chess notation,            #
#  such as a1, f5, h3, etc.                                       #
#                                                                 #
#  If start-square-number not supplied,                           #
#+ then starts on a random square somewhere on the board.         #
#                                                                 #
# "stupid" as second parameter sets the stupid strategy.          #
#                                                                 #
#  Examples:                                                      #
#  ktour.sh 23          starts on square #23 (h3)                 #
#  ktour.sh g6 stupid   starts on square #46,                     #
#                       using "stupid" (non-Warnsdorff) strategy. #
###################################################################

DEBUG=      # Set this to echo debugging info to stdout.
SUCCESS=0
FAIL=99
BADMOVE=-999
FAILURE=1
LINELEN=21  # How many moves to display per line.
# ---------------------------------------- #
# Board array params
ROWS=8   # 8 x 8 board.
COLS=8
let "SQUARES = $ROWS * $COLS"
let "MAX = $SQUARES - 1"
MIN=0
# 64 squares on board, indexed from 0 to 63.

VISITED=1
UNVISITED=-1
UNVSYM="##"
# ---------------------------------------- #
# Global variables.
startpos=    # Starting position (square #, 0 - 63).
currpos=     # Current position.
movenum=     # Move number.
CRITPOS=37   # Have to patch for f5 starting position!

declare -i board
# Use a one-dimensional array to simulate a two-dimensional one.
# This can make life difficult and result in ugly kludges; see below.
declare -i moves  # Offsets from current knight position.


initialize_board ()
{
  local idx

  for idx in {0..63}
  do
    board[$idx]=$UNVISITED
  done
}



print_board ()
{
  local idx

  echo "    _____________________________________"
  for row in {7..0}               #  Reverse order of rows ...
  do                              #+ so it prints in chessboard order.
    let "rownum = $row + 1"       #  Start numbering rows at 1.
    echo -n "$rownum  |"          #  Mark board edge with border and
    for column in {0..7}          #+ "algebraic notation."
    do
      let "idx = $ROWS*$row + $column"
      if [ ${board[idx]} -eq $UNVISITED ]
      then
        echo -n "$UNVSYM   "      ##
      else                        # Mark square with move number.
        printf "%02d " "${board[idx]}"; echo -n "  "
      fi
    done
    echo -e -n "\b\b\b|"  # \b is a backspace.
    echo                  # -e enables echoing escaped chars.
  done

  echo "    -------------------------------------"
  echo "     a    b    c    d    e    f    g    h"
}



failure()
{ # Whine, then bail out.
  echo
  print_board
  echo
  echo    "   Waah!!! Ran out of squares to move to!"
  echo -n "   Knight's Tour attempt ended"
  echo    " on $(to_algebraic $currpos) [square #$currpos]"
  echo    "   after just $movenum moves!"
  echo
  exit $FAIL
}



xlat_coords ()   #  Translate x/y coordinates to board position
{                #+ (board-array element #).
  #  For user input of starting board position as x/y coords.
  #  This function not used in initial release of ktour.sh.
  #  May be used in an updated version, for compatibility with
  #+ standard implementation of the Knight's Tour in C, Python, etc.
  if [ -z "$1" -o -z "$2" ]
  then
    return $FAIL
  fi

  local xc=$1
  local yc=$2

  let "board_index = $xc * $ROWS + yc"

  if [ $board_index -lt $MIN -o $board_index -gt $MAX ]
  then
    return $FAIL    # Strayed off the board!
  else
    return $board_index
  fi
}



to_algebraic ()   #  Translate board position (board-array element #)
{                 #+ to standard algebraic notation used by chess players.
  if [ -z "$1" ]
  then
    return $FAIL
  fi

  local element_no=$1   # Numerical board position.
  local col_arr=( a b c d e f g h )
  local row_arr=( 1 2 3 4 5 6 7 8 )

  let "row_no = $element_no / $ROWS"
  let "col_no = $element_no % $ROWS"
  t1=${col_arr[col_no]}; t2=${row_arr[row_no]}
  local apos=$t1$t2   # Concatenate.
  echo $apos
}



from_algebraic ()   #  Translate standard algebraic chess notation
{                   #+ to numerical board position (board-array element #).
                    #  Or recognize numerical input & return it unchanged.
  if [ -z "$1" ]
  then
    return $FAIL
  fi   # If no command-line arg, then will default to random start pos.

  local ix
  local ix_count=0
  local b_index     # Board index [0-63]
  local alpos="$1"

  arow=${alpos:0:1} # position = 0, length = 1
  acol=${alpos:1:1}

  if [[ $arow =~ [[:digit:]] ]]   #  Numerical input?
  then       #  POSIX char class
    if [[ $acol =~ [[:alpha:]] ]] # Number followed by a letter? Illegal!
      then return $FAIL
    else if [ $alpos -gt $MAX ]   # Off board?
      then return $FAIL
    else return $alpos            #  Return digit(s) unchanged . . .
      fi                          #+ if within range.
    fi
  fi

  if [[ $acol -eq $MIN || $acol -gt $ROWS ]]
  then        # Outside of range 1 - 8?
    return $FAIL
  fi

  for ix in a b c d e f g h
  do  # Convert column letter to column number.
   if [ "$arow" = "$ix" ]
   then
     break
   fi
  ((ix_count++))    # Find index count.
  done

  ((acol--))        # Decrementing converts to zero-based array.
  let "b_index = $ix_count + $acol * $ROWS"

  if [ $b_index -gt $MAX ]   # Off board?
  then
    return $FAIL
  fi
    
  return $b_index

}


generate_moves ()   #  Calculate all valid knight moves,
{                   #+ relative to current position ($1),
                    #+ and store in ${moves} array.
  local kt_hop=1    #  One square  :: short leg of knight move.
  local kt_skip=2   #  Two squares :: long leg  of knight move.
  local valmov=0    #  Valid moves.
  local row_pos; let "row_pos = $1 % $COLS"


  let "move1 = -$kt_skip + $ROWS"           # 2 sideways to-the-left,  1 up
    if [[ `expr $row_pos - $kt_skip` -lt $MIN ]]   # An ugly, ugly kludge!
    then                                           # Can't move off board.
      move1=$BADMOVE                               # Not even temporarily.
    else
      ((valmov++))
    fi
  let "move2 = -$kt_hop + $kt_skip * $ROWS" # 1 sideways to-the-left,  2 up
    if [[ `expr $row_pos - $kt_hop` -lt $MIN ]]    # Kludge continued ...
    then
      move2=$BADMOVE
    else
      ((valmov++))
    fi
  let "move3 =  $kt_hop + $kt_skip * $ROWS" # 1 sideways to-the-right, 2 up
    if [[ `expr $row_pos + $kt_hop` -ge $COLS ]]
    then
      move3=$BADMOVE
    else
      ((valmov++))
    fi
  let "move4 =  $kt_skip + $ROWS"           # 2 sideways to-the-right, 1 up
    if [[ `expr $row_pos + $kt_skip` -ge $COLS ]]
    then
      move4=$BADMOVE
    else
      ((valmov++))
    fi
  let "move5 =  $kt_skip - $ROWS"           # 2 sideways to-the-right, 1 dn
    if [[ `expr $row_pos + $kt_skip` -ge $COLS ]]
    then
      move5=$BADMOVE
    else
      ((valmov++))
    fi
  let "move6 =  $kt_hop - $kt_skip * $ROWS" # 1 sideways to-the-right, 2 dn
    if [[ `expr $row_pos + $kt_hop` -ge $COLS ]]
    then
      move6=$BADMOVE
    else
      ((valmov++))
    fi
  let "move7 = -$kt_hop - $kt_skip * $ROWS" # 1 sideways to-the-left,  2 dn
    if [[ `expr $row_pos - $kt_hop` -lt $MIN ]]
    then
      move7=$BADMOVE
    else
      ((valmov++))
    fi
  let "move8 = -$kt_skip - $ROWS"           # 2 sideways to-the-left,  1 dn
    if [[ `expr $row_pos - $kt_skip` -lt $MIN ]]
    then
      move8=$BADMOVE
    else
      ((valmov++))
    fi   # There must be a better way to do this.

  local m=( $valmov $move1 $move2 $move3 $move4 $move5 $move6 $move7 $move8 )
  # ${moves[0]} = number of valid moves.
  # ${moves[1]} ... ${moves[8]} = possible moves.
  echo "${m[*]}"    # Elements of array to stdout for capture in a var.

}



is_on_board ()  # Is position actually on the board?
{
  if [[ "$1" -lt "$MIN" || "$1" -gt "$MAX" ]]
  then
    return $FAILURE
  else
    return $SUCCESS
  fi
}



do_move ()      # Move the knight!
{
  local valid_moves=0
  local aapos
  currposl="$1"
  lmin=$ROWS
  iex=0
  squarel=
  mpm=
  mov=
  declare -a p_moves

  ########################## DECIDE-MOVE #############################
  if [ $startpos -ne $CRITPOS ]
  then   # CRITPOS = square #37
    decide_move
  else                     # Needs a special patch for startpos=37 !!!
    decide_move_patched    # Why this particular move and no other ???
  fi
  ####################################################################

  (( ++movenum ))          # Increment move count.
  let "square = $currposl + ${moves[iex]}"

  ##################    DEBUG    ###############
  if [ "$DEBUG" ]
    then debug   # Echo debugging information.
  fi
  ##############################################

  if [[ "$square" -gt $MAX || "$square" -lt $MIN ||
        ${board[square]} -ne $UNVISITED ]]
  then
    (( --movenum ))              #  Decrement move count,
    echo "RAN OUT OF SQUARES!!!" #+ since previous one was invalid.
    return $FAIL
  fi

  board[square]=$movenum
  currpos=$square       # Update current position.
  ((valid_moves++));    # moves[0]=$valid_moves
  aapos=$(to_algebraic $square)
  echo -n "$aapos "
  test $(( $Moves % $LINELEN )) -eq 0 && echo
  # Print LINELEN=21 moves per line. A valid tour shows 3 complete lines.
  return $valid_moves   # Found a square to move to!
}



do_move_stupid()   #  Dingbat algorithm,
{                  #+ courtesy of script author, *not* Warnsdorff.
  local valid_moves=0
  local movloc
  local squareloc
  local aapos
  local cposloc="$1"

  for movloc in {1..8}
  do       # Move to first-found unvisited square.
    let "squareloc = $cposloc + ${moves[movloc]}"
    is_on_board $squareloc
    if [ $? -eq $SUCCESS ] && [ ${board[squareloc]} -eq $UNVISITED ]
    then   # Add conditions to above if-test to improve algorithm.
      (( ++movenum ))
      board[squareloc]=$movenum
      currpos=$squareloc     # Update current position.
      ((valid_moves++));     # moves[0]=$valid_moves
      aapos=$(to_algebraic $squareloc)
      echo -n "$aapos "
      test $(( $Moves % $LINELEN )) -eq 0 && echo   # Print 21 moves/line.
      return $valid_moves    # Found a square to move to!
    fi
  done

  return $FAIL
  #  If no square found in all 8 loop iterations,
  #+ then Knight's Tour attempt ends in failure.

  #  Dingbat algorithm will typically fail after about 30 - 40 moves,
  #+ but executes _much_ faster than Warnsdorff's in do_move() function.
}



decide_move ()         #  Which move will we make?
{                      #  But, fails on startpos=37 !!!
  for mov in {1..8}
  do
    let "squarel = $currposl + ${moves[mov]}"
    is_on_board $squarel
    if [[ $? -eq $SUCCESS && ${board[squarel]} -eq $UNVISITED ]]
    then   #  Find accessible square with least possible future moves.
           #  This is Warnsdorff's algorithm.
           #  What happens is that the knight wanders toward the outer edge
           #+ of the board, then pretty much spirals inward.
           #  Given two or more possible moves with same value of
           #+ least-possible-future-moves, this implementation chooses
           #+ the _first_ of those moves.
           #  This means that there is not necessarily a unique solution
           #+ for any given starting position.

      possible_moves $squarel
      mpm=$?
      p_moves[mov]=$mpm
      
      if [ $mpm -lt $lmin ]  # If less than previous minimum ...
      then #     ^^
        lmin=$mpm            # Update minimum.
        iex=$mov             # Save index.
      fi

    fi
  done
}



decide_move_patched ()         #  Decide which move to make,
{  #        ^^^^^^^            #+ but only if startpos=37 !!!
  for mov in {1..8}
  do
    let "squarel = $currposl + ${moves[mov]}"
    is_on_board $squarel
    if [[ $? -eq $SUCCESS && ${board[squarel]} -eq $UNVISITED ]]
    then
      possible_moves $squarel
      mpm=$?
      p_moves[mov]=$mpm
      
      if [ $mpm -le $lmin ]  # If less-than-or equal to prev. minimum!
      then #     ^^
        lmin=$mpm
        iex=$mov
      fi

    fi
  done                       # There has to be a better way to do this.
}



possible_moves ()            #  Calculate number of possible moves,
{                            #+ given the current position.

  if [ -z "$1" ]
  then
    return $FAIL
  fi

  local curr_pos=$1
  local valid_movl=0
  local icx=0
  local movl
  local sq
  declare -a movesloc

  movesloc=( $(generate_moves $curr_pos) )

  for movl in {1..8}
  do
    let "sq = $curr_pos + ${movesloc[movl]}"
    is_on_board $sq
    if [ $? -eq $SUCCESS ] && [ ${board[sq]} -eq $UNVISITED ]
    then
      ((valid_movl++));
    fi
  done

  return $valid_movl         # Found a square to move to!
}


strategy ()
{
  echo

  if [ -n "$STUPID" ]
  then
    for Moves in {1..63}
    do
      cposl=$1
      moves=( $(generate_moves $currpos) )
      do_move_stupid "$currpos"
      if [ $? -eq $FAIL ]
      then
        failure
      fi
      done
  fi

  #  Don't need an "else" clause here,
  #+ because Stupid Strategy will always fail and exit!
  for Moves in {1..63}
  do
    cposl=$1
    moves=( $(generate_moves $currpos) )
    do_move "$currpos"
    if [ $? -eq $FAIL ]
    then
      failure
    fi

  done
        #  Could have condensed above two do-loops into a single one,
  echo  #+ but this would have slowed execution.

  print_board
  echo
  echo "Knight's Tour ends on $(to_algebraic $currpos) [square #$currpos]."
  return $SUCCESS
}

debug ()
{       # Enable this by setting DEBUG=1 near beginning of script.
  local n

  echo "================================="
  echo "  At move number  $movenum:"
  echo " *** possible moves = $mpm ***"
# echo "### square = $square ###"
  echo "lmin = $lmin"
  echo "${moves[@]}"

  for n in {1..8}
  do
    echo -n "($n):${p_moves[n]} "
  done

  echo
  echo "iex = $iex :: moves[iex] = ${moves[iex]}"
  echo "square = $square"
  echo "================================="
  echo
} # Gives pretty complete status after ea. move.



# =============================================================== #
# int main () {
from_algebraic "$1"
startpos=$?
if [ "$startpos" -eq "$FAIL" ]          # Okay even if no $1.
then   #         ^^^^^^^^^^^              Okay even if input -lt 0.
  echo "No starting square specified (or illegal input)."
  let "startpos = $RANDOM % $SQUARES"   # 0 - 63 permissable range.
fi


if [ "$2" = "stupid" ]
then
  STUPID=1
  echo -n "     ### Stupid Strategy ###"
else
  STUPID=''
  echo -n "  *** Warnsdorff's Algorithm ***"
fi


initialize_board

movenum=0
board[startpos]=$movenum   # Mark each board square with move number.
currpos=$startpos
algpos=$(to_algebraic $startpos)

echo; echo "Starting from $algpos [square #$startpos] ..."; echo
echo -n "Moves:"

strategy "$currpos"

echo

exit 0   # return 0;

# }      # End of main() pseudo-function.
# =============================================================== #


# Exercises:
# ---------
#
# 1) Extend this example to a 10 x 10 board or larger.
# 2) Improve the "stupid strategy" by modifying the
#    do_move_stupid function.
#    Hint: Prevent straying into corner squares in early moves
#          (the exact opposite of Warnsdorff's algorithm!).
# 3) This script could stand considerable improvement and
#    streamlining, especially in the poorly-written
#    generate_moves() function
#    and in the DECIDE-MOVE patch in the do_move() function.
#    Must figure out why standard algorithm fails for startpos=37 ...
#+   but _not_ on any other, including symmetrical startpos=26.
#    Possibly, when calculating possible moves, counts the move back
#+   to the originating square. If so, it might be a relatively easy fix.

Exemple A.46. Carrés magiques

#!/bin/bash
# msquare.sh
# Magic Square generator (odd-order squares only!)

# Author: mendel cooper
# reldate: 19 Jan. 2009
# License: Public Domain
# A C-program by Kwon Young Shin inspired this script.
# See http://user.chollian.net/~brainstm/MagicSquare.htm ...

# Definition: A "magic square" is a two-dimensional array
#             of integers in which all the rows, columns,
#             and *long* diagonals add up to the same number.
#             Being "square," the array has the same number
#             of rows and columns.
# An example of a magic square of order 3 is:
#   8  1  6   
#   3  5  7   
#   4  9  2   
# All the rows, columns, and long diagonals add up to 15.


# Globals
EVEN=2
MAXSIZE=31   # 31 rows x 31 cols.
E_usage=90   # Invocation error.
dimension=
declare -i square

usage_message ()
{
  echo "Usage: $0 square-size"
  echo "   ... where \"square-size\" is an ODD integer"
  echo "       in the range 3 - 31."
  #  Actually works for squares up to order 159,
  #+ but large squares will not display pretty-printed in a term window.
  #  Try increasing MAXSIZE, above.
  exit $E_usage
}


calculate ()       # Here's where the actual work gets done.
{
  local row col index dimadj j k cell_val=1
  dimension=$1

  let "dimadj = $dimension * 3"; let "dimadj /= 2"   # x 1.5, then truncate.

  for ((j=0; j < dimension; j++))
  do
    for ((k=0; k < dimension; k++))
    do  # Calculate indices, then convert to 1-dim. array index.
        # Bash doesn't support multidimensional arrays. Pity.
      let "col = $k - $j + $dimadj"; let "col %= $dimension"
      let "row = $j * 2 - $k + $dimension"; let "row %= $dimension"
      let "index = $row*($dimension) + $col"
      square[$index]=cell_val; ((cell_val++))
    done
  done
}     # Plain math, no visualization required.


print_square ()               # Output square, one row at a time.
{
  local row col idx d1
  let "d1 = $dimension - 1"   # Adjust for zero-indexed array.
 
  for row in $(seq 0 $d1)
  do

    for col in $(seq 0 $d1)
    do
      let "idx = $row * $dimension + $col"
      printf "%3d " "${square[idx]}"; echo -n "  "
    done   # Displays up to 13-order neatly in 80-column term window.

    echo   # Newline after each row.
  done
}


#################################################
if [[ -z "$1" ]] || [[ "$1" -gt $MAXSIZE ]]
then
  usage_message
fi

let "test_even = $1 % $EVEN"
if [ $test_even -eq 0 ]
then           # Can't handle even-order squares.
  usage_message
fi

calculate $1
print_square   # echo "${square[@]}"   # DEBUG

exit $?
#################################################


# Exercises:
# ---------
# 1) Add a function to calculate the sum of each row, column,
#    and *long* diagonal. The sums must match.
#    This is the "magic constant" of that particular order square.
# 2) Have the print_square function auto-calculate how much space
#    to allot between square elements for optimized display.
#    This might require parameterizing the "printf" line.
# 3) Add appropriate functions for generating magic squares
#    with an *even* number of rows/columns.
#    This is non-trivial(!).
#    See the URL for Kwon Young Shin, above, for help.

Exemple A.47. Taquin

#!/bin/bash
# fifteen.sh

# Classic "Fifteen Puzzle"
# Author: Antonio Macchi
# Lightly edited and commented by ABS Guide author.
# Used in ABS Guide with permission. (Thanks!)

#  The invention of the Fifteen Puzzle is attributed to either
#+ Sam Loyd or Noyes Palmer Chapman.
#  The puzzle was wildly popular in the late 19th-century.

#  Object: Rearrange the numbers so they read in order,
#+ from 1 - 15:   ________________
#                |  1   2   3   4 |
#                |  5   6   7   8 |
#                |  9  10  11  12 |
#                | 13  14  15     |
#                 ----------------


#######################
# Constants           #
  SQUARES=16          #
  FAIL=70             #
  E_PREMATURE_EXIT=80 #
#######################


########
# Data #
########

Puzzle=( 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 " " )


#############
# Functions #
#############

function swap
{
  local tmp

  tmp=${Puzzle[$1]}
  Puzzle[$1]=${Puzzle[$2]}
  Puzzle[$2]=$tmp
}


function Jumble
{ # Scramble the pieces at beginning of round.
  local i pos1 pos2

  for i in {1..100}
  do
    pos1=$(( $RANDOM % $SQUARES))
    pos2=$(( $RANDOM % $SQUARES ))
    swap $pos1 $pos2
  done
}


function PrintPuzzle
{
  local i1 i2 puzpos
  puzpos=0

  clear
  echo "Enter  quit  to exit."; echo   # Better that than Ctl-C.

  echo ",----.----.----.----."   # Top border.
  for i1 in {1..4}
  do
    for i2 in {1..4} 
    do
      printf "| %2s " "${Puzzle[$puzpos]}"
      (( puzpos++ ))
    done
    echo "|"                     # Right-side border.
    test $i1 = 4 || echo "+----+----+----+----+"
  done
  echo "'----'----'----'----'"   # Bottom border.
}


function GetNum
{ # Test for valid input.
  local puznum garbage

  while true
  do 
          echo "Moves: $moves" # Also counts invalid moves.
    read -p "Number to move: " puznum garbage
      if [ "$puznum" = "quit" ]; then echo; exit $E_PREMATURE_EXIT; fi
    test -z "$puznum" -o -n "${puznum//[0-9]/}" && continue
    test $puznum -gt 0 -a $puznum -lt $SQUARES && break
  done
  return $puznum
}


function GetPosFromNum
{ # $1 = puzzle-number
  local puzpos

  for puzpos in {0..15}
  do
    test "${Puzzle[$puzpos]}" = "$1" && break
  done
  return $puzpos
}


function Move
{ # $1=Puzzle-pos
  test $1 -gt 3 && test "${Puzzle[$(( $1 - 4 ))]}" = " "\
       && swap $1 $(( $1 - 4 )) && return 0
  test $(( $1%4 )) -ne 3 && test "${Puzzle[$(( $1 + 1 ))]}" = " "\
       && swap $1 $(( $1 + 1 )) && return 0
  test $1 -lt 12 && test "${Puzzle[$(( $1 + 4 ))]}" = " "\
       && swap $1 $(( $1 + 4 )) && return 0
  test $(( $1%4 )) -ne 0 && test "${Puzzle[$(( $1 - 1 ))]}" = " " &&\
       swap $1 $(( $1 - 1 )) && return 0
  return 1
}


function Solved
{
  local pos

  for pos in {0..14}
  do
    test "${Puzzle[$pos]}" = $(( $pos + 1 )) || return $FAIL
    # Check whether number in each square = square number.
  done
  return 0   # Successful solution.
}


################### MAIN () ########################
moves=0
Jumble

while true   # Loop continuously until puzzle solved.
do
  echo; echo
  PrintPuzzle
  echo
  while true
  do
    GetNum
    puznum=$?
    GetPosFromNum $puznum
    puzpos=$?
    ((moves++))
    Move $puzpos && break
  done
  Solved && break
done

echo;echo
PrintPuzzle
echo; echo "BRAVO!"; echo

exit 0
####################################################

#  Exercise:
#  --------
#  Rewrite the script to display the letters A - O,
#+ rather than the numbers 1 - 15.

Exemple A.48. Tours de Hanoi, version graphique

#! /bin/bash
# The Towers Of Hanoi
# Original script (hanoi.bash) copyright (C) 2000 Amit Singh.
# All Rights Reserved.
# http://hanoi.kernelthread.com

#  hanoi2.bash
#  Version 2: modded for ASCII-graphic display.
#  Uses code contributed by Antonio Macchi,
#+ with heavy editing by ABS Guide author.
#  This variant also falls under the original copyright, see above.
#  Used in ABS Guide with Amit Singh's permission (thanks!).


#   Variables   #
E_NOPARAM=86
E_BADPARAM=87   # Illegal no. of disks passed to script.
E_NOEXIT=88
DISKS=$1
Moves=0

MWIDTH=7
MARGIN=2
# Arbitrary "magic" constants, work okay for relatively small # of disks.
# BASEWIDTH=51   # Original code.
let "basewidth = $MWIDTH * $DISKS + $MARGIN" # "Base" beneath rods.
# Above "algorithm" could likely stand improvement.

# Display variables.
let "disks1 = $DISKS - 1"
let "spaces1 = $DISKS" 
let "spaces2 = 2 * $DISKS" 

let "lastmove_t = $DISKS - 1"                # Final move?


declare -a Rod1 Rod2 Rod3

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


function repeat  {  # $1=char $2=number of repetitions
  local n           # Repeat-print a character.
  
  for (( n=0; n<&$2; n++ )); do
    echo -n "$1"
  done
}

function FromRod  {
  local rod summit weight sequence

  while true; do
    rod=$1
    test ${rod/[^123]/} || continue

    sequence=$(echo $(seq 0 $disks1 | tac))
    for summit in $sequence; do
      eval weight=\${Rod${rod}[$summit]}
      test $weight -ne 0 &&
           { echo "$rod $summit $weight"; return; }
    done
  done
}


function ToRod  { # $1=previous (FromRod) weight
  local rod firstfree weight sequence
  
  while true; do
    rod=$2
    test ${rod/[^123]} || continue

    sequence=$(echo $(seq 0 $disks1 | tac))
    for firstfree in $sequence; do
      eval weight=\${Rod${rod}[$firstfree]}
      test $weight -gt 0 && { (( firstfree++ )); break; }
    done
    test $weight -gt $1 -o $firstfree = 0 &&
         { echo "$rod $firstfree"; return; }
  done
}


function PrintRods  {
  local disk rod empty fill sp sequence


  repeat " " $spaces1
  echo -n "|"
  repeat " " $spaces2
  echo -n "|"
  repeat " " $spaces2
  echo "|"

  sequence=$(echo $(seq 0 $disks1 | tac))
  for disk in $sequence; do
    for rod in {1..3}; do
      eval empty=$(( $DISKS - (Rod${rod}[$disk] / 2) ))
      eval fill=\${Rod${rod}[$disk]}
      repeat " " $empty
      test $fill -gt 0 && repeat "*" $fill || echo -n "|"
      repeat " " $empty
    done
    echo
  done
  repeat "=" $basewidth   # Print "base" beneath rods.
  echo
}


display ()
{
  echo
  PrintRods

  # Get rod-number, summit and weight
  first=( `FromRod $1` )
  eval Rod${first[0]}[${first[1]}]=0

  # Get rod-number and first-free position
  second=( `ToRod ${first[2]} $2` )
  eval Rod${second[0]}[${second[1]}]=${first[2]}


echo; echo; echo
if [ "${Rod3[lastmove_t]}" = 1 ]
then   # Last move? If yes, then display final position.
    echo "+  Final Position: $Moves moves"; echo
    PrintRods
  fi
}

# From here down, almost the same as original (hanoi.bash) script.

dohanoi() {   # Recursive function.
    case $1 in
    0)
        ;;
    *)
        dohanoi "$(($1-1))" $2 $4 $3
        if [ "$Moves" -ne 0 ]
        then
          echo "+  Position after move $Moves"
        fi
        ((Moves++))
        echo -n "   Next move will be:  "
        echo $2 "-->" $3
          display $2 $3
        dohanoi "$(($1-1))" $4 $3 $2
        ;;
    esac
}

setup_arrays ()
{
  local dim n elem

  let "dim1 = $1 - 1"
  elem=$dim1

  for n in $(seq 0 $dim1)
  do
   let "Rod1[$elem] = 2 * $n + 1"
   Rod2[$n]=0
   Rod3[$n]=0
   ((elem--))
  done
}


###   Main   ###

setup_arrays $DISKS
echo; echo "+  Start Position"

case $# in
    1) case $(($1>0)) in     # Must have at least one disk.
       1)
           disks=$1
           dohanoi $1 1 3 2
#          Total moves = 2^n - 1, where n = # of disks.
           echo
           exit 0;
           ;;
       *)
           echo "$0: Illegal value for number of disks";
           exit $E_BADPARAM;
           ;;
       esac
    ;;
    *)
       echo "usage: $0 N"
       echo "       Where \"N\" is the number of disks."
       exit $E_NOPARAM;
       ;;
esac

exit $E_NOEXIT   # Shouldn't exit here.

# Note:
# Redirect script output to a file, otherwise it scrolls off display.

Exemple A.49. Tours de Hanoi, autre version graphique

#! /bin/bash
# The Towers Of Hanoi
# Original script (hanoi.bash) copyright (C) 2000 Amit Singh.
# All Rights Reserved.
# http://hanoi.kernelthread.com

#  hanoi2.bash
#  Version 2: modded for ASCII-graphic display.
#  Uses code contributed by Antonio Macchi,
#+ with heavy editing by ABS Guide author.
#  This variant also falls under the original copyright, see above.
#  Used in ABS Guide with Amit Singh's permission (thanks!).


#   Variables   #
E_NOPARAM=86
E_BADPARAM=87   # Illegal no. of disks passed to script.
E_NOEXIT=88
DELAY=2         # Interval, in seconds, between moves. Change, if desired.
DISKS=$1
Moves=0

MWIDTH=7
MARGIN=2
# Arbitrary "magic" constants, work okay for relatively small # of disks.
# BASEWIDTH=51   # Original code.
let "basewidth = $MWIDTH * $DISKS + $MARGIN" # "Base" beneath rods.
# Above "algorithm" could likely stand improvement.

# Display variables.
let "disks1 = $DISKS - 1"
let "spaces1 = $DISKS" 
let "spaces2 = 2 * $DISKS" 

let "lastmove_t = $DISKS - 1"                # Final move?


declare -a Rod1 Rod2 Rod3

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


function repeat  {  # $1=char $2=number of repetitions
  local n           # Repeat-print a character.
  
  for (( n=0; n<$2; n++ )); do
    echo -n "$1"
  done
}

function FromRod  {
  local rod summit weight sequence

  while true; do
    rod=$1
    test ${rod/[^123]/} || continue

    sequence=$(echo $(seq 0 $disks1 | tac))
    for summit in $sequence; do
      eval weight=\${Rod${rod}[$summit]}
      test $weight -ne 0 &&
           { echo "$rod $summit $weight"; return; }
    done
  done
}


function ToRod  { # $1=previous (FromRod) weight
  local rod firstfree weight sequence
  
  while true; do
    rod=$2
    test ${rod/[^123]} || continue

    sequence=$(echo $(seq 0 $disks1 | tac))
    for firstfree in $sequence; do
      eval weight=\${Rod${rod}[$firstfree]}
      test $weight -gt 0 && { (( firstfree++ )); break; }
    done
    test $weight -gt $1 -o $firstfree = 0 &&
         { echo "$rod $firstfree"; return; }
  done
}


function PrintRods  {
  local disk rod empty fill sp sequence

  tput cup 5 0

  repeat " " $spaces1
  echo -n "|"
  repeat " " $spaces2
  echo -n "|"
  repeat " " $spaces2
  echo "|"

  sequence=$(echo $(seq 0 $disks1 | tac))
  for disk in $sequence; do
    for rod in {1..3}; do
      eval empty=$(( $DISKS - (Rod${rod}[$disk] / 2) ))
      eval fill=\${Rod${rod}[$disk]}
      repeat " " $empty
      test $fill -gt 0 && repeat "*" $fill || echo -n "|"
      repeat " " $empty
    done
    echo
  done
  repeat "=" $basewidth   # Print "base" beneath rods.
  echo
}


display ()
{
  echo
  PrintRods

  # Get rod-number, summit and weight
  first=( `FromRod $1` )
  eval Rod${first[0]}[${first[1]}]=0

  # Get rod-number and first-free position
  second=( `ToRod ${first[2]} $2` )
  eval Rod${second[0]}[${second[1]}]=${first[2]}


  if [ "${Rod3[lastmove_t]}" = 1 ]
  then   # Last move? If yes, then display final position.
    tput cup 0 0
    echo; echo "+  Final Position: $Moves moves"
    PrintRods
  fi

  sleep $DELAY
}

# From here down, almost the same as original (hanoi.bash) script.

dohanoi() {   # Recursive function.
    case $1 in
    0)
        ;;
    *)
        dohanoi "$(($1-1))" $2 $4 $3
        if [ "$Moves" -ne 0 ]
        then
          tput cup 0 0
          echo; echo "+  Position after move $Moves"
        fi
        ((Moves++))
        echo -n "   Next move will be:  "
        echo $2 "-->" $3
        display $2 $3
        dohanoi "$(($1-1))" $4 $3 $2
        ;;
    esac
}

setup_arrays ()
{
  local dim n elem

  let "dim1 = $1 - 1"
  elem=$dim1

  for n in $(seq 0 $dim1)
  do
   let "Rod1[$elem] = 2 * $n + 1"
   Rod2[$n]=0
   Rod3[$n]=0
   ((elem--))
  done
}


###   Main   ###

trap "tput cnorm" 0
tput civis
clear

setup_arrays $DISKS

tput cup 0 0
echo; echo "+  Start Position"

case $# in
    1) case $(($1>0)) in     # Must have at least one disk.
       1)
           disks=$1
           dohanoi $1 1 3 2
#          Total moves = 2^n - 1, where n = # of disks.
           echo
           exit 0;
           ;;
       *)
           echo "$0: Illegal value for number of disks";
           exit $E_BADPARAM;
           ;;
       esac
    ;;
    *)
       echo "usage: $0 N"
       echo "       Where \"N\" is the number of disks."
       exit $E_NOPARAM;
       ;;
esac

exit $E_NOEXIT   # Shouldn't exit here.

#  Exercise:
#  --------
#  There is a minor bug in the script that causes the display of
#+ the next-to-last move to be skipped.
#+ Fix this.

Exemple A.50. Autre version du script getopt-simple.sh

#!/bin/bash
# UseGetOpt.sh

# Author: Peggy Russell <prusselltechgroup@gmail.com>

UseGetOpt () {
  declare inputOptions
  declare -r E_OPTERR=85
  declare -r ScriptName=${0##*/}
  declare -r ShortOpts="adf:hlt"
  declare -r LongOpts="aoption,debug,file:,help,log,test"

DoSomething () {
    echo "The function name is '${FUNCNAME}'"
    #  Recall that $FUNCNAME is an internal variable
    #+ holding the name of the function it is in.
  }

  inputOptions=$(getopt -o "${ShortOpts}" --long \
              "${LongOpts}" --name "${ScriptName}" -- "${@}")

  if [[ ($? -ne 0) || ($# -eq 0) ]]; then
    echo "Usage: ${ScriptName} [-dhlt] {OPTION...}"
    exit $E_OPTERR
  fi

  eval set -- "${inputOptions}"

  # Only for educational purposes. Can be removed.
  #-----------------------------------------------
  echo "++ Test: Number of arguments: [$#]"
  echo '++ Test: Looping through "$@"'
  for a in "$@"; do
    echo "  ++ [$a]"
  done
  #-----------------------------------------------

  while true; do
    case "${1}" in
      --aoption | -a)  # Argument found.
        echo "Option [$1]"
        ;;

      --debug | -d)    # Enable informational messages.
        echo "Option [$1] Debugging enabled"
        ;;

      --file | -f)     #  Check for optional argument.
        case "$2" in   #+ Double colon is optional argument.
          "")          #  Not there.
              echo "Option [$1] Use default"
              shift
              ;;

          *) # Got it
             echo "Option [$1] Using input [$2]"
             shift
             ;;

        esac
        DoSomething
        ;;

      --log | -l) # Enable Logging.
        echo "Option [$1] Logging enabled"
        ;;

      --test | -t) # Enable testing.
        echo "Option [$1] Testing enabled"
        ;;

      --help | -h)
        echo "Option [$1] Display help"
        break
        ;;

      --)   # Done! $# is argument number for "--", $@ is "--"
        echo "Option [$1] Dash Dash"
        break
        ;;

       *)
        echo "Major internal error!"
        exit 8
        ;;

    esac
    echo "Number of arguments: [$#]"
    shift
  done

  shift
  # Only for educational purposes. Can be removed.
  #----------------------------------------------------------------------
  echo "++ Test: Number of arguments after \"--\" is [$#] They are: [$@]"
  echo '++ Test: Looping through "$@"'
  for a in "$@"; do
    echo "  ++ [$a]"
  done
  #----------------------------------------------------------------------
  
}

################################### M A I N ########################
#  If you remove "function UseGetOpt () {" and corresponding "}",
#+ you can uncomment the "exit 0" line below, and invoke this script
#+ with the various options from the command-line.
#-------------------------------------------------------------------
# exit 0

echo "Test 1"
UseGetOpt -f myfile one "two three" four

echo;echo "Test 2"
UseGetOpt -h

echo;echo "Test 3 - Short Options"
UseGetOpt -adltf myfile  anotherfile

echo;echo "Test 4 - Long Options"
UseGetOpt --aoption --debug --log --test --file myfile anotherfile

exit

Exemple A.51. La version de l'exemple UseGetOpt.sh utilisée dans l'appendice de complétion par Tab

#!/bin/bash

#  UseGetOpt-2.sh
#  Modified version of the script for illustrating tab-expansion
#+ of command-line options.
#  See the "Introduction to Tab Expansion" appendix.

#  Possible options: -a -d -f -l -t -h
#+                   --aoption, --debug --file --log --test -- help --

#  Author of original script: Peggy Russell <prusselltechgroup@gmail.com>


# UseGetOpt () {
  declare inputOptions
  declare -r E_OPTERR=85
  declare -r ScriptName=${0##*/}
  declare -r ShortOpts="adf:hlt"
  declare -r LongOpts="aoption,debug,file:,help,log,test"

DoSomething () {
    echo "The function name is '${FUNCNAME}'"
  }

  inputOptions=$(getopt -o "${ShortOpts}" --long \
              "${LongOpts}" --name "${ScriptName}" -- "${@}")

  if [[ ($? -ne 0) || ($# -eq 0) ]]; then
    echo "Usage: ${ScriptName} [-dhlt] {OPTION...}"
    exit $E_OPTERR
  fi

  eval set -- "${inputOptions}"


  while true; do
    case "${1}" in
      --aoption | -a)  # Argument found.
        echo "Option [$1]"
        ;;

      --debug | -d)    # Enable informational messages.
        echo "Option [$1] Debugging enabled"
        ;;

      --file | -f)     #  Check for optional argument.
        case "$2" in   #+ Double colon is optional argument.
          "")          #  Not there.
              echo "Option [$1] Use default"
              shift
              ;;

          *) # Got it
             echo "Option [$1] Using input [$2]"
             shift
             ;;

        esac
        DoSomething
        ;;

      --log | -l) # Enable Logging.
        echo "Option [$1] Logging enabled"
        ;;

      --test | -t) # Enable testing.
        echo "Option [$1] Testing enabled"
        ;;

      --help | -h)
        echo "Option [$1] Display help"
        break
        ;;

      --)   # Done! $# is argument number for "--", $@ is "--"
        echo "Option [$1] Dash Dash"
        break
        ;;

       *)
        echo "Major internal error!"
        exit 8
        ;;

    esac
    echo "Number of arguments: [$#]"
    shift
  done

  shift
  
#  }

exit

Exemple A.52. Voir tour à tour tous les fonds colorés existants

#!/bin/bash

# show-all-colors.sh
# Displays all 256 possible background colors, using ANSI escape sequences.
# Author: Chetankumar Phulpagare
# Used in ABS Guide with permission.

T1=8
T2=6
T3=36
offset=0

for num1 in {0..7}
do {
   for num2 in {0,1}
       do {
          shownum=`echo "$offset + $T1 * ${num2} + $num1" | bc`
          echo -en "\E[0;48;5;${shownum}m color ${shownum} \E[0m"
          }
       done
   echo
   }
done

offset=16
for num1 in {0..5}
do {
   for num2 in {0..5}
       do {
          for num3 in {0..5}
              do {
                 shownum=`echo "$offset + $T2 * ${num3} \
                 + $num2 + $T3 * ${num1}" | bc`
                 echo -en "\E[0;48;5;${shownum}m color ${shownum} \E[0m"
                 }
               done
          echo
          }
       done
}
done

offset=232
for num1 in {0..23}
do {
   shownum=`expr $offset + $num1`
   echo -en "\E[0;48;5;${shownum}m ${shownum}\E[0m"
}
done

echo

Exemple A.53. Pratiquer le code Morse

#!/bin/bash
# sam.sh, v. 01
# Still Another Morse (code training script)
# Author: Mendel Cooper
# License: GPL3
# Reldate: 05/25/11

# Morse code training script.
# Converts arguments to audible dots and dashes.
# Note: lowercase input only at this time.



# Get the wav files from the source tarball:
# http://bash.webofcrafts.net/abs-guide-latest.tar.bz2
DOT='soundfiles/dot.wav'
DASH='soundfiles/dash.wav'
# Maybe move soundfiles to /usr/local/sounds?

LETTERSPACE=300000  # Microseconds.
WORDSPACE=980000
# Nice and slow, for beginners. Maybe 5 wpm?

EXIT_MSG="May the Morse be with you!"
E_NOARGS=75         # No command-line args?



declare -A morse    # Associative array!
# ======================================= #
morse[a]="dot; dash"
morse[b]="dash; dot; dot; dot"
morse[c]="dash; dot; dash; dot"
morse[d]="dash; dot; dot"
morse[e]="dot"
morse[f]="dot; dot; dash; dot"
morse[g]="dash; dash; dot"
morse[h]="dot; dot; dot; dot"
morse[i]="dot; dot;"
morse[j]="dot; dash; dash; dash"
morse[k]="dash; dot; dash"
morse[l]="dot; dash; dot; dot"
morse[m]="dash; dash"
morse[n]="dash; dot"
morse[o]="dash; dash; dash"
morse[p]="dot; dash; dash; dot"
morse[q]="dash; dash; dot; dash"
morse[r]="dot; dash; dot"
morse[s]="dot; dot; dot"
morse[t]="dash"
morse[u]="dot; dot; dash"
morse[v]="dot; dot; dot; dash"
morse[w]="dot; dash; dash"
morse[x]="dash; dot; dot; dash"
morse[y]="dash; dot; dash; dash"
morse[z]="dash; dash; dot; dot"
morse[0]="dash; dash; dash; dash; dash"
morse[1]="dot; dash; dash; dash; dash"
morse[2]="dot; dot; dash; dash; dash"
morse[3]="dot; dot; dot; dash; dash"
morse[4]="dot; dot; dot; dot; dash"
morse[5]="dot; dot; dot; dot; dot"
morse[6]="dash; dot; dot; dot; dot"
morse[7]="dash; dash; dot; dot; dot"
morse[8]="dash; dash; dash; dot; dot"
morse[8]="dash; dash; dash; dash; dot"
# The following must be escaped or quoted.
morse[?]="dot; dot; dash; dash; dot; dot"
morse[.]="dot; dash; dot; dash; dot; dash"
morse[,]="dash; dash; dot; dot; dash; dash"
morse[/]="dash; dot; dot; dash; dot"
morse[\@]="dot; dash; dash; dot; dash; dot"
# ======================================= #

play_letter ()
{
  eval ${morse[$1]}   # Play dots, dashes from appropriate sound files.
  usleep $LETTERSPACE # Pause in between letters.
}

extract_letters ()
{                     # Slice string apart, letter by letter.
  local pos=0         # Starting at left end of string.
  local len=1         # One letter at a time.
  strlen=${#1}

  while [ $pos -lt $strlen ]
  do
    letter=${1:pos:len}
    #      ^^^^^^^^^^^^    See Chapter 10.1.
    play_letter $letter
    echo -n "*"       #    Mark letter just played.
    ((pos++))
  done
}

######### Play the sounds ############
dot()  { aplay "$DOT" 2&>/dev/null;  }
dash() { aplay "$DASH" 2&>/dev/null; }
######################################

no_args ()
{
    declare -a usage
    usage=( $0 word1 word2 ... )

    echo "Usage:"; echo
    echo ${usage[*]}
    for index in 0 1 2 3
    do
      extract_letters ${usage[index]}     
      usleep $WORDSPACE
      echo -n " "     # Print space between words.
    done
#   echo "Usage: $0 word1 word2 ... "
    echo; echo
}


# int main()
# {

clear                 # Clear the terminal screen.
echo "            SAM"
echo "Still Another Morse code trainer"
echo "    Author: Mendel Cooper"
echo; echo;

if [ -z "$1" ]
then
  no_args
  echo; echo; echo "$EXIT_MSG"; echo
  exit $E_NOARGS
fi

echo; echo "$*"       # Print text that will be played.

until [ -z "$1" ]
do
  extract_letters $1
  shift           # On to next word.
  usleep $WORDSPACE
  echo -n " "     # Print space between words.
done

echo; echo; echo "$EXIT_MSG"; echo

exit 0
# }

#  Exercises:
#  ---------
#  1) Have the script accept either lowercase or uppercase words
#+    as arguments. Hint: Use 'tr' . . .
#  2) Have the script optionally accept input from a text file.

Exemple A.54. Encoder/décoder le Base64

#!/bin/bash
# base64.sh: Bash implementation of Base64 encoding and decoding.
#
# Copyright (c) 2011 vladz [vladz@devzero.fr]
# Used in ABSG with permission (thanks!).
#
#  Encode or decode original Base64 (and also Base64url)
#+ from STDIN to STDOUT.
#
#    Usage:
#
#    Encode
#    $ ./base64.sh < binary-file > binary-file.base64
#    Decode
#    $ ./base64.sh -d < binary-file.base64 > binary-file
#
# Reference:
#
#    [1]  RFC4648 - "The Base16, Base32, and Base64 Data Encodings"
#         http://tools.ietf.org/html/rfc4648#section-5

# The base64_charset[] array contains entire base64 charset,
# and additionally the character "=" ...
base64_charset=( {A..Z} {a..z} {0..9} + / = )
                # Nice illustration of brace expansion.

#  Uncomment the ### line below to use base64url encoding instead of
#+ original base64.
### base64_charset=( {A..Z} {a..z} {0..9} - _ = )

#  Output text width when encoding
#+ (64 characters, just like openssl output).
text_width=64

function display_base64_char {
#  Convert a 6-bit number (between 0 and 63) into its corresponding values
#+ in Base64, then display the result with the specified text width.
  printf "${base64_charset[$1]}"; (( width++ ))
  (( width % text_width == 0 )) && printf "\n"
}

function encode_base64 {
# Encode three 8-bit hexadecimal codes into four 6-bit numbers.
  #    We need two local int array variables:
  #    c8[]: to store the codes of the 8-bit characters to encode
  #    c6[]: to store the corresponding encoded values on 6-bit
  declare -a -i c8 c6

  #  Convert hexadecimal to decimal.
  c8=( $(printf "ibase=16; ${1:0:2}\n${1:2:2}\n${1:4:2}\n" | bc) )

  #  Let's play with bitwise operators
  #+ (3x8-bit into 4x6-bits conversion).
  (( c6[0] = c8[0] >> 2 ))
  (( c6[1] = ((c8[0] &  3) << 4) | (c8[1] >> 4) ))

  # The following operations depend on the c8 element number.
  case ${#c8[*]} in 
    3) (( c6[2] = ((c8[1] & 15) << 2) | (c8[2] >> 6) ))
       (( c6[3] = c8[2] & 63 )) ;;
    2) (( c6[2] = (c8[1] & 15) << 2 ))
       (( c6[3] = 64 )) ;;
    1) (( c6[2] = c6[3] = 64 )) ;;
  esac

  for char in ${c6[@]}; do
    display_base64_char ${char}
  done
}

function decode_base64 {
# Decode four base64 characters into three hexadecimal ASCII characters.
  #  c8[]: to store the codes of the 8-bit characters
  #  c6[]: to store the corresponding Base64 values on 6-bit
  declare -a -i c8 c6

  # Find decimal value corresponding to the current base64 character.
  for current_char in ${1:0:1} ${1:1:1} ${1:2:1} ${1:3:1}; do
     [ "${current_char}" = "=" ] && break

     position=0
     while [ "${current_char}" != "${base64_charset[${position}]}" ]; do
        (( position++ ))
     done

     c6=( ${c6[*]} ${position} )
  done

  #  Let's play with bitwise operators
  #+ (4x8-bit into 3x6-bits conversion).
  (( c8[0] = (c6[0] << 2) | (c6[1] >> 4) ))

  # The next operations depends on the c6 elements number.
  case ${#c6[*]} in
    3) (( c8[1] = ( (c6[1] & 15) << 4) | (c6[2] >> 2) ))
       (( c8[2] = (c6[2] & 3) << 6 )); unset c8[2] ;;
    4) (( c8[1] = ( (c6[1] & 15) << 4) | (c6[2] >> 2) ))
       (( c8[2] = ( (c6[2] &  3) << 6) |  c6[3] )) ;;
  esac

  for char in ${c8[*]}; do
     printf "\x$(printf "%x" ${char})"
  done
}

# main ()
if [ $# -eq 0 ]; then   # encode

  # Make a hexdump of stdin and reformat in 3-byte groups.
  content=$(cat - | xxd -ps -u | sed -r "s/(\w{6})/\1 /g" |
            tr -d "\n")

  for chars in ${content}; do encode_base64 ${chars}; done

  echo

elif [ "$1" = "-d" ]; then   # decode

  # Reformat STDIN in pseudo 4x6-bit groups.
  content=$(cat - | tr -d "\n" | sed -r "s/(.{4})/\1 /g")

  for chars in ${content}; do decode_base64 ${chars}; done

else   # display usage
  echo
  printf "Usage: $0 < Infile > Outfile\n"
  printf "       $0 -d < Infile > Outfile\n"
  printf "                 -d   decode\n\n"
fi

Exemple A.55. L'algorithme de chiffrement de Cronsfeld

#!/bin/bash
# gronsfeld.bash

# License: GPL3
# Reldate 06/23/11

#  This is an implementation of the Gronsfeld Cipher.
#  It's essentially a stripped-down variant of the 
#+ polyalphabetic Vigenère Tableau, but with only 10 alphabets.
#  The classic Gronsfeld has a numerical sequence as the key word,
#+ but instead we substitute a letter string, for ease of use.
#  Allegedly, this cipher was invented by the eponymous Count Gronsfeld
#+ in the 17th Century. It was at one time considered to be unbreakable.
#  Note that this is ###not### a secure cipher by modern standards.

#  Global Variables  #
Enc_suffix="29378"   #  Encrypted text output with this 5-digit suffix. 
                     #  This functions as a decryption flag,
                     #+ and when used to generate passwords adds security.
Default_key="gronsfeldk"
                     #  The script uses this if key not entered below
                     #  (at "Keychain").
                     #  Change the above two values frequently
                     #+ for added security.

GROUPLEN=5           #  Output in groups of 5 letters, per tradition.
alpha1=( abcdefghijklmnopqrstuvwxyz )
alpha2=( {A..Z} )    #  Output in all caps, per tradition.
                     #  Use   alpha2=( {a..z} )   for password generator.
wraplen=26           #  Wrap around if past end of alphabet.
dflag=               #  Decrypt flag (set if $Enc_suffix present).
E_NOARGS=76          #  Missing command-line args?
DEBUG=77             #  Debugging flag.
declare -a offsets   #  This array holds the numerical shift values for
                     #+ encryption/decryption.

########Keychain#########
key=  ### Put key here!!!
      # 10 characters!
#########################



# Function
: ()
{ # Encrypt or decrypt, depending on whether $dflag is set.
  # Why ": ()" as a function name? Just to prove that it can be done.

  local idx keydx mlen off1 shft
  local plaintext="$1"
  local mlen=${#plaintext}

for (( idx=0; idx<$mlen; idx++ ))
do
  let "keydx = $idx % $keylen"
  shft=${offsets[keydx]}

  if [ -n "$dflag" ]
  then                  # Decrypt!
    let "off1 = $(expr index "${alpha1[*]}" ${plaintext:idx:1}) - $shft"
    # Shift backward to decrypt.
  else                  # Encrypt!
    let "off1 = $(expr index "${alpha1[*]}" ${plaintext:idx:1}) + $shft"
    # Shift forward to encrypt.
    test $(( $idx % $GROUPLEN)) = 0 && echo -n " "  # Groups of 5 letters.
    #  Comment out above line for output as a string without whitespace,
    #  for example, if using the script as a password generator.
  fi

  ((off1--))   # Normalize.

      if [ $off1 -lt 0 ]
      then     # Catch negative indices.
        let "off1 += $wraplen"
      fi

  ((off1 %= $wraplen))   # Wrap around if past end of alphabet.

  echo -n "${alpha2[off1]}"

done

  if [ -z "$dflag" ]
  then
    echo " $Enc_suffix"
#   echo "$Enc_suffix"  # For password generator.
  else
    echo
  fi
} # End encrypt/decrypt function.



# int main () {

# Check if command-line args.
if [ -z "$1" ]
then
   echo "Usage: $0 TEXT TO ENCODE/DECODE"
   exit $E_NOARGS
fi

if [ ${!#} == "$Enc_suffix" ]
#    ^^^^^ Final command-line arg.
then
  dflag=ON
  echo -n "+"           # Flag decrypted text with a "+" for easy ID.
fi

if [ -z "$key" ]
then
  key="$Default_key"    # "gronsfeldk" per above.
fi

keylen=${#key}

for (( idx=0; idx<$keylen; idx++ ))
do  # Calculate shift values for encryption/decryption.
  offsets[idx]=$(expr index "${alpha1[*]}" ${key:idx:1})   # Normalize.
  ((offsets[idx]--))  #  Necessary because "expr index" starts at 1,
                      #+ whereas array count starts at 0.
  # Generate array of numerical offsets corresponding to the key.
  # There are simpler ways to accomplish this.
done

args=$(echo "$*" | sed -e 's/ //g' | tr A-Z a-z | sed -e 's/[0-9]//g')
# Remove whitespace and digits from command-line args.
# Can modify to also remove punctuation characters, if desired.

         # Debug:
         # echo "$args"; exit $DEBUG

: "$args"               # Call the function named ":".
# : is a null operator, except . . . when it's a function name!

exit $?    # } End-of-script


#   **************************************************************   #
#   This script can function as a  password generator,
#+  with several minor mods, see above.
#   That would allow an easy-to-remember password, even the word
#+ "password" itself, which encrypts to vrgfotvo29378
#+  a fairly secure password not susceptible to a dictionary attack.
#   Or, you could use your own name (surely that's easy to remember!).
#   For example, Bozo Bozeman encrypts to hfnbttdppkt29378.
#   **************************************************************   #

Pour finir cette section, une revue des bases... et plus encore.

Exemple A.56. Les bases revisitées

#!/bin/bash
# basics-reviewed.bash

# Extension du fichier == *.bash == spécifique à Bash

#   Copyright (c) Michael S. Zick, 2003; All rights reserved.
#   License: Use in any form, for any purpose.
#   Revision: $ID$
#
#              Édité pour la présentation par M.C.
#   (auteur du "Guide d'écriture avancée des scripts Bash")
#   Corrections et mises à jour (04/08) par Cliff Bamford.


#  Ce script a été testé sous Bash version 2.04, 2.05a et
#+ 2.05b.
#  Il pourrait ne pas fonctionner avec les versions précédentes.
#  Ce script de démonstration génère une erreur "command not found"
#+ --intentionnelle--. Voir ligne 436.

#  Le mainteneur actuel de Bash maintainer, Chet Ramey, a corrigé les éléments
#+ notés pour les versions ultérieures de Bash.



        ###-------------------------------------------###
        ###  Envoyez la sortie de ce script à 'more'  ###
        ###+ sinon cela dépassera la page.            ###
        ###                                           ###
        ###  Vous pouvez aussi rediriger sa sortie    ###
        ###+ vers un fichier pour l'examiner.         ###  
        ###-------------------------------------------###



#  La plupart des points suivants sont décrit en détail dans
#+ le guide d'écriture avancé du script Bash.
#  Ce script de démonstration est principalement une présentation réorganisée.
#      -- msz

# Les variables ne sont pas typées sauf cas indiqués.

#  Les variables sont nommées. Les noms doivent contenir un caractère qui
#+ n'est pas un chiffre.
#  Les noms des descripteurs de fichiers (comme dans, par exemple, 2>&1)
#+ contiennent UNIQUEMENT des chiffres.

# Les paramètres et les éléments de tavbleau Bash sont numérotés.
# (Les paramètres sont très similaires aux tableaux Bash.)

# Un nom de variable pourrait être indéfini (référence nulle).
unset VarNullee

# Un nom de variable pourrait être défini mais vide (contenu nul).
VarVide=''                         # Deux guillemets simples, adjacents.

# Un nom de variable pourrait être défini et non vide.
VarQuelquechose='Littéral'

# Une variable pourrait contenir:
#   * Un nombre complet, entier signé sur 32-bit (voire plus)
#   * Une chaîne
# Une variable pourrait aussi être un tableau.

#  Une chaîne pourrait contenir des espaces et pourrait être traitée
#+ comme s'il s'agissait d'un nom de fonction avec des arguments optionnelles.

#  Les noms des variables et les noms des functions sont dans différents
#+ espaces de noms.


#  Une variable pourrait être défini comme un tableau Bash soit explicitement
#+ soit implicitement par la syntaxe de l'instruction d'affectation.
#  Explicite:
declare -a VarTableau



# La commande echo est intégrée.
echo $VarQuelquechose

# La commande printf est intégrée.
# Traduire %s comme "Format chaîne"
printf %s $VarQuelquechose      #  Pas de retours chariot spécifiés,
                                #+ aucune sortie.
echo                            # Par défaut, seulement un retour chariot.




#  L'analyseur de mots de Bash s'arrête sur chaque espace blanc mais son
#+ manquement est significatif.
#  (Ceci reste vrai en général ; Il existe évidemment des exceptions.)




# Traduire le signe SIGNE_DOLLAR comme Contenu-de.

# Syntaxe étendue pour écrire Contenu-de :
echo ${VarQuelquechose}

#  La syntaxe étendue ${ ... } permet de spécifier plus que le nom de la
#+ variable.
#  En général, $VarQuelquechose peut toujours être écrit ${VarQuelquechose}.

# Appelez ce script avec des arguments pour visualiser l'action de ce qui suit.



#  En dehors des doubles guillemets, les caractères spéciaux @ et *
#+ spécifient un comportement identique.
#  Pourrait être prononcé comme Tous-Éléments-De.

#  Sans spécifier un nom, ils réfèrent un paramètre prédéfini Bash-Array.



# Références de motifs globaux
echo $*                       # Tous les paramètres du script ou de la fonction
echo ${*}                     # Pareil

# Bash désactive l'expansion de nom de fichier pour les motifs globaux.
# Seuls les caractères correspondants sont actifs.


# Références de Tous-Éléments-De
echo $@                         # Identique à ci-dessus
echo ${@}                       # Identique à ci-dessus




#  À l'intérieur des guillemets doubles, le comportement des références de
#+ motifs globaux dépend du paramètrage de l'IFS (Input Field Separator, soit
#+ séparateur de champ d'entrée).
#  À l'intérieur des guillemets doubles, les références à Tous-Éléments-De
#+ se comportent de façon identique.


#  Spécifier uniquement le nom de la variable contenant une chaîne réfère tous
#+ les éléments (caractères) d'une chaîne.


#  Spécifier un élément (caractère) d'une chaîne,
#+ la notation de référence de syntaxe étendue (voir ci-dessous) POURRAIT être
#+ utilisée.




#  Spécifier uniquement le nom d'un tableau Bash référence l'élément 0,
#+ PAS le PREMIER DÉFINI, PAS le PREMIER AVEC CONTENU.

#  Une qualification supplémentaire est nécessaire pour référencer d'autres
#+ éléments, ce qui signifie que la référence DOIT être écrite dans la syntaxe
#+ étendue. La forme générale est ${nom[indice]}.

#  Le format de chaîne pourrait aussi être utilisé ${nom:indice}
#+ pour les tableaux Bash lors de la référence de l'élément zéro.


#  Les tableaux Bash sont implémentés en interne comme des listes liés,
#+ pas comme une aire fixe de stockage comme le font certains langages de
#+ programmation.


#   Caractéristiques des tableaux Bash (Bash-Arrays):
#   ------------------------------------------------

#   Sans autre indication, les indices des tableaux Bash
#+  commencent avec l'indice numéro 0. Littéralement : [0]
#   Ceci s'appelle un indice base 0.
###
#   Sans autre indication, les tableaux Bash ont des indices continus
#+  (indices séquentielles, sans trou/manque).
###
#   Les indices négatifs ne sont pas autorisés.
###
#   Les éléments d'un tableau Bash n'ont pas besoin de tous être du même type.
###
#   Les éléments d'un tableau Bash pourraient être indéfinis (référence nulle).
#       C'est-à-dire qu'un tableau Bash pourrait être "subscript sparse."
###
#   Les éléments d'un tableau Bash pourraient être définis et vides
#+  (contenu nul).
###
#   Les éléments d'un tableau Bash pourraient être :
#     * Un entier codé sur 32 bits (ou plus)
#     * Une chaîne
#     * Une chaîne formattée de façon à ce qu'elle soit en fait le nom d'une
#       fonction avec des arguments optionnelles
###
#   Les éléments définis d'un tableau Bash pourraient ne pas être définis
#   (unset).
#       C'est-à-dire qu'un tableau Bash à indice continu pourrait être modifié
#       en un tableau Bash à indice disparate.
###
#   Des éléments pourraient être ajoutés dans un tableau Bash en définissant un
#   élément non défini précédemment.
###
# Pour ces raisons, je les ai appelé des tableaux Bash ("Bash-Arrays").
# Je retourne maintenant au terme générique "tableau".
#     -- msz




echo "========================================================="

#  Lignes 202 à 334 fournies par Cliff Bamford. (Merci !)
#  Démo --- Interaction avec les tableaux, les guillemets, IFS, echo, * et @
#+ --- tous modifient la façon dont cela fonctionne

ArrayVar[0]='zero'                    # 0 normal
ArrayVar[1]=one                       # 1 valeur litérale sans guillemet
ArrayVar[2]='two'                     # 2 normal
ArrayVar[3]='three'                   # 3 normal
ArrayVar[4]='I am four'               # 4 normal avec des espaces
ArrayVar[5]='five'                    # 5 normal
unset ArrayVar[6]                     # 6 indéfini
ArrayValue[7]='seven'                 # 7 normal
ArrayValue[8]=''                      # 8 défini mais vide
ArrayValue[9]='nine'                  # 9 normal


echo '--- Voici le tableau que nous utilisons pour ce test'
echo
echo "ArrayVar[0]='zero'             # 0 normal"
echo "ArrayVar[1]=one                # 1 valeur litérale sans guillemet"
echo "ArrayVar[2]='two'              # 2 normal"
echo "ArrayVar[3]='three'            # 3 normal"
echo "ArrayVar[4]='I am four'        # 4 normal avec des espaces"
echo "ArrayVar[5]='five'             # 5 normal"
echo "unset ArrayVar[6]              # 6 indéfini"
echo "ArrayValue[7]='seven'          # 7 normal"
echo "ArrayValue[8]=''               # 8 défini mais vide"
echo "ArrayValue[9]='nine'           # 9 normal"
echo


echo
echo '---Cas 0 : Sans double guillemets, IFS par défaut (espace, tabulation, retour à la ligne) ---'
IFS=$'\x20'$'\x09'$'\x0A'            # Exactement dans cet ordre.
echo 'Voici : printf %q {${ArrayVar[*]}'
printf %q ${ArrayVar[*]}
echo
echo 'Voici : printf %q {${ArrayVar[@]}'
printf %q ${ArrayVar[@]}
echo
echo 'Voici : echo ${ArrayVar[*]}'
echo  ${ArrayVar[@]}
echo 'Voici : echo {${ArrayVar[@]}'
echo ${ArrayVar[@]}

echo
echo '---Cas 1 : Dans des guillemets doubles - IFS par défaut ---'
IFS=$'\x20'$'\x09'$'\x0A'           #  These three bytes,
echo 'Voici : printf %q "{${ArrayVar[*]}"'
printf %q "${ArrayVar[*]}"
echo
echo 'Voici : printf %q "{${ArrayVar[@]}"'
printf %q "${ArrayVar[@]}"
echo
echo 'Voici : echo "${ArrayVar[*]}"'
echo  "${ArrayVar[@]}"
echo 'Voici : echo "{${ArrayVar[@]}"'
echo "${ArrayVar[@]}"

echo
echo '---Cas 2 : Dans des guillemets doubles - IFS vaut q'
IFS='q'
echo 'Voici : printf %q "{${ArrayVar[*]}"'
printf %q "${ArrayVar[*]}"
echo
echo 'Voici : printf %q "{${ArrayVar[@]}"'
printf %q "${ArrayVar[@]}"
echo
echo 'Voici : echo "${ArrayVar[*]}"'
echo  "${ArrayVar[@]}"
echo 'Voici : echo "{${ArrayVar[@]}"'
echo "${ArrayVar[@]}"

echo
echo '---Cas 3 : Dans des guillemets doubles - IFS vaut ^'
IFS='^'
echo 'Voici : printf %q "{${ArrayVar[*]}"'
printf %q "${ArrayVar[*]}"
echo
echo 'Voici : printf %q "{${ArrayVar[@]}"'
printf %q "${ArrayVar[@]}"
echo
echo 'Voici : echo "${ArrayVar[*]}"'
echo  "${ArrayVar[@]}"
echo 'Voici : echo "{${ArrayVar[@]}"'
echo "${ArrayVar[@]}"

echo
echo '---Cas 4 : Dans des guillemets doubles - IFS vaut ^ suivi par  
espace, tabulation, retour à la ligne'
IFS=$'^'$'\x20'$'\x09'$'\x0A'       # ^ + espace tabulation retour à la ligne
echo 'Voici : printf %q "{${ArrayVar[*]}"'
printf %q "${ArrayVar[*]}"
echo
echo 'Voici : printf %q "{${ArrayVar[@]}"'
printf %q "${ArrayVar[@]}"
echo
echo 'Voici : echo "${ArrayVar[*]}"'
echo  "${ArrayVar[@]}"
echo 'Voici : echo "{${ArrayVar[@]}"'
echo "${ArrayVar[@]}"

echo
echo '---Cas 6 : Dans des guillemets doubles - IFS configuré mais vide'
IFS=''
echo 'Voici : printf %q "{${ArrayVar[*]}"'
printf %q "${ArrayVar[*]}"
echo
echo 'Voici : printf %q "{${ArrayVar[@]}"'
printf %q "${ArrayVar[@]}"
echo
echo 'Voici : echo "${ArrayVar[*]}"'
echo  "${ArrayVar[@]}"
echo 'Voici : echo "{${ArrayVar[@]}"'
echo "${ArrayVar[@]}"

echo
echo '---Cas 7 : Dans des guillemets doubles - IFS indéfini'
unset IFS
echo 'Voici : printf %q "{${ArrayVar[*]}"'
printf %q "${ArrayVar[*]}"
echo
echo 'Voici : printf %q "{${ArrayVar[@]}"'
printf %q "${ArrayVar[@]}"
echo
echo 'Voici : echo "${ArrayVar[*]}"'
echo  "${ArrayVar[@]}"
echo 'Voici : echo "{${ArrayVar[@]}"'
echo "${ArrayVar[@]}"

echo
echo '---Fin des cas---'
echo "========================================================="; echo



# Remettre la valeur par défaut d'IFS.
# Par défaut, il s'agit exactement de ces trois octets.
IFS=$'\x20'$'\x09'$'\x0A'           # Dans cet ordre.

# Interprétation des affichages précédents :
#   Un motif global est de l'entrée/sortie ; le paramètrage de l'IFS est pris
en compte.
###
#   Un Tous-Éléments-De ne prend pas en compte le paramètrage de l'IFS.
###
#   Notez les affichages différents en utilisant la commande echo et l'opérateur
#+ de format entre guillemets de la commande printf.


#  Rappel :
#   Les paramètres sont similaires aux tableaux et ont des comportements
similaires.
###
#  Les exemples ci-dessous démontrent les variantes possibles.
#  Pour conserver la forme d'un tableau à indice non continu, un supplément au
script
#+ est requis.
###
#  Le code source de Bash dispose d'une routine d'affichage du format
#+ d'affectation [indice]=valeur   .
#  Jusqu'à la version 2.05b, cette routine n'est pas utilisée
#+ mais cela pourrait changer dans les versions suivantes.



# La longueur d'une chaîne, mesurée en éléments non nuls (caractères) :
echo
echo '- - Références sans guillemets - -'
echo 'Nombre de caractères non nuls : '${#VarQuelquechose}' caractères.'

# test='Lit'$'\x00''eral'           # $'\x00' est un caractère nul.
# echo ${#test}                     # Vous avez remarqué ?



#  La longueur d'un tableau, mesurée en éléments définis,
#+ ceci incluant les éléments à contenu nul.
echo
echo 'Nombre de contenu défini : '${#VarTableau[@]}' éléments.'
# Ce n'est PAS l'indice maximum (4).
# Ce n'est PAS l'échelle des indices (1...4 inclus).
# C'EST la longueur de la liste chaînée.
###
#  L'indice maximum et l'échelle d'indices pourraient être trouvées avec
#+ un peu de code supplémentaire.

# La longueur d'une chaîne, mesurée en éléments non nuls (caractères):
echo
echo '- - Références du motif global, entre guillemets - -'
echo 'Nombre de caractères non nuls : '"${#VarQuelquechose}"'.'

#  La longueur d'un tableau, mesuré avec ses éléments définis,
#+ ceci incluant les éléments à contenu nul.
echo
echo "Nombre d'éléments définis: '"${#VarTableau[*]}"' éléments."

#  Interprétation : la substitution n'a pas d'effet sur l'opération ${# ... }.
#  Suggestion :
#  Toujours utiliser le caractère Tous-Éléments-De
#+ si cela correspond au comportement voulu (indépendence par rapport à l'IFS).



#  Définir une fonction simple.
#  J'inclus un tiret bas dans le nom pour le distinguer des exemples ci-dessous.
###
#  Bash sépare les noms de variables et les noms de fonctions
#+ grâce à des espaces de noms différents.
#  The Mark-One eyeball isn't that advanced.
###
_simple() {
    echo -n 'FonctionSimple'$@      #  Les retours chariots disparaissent dans
le résultat.
}


# La notation ( ... ) appelle une commande ou une fonction.
# La notation $( ... ) est prononcée Résultat-De.


# Appelle la fonction _simple
echo
echo '- - Sortie de la fonction _simple - -'
_simple                             # Essayez de passer des arguments.
echo
# or
(_simple)                           # Essayez de passer des arguments.
echo

echo "- Existe-t'il une variable de ce nom ? -"
echo $_simple indéfinie             # Aucune variable de ce nom.

# Appelle le résultat de la fonction _simple (message d'erreur attendu)

###
$(_simple)                          # Donne un message d'erreur :
#                          line 436: FonctionSimple: command not found
#                          ---------------------------------------

echo
###

#  Le premier mot du résultat de la fonction _simple
#+ n'est ni une commande Bash valide ni le nom d'une fonction définie.
###
# Ceci démontre que la sortie de _simple est sujet à évaluation.
###
# Interprétation :
#   Une fonction peut être utilisée pour générer des commandes Bash en ligne.


# Une fonction simple où le premier mot du résultat EST une commande Bash :
###
_print() {
    echo -n 'printf %q '$@
}

echo '- - Affichage de la fonction _print - -'
_print parm1 parm2                  # Une sortie n'est PAS une commande.
echo

$(_print parm1 parm2)               #  Exécute : printf %q parm1 parm2
                                    #  Voir ci-dessus les exemples IFS
                                    #+ pour les nombreuses possibilités.
echo

$(_print $VarQuelquechose)             # Le résultat prévisible.
echo



# Variables de fonctions
# ----------------------

echo
echo '- - Variables de fonctions - -'
# Une variable pourrait représenter un entier signé, une chaîne ou un tableau.
# Une chaîne pourrait être utilisée comme nom de fonction avec des arguments
optionnelles.

# set -vx                           #  À activer si désiré
declare -f funcVar                  #+ dans l'espace de noms des fonctions

funcVar=_print                      # Contient le nom de la fonction.
$funcVar parm1                      # Identique à _print à ce moment.
echo

funcVar=$(_print )                  # Contient le résultat de la fonction.
$funcVar                            # Pas d'entrée, pas de sortie.
$funcVar $VarQuelquechose           # Le résultat prévisible.
echo

funcVar=$(_print $VarQuelquechose)  #  $VarQuelquechose remplacé ICI.
$funcVar                            #  L'expansion fait parti du contenu
echo                                #+ des variables.

funcVar="$(_print $VarQuelquechose)" #  $VarQuelquechose remplacé ICI.
$funcVar                             #  L'expansion fait parti du contenu
echo                                #+ des variables.

#  La différence entre les versions sans guillemets et avec double guillemets
#+ ci-dessus est rencontrée dans l'exemple "protect_literal.sh".
#  Le premier cas ci-dessus est exécuté comme deux mots Bash sans guillemets.
#  Le deuxième cas est exécuté comme un mot Bash avec guillemets.




# Remplacement avec délai
# -----------------------

echo
echo '- - Remplacement avec délai - -'
funcVar="$(_print '$VarQuelquechose')" # Pas de remplacement, simple mot Bash.
eval $funcVar                          # $VarQuelquechose remplacé ICI.
echo

VarQuelquechose='NouvelleChose'
eval $funcVar                       # $VarQuelquechose remplacé ICI.
echo

# Restaure la configuration initiale.
VarQuelquechose=Literal

#  Il existe une paire de fonctions démontrées dans les exemples
#+ "protect_literal.sh" et "unprotect_literal.sh".
#  Il s'agit de fonctions à but général pour des littérales à remplacements avec
délai
#+ contenant des variables.





# REVUE :
# ------

#  Une chaîne peut être considérée comme un tableau classique d'éléments de type
#+ caractère.
#  Une opération sur une chaîne s'applique à tous les éléments (caractères) de
#+ la chaîne (enfin, dans son concept).
###
#  La notation ${nom_tableau[@]} représente tous les éléments du tableau Bash
#+ nom_tableau.
###
#  Les opérations sur les chaînes de syntaxe étendue sont applicables à tous les
#+ éléments d'un tableau.
###
#  Ceci peut être pensé comme une boucle For-Each sur un vecteur de chaînes.
###
#  Les paramètres sont similaires à un tableau.
#  L'initialisation d'un paramètre de type tableau pour un script
#+ et d'un paramètre de type tableau pour une fonction diffèrent seulement
#+ dans l'initialisation de ${0}, qui ne change jamais sa configuration.
###
#  L'indice zéro du tableau, paramètre d'un script, contient le nom du script.
###
#  L'indice zéro du tableau, paramètre de fonction, NE CONTIENT PAS le nom de la
#+ fonction.
#  Le nom de la fonction courante est accédé par la variable $NOM_FONCTION.
###
#  Une liste rapide et revue suit (rapide mais pas courte).

echo
echo '- - Test (mais sans changement) - -'
echo '- référence nulle -'
echo -n ${VarNulle-'NonInitialisée'}' '  # NonInitialisée
echo ${VarNulle}                         # NewLine only
echo -n ${VarNulle:-'NonInitialisée'}' ' # NonInitialisée
echo ${VarNulle}                         # Newline only

echo '- contenu nul -'
echo -n ${VarVide-'Vide'}' '             # Seulement l'espace
echo ${VarVide}                          # Nouvelle ligne seulement
echo -n ${VarVide:-'Vide'}' '            # Vide
echo ${VarVide}                          # Nouvelle ligne seulement

echo '- contenu -'
echo ${VarQuelquechose-'Contenu'}        # Littéral
echo ${VarQuelquechose:-'Contenu'}       # Littéral

echo '- Tableau à indice non continu -'
echo ${VarTableau[@]-'non initialisée'}

# Moment ASCII-Art
# État               O==oui, N==non
#                    -       :-
# Non initialisé     O       O       ${# ... } == 0
# Vide               N       O       ${# ... } == 0
# Contenu            N       N       ${# ... } > 0

#  Soit la première partie des tests soit la seconde pourrait être une chaîne
#+ d'appel d'une commande ou d'une fonction.
echo
echo '- - Test 1 pour indéfini - -'
declare -i t
_decT() {
    t=$t-1
}

# Référence nulle, initialisez à t == -1
t=${#VarNulle}                           # Résultats en zéro.
${VarNulle- _decT }                      # La fonction s'exécute, t vaut maintenant -1.
echo $t

# Contenu nul, initialisez à t == 0
t=${#VarVide}                          # Résultats en zéro.
${VarVide- _decT }                     # Fontion _decT NON exécutée.
echo $t

# Contenu, initialisez à t == nombre de caractères non nuls
VarQuelquechose='_simple'                  # Initialisez avec un nom de fonction valide.
t=${#VarQuelquechose}                      # longueur différente de zéro
${VarQuelquechose- _decT }                 # Fonction _simple exécutée.
echo $t                                    # Notez l'action Append-To.

# Exercice : nettoyez cet exemple.
unset t
unset _decT
VarQuelquechose=Literal

echo
echo '- - Test et modification - -'
echo '- Affectation si référence nulle -'
echo -n ${VarNulle='NonInitialisée'}' '          # NonInitialisée NonInitialisée
echo ${VarNulle}
unset VarNulle

echo '- Affectation si référence nulle -'
echo -n ${VarNulle:='NonInitialisée'}' '         # NonInitialisée NonInitialisée
echo ${VarNulle}
unset VarNulle

echo "- Pas d'affectation si contenu nul -"
echo -n ${VarVide='Vide'}' '          # Espace seulement
echo ${VarVide}
VarVide=''

echo "- Affectation si contenu nul -"
echo -n ${VarVide:='Vide'}' '         # Vide Vide
echo ${VarVide}
VarVide=''

echo "- Aucun changement s'il a déjà un contenu -"
echo ${VarQuelquechose='Contenu'}          # Littéral
echo ${VarQuelquechose:='Contenu'}         # Littéral


# Tableaux Bash à indice non continu
###
#  Les tableaux Bash ont des indices continus, commençant à zéro
#+  sauf indication contraire.
###
#  L'initialisation de VarTableau était une façon de le "faire autrement".
#+ Voici un autre moyen :
###
echo
declare -a TableauNonContinu
TableauNonContinu=( [1]=un [2]='' [4]='quatre' )
# [0]=référence nulle, [2]=contenu nul, [3]=référence nulle

echo '- - Liste de tableaux à indice non continu - -'
# À l'intérieur de guillemets doubles, IFS par défaut, motif global

IFS=$'\x20'$'\x09'$'\x0A'
printf %q "${TableauNonContinu[*]}"
echo

#  Notez que l'affichage ne distingue pas entre "contenu nul" et "référence nulle".
#  Les deux s'affichent comme des espaces blancs échappés.
###
#  Notez aussi que la sortie ne contient PAS d'espace blanc échappé
#+ pour le(s) "référence(s) nulle(s)" avant le premier élément défini.
###
# Ce comportement des versions 2.04, 2.05a et 2.05b a été rapporté et
#+ pourrait changer dans une prochaine version de Bash.

#  Pour afficher un tableau sans indice continu et maintenir la relation
#+ [indice]=valeur sans changement requiert un peu de programmation.
#  Un bout de code possible :
###
# local l=${#TableauNonContinu[@]}  # Nombre d'éléments définis
# local f=0                         # Nombre d'indices trouvés
# local i=0                         # Indice à tester
(                                   # Fonction anonyme en ligne
    for (( l=${#TableauNonContinu[@]}, f = 0, i = 0 ; f < l ; i++ ))
    do
        # 'si défini alors...'
        ${TableauNonContinu[$i]+ eval echo '\ ['$i']='${TableauNonContinu[$i]} ; (( f++ )) }
    done
)

# Le lecteur arrivant au fragment de code ci-dessus pourrait vouloir voir
#+ la liste des commandes et les commandes multiples sur une ligne dans le texte
#+ du guide de l'écriture avancée de scripts shell Bash.
###
#  Note :
#  La version "read -a nom_tableau" de la commande "read" commence à remplir
#+ nom_tableau à l'indice zéro.
#  TableauNonContinu ne définit pas de valeur à l'indice zéro.
###
#  L'utilisateur ayant besoin de lire/écrire un tableau non contigu pour soit
#+ un stockage externe soit une communication par socket doit inventer une paire
#+ de code lecture/écriture convenant à ce but.
###
# Exercice : nettoyez-le.

unset TableauNonContinu

echo
echo '- - Alternative conditionnel (mais sans changement)- -'
echo "- Pas d'alternative si référence nulle -"
echo -n ${VarNulle+'NonInitialisee'}' '
echo ${VarNulle}
unset VarNulle

echo "- Pas d'alternative si référence nulle -"
echo -n ${VarNulle:+'NonInitialisee'}' '
echo ${VarNulle}
unset VarNulle

echo "- Alternative si contenu nul -"
echo -n ${VarVide+'Vide'}' '               # Vide
echo ${VarVide}
VarVide=''

echo "- Pas d'alternative si contenu nul -"
echo -n ${VarVide:+'Vide'}' '              # Espace seul
echo ${VarVide}
VarVide=''

echo "- Alternative si contenu déjà existant -"

# Alternative littérale
echo -n ${VarQuelquechose+'Contenu'}' '        # Contenu littéral
echo ${VarQuelquechose}

# Appelle une fonction
echo -n ${VarQuelquechose:+ $(_simple) }' '    # Littéral FonctionSimple
echo ${VarQuelquechose}
echo

echo '- - Tableau non contigu - -'
echo ${VarTableau[@]+'Vide'}                   # Un tableau de 'vide'(s)
echo

echo '- - Test 2 pour indéfini - -'

declare -i t
_incT() {
    t=$t+1
}

#  Note:
#  C'est le même test utilisé dans le fragment de code
#+  pour le tableau non contigu.

# Référence nulle, initialisez : t == -1
t=${#VarNulle}-1                     # Les résultats dans moins-un.
${VarNulle+ _incT }                  # Ne s'exécute pas.
echo $t' Null reference'

# Contenu nul, initialisez : t == 0
t=${#VarVide}-1                    # Les résultats dans moins-un.
${VarVide+ _incT }                 # S'exécute.
echo $t'  Null content'

# Contenu, initialisez : t == (nombre de caractères non nuls)
t=${#VarQuelquechose}-1                # longueur non nul moins un
${VarQuelquechose+ _incT }             # S'exécute.
echo $t'  Contents'

# Exercice : nettoyez cet exemple.
unset t
unset _incT

# ${name?err_msg} ${name:?err_msg}
#  Ceci suit les mêmes règles mais quitte toujours après
#+ si une action est spécifiée après le point d'interrogation.
#  L'action suivant le point d'interrogation pourrait être un littéral
#+ ou le résultat d'une fonction.
###
#  ${nom?} ${nom:?} sont seulement des tests, le retour peut être testé.




# Opérations sur les éléments
# ---------------------------

echo
echo '- - Sélection du sous-élément de queue - -'

#  Chaînes, tableaux et paramètres de position

#  Appeler ce script avec des arguments multiples
#+ pour voir les sélections du paramètre.

echo '- Tous -'
echo ${VarQuelquechose:0}           # tous les caractères non nuls
echo ${VarTableau[@]:0}             # tous les éléments avec contenu
echo ${@:0}                         # tous les paramètres avec contenu
                                    # ignore paramètre[0]

echo
echo '- Tous après -'
echo ${VarQuelquechose:1}           # tous les non nuls après caractère[0]
echo ${VarTableau[@]:1}             # tous après élément[0] avec contenu
echo ${@:2}                         # tous après param[1] avec contenu

echo
echo '- Intervalle après -'
echo ${VarQuelquechose:4:3}         # ral
                                    # trois caractères après
                                    # caractère[3]

echo '- Sparse array gotch -'
echo ${VarTableau[@]:1:2}   #  quatre - le premier élément avec contenu.
                            #  Deux éléments après (s'ils existent).
                            #  le PREMIER AVEC CONTENU
                            #+ (le PREMIER AVEC CONTENU doit être
                            #+ considéré comme s'il s'agissait de
                            #+ l'indice zéro).
#  Éxécuté comme si Bash considère SEULEMENT les éléments de tableau avec CONTENU
#  printf %q "${VarTableau[@]:0:3}"    # Essayez celle-ci

#  Dans les versions 2.04, 2.05a et 2.05b,
#+ Bash ne gère pas les tableaux non contigu comme attendu avec cette notation.
#
#  Le mainteneur actuel de Bash, Chet Ramey, a corrigé ceci.


echo '- Tableaux contigus -'
echo ${@:2:2}               # Deux paramètres suivant paramètre[1]

# Nouvelles victimes des exemples de vecteurs de chaînes :
chaineZ=abcABC123ABCabc
tableauZ=( abcabc ABCABC 123123 ABCABC abcabc )
noncontiguZ=( [1]='abcabc' [3]='ABCABC' [4]='' [5]='123123' )

echo
echo ' - - Chaîne victime - -'$chaineZ'- - '
echo ' - - Tableau victime - -'${tableauZ[@]}'- - '
echo ' - - Tableau non contigu - -'${noncontiguZ[@]}'- - '
echo ' - [0]==réf. nulle, [2]==réf. nulle, [4]==contenu nul - '
echo ' - [1]=abcabc [3]=ABCABC [5]=123123 - '
echo ' - nombre de références non nulles : '${#noncontiguZ[@]}' elements'

echo
echo "- - Suppression du préfixe d'un sous élément - -"
echo '- - la correspondance de motif globale doit inclure le premier caractère. - -'
echo "- - Le motif global doit être un littéral ou le résultat d'une fonction. - -"
echo


# Fonction renvoyant un motif global simple, littéral
_abc() {
    echo -n 'abc'
}

echo '- Préfixe court -'
echo ${chaineZ#123}                 # Non modifié (pas un préfixe).
echo ${chaineZ#$(_abc)}             # ABC123ABCabc
echo ${tableauZ[@]#abc}             # Appliqué à chaque élément.

# echo ${noncontiguZ[@]#abc}            # Version-2.05b quitte avec un « core dump ».
# Corrigé depuis par Chet Ramey.

# Le -it serait sympa- Premier-Indice-De
# echo ${#noncontiguZ[@]#*}             # Ce n'est PAS du Bash valide.

echo
echo '- Préfixe le plus long -'
echo ${chaineZ##1*3}                # Non modifié (pas un préfixe)
echo ${chaineZ##a*C}                # abc
echo ${tableauZ[@]##a*c}            # ABCABC 123123 ABCABC

# echo ${noncontiguZ[@]##a*c}           # Version-2.05b quitte avec un « core dump ».
# Corrigé depuis par Chet Ramey.

echo
echo '- - Suppression du sous-élément suffixe - -'
echo '- - La correspondance du motif global doit inclure le dernier caractère. - -'
echo '- - Le motif global pourrait être un littéral ou un résultat de fonction. - -'
echo
echo '- Suffixe le plus court -'
echo ${chaineZ%1*3}                 # Non modifié (pas un suffixe).
echo ${chaineZ%$(_abc)}             # abcABC123ABC
echo ${tableauZ[@]%abc}             # Appliqué à chaque élément.

# echo ${noncontiguZ[@]%abc}            # Version-2.05b quitte avec un « core dump ».
# Corrigé depuis par Chet Ramey.

# Le -it serait sympa- Dernier-Indice-De
# echo ${#noncontiguZ[@]%*}             # Ce n'est PAS du Bash valide.

echo
echo '- Suffixe le plus long -'
echo ${chaineZ%%1*3}                # Non modifié (pas un suffixe)
echo ${chaineZ%%b*c}                # a
echo ${tableauZ[@]%%b*c}            # a ABCABC 123123 ABCABC a

# echo ${noncontiguZ[@]%%b*c}           # Version-2.05b quitte avec un « core dump ».
# Corrigé depuis par Chet Ramey.

echo
echo '- - Remplacement de sous-élements - -'
echo "- - Sous-élément situé n'importe où dans la chaîne. - -"
echo '- - La première spécification est un motif global. - -'
echo '- - Le motif global pourrait être un littéral ou un résultat de fonction de motif global. - -'
echo '- - La seconde spécification pourrait être un littéral ou un résultat de fonction. - -'
echo '- - La seconde spécification pourrait être non spécifiée. Prononcez-ça comme :'
echo '    Remplace-Avec-Rien (Supprime) - -'
echo



# Fonction renvoyant un motif global simple, littéral
_123() {
    echo -n '123'
}

echo '- Remplace la première occurrence -'
echo ${chaineZ/$(_123)/999}         # Modifié (123 est un composant).
echo ${chaineZ/ABC/xyz}             # xyzABC123ABCabc
echo ${tableauZ[@]/ABC/xyz}         # Appliqué à chaque élément.
echo ${noncontiguZ[@]/ABC/xyz}      # Fonctionne comme attendu.

echo
echo '- Supprime la première first occurrence -'
echo ${chaineZ/$(_123)/}
echo ${chaineZ/ABC/}
echo ${tableauZ[@]/ABC/}
echo ${noncontiguZ[@]/ABC/}

#  Le remplacement ne doit pas être un littéral,
#+ car le résultat de l'appel d'une fonction est permis.
#  C'est général pour toutes les formes de remplacement.
echo
echo '- Remplace la première occurence avec Résultat-De -'
echo ${chaineZ/$(_123)/$(_simple)}   # Fonctionne comme attendu.
echo ${tableauZ[@]/ca/$(_simple)}    # Appliqué à chaque élément.
echo ${noncontiguZ[@]/ca/$(_simple)} # Fonctionne comme attendu.

echo
echo '- Remplace toutes les occurrences -'
echo ${chaineZ//[b2]/X}              # X-out b et 2
echo ${chaineZ//abc/xyz}             # xyzABC123ABCxyz
echo ${tableauZ[@]//abc/xyz}         # Appliqué à chaque élément.
echo ${noncontiguZ[@]//abc/xyz}      # Fonctionne comme attendu.

echo
echo '- Supprime toutes les occurrences -'
echo ${chaineZ//[b2]/}
echo ${chaineZ//abc/}
echo ${tableauZ[@]//abc/}
echo ${noncontiguZ[@]//abc/}

echo
echo '- - Remplacement du sous-élément préfixe - -'
echo '- - La correspondance doit inclure le premier caractère. - -'
echo

echo '- Remplace les occurrences du préfixe -'
echo ${chaineZ/#[b2]/X}             # Non modifié (n'est pas non plus un préfixe).
echo ${chaineZ/#$(_abc)/XYZ}        # XYZABC123ABCabc
echo ${tableauZ[@]/#abc/XYZ}        # Appliqué à chaque élément.
echo ${noncontiguZ[@]/#abc/XYZ}     # Fonctionne comme attendu.

echo
echo '- Supprime les occurrences du préfixe -'
echo ${chaineZ/#[b2]/}
echo ${chaineZ/#$(_abc)/}
echo ${tableauZ[@]/#abc/}
echo ${noncontiguZ[@]/#abc/}

echo
echo '- - Remplacement du sous-élément suffixe - -'
echo '- - La correspondance doit inclure le dernier caractère. - -'
echo

echo '- Remplace les occurrences du suffixe -'
echo ${chaineZ/%[b2]/X}             # Non modifié (n'est pas non plus un suffixe).
echo ${chaineZ/%$(_abc)/XYZ}        # abcABC123ABCXYZ
echo ${tableauZ[@]/%abc/XYZ}        # Appliqué à chaque élément.
echo ${noncontiguZ[@]/%abc/XYZ}     # Fonctionne comme attendu.

echo
echo '- Supprime les occurrences du suffixe -'
echo ${chaineZ/%[b2]/}
echo ${chaineZ/%$(_abc)/}
echo ${tableauZ[@]/%abc/}
echo ${noncontiguZ[@]/%abc/}

echo
echo '- - Cas spéciaux du motif global nul - -'
echo

echo '- Tout préfixe -'
# motif de sous-chaîne nul, signifiant 'préfixe'
echo ${chaineZ/#/NEW}               # NEWabcABC123ABCabc
echo ${tableauZ[@]/#/NEW}           # Appliqué à chaque élément.
echo ${noncontiguZ[@]/#/NEW}        # Aussi appliqué au contenu nul.
                                    # Cela semble raisonnable.

echo
echo '- Tout suffixe -'
# motif de sous-chaîne nul, signifiant 'suffixe'
echo ${chaineZ/%/NEW}               # abcABC123ABCabcNEW
echo ${tableauZ[@]/%/NEW}           # Appliqué à chaque élément.
echo ${noncontiguZ[@]/%/NEW}        # Aussi appliqué au contenu nul.
                                    # Cela semble raisonnable.

echo
echo '- - Cas spécial pour le motif global For-Each - -'
echo '- - - - Ceci est un rêve - - - -'
echo

_GenFunc() {
    echo -n ${0}                    # Illustration seulement.
    # Actuellement, ce serait un calcul arbitraire.
}

#  Toutes les occurrences, correspondant au motif NImporteQuoi.
#  Actuellement, //*/ n'établit pas une correspondance avec un motif nul
#+ ainsi qu'avec une référence nulle.
#  /#/ et /%/ correspondent à un contenu nul mais pas à une référence nulle.
echo ${noncontiguZ[@]//*/$(_GenFunc)}


#  Une syntaxe possible placerait la notation du paramètre utilisé
#+ à l'intérieur du moyen de construction.
#   ${1} - L'élément complet
#   ${2} - Le préfixe, S'il existe, du sous-élément correspondant
#   ${3} - Le sous-élément correspondant
#   ${4} - Le suffixe, S'il existe, du sous-élément correspondant
#
# echo ${noncontiguZ[@]//*/$(_GenFunc ${3})}   # Pareil que ${1}, ici.
# Cela sera peut-être implémenté dans une future version de Bash.


exit 0