Annexe A. Scripts contribués

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

Exemple A-1. manview: Visualiser des pages man formatées

#!/bin/bash
# manview.sh: Formatte la source d'une page man pour une visualisation.

#  Ceci est utile lors de l'écriture de la source d'une page man et que vous
#+ voulez voir les résultats intermédiaires lors de votre travail.

E_MAUVAISARGS=65

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

groff -Tascii -man $1 | less
# De la page man de groff.

# Si la page man inclut des tables et/ou des équations,
# alors le code ci-dessus échouera.
# La ligne suivant peut gérer de tels cas.
#
#   gtbl < "$1" | geqn -Tlatin1 | groff -Tlatin1 -mtty-char -man
#
#   Merci, S.C.

exit 0

Exemple A-2. mailformat: Formater un courrier électronique

#!/bin/bash
# mail-format.sh: Formatte les courriers électroniques.

#  Supprime les caractères '>', les tabulations et coupe 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.

#  Suppression du caractère '>', des tabulations en début de ligne
#+ et coupage des lignes après $LONGUEUR_MAX caractères.
sed '
s/^>//
s/^  *>//
s/^  *//
s/		*//
' $1 | fold -s --width=$LONGUEUR_MAX
          #  L'option -s de "fold" coupe les lignes à un espace blanc, 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'utilitaire de manipulation de texte et un langage de
#+ scripts efficace apportent une alternative à des exécutables gonflés.

exit 0

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

Ce script est une modification de l'Exemple 12-15.

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


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

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

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


for fichier in *$1*    # Vérifie tous les fichiers correspondant du répertoire.
do
   if [ -f "$fichier" ]  # If finds match...
   then
     fname=`basename $fichier`             # Supprime le chemin.
     n=`echo $fname | sed -e "s/$1/$2/"`   # Substitue nouveau par ancien 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-4. blank-rename: Renommer les fichiers dont le nom contient des espaces

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

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

UN=1                     # Pour obtenir le singulier/puriel 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 des espace(s).
     then
       nomf=$fichier                       # Supprime le chemin.
       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 fichiers renommés."
else 
  echo "$nombre fichiers renommés."
fi 

exit 0

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

#!/bin/bash

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

#  Notez que c'est toujours peu 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 la paquetage de cryptage de fichier de l'auteur,
#+ basé sur l'algorythme classique "onetime pad",
#+ et disponible à partir de:
#+ Site primaire:  ftp://metalab.unc.edu /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.
# "bell" fait sonner une cloche après chaque transfert.

exit 0

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

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

CDROM=/dev/cdrom                           # périphérique CD ROM
OF=/home/bozo/projects/cdimage.iso         # fichier de sortie
#       /xxxx/xxxxxxx/                     A modifier suivant votre système.
TAILLEBLOC=2048
VITESSE=2                                  # May use higher speed if supported.

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 "Supprimer 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=0,0 $OF
# Utilise le paquetage "cdrecord" de Joerg Schilling's (voir sa docs).
# 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-7. collatz: Séries de Collatz

#!/bin/bash
# collatz.sh

#  Le célèbre "hailstone" ou la série de Collatz.
#  ----------------------------------------------
#  1) Obtenir un entier "de recherche" à partir de la ligne de commande.
#  2) NOMBRE <--- 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-8. 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

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 bisextile + 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 / liste-et".
  #
  # Exercice: Implémenter une vérification de date plus rigoureuse.
}


supprime_zero_devant () # Il est mieux de supprimer les zéros possibles
{                     # du jour et/ou du mois car 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 3 Jan. 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://home.t-online.de/home/berndt.schwerdtfeger/cal.htm


  if [ "$Jours" -gt "$MAXRETVAL" ] # Si supérieur à 256,
  then                             # alors devient négatif
    let "dindex = 0 - $Jours"      # qui pourra être renvoyé par la fonction.
  else let "dindex = $Jours"
  fi

  return $dindex

}  


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


abs ()                             # Valeur absolut.
{                                  # 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 tous zéros devant.
day=$?                               # sur le jour et/ou le mois.
supprime_zero_devant $mois
month=$?

index_jour $jour $mois $annee
date1=$?

abs $date1                         # S'assure que c'est positif en récupérant
date1=$value                       # la valeur absolue.

Analyse_Date $2
verifie_date $jour $mois $annee

supprime_zero_devant $jour
day=$?
supprime_zero_devant $mois
month=$?

index_jour $jour $mois $annee
date2=$?

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

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-9. makedict: Créer un << dictionnaire >>

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

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

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


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 majuscule.
        tr ' ' '\012' |              #  Nouveau: modification des espaces en
                                     #+ retours chariot.
#       tr -cd '\012[a-z][0-9]' |    #  Suppression de tout ce qui n'est pas
                                     #  alphanumérique
                                     #+ (script original).
        tr -c '\012a-z'  '\012' |    #  Plutôt que de supprimer
                                     #+ modification des non alpha en retours
				     #+ chariot.
        sort |                       #  Les options $SORT ne sont plus
	                             #+ nécessaires maintenant.
        uniq |                       # Suppression des mots dupliqués.
        grep -v '^#' |               #  Suppression des lignes commençant avec
	                             #+ le symbole '#'.
        grep -v '^$'                 # Suppression des lignes blanches.

exit 0	

Exemple A-10. soundex: Conversion phonétique

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

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


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

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


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

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

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

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

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

}  


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


# Change tous les caractères en entrée par des minuscules.
# ------------------------------------------------
nom=$( echo $nom_en_entree | tr A-Z a-z )
# ------------------------------------------------
# Au cas où cet argument est un mixe 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-11. << life: Jeu de la Vie >>

#!/bin/bash
# life.sh: "Life in the Slow Lane"

# ##################################################################### #
# This is the Bash script version of John Conway's "Game of Life".      #
# "Life" is a simple implementation of cellular automata.               #
# --------------------------------------------------------------------- #
# On a rectangular grid, let each "cell" be either "living" or "dead".  #
# Designate a living cell with a dot, and a dead one with a blank space.#
#  Begin with an arbitrarily drawn dot-and-blank grid,                  #
#+ and let this be the starting generation, "generation 0".             #
# Determine each successive generation by the following rules:          #
# 1) Each cell has 8 neighbors, the adjoining cells                     #
#+   left, right, top, bottom, and the 4 diagonals.                     #
#                       123                                             #
#                       4*5                                             #
#                       678                                             #
#                                                                       #
# 2) A living cell with either 2 or 3 living neighbors remains alive.   #
# 3) A dead cell with 3 living neighbors becomes alive (a "birth").     #
SURVIVE=2                                                               #
BIRTH=3                                                                 #
# 4) All other cases result in dead cells.                              #
# ##################################################################### #


startfile=gen0   # Read the starting generation from the file "gen0".
                 # Default, if no other file specified when invoking script.
                 #
if [ -n "$1" ]   # Specify another "generation 0" file.
then
  if [ -e "$1" ] # Check for existence.
  then
    startfile="$1"
  fi  
fi  


ALIVE1=.
DEAD1=_
                 # Represent living and "dead" cells in the start-up file.

#  This script uses a 10 x 10 grid (may be increased,
#+ but a large grid will will cause very slow execution).
ROWS=10
COLS=10

GENERATIONS=10          #  How many generations to cycle through.
                        #  Adjust this upwards,
                        #+ if you have time on your hands.

NONE_ALIVE=80           #  Exit status on premature bailout,
                        #+ if no cells left alive.
TRUE=0
FALSE=1
ALIVE=0
DEAD=1

avar=                   #  Global; holds current generation.
generation=0            # Initialize generation count.

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


let "cells = $ROWS * $COLS"
                        # How many cells.

declare -a initial      # Arrays containing "cells".
declare -a current

display ()
{

alive=0                 # How many cells "alive".
                        # Initially zero.

declare -a arr
arr=( `echo "$1"` )     # Convert passed arg to array.

element_count=${#arr[*]}

local i
local rowcheck

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

  # Insert newline at end of each row.
  let "rowcheck = $i % ROWS"
  if [ "$rowcheck" -eq 0 ]
  then
    echo                # Newline.
    echo -n "      "    # Indent.
  fi  

  cell=${arr[i]}

  if [ "$cell" = . ]
  then
    let "alive += 1"
  fi  

  echo -n "$cell" | sed -e 's/_/ /g'
  # Print out array and change underscores to spaces.
done  

return

}

IsValid ()                            # Test whether cell coordinate valid.
{

  if [ -z "$1"  -o -z "$2" ]          # Mandatory arguments missing?
  then
    return $FALSE
  fi

local row
local lower_limit=0                   # Disallow negative coordinate.
local upper_limit
local left
local right

let "upper_limit = $ROWS * $COLS - 1" # Total number of cells.


if [ "$1" -lt "$lower_limit" -o "$1" -gt "$upper_limit" ]
then
  return $FALSE                       # Out of array bounds.
fi  

row=$2
let "left = $row * $ROWS"             # Left limit.
let "right = $left + $COLS - 1"       # Right limit.

if [ "$1" -lt "$left" -o "$1" -gt "$right" ]
then
  return $FALSE                       # Beyond row boundary.
fi  

return $TRUE                          # Valid coordinate.

}  


IsAlive ()              # Test whether cell is alive.
                        # Takes array, cell number, state of cell as arguments.
{
  GetCount "$1" $2      # Get alive cell count in neighborhood.
  local nhbd=$?


  if [ "$nhbd" -eq "$BIRTH" ]  # Alive in any case.
  then
    return $ALIVE
  fi

  if [ "$3" = "." -a "$nhbd" -eq "$SURVIVE" ]
  then                  # Alive only if previously alive.
    return $ALIVE
  fi  

  return $DEAD          # Default.

}  


GetCount ()             # Count live cells in passed cell's neighborhood.
                        # Two arguments needed:
			# $1) variable holding array
			# $2) cell number
{
  local cell_number=$2
  local array
  local top
  local center
  local bottom
  local r
  local row
  local i
  local t_top
  local t_cen
  local t_bot
  local count=0
  local ROW_NHBD=3

  array=( `echo "$1"` )

  let "top = $cell_number - $COLS - 1"    # Set up cell neighborhood.
  let "center = $cell_number - 1"
  let "bottom = $cell_number + $COLS - 1"
  let "r = $cell_number / $ROWS"

  for ((i=0; i<$ROW_NHBD; i++))           # Traverse from left to right. 
  do
    let "t_top = $top + $i"
    let "t_cen = $center + $i"
    let "t_bot = $bottom + $i"


    let "row = $r"                        # Count center row of neighborhood.
    IsValid $t_cen $row                   # Valid cell position?
    if [ $? -eq "$TRUE" ]
    then
      if [ ${array[$t_cen]} = "$ALIVE1" ] # Is it alive?
      then                                # Yes?
        let "count += 1"                  # Increment count.
      fi	
    fi  

    let "row = $r - 1"                    # Count top row.          
    IsValid $t_top $row
    if [ $? -eq "$TRUE" ]
    then
      if [ ${array[$t_top]} = "$ALIVE1" ] 
      then
        let "count += 1"
      fi	
    fi  

    let "row = $r + 1"                    # Count bottom row.
    IsValid $t_bot $row
    if [ $? -eq "$TRUE" ]
    then
      if [ ${array[$t_bot]} = "$ALIVE1" ] 
      then
        let "count += 1"
      fi	
    fi  

  done  


  if [ ${array[$cell_number]} = "$ALIVE1" ]
  then
    let "count -= 1"        #  Make sure value of tested cell itself
  fi                        #+ is not counted.


  return $count
  
}

next_gen ()               # Update generation array.
{

local array
local i=0

array=( `echo "$1"` )     # Convert passed arg to array.

while [ "$i" -lt "$cells" ]
do
  IsAlive "$1" $i ${array[$i]}   # Is cell alive?
  if [ $? -eq "$ALIVE" ]
  then                           #  If alive, then
    array[$i]=.                  #+ represent the cell as a period.
  else  
    array[$i]="_"                #  Otherwise underscore
   fi                            #+ (which will later be converted to space).  
  let "i += 1" 
done   


# let "generation += 1"   # Increment generation count.

# Set variable to pass as parameter to "display" function.
avar=`echo ${array[@]}`   # Convert array back to string variable.
display "$avar"           # Display it.
echo; echo
echo "Generation $generation -- $alive alive"

if [ "$alive" -eq 0 ]
then
  echo
  echo "Premature exit: no more cells alive!"
  exit $NONE_ALIVE        #  No point in continuing
fi                        #+ if no live cells.

}


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

# main ()

# Load initial array with contents of startup file.
initial=( `cat "$startfile" | sed -e '/#/d' | tr -d '\n' |\
sed -e 's/\./\. /g' -e 's/_/_ /g'` )
# Delete lines containing '#' comment character.
# Remove linefeeds and insert space between elements.

clear          # Clear screen.

echo #         Title
echo "======================="
echo "    $GENERATIONS generations"
echo "           of"
echo "\"Life in the Slow Lane\""
echo "======================="


# -------- Display first generation. --------
Gen0=`echo ${initial[@]}`
display "$Gen0"           # Display only.
echo; echo
echo "Generation $generation -- $alive alive"
# -------------------------------------------


let "generation += 1"     # Increment generation count.
echo

# ------- Display second generation. -------
Cur=`echo ${initial[@]}`
next_gen "$Cur"          # Update & display.
# ------------------------------------------

let "generation += 1"     # Increment generation count.

# ------ Main loop for displaying subsequent generations ------
while [ "$generation" -le "$GENERATIONS" ]
do
  Cur="$avar"
  next_gen "$Cur"
  let "generation += 1"
done
# ==============================================================

echo

exit 0

# --------------------------------------------------------------
# The grid in this script has a "boundary problem".
# The the top, bottom, and sides border on a void of dead cells.
# Exercise: Change the script to have the grid wrap around,
# +         so that the left and right sides will "touch",      
# +         as will the top and bottom.

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

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

+++

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

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

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

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

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

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

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

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


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

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

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

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

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

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

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


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

+

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

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

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


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


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

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

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

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

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

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

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

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

exit 0

+

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

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

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

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

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

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

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


  exit 0

+

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

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

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

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


LIMITE=1000                    # Premiers de 2 à 1000

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

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

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

Premiers 1

exit 0

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

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

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

+

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

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

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

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


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

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

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

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

+

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

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

#!/bin/bash

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

# Conversion to bash v2 syntax done by Chet Ramey

# Commentary:
# Code:

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

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

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

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

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

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

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

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

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

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

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

    return 1
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

# provide string

# string.bash ends here


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

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

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

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



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


exit 0

+

+

Stephane Chazelas montre la programmation objet dans un script Bash.

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

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


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

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

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

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

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

echo

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

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

echo

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

exit 0