Chapitre 6. Sortie et code de sortie (ou d'état)

 

...il existe des coins sombres dans le shell Bourne et les gens les utilisent tous.

 Chet Ramey

La commande exit est utilisable pour terminer un script, comme dans un programme C. Elle peut également renvoyer une valeur, qui sera disponible pour le processus parent du script.

Chaque commande renvoie un code de sortie (quelque fois nommé état de retour ). Une commande ayant réussi renvoie un 0, alors qu'une ayant échoué renvoie une valeur différente de zéro qui est habituellement interprétable comme un code d'erreur. Les commandes, programmes et utilitaires UNIX ayant bien fonctionné, renvoient un code de sortie 0 relatif à leur exécution réussie, bien qu'il y ait quelques exceptions.

De même, les fonctions dans un script et le script lui-même renvoient un code de sortie. La dernière commande exécutée dans la fonction ou le script détermine le code de sortie. A l'intérieur d'un script, une commande exit nnn peut être employée pour retourner un code de sortie nnn au shell. (nnn doit être un nombre décimal compris entre 0 et 255).

Note

Lorsqu'un script se termine avec un exit sans paramètres, le code de sortie du script est le code de sortie de la dernière commande exécutée dans le script (sans compter le exit).

$? lit le code de sortie de la dernière commande exécutée. Après la fin d'une fonction, $? donne le code de sortie de la dernière commande exécutée dans la fonction. C'est la manière de Bash de donner aux fonctions une << valeur de retour >>. Après la fin d'un script, un $? sur la ligne de commande indique le code de sortie du script, c'est-à-dire celui de la dernière commande exécutée dans le script qui est, par convention, 0 en cas de succès ou un entier compris entre 1 et 255 en cas d'erreur.

Exemple 6-1. exit / code de sortie

#!/bin/bash

echo bonjour
echo $?    # Code de sortie 0 renvoyé car la commande s'est correctement
           # exécutée.

lskdf      # Commande non reconnue.
echo $?    # Code de sortie différent de zéro car la commande a échoué.

echo

exit 113   # Retournera 113 au shell.
           # Pour vérifier ceci, tapez "echo $?" une fois le script terminé.

#  Par convention, un 'exit 0' indique un succès,
#+ alors qu'un code de sortie différent de zéro indique une erreur ou une
#+ condition anormale.

$? est particulièrement utile pour tester le résultat d'une commande dans un script (voir Exemple 12-27 et Exemple 12-13).

Note

Le !, qualificateur logique du << non >>, inverse le résultat d'un test ou d'une commande et ceci affecte son code de sortie.

Exemple 6-2. Inverser une condition en utilisant !

true  # la commande intégrée "true"
echo "code de sortie de \"true\" = $?"     # 0

! true
echo "code de sortie de \"! true\" = $?"   # 1
# Notez que "!" nécessite un espace.
#    !true   renvoie une erreur "command not found"

# Merci, S.C.

Attention

Certains codes de sortie ont une signification spéciale et ne devraient pas être employés par l'utilisateur dans un script.

>code de sortieCONstdin<< console >> (stdin)PRN/dev/lp0périphérique imprimante (générique)LPT1/dev/lp0premier périphérique imprimanteCOM1/dev/ttyS0premier port série

Les fichiers batch contiennent habituellement des commandes DOS. Elles doivent être remplacées par leur équivalent UNIX pour convertir un fichier batch en script shell.

Tableau H-2. Commandes DOS et leur équivalent UNIX

Commande DOSEquivalent UNIXEffet
ASSIGNlnlie un fichier ou un répertoire
ATTRIBchmodchange les permissions d'un fichier
CDcdchange de répertoire
CHDIRcdchange de répertoire
CLSclearefface l'écran
COMPdiff, comm, cmpcompare des fichiers
COPYcpcopie des fichiers
Ctl-CCtl-Cbreak (signal)
Ctl-ZCtl-DEOF (end-of-file, fin de fichier)
DELrmsupprime le(s) fichier(s)
DELTREErm -rfsupprime le répertoire récursivement
DIRls -laffiche le contenu du répertoire
ERASErmsupprime le(s) file(s)
EXITexitsort du processus courant
FCcomm, cmpcompare des fichiers
FINDgrepcherche des chaînes de caractères dans des fichiers
MDmkdircrée un répertoire
MKDIRmkdircrée un répertoire
MOREmoreaffiche un fichier page par page
MOVEmvdéplace
PATH$PATHchemin vers les exécutables
RENmvrenomme (ou déplace)
RENAMEmvrenomme (ou déplace)
RDrmdirsupprime un répertoire
RMDIRrmdirsupprime un répertoire
SORTsorttrie un fichier
TIMEdateaffiche l'heure système
TYPEcatenvoie le fichier vers stdout
XCOPYcpcopie de fichier (étendue)

Note

Virtuellement, tous les opérateurs et commandes shell et UNIX ont beaucoup plus d'options et d'améliorations que leur équivalent DOS et fichier batch. Beaucoup de fichiers batch DOS reposent sur des utilitaires supplémentaires, tel que ask.com, un équivalent limité de read.

DOS supporte un sous-ensemble très limité et incompatible de caractères d'expansion de noms de fichier, reconnaissant seulement les caractères * et ?.

Convertir un fichier batch DOS en script shell est généralement assez simple et le résultat est souvent bien meilleur que l'original.

Exemple H-1. VIEWDATA.BAT: Fichier Batch DOS

REM VIEWDATA

REM INSPIRED BY AN EXAMPLE IN "DOS POWERTOOLS"
REM                           BY PAUL SOMERSON


@ECHO OFF

IF !%1==! GOTO VIEWDATA
REM  IF NO COMMAND-LINE ARG...
FIND "%1" C:\BOZO\BOOKLIST.TXT
GOTO EXIT0
REM  PRINT LINE WITH STRING MATCH, THEN EXIT.

:VIEWDATA
TYPE C:\BOZO\BOOKLIST.TXT | MORE
REM  SHOW ENTIRE FILE, 1 PAGE AT A TIME.

:EXIT0

La conversion de ce script en est une belle amélioration.

Exemple H-2. viewdata.sh: Conversion du script shell VIEWDATA.BAT

#!/bin/bash
# Conversion de VISUDONNEES.BAT en script shell.

FICHIERDONNEES=/home/bozo/datafiles/book-collection.data
SANSARGUMENT=1

# @ECHO OFF       Commande inutile ici.

if [ $# -lt "$SANSARGUMENT" ]   # IF !%1==! GOTO VIEWDATA
then
  less $FICHIERDONNEES          # TYPE C:\MYDIR\BOOKLIST.TXT | MORE
else
  grep "$1" $FICHIERDONNEES     # FIND "%1" C:\MYDIR\BOOKLIST.TXT
fi  

exit 0                    # :EXIT0

# Les GOTOs, labels, smoke-and-mirrors et flimflam sont inutiles.
# Le script converti est court, joli et propre, ce qu'on ne peut pas dire de
# l'original.

Le site Shell Scripts on the PC de Ted Davis a un ensemble complet de tutoriels sur l'art démodé de la programmation de fichiers batch. Certaines de ses echniques ingénieuses peuvent raisonnablement être utilisées dans des scripts shell.

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