Le shell Bash et plus encore

Gazette Linux n°109 — Décembre 2004

William Park

Article paru dans le n°109 de la Gazette Linux de décembre 2004.

Traduction française par Antonin Mellier .

Relecture de la traduction française par Joëlle Cornavin .

Article publié sous Open Publication License. La Linux Gazette n'est ni produite, ni sponsorisée, ni avalisée par notre hébergeur principal, SSC, Inc.


Table des matières
1. Création d'un correctif et compilation d'un shell Bash
1.1. Compilation
1.2. Création du correctif
1.3. Builtins dynamiquement chargeables
1.4. Tout-en-un
2. strcat, strcpy, strlen et strcmp
3. ASCII et <ctype.h>
4. E/S formatées
4.1. sscanf
4.2. Lecture et affichage de lignes DOS
4.3. Émulation simple de Awk
4.4. Indentation dans << here-document
5. Générateurs de séquences {a..b} et {a--b}
5.1. Séquence d'entiers {a..b}
5.2. Séquences de caractères {a--b}
6. Filtrage de contenu ${var|...}
7. Instruction case
7.1. Le motif (pattern) regex
7.2. Continuation
7.3. Condition de sortie
8. Les boucles for/while/until
8.1. Boucles for à variables multiples
8.2. Condition de sortie
9. Exception et bloc try
10. Récapitulatif

1. Création d'un correctif et compilation d'un shell Bash

Dans un précédent article, j'ai présenté des fonctions shell qui émulent les fonctions C strcat(3), strcpy(3), strlen(3) et strcmp(3). Comme la tâche principale du shell est d'analyser du texte, une solution shell pure était possible pour les opérations sur les chaînes de caractères trouvées dans <string.h>. Néanmoins, c'est rare. Il n'est pas toujours possible d'émuler du C en shell, en particulier pour accéder à des bibliothèques système bas niveau et à des applications tierces. Même si c'était possible, vous réinventeriez la roue en ignorant le travail qui a été effectué pour les bibliothèques C. De plus, les scripts shell sont, sans exception, des ordres de grandeur plus lents. Cependant, le shell a l'avantage de permettre un développement et une maintenance aisés, parce qu'il est plus facile à écrire et à lire.

Ce dont on a besoin, ensuite, est la capacité d'écrire un script d'invocation (shell wrapper) avec des liaisons vers des routines C. Un mécanisme shell qui permet d'écrire une extension C est appelé builtin, par exemple read, echo et printf, etc. Quand certaines fonctionnalités requièrent des changements dans la façon dont le shell interprète une expression, alors des modifications doivent être apportées dans le code d'analyse syntaxique (parsing) du shell. Quand vous avez besoin de vitesse, une extension C est ce qu'il y a de mieux.

Mon correctif (patch) pour le shell Bash 3.0 est disponible aux adresses suivantes :

La dernière archive tar (tarball) bashdiff-1.11.tar.gz contient deux fichiers diff :

  1. bashdiff-core-1.11.diff porte sur les fonctionnalités qui seront compilées statiquement dans le shell. Il en ajoute de nouvelles en modifiant le code d'analyse syntaxique de Bash. Cette opération est entièrement réversible en ce sens qu'aucune signification existante n'est changée. Donc, ce qui fonctionne dans votre ancien shell, fonctionnera aussi dans le nouveau. Par exemple, il ajoute :

    • une nouvelle expansion d'accolades {a..b} : génération d'entiers et de lettres, paramètres positionnels et expansion des tableaux

    • une nouvelle expansion de paramètres ${var|...} : filtrage du contenu, inclusion des listes (comme Python)

    • une nouvelle substitution de commandes $(=...) : ancrage de la virgule flottante dans Awk

    • une extension des instructions case : expressions régulières, suites, sections then/else

    • une extension pour les boucles while/until : sections then/else, variables multiples pour les boucles for

    • des blocs de test avec exception d'entier (comme en Python)

    • un nouvel opérateur <<+ here-document : indentation relative

  1. bashdiff-william-1.11.diff porte sur les builtins dynamiquement chargeables (loadables) qui sont disponibles à part dans votre session shell. Il ajoute de nouvelles commandes, pour faire l'interface avec les bibliothèques système/applications et pour fournir un wrapper plus rapide pour les opérations courantes. Par exemple, il ajoute :

    • une extension aux builtins read/echo : lignes DOS

    • des wrappers sscanf(3), <string.h> et <ctype.h>, la conversion ASCII/chaînes de caractères

    • un nouveau builtin raise pour les blocs try

    • une découpe/un collage de tableaux, un filtre/map/zip/unzip de tableaux (comme en Python)

    • des opérations sur la fonction regex(3) : correspondance, découpage, recherche, remplacement, rappel

    • un générateur de modèles HTML (comme PHP, JSP, ASP)

    • une interface pour les base de données GDBM, SQLite, PostgreSQL, et MySQL

    • une interface d'analyseExpat XML

    • opérations piles/files d'attente sur les tableaux et les paramètres positionnels

    • graphique/tracé x-y

Toutes les fonctionnalités sont décrites dans les fichiers d'aide internes du shell auquel on peut accéder avec la commande help.


1.1. Compilation

Avant de vous lancer dans un shell corrigé, vous devez savoir comment compiler depuis le source, étant donné que le correctif s'effectue par rapport à l'arborescence source. Voici les étapes nécessaires pour télécharger et compiler le shell standard Bash 3.0 :



wget ftp://ftp.gnu.org/pub/gnu/bash/bash-3.0.tar.gz
tar -xzf bash-3.0.tar.gz
cd bash-3.0
    ./configure
    make

On a maintenant un bash binaire exécutable qui est comme votre bash actuel, normalement /bin/bash . Vous pouvez l'essayer en saisissant :



./bash              # en utilisant un Bash-3.0 fraîchement compilé
date
ls
exit                # revient à votre session de shell antérieure


1.2. Création du correctif

Pour compiler mon shell corrigé, les étapes sont sensiblement les mêmes que précédemment. Téléchargez une archive tar (tarball), appliquez mon correctif dans l'arborescence source (à partir des étapes précédentes) et compilez. bashdiff.tar.gz pointera toujours sur le dernier correctif, qui est ici bashdiff-1.10.tar.gz.



wget http://home.eol.ca/~parkw/bashdiff/bashdiff-1.10.tar.gz
tar -xzf bashdiff-1.10.tar.gz
mv bash-3.0 bash            # ce n'est plus un Bash-3.0 standard
cd bash
    make distclean
    patch -p1 < ../bashdiff-core-1.10.diff
    patch -p1 < ../bashdiff-william-1.10.diff
    autoconf
    ./configure
    make
    make install            # en tant que root
    cd examples/loadables/william
        make
        make install        # en tant que root
        ldconfig            # en tant que root

Maintenant, vous avez :

  • bash, qui est le shell principal exactement comme avant ; il sera installé dans /usr/local/bin/bash et

  • william.so, qui est un objet partagé contenant des modules chargeables à la demande (loadables) ; il sera installé dans /usr/local/lib/libwilliam.so avec un lien symbolique vers /usr/local/lib/william.so. Dans la version 1.10, il y a 33 builtins   chargeables à la demande, à savoir :

    • Lsql

    • Msql

    • Psql

    • gdbm

    • xml

    • array

    • arraymap

    • arrayzip

    • arrayunzip

    • basp

    • match

    • vplot

    • pp_append

    • pp_collapse

    • pp_flip

    • pp_overwrite

    • pp_pop

    • pp_push

    • pp_rotateleft

    • pp_rotateright

    • pp_set

    • pp_sort

    • pp_swap

    • pp_transpose

    • sscanf

    • strcat

    • strcpy

    • strlen

    • strcmp

    • tonumber

    • tostring

    • chnumber

    • isnumber


1.3. Builtins dynamiquement chargeables

Si votre shell comporte enable -[fd], alors vous pouvez charger/décharger des commandes builtin dynamiquement, d'où le nom. La procédure est simple. Par exemple :



enable -f william.so vplot

chargera la commande vplot depuis la bibliothèque partagée william.so que vous venez de compiler et d'installer. Utilisez ./william.so si vous ne l'avez pas encore installée. Une fois chargée, vous pouvez l'utiliser exactement comme les builtins standard qui sont liées statiquement dans le shell. Donc :



help vplot
help -s vplot

affichera la version longue et courte du fichier d'aide de la commande, alors que :



vplot [-0 -x columns -y lines -X xtitle -Y ytitle] {xy | x y | x1 y1 x2 y2 ...}

et



x=( `seq -100 100` )
y=( `for i in ${x[*]}; do echo $((i*i)); done` )    # y = x^2
vplot x y

affichera le tracé x-y d'une parabole dans votre terminal. Pour la décharger, saisissez :



enable -d vplot


1.4. Tout-en-un

Les modules chargeables à la demande (loadables) sont pratiques si vous voulez juste charger les builtins dont vous avez besoin et si vous ne souhaitez ou ne pouvez pas changer votre shell de connexion. De plus, les modules chargeables sont plus faciles à compiler à partir d'une version précédente (incrémentiellement), ce qui est important puisque de nouveaux builtins sont ajoutés ou mis à jour plus souvent que le code d'analyse principal du shell.

Néanmoins, vous pouvez être amené à compiler et inclure le tout dans un seul exécutable, comme sous Windows par exemple. Pour compiler un fichier binaire « tout-en-un », vous devez saisir quelques lignes de code supplémentaires. Il faut générer le fichier binaire par défaut du bash, car nous avons besoin de tous les fichiers .h et .o.



cd bash
    make bash
    make bash+william       # tout en un
    make install-bin        # installe seulement 'bash', 'bashbug', 'bash+william'

Ici, bash+william est identique au bash, sauf que tous les builtins sont liés statiquement dedans. Je recommande le fichier binaire simple bash+william aux débutants, car il évite d'avoir à se rappeler de ce qu'il faut charger et décharger. Tout est là sous la main.


2. strcat, strcpy, strlen et strcmp

Dans un apam_count, ham_count, atime) VALUES ('1','1–/:','0','1','1224827014') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827014')¶`I¾ÃfE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','eQË­','0','1','1224827010') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827010')¶`I¾ÂÄiE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1',' ;.q ','0','1','1224827012') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827012')¶`I¾€ÆjE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','¶{² ','0','1','1224827014') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827014')¶`I¾>ÈfE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','øê=q','0','1','1224827010') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827010')¶`I¾üÉiE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','íã(Œ','0','1','1224827012') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827012')¶`I¾ºËjE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','D‡Âa(','0','1','1224827014') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827014')¶`I¾xÍiE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','¬Wk¦','0','1','1224827012') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827012')¶`I¾6ÏfE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','¬Wk¦','0','1','1224827010') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827010')¶`I¾ôÐjE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','Ì|·','0','1','122482701ajuscule un mot, par exemple, est extrêmement verbeux dans un shell normal :



word=abc123
first=`echo ${word:0:1} | tr 'a-z' 'A-Z'`
rest=`echo ${word:1} | tr 'A-Z' 'a-z'`
echo $first$rest

ce qui ne fonctionne que dans les paramètres régionaux (locales) en anglais, à cause des registres explicites [a-z] et [A-Z]. En revanche, en C, il suffit d'appeler isupper(3), islower(3), toupper(3) et tolower(3), qui fonctionnent avec tous les paramètres régionaux que la bibliothèque C prend en charge.

Ce dont nous avons besoin, ce sont des scripts d'invocation shell wrappers) pour les fonctions C standard suivantes : toupper(3), tolower(3), toascii(3), toctrl(), isalnum(3), isalpha(3), isascii(3),isblank(3),iscntrl(3), isdigit(3), isgraph(3), islower(3), isprint(3), ispunct(3), isspace(3), isupper(3), isxdigit(3) et isword(). La plupart d'entre elles étant définies dans <ctype.h>, les opérations sur les caractères peuvent être effectuées simplement et efficacement.

Maintenant que Bash couvre plutôt bien les fonctions de <string.h> et <ctype.h>, vous pouvez effectuer des opérations sur des caractères et des chaînes de caractères dans un script shell pratiquement de la même manière que dans du code C. Le texte ainsi que les données binaires sont gérées avec facilité et cohérence. Ces quelques modifications à elles seules représentent déjà une grande amélioration par rapport au shell standard.


4. E/S formatées

4.1. sscanf

Une des premières choses que l'on apprend dans tout langage est la lecture et l'affichage. En C, on utilise les fonctions printf(3), scanf(3) et autres définies dans <stdio.h>. Pour afficher dans le shell, on fait appel aux builtins echo et printf. Curieusement, pourtant, il manque une version shell de scanf(3). Par exemple, pour analyser les 4 nombres de 11.22.33.44, vous pouvez saisir :



IFS=. read a b c d <<< 11.22.33.44

Cependant, si votre champ n'est pas aussi bien délimité que ci-dessus, les choses se compliquent.

J'ai ajouté la version shell de la fonction C sscanf(3) :

  • sscanf input format var1 [... var9]

Du fait que le shell n'a que des données de type chaînes de caractères, il ne prend en charge que des formats de chaînes, c'est-à-dire %s, %c, %[...], %[^...] et jusqu'à 9 variables. Donc, vous pouvez analyser des chaînes formatées tout comme vous le feriez en C, par exemple :



sscanf 11.22.33.44 '%[0-9].%[0-9].%[0-9].%[0-9]' a b c d
declare -p a b c d          # a=11 b=22 c=33 d=44

sscanf 'abc 123 45xy' '%s %s %[0-9]%[a-z]' a b c d
declare -p a b c d          # a=abc b=123 c=45 d=xy


4.2. Lecture et affichage de lignes DOS

De temps en temps, il est nécessaire d'afficher et de lire des lignes DOS qui se terminent par \r\n (CR/NL). Bien que vous puissiez afficher \r explicitement, l'insertion automatique de \r juste devant \n est difficile à réaliser en shell. Pour la lecture, vous devez explicitement supprimer le \r de fin.

J'ai créé un correctif des builtins standard echo et read   pour qu'ils puissent lire et écrire des lignes DOS :

  • echo [-....] -D [arg ...]

  • read [-...] -D [nom ...]

Par exemple :



echo abc | od -c                    # a b c \n
echo -D abc | od -c                 # a b c \r \n

read a b <<< $'11 22 \r'            # a=11 b=$'22 \r'
read -D a b <<< $'11 22 \r'         # a=11 b=22


4.3. Émulation simple de Awk

Souvent, il est nécessaire d'analyser des lignes et de travailler avec des variables de type Awk comme NF, NR, $1, $2, ..., $NF. Cependant, quand vous utilisez Awk, il est difficile de ramener ces variables dans le shell ; vous devez les écrir en syntaxe shell dans un fichier temporaire pour ensuite le reprendre. Cela rend fastidieux de faire des allers-retours entre le shell et Awk.

J'ai créé un correctif du builtin standard read pour offrir une émulation simple de Awk, en créant les variables NF et NR, et en affectant les champs à $1, $2, ..., $NF.

  • read [-...] -A [nom ...]

Par exemple :



IFS=. read -A <<< 11.22.33.44
echo $#: $*                 # 4: 11 22 33 44
declare -p NF NR

En outre, comme dans Awk, chaque appel pour lire -A incrémentera NR.


4.4. Indentation dans << here-document

<< est l'opérateur de redirection d'entrée, où l'entrée standard est tirée du texte même du source du script. << conservera les espaces blancs de début et <<- supprimera toutes les tabulations de début. Le problème avec <<- est que l'indentation relative est perdue.

J'ai ajouté un nouvel opérateur <<+ qui conserve l'indentation par tabulation du here-document par rapport à la première ligne. Il est accessible directement via le shell (c'est-à-dire ./bash ou /usr/local/bin/bash), parce que le correctif est intégré dans le code d'analyse syntaxique principal. Donc :



cat <<+ EOF
        first line
        second line
EOF

affichera



first line
        second line


5. Générateurs de séquences {a..b} et {a--b}

5.1. Séquence d'entiers {a..b}

Bash 3..0 (et Zsh) comportent l'expression {a..b} qui génère une séquence d'entiers en tant qu'élément de l'expansion d'accolades, mais on ne peut pas utiliser la substitution de variables car l'expression {a..b} doit contenir explicitement des entiers.

Mon correctif étend l'expansion d'accolades de façon à inclure la substitution de variables, de paramètres et de tableaux, de même qu'un générateur de séquences de lettres uniques. Par exemple ;:



a=1 b=10 x=a y=b
    echo {1..10}
    echo {a..b}
    echo {!x..!y}           # utiliser 'set +H' pour supprimer l'expansion de !
set -- `seq 10`
    echo {**}
    echo {##}
    echo {1..#}
z=( `seq 10` )
    echo {^z}

donneront toutes le même résultat, c'est-à-dire 1 2 ... 10. D'autres détails sont disponibles dbayes_token (id, token, spam_count, ham_count, atime) VALUES ('1',' «Ôä[','0','1','1224827012') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827012')¶`I¾,žjE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','_š)','0','1','1224827014') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827014')¶`I¾êžfE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','­Btj6','0','1','1224827010') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827010')¶`I¾¨žiE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','¨4• H','0','1','1224827012') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827012')¶`I¾fžfE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','&P¤¼Í','0','1','1224827010') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827010')¶`I¾$žjE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1',';,`Œ7','0','1','1224827014') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827014')¶`I¾âžiE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','šÔÊ4ÿ','0','1','1224827012') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827012')¶`I¾ žfE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','õ’&â}','0','1','1224827010') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827010')¶`I¾^žjE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) VALUES ('1','Qö Ô^','0','1','1224827014') ON DUPLICATE KEY UPDATE spam_count = GREATEST(spam_count + '0', 0), ham_count = GREATEST(ham_count + '1', 0), atime = GREATEST(atime, '1224827014')¶`I¾!žiE @stdpourrielsINSERT INTO bayes_token (id, token, spam_count, ham_count, atime) ="literal" >?, alors la chaîne originelle est incluse dans l'expansion de paramètres uniquement si command var renvoie success (0). Si tel n'est pas le cas, alors elle est supprimée de l'expansion. Le contenu reste inchangé mais pouvez décider de l'inclure ou non. Par conséquent, ${var|?true} sera équivalent à ${var}, puisque true renvoie toujours success (0). Par exemple :



b=( `date` )
func () { [[ $1 == [A-Z]* ]]; }
echo ${b[*]|?func}          # seulement des mots en majuscules

set -- `date`
func () { [[ $1 == *[!0-9]* ]]; }
echo ${*|?func}             # seulement des caractères autres que des nombres

  • ${var|/regex}

  • ${var|=glob}

  • ${var|~regex}

  • ${var|!glob}

    Si l'on veut faire un filtrage spécial, vous pouvez spécifier un motif [pattern] glob(7) ou regex(7) à faire correspondre par rapport à certains élements de la variable : ${var|=glob} et ${var|/regex} inclueront la chaîne uniquement s'il y a une correspondance ; à l'inverse, ${var|!glob} et ${var|~regex} inclueront la chaîne uniquement s'il n'y a pas de correspondance. Les exemples ci-dessus peuvent s'écrire sous la forme suivante :

    

b=( `date` )
    echo ${b[*]|=[A-Z]*}        # seulement des mots en majuscules
    
    set -- `date`
    echo ${*|=*[!0-9]*}         # seulement des caractères autres que des nombres
    
    
  • ${var|:a:b}

    Vous pouvez extraire une plage [a:b] de style Python à l'aide de ${var|:a:b}, qui est identique à la syntaxe shell standard ${var:a:n}. Si var est une chaîne, alors ce sera une sous-chaîne ; si var est une liste, alors ce sera une sous-liste. Par exemple :

    

a=0123456789
    echo ${a|::3} ${a|:-3:} ${a|:1:-1}          # 012 789 12345678
    
    set -- {a--z}
    echo ${*|::3} ${*|:-3:} ${*|:1:-1}
    
    

    affichera, respectivement, les trois premiers caractères ou éléments de la liste, les trois derniers et tous les éléments, sauf le premier et le dernier.

  • ${var|*n}

    Si vous devez dupliquer une chaîne ou une liste, alors ${var|*n} copiera la chaîne ou la liste n  fois. Par exemple :

    

a=abc123
    echo ${a|*3}                # 3 fois
    
    set -- a b c
    echo ${*|*2+3}              # 5 fois
    
    

  • 7. Instruction case

    7.1. Le motif (pattern) regex

    La syntaxe standard de l'instruction case est la suivante :

    

case WORD in
        glob [| glob]...) COMMANDS ;;
            ...
    esac
    
    

    J'ai étendu la syntaxe à :

    

case WORD in
        glob [| glob]...) COMMANDS ;;
        regex [| regex]...)) COMMANDS ;;
        ...
    esac
    
    

    de sorte que la liste motif sera interprétée en tant que regex si l'expression se termine par une double parenthèse )). Pour le reste, le fonctionnement reste le même.. Bien que Bash 3.0 ait [[ string =~ regex ]], une instruction case est encore une meilleure syntaxe pour deux motifs ou plus, ou si vous devez tester à la fois glob et regex dans le même contexte.

    Alors que glob reconnaît la chaîne entière pour renvoyer success, regex peut reconnaître une sous-chaîne. S'il y a une correspondance, alors la variable tableau SUBMATCH contiendra la sous-chaîne correspondante dans SUBMATCH[0] et tout groupe entre parenthèses dans le motif regex dans SUBMATCH[1], SUBMATCH[2], etc. Par exemple :

    

case .abc123. in
        '([a-z]+)([0-9]+)' )) echo yes ;;
    esac
    declare -p SUBMATCH
    
    

    aboutira à une correspondance, et

    • SUBMATCH[0]=a<bc123, l'expression régulière entière ([a-z]+)([0-9]+)

    • SUBMATCH[1]=abc, le premier groupe ([a-z]+)

    • SUBMATCH[2]=123, le second groupe ([0-9]+)


    7.2. Continuation

    En Zsh et en Ksh, vous pouvez continuer avec la liste de commandes suivante si vous utilisez ;& au lieu de ;;. Donc :

    

case WORD in
       motif1) commande1 ;;&
       motif2) commande2 ;;
        ...
    easc
    
    

    command1 sera exécutée si pattern1 est vérifié. Ensuite, l'exécution continuera avec command2, puis la liste de commande suivante, tant qu'elle ne rencontrera pas de double point-virgule. Or, avec le Bash c'est également possible.

    De plus, si vous terminez la liste de commande par ;;&,

    

case WORD in
       motif1) commande1 ;;&
       motif2) commande2 ;;
        ...
    easc
    
    

    commande1 s'exécutera si motif1 correspond. L'exécution continuera ensuite en testant motif2 au lieu de sortir de l'instruction case. Par conséquent, il testera toute la liste de motifs de la liste, qu'il y ait ou non une correspondance avérée. Zsh et Ksh n'offrent pas cette fonctionnalité.


    7.3. Condition de sortie

    Souvent, il est nécessaire de connaître la condition de sortie d'une instruction case. Vous pouvez utiliser *) comme motif par défaut, mais il n'est pas évident de déterminer s'il y a eu une correspondance puisque vous sortez de l'instruction case. Avec mon correctif, vous pouvez ajouter une section then et else optionnelle à la fin de l'instruction case juste après esac et de traiter l'instruction case comme si c'était une vraie instruction if . Voici à quoi ressemblera la nouvelle syntaxe :

    

case ... in             case ... in             case ... in
        ...                     ...                     ...
    esac then               esac then               esac else
        COMMANDS                COMMANDS                COMMANDS
    else                    fi                      fi
        COMMANDS
    fi
    
    

    esac then et esac else ne peuvent pas être séparés par ; ou par un retour à la ligne. Le then-COMMANDS sera exécuté si il y a une correspondance, sinon ce sera else-COMMANDS s'il n'y en a pas.

    Par exemple :

    

case abc123 in
        [A-Z]*) echo matched ;;
    esac then
        echo yes
    else
        echo no         # pas de correspondance
    fi
    
    

    affichera no, alors que

    

case Xabc123 in
        [A-Z]*) echo matched ;;         # correspondance
    esac then
        echo yes                        # correspondance
    else
        echo no
    fi
    
    

    affichera matched et yes.


    8. Les boucles for/while/until

    8.1. Boucles for à variables multiples

    Dans le shell standard, on ne peut utiliser qu'une seule variable dans une boucle for. J'ai ajouté une syntaxe à plusieurs variables, de sorte que

    

for a,b,c in {1..10}; do
        echo $a $b $c
    done
    
    

    affichera

    

1 2 3
    4 5 6
    7 8 9
    10
    
    

    comme vous l'espérez. Ici, les variables doivent être séparées par des virgules. S'il y a trop peu d'éléments à affecter dans la dernière itération, les variables restantes seront affectées à la chaîne vide (null).


    8.2. Condition de sortie

    Comme dans l'instruction case, vous devez souvent savoir si vous êtes sorti de la boucle normalement ou en employant l'instruction break. Avec mon correctif, vous pouvez ajouter des sections then et else optionnelles à la fin des boucles for, while et until juste après done. Voici à quoi ressemblera la nouvelle syntaxe :

    

[for|while|until] ...; do       [for|while|until] ...; do       [for|while|until] ...; do
        ...                             ...                             ...
    done then                       done then                       done else
        COMMANDS                        COMMANDS                        COMMANDS
    else                            fi                              fi
        COMMANDS
    fi
    
    

    done then et done else ne peuvent pas être séparés par ; ou par un retour à la ligne. Ici, then-COMMANDS sera exécuté si la boucle s'est terminée normalement, sinon else-COMMANDS sera exécuté si break a été utilisé. Par « normalement », je veux dire que la boucle for a épuisé toute la liste d'éléments, que le test sur le while a échoué ou que le test until a réussi.

    Par exemple :

    

for i in 1 2 3; do
        echo $i
        break
    done then
        echo normal
    else
        echo used break         # 1 
    fi
    
    

    affichera 1 uniquement pour la première itération, puis il sortira de la boucle. Mais :

    

for i in 1 2 3; do
        echo $i
    done then
        echo normal             # 1 2 3
    else
        echo used break
    fi
    
    

    affichera 1 2 3 et la condition de sortie s'effectuera normalement. Il en sera de même pour les boucles while et until.

    La capacité de tester la condition de sortie améliore la lisibilité des scripts shell, car il n'est plus nécessaire d'utiliser une variable comme drapeau (flag). Python possède un mécanisme similaire pour tester la condition de sortie d'une boucle, mais il utilise la valeur de retour du test. On a donc une sortie de la boucle while quand le test échoue et Python utilise else comme condition normale de sortie, ce qui crée un peu de confusion.


    9. Exception et bloc try

    En pratique, tous les langages modernes ont la capacité de lever une exception pour sortir d'un code très imbriqué, traiter les erreurs ou faire des sauts multipoint. J'ai ajouté un nouveau bloc try au Bash qui saisira les exceptions levées par le nouveau builtin raise.

    • raise [n]

    • try

      

    COMMANDS
      done in 
          NUMBER [| NUMBER].... ) COMMANDS ;;
          ...
      esac
      
      

      done in ne peut pas être séparé par ; ou par un retour à la ligne. En outre, la liste de motif, dans l'instruction de type « instruction case », doit être explicitement un nombre entier.

    Ceci combine les éléments des boucles, le builtin de sortie et l'instruction cas. À l'intérieur d'un bloc try, le builtin raise peut être utilisé pour lever une exception sur un entier. L'exécution sortie ensuite du bloc try, exactement comme pour quitter les boucles for/until/while. On peut utiliser une instruction de type case pour prendre capturer l'exception. Si l'exception est capturée, alors elle est réinitialisée et l'exécution continue en suivant le bloc try. Si l'exception n'est pas capturée, alors l'exécution repart jusqu'à ce qu'elle soit capturée ou jusqu'à ce qu'il n'y ait plus de blocs try.

    Par exemple :

    

try
        echo a
            while true; do  # infinite loop
                echo aa
                raise
                echo bb
            done
            echo b
        done
    
    

    affichera a aa et

    

    try
            echo a
            raise 2
            echo b
        done in
            0) echo normal ;;
            1) echo raised one ;;
            2) echo raised two ;;   # raise 2
        esac
    
    

    affichera a et l'exception sera 2.


    10. Récapitulatif

    Dans le prochain article, j'aborderai les builtins dynamiquement chargeables liés aux tableaux, le découpage des expressions régulières, l'interfaçage avec des bibliothèques externes comme une base de données SQL et un analyseur syntaxique XML et je traiterai aussi d'autres applications intéressantes comme les modèles (templates) HTML et d'un système de blocage de spam POP3.

    J'ai appris Unix avec le shell Bourne originel. Et, après une expédition dans la jungle des langages, je suis revenu au shell. Dernièrement, j'ai contribué à de nouvelles fonctionnalités de Bash, rendant la monnaie de leur pièce aux autres langages de scriptage. Slackware est ma distribution favorite depuis le début, parce que je peux saisir au clavier. Dans ma boite à outils, j'ai Vim, Bash, Mutt, Tin, Tex/Latex, Python, Awk et Sed. Même ma ligne de commande est en mode Vi.