Aller au contenu

Administration Unix - CM séance 07

8. Les expansions du shell bash

Pour interpréter une ligne de commande, bash la découpe en mots selon les blancs (espace, tabulation) et les paires '' "" `` () {} [], puis il réalise une expansion pour chaque mot sur des caractères spéciaux.

Par exemple :

$ wc -l plan.txt tp*.txt cm??.txt

est découpé en 5 mots : wc, -l, plan.txt, tp*.txt, cm??.txt puis les mots comportant des *? (qui sont des motifs de fichiers) sont remplacés par la liste des fichiers qui correspondent.

Lorsque des "" () {} [] sont imbriqués, les expansions sont faites du niveau le plus interne vers le plus externe.

Les opérations ont lieu dans cet ordre, pour un même niveau :

  1. Expansion des accolades   {ga,bu}
  2. Développement du tilde   ~ ~thiel
  3. Substitution des arguments   $1 $# $* "$@"
  4. Substitution des variables   $foo ${foo} ${t[]}
  5. Substitution des commandes   $()
  6. Évaluation arithmétique   $(())
  7. Découpage des mots sur $IFS
  8. Développement des noms de fichiers   * ? []

8.1. Expansion des accolades

L'expansion des accolades permet de générer des chaînes de caractères.

Syntaxe :

préfixe{mot1,mot2,...,motn}suffixe

ceci est expansé en :

préfixemot1suffixe préfixemot2suffixe ... préfixemotnsuffixe

Précisions :

  • le préfixe et le suffixe sont optionnels ;
  • dans les {}, les mots sont séparés par des , sans blancs ;
  • il faut un moins une virgule dans {}, donc au moins 2 mots.

Exemples :

$ echo {ga,bu,zo}
ga bu zo
$ echo A{ga,bu,zo}B
AgaB AbuB AzoB

Lorsqu'il y a plusieurs accolades, bash fait le produit cartésien des listes, c'est-à-dire toutes les combinaisons possibles :

$ echo {ga,bu}{zo,meu}
gazo gameu buzo bumeu
$ echo A{ga,bu}B{zo,meu}C
AgaBzoC AgaBmeuC AbuBzoC AbuBmeuC

On peut les imbriquer :

$ echo A{ga,B{bu,zo}C}D
AgaD ABbuCD ABzoCD

On peut protéger les caractères accolades, espace, virgule avec \ ou '' ou "" :

$ echo {\{bim\},bam,b\,o\ um}
{bim} bam b,o um
$ echo {"{bim}",bam,'b,o um'}
{bim} bam b,o um

L'expansion des {} peut aussi générer des séquences :

$ echo {1..10}
1 2 3 4 5 6 7 8 9 10
$ echo {1..10..3}        # par pas de 3
1 4 7 10
$ echo {01..10}          # avec des 0 à gauche
01 02 03 04 05 06 07 08 09 10
$ echo {e..k}
e f g h i j k

Exemple concret :

$ wc -l au{01..03}-{cm,tp}.md

sera expansé en

wc -l au01-cm.md au01-tp.md au02-cm.md au02-tp.md au03-cm.md au03-tp.md

C'est également utile pour créer des répertoires ou des fichiers :

$ mkdir -p /tmp/essai/{foo,bar}/tp{01..03}
$ tree /tmp/essai
/tmp/essai
├── bar
│   ├── tp01
│   ├── tp02
│   └── tp03
└── foo
    ├── tp01
    ├── tp02
    └── tp03
$ touch /tmp/essai/{foo,bar}/tp{01..03}/{trace,erreurs}-{a..c}{1..3}.txt

Comme l'expansion des {} a lieu en premier, elle va recopier les motifs éventuels :

$ du -sk /usr/share/icons/{ubuntu*,??color}/{??x??,256*}/apps

sera expansé en

du -sk /usr/share/icons/ubuntu*/??x??/apps /usr/share/icons/ubuntu*/256*/apps
       /usr/share/icons/??color/??x??/apps /usr/share/icons/??color/256*/apps

dont les motifs seront expansés à leur tour.

8.2. Développement du tilde

Sur les systèmes Unix, le caractère ~ (tilde) désigne le répertoire $HOME c'est-à-dire le répertoire principal de l'utilisateur.

Origine : les symbole ~ et home étaient sur la même touche sur des terminaux très populaires au milieu des années 1970 (modèle ADM-3A de Lear-Siegler). Ce terminal a aussi influencé certaines commandes de vi, par ex. hjkl.

Syntaxe :

  • ~   répertoire principal $HOME
  • ~user   répertoire principal de user ; par exemple : ~dupond

Plus rare :

  • ~+   répertoire courant $PWD
  • ~-   répertoire précédent $OLDPWD

Délicat à manipuler :

  • ne doit pas être protégé par "" ou ''
  • hors d'une affectation, doit figurer au début du mot
  • dans une affectation, doit figurer après = ou :
$ PATH=.:~/bin:/bin:/usr/bin
$ echo $PATH
.:/home/thiel/bin:/bin:/usr/bin     # ~ bien expansé
$ PATH=".:~/bin:/bin:/usr/bin"
$ echo $PATH
.:~/bin:/bin:/usr/bin               # ~ non expansé à cause des ""
$ echo .:~/bin:/bin:/usr/bin
.:~/bin:/bin:/usr/bin               # ~ non expansé, cas n°2

8.3. Substitution des arguments

La substitution des arguments a ensuite lieu :

  • $0   le script avec son chemin
  • $1   l'argument 1
  • ${10}   l'argument 10
  • $#   le nombre d'arguments
  • $*   la liste des arguments
  • "$@"   la liste des arguments, protégés par des ""

Voir cours n°5 section 6.4.

Il est également possible de leur ajouter des opérateurs sur les variables, voir section suivante.

8.4. Substitution des variables

Les variables sous la forme $nomvar ou ${nomvar} sont substituées par leur valeur. Une variable inexistante est substituée par la chaîne vide.

Dans une chaîne encadrée par des "" ou des '' :

  • entre "", les $ sont expansés (partial or weak quoting) ;
  • entre '' elles ne le sont pas (full or strong quoting).
$ boisson="café"
$ echo "Je bois du $boisson"
Je bois du café
$ echo 'Je bois du $boisson'
Je bois du $boisson

Remarque : une autre façon de ne pas expanser la variable est de la protéger avec un \ :

$ echo "Je bois du \$boisson"
Je bois du $boisson

bash permet de faire certaines opérations lors de la substitution. La syntaxe générale est : ${nomvarOPERATEUR}

Il existe de nombreux opérateurs , qui fonctionnent aussi sur les arguments ; ils permettent d'éviter l'emploi de commandes externes (sed, awk, perl,...).

On peut citer les opérateurs suivants :

Opérateurs d'existence

  • ${nomvar-valeur}   $nomvar si définie, sinon valeur
  • ${nomvar=valeur}   $nomvar si définie, sinon valeur et affectation
  • ${nomvar+valeur}   valeur si nomvar définie, sinon vide

Exemples :

$ echo $a                   # (affiche : ligne vide)
$ echo ${a-pas glop}        # pas glop
$ echo ${a+glop glop}       # (ligne vide)
$ echo ${a=houba houba}     # houba houba
$ echo $a                   # houba houba
$ echo ${a=houpa hop}       # houba houba
$ echo ${a+glop glop}       # glop glop

Opérateurs de sous-chaînes

  • ${nomvar:i}   sous-chaîne depuis l'indice i >= 0
  • ${nomvar:i:k}   sous-chaîne depuis l'indice i >= 0 de longueur k
  • ${nomvar:i:1}   caractère d'indice i >= 0
  • ${#nomvar}   longueur courante de $nomvar

Exemples :

$ episode="la menace fantôme"
$ echo ${episode:3}
menace fantôme
$ echo ${episode:3:6}
menace
$ echo ${#episode}
17

Opérateurs de casse

  • ${nomvar^^}   $nomvar en majuscules
  • ${nomvar,,}   $nomvar en minuscules

Exemple :

$ b="Le langage TeX"
$ echo ${b^^}
LE LANGAGE TEX
$ echo ${b,,}
le langage tex

Opérateurs début et fin

  • ${nomvar#motif}   supprime le plus court début correspondant à motif
  • ${nomvar##motif}   idem pour le plus long
  • ${nomvar%motif}   supprime la plus courte fin correspondant à motif
  • ${nomvar%%motif}   idem pour la plus longie

Exemples :

$ c="/usr/local/share/icons.old.tar.gz"
$ echo ${c#/usr/local}
/share/icons.old.tar.gz
$ echo ${c#/usr/bin}
/usr/local/share/icons.old.tar.gz       # motif non trouvé ⟶ $c
$ echo ${c#/*/}
local/share/icons.old.tar.gz
$ echo ${c##/*/}
icons.old.tar.gz
$ echo ${c%.*}
/usr/local/share/icons.old.tar
$ echo ${c%%.*}
/usr/local/share/icons

Application : tester si un nom de fichier $file possède l'extension $ext

test "$file" = "${file%.$ext}.$ext"

Rechercher-remplacer

  • ${nomvar/motif/txt}   remplace la plus longue correspondance de motif par txt
  • ${nomvar//motif/txt}   remplace toutes les correspondances de motif par txt

Exemple :

$ d="le café décaféiné avec le sucre"
$ echo "${d/le/du}"
du café décaféiné avec le sucre
$ echo "${d//le/du}"
du café décaféiné avec du sucre
$ echo "${d/c*é/thé}"
le thé avec le sucre

Remarques :

  • ces substitutions ne modifient pas la variable, sauf l'opérateur =
  • elles ne peuvent pas être cumulées ; par exemple on ne peut pas écrire

    ${nomvar#debut%fin}     # ERRONÉ
    

    il faudra le faire en 2 étapes :

    tmp=${nomvar#debut}
    ${tmp%fin}
    

Sur les arguments

  • comme déjà dit, tout ces opérateurs s'appliquent également aux arguments :

    $ set abra ca dabra
    $ echo $3
    dabra
    $ echo ${3%bra}
    da
    
  • avec $* et $@, l'opérateur est appliqué à chaque argument :

    $ echo ${*%bra}
    a ca da
    $ echo "${@^^}"
    ABRA CA DABRA
    
  • ou appliqué à la liste pour l'opérateur : :

    $ echo ${*:2}
    ca dabra
    $ echo ${*:1:2}
    abra ca
    

8.5. Substitution des commandes

Il s'agit d'un mécanisme très puissant : une commande est substituée par ce qu'elle a affiché sur sa sortie standard.

Syntaxe :

`commande arguments`        # ancienne syntaxe sh, avec des "back-quotes" --> À ÉVITER
$(commande arguments)       # syntaxe bash moderne

Exemples :

echo "Fichier créé le : $(date)"
echo "ma machine s'appelle $(uname -n)"
nb_fichiers=$(ls *.txt | wc -l)
x=5; x=$(expr $x + 1)             # une façon d'incrémenter x

La commande est exécutée dans un sous-shell :

$ echo $BASHPID
4506
$ echo $(echo $BASHPID)
5604

Les dernières lignes vides sont coupées :

$ echo "'$(echo ; echo "    bonjour   " ; echo ; echo)'"
'
    bonjour    '

Un usage courant est de mémoriser tout le contenu d'un fichier dans une variable (sauf les dernières lignes vides !) :

$ echo -e "bonjour\nles amis\n\n" >| essai.txt
$ texte=$(cat essai.txt)
$ echo "$texte"
bonjour
les amis

Il existe un raccourci : texte=$(< essai.txt)

Il est possible d'imbriquer les parenthèses :

msg="La taille est $(wc -c < $(which test)) octets"

La substitution de commande est particulièrement utile pour récupérer le résultat affiché par une fonction :

calculer_taille_fichier() # fichier
{
    wc -c < "$1"
}
taille=$(calculer_taille_fichier ~/.bashrc)
echo "$taille"

recuperer_homedir() # user
{
    grep "$1" /etc/passwd | cut -d: -f 6
}
echo "Le home de $USER est $(recuperer_homedir "$USER")"

Important :

Dans une fonction, return fait sortir immédiatement de la fonction :

  • return   avec le code de terminaison de la dernière commande exécutée ;
  • return n   avec le code de terminaison n (0 succès, 1..255 échec)

Pour renvoyer un résultat, n'utilisez pas return, mais affichage et $() ; on en reparlera au cours suivant.

8.6. Évaluation arithmétique

bash est capable d'effectuer des calculs, avec des entiers UNIQUEMENT.

La syntaxe est

  • ((expression))   évalue l'expression ; réussit si elle est vraie
  • $((expression))   évalue l'expression puis substitue par sa valeur

Dans expression :

  • opérateurs et syntaxe du C
  • pas de découpage sur les blancs
  • on peut omettre les $ si non-ambigu.

Exemples :

$ echo $((20+30))
50
$ x=10 ; y=20 ; echo $((x+y))           # ou echo $(($x+$y))
30
$ x=5 ; y=7 ; ((z=x+y)) ; echo $z       # ou z=$((x+y))
12
$ ((x=3, y=4, z=x+y)) ; echo $z
7

Calculs :

  • les priorités sont respectées :

    $ echo $((10-3*(4+5)))
    17
    
  • la division est entière :

    $ echo $((17/7))
    2
    $ echo $((17.0/7))
    bash: 17.0/7 : erreur de syntaxe ...
    
  • pour les nombres réels, on utilise la commande bc :

    $ echo "scale=8; 17/7" | bc
    2.42857142
    $ echo "la racine carrée de 2 est $(echo "scale=4; sqrt(2)" | bc)"
    la racine carrée de 2 est 1.4142
    

Expressions du C :

  • opérateurs logiques ⟶ 0 (faux) ou 1 (vrai)

    $ echo $((10==10))
    1
    
  • opérateur ternaire :

    $ x=3 ; y=5
    $ echo $((x >= y ? x : y))      # si x >= y alors x sinon y
    5
    
  • expression avec virgules : évaluée de gauche à droite, valeur à droite

    $ echo $((x=y=4, y++, x+y))
    9
    

Ajouts par rapport au C :

  • calcul de puissances :

    $ echo $((2**5))
    32
    
  • bases : 2 à 64

    $ echo $((0100))            # base 8, idem en C
    64
    $ echo $((0x100))           # base 16, idem en C
    256
    $ echo $((2#100))           # base 2, en C GNU : 0b100
    4
    $ k=5 ; echo $(($k#100))    # $ obligatoire pour développer $k avant
    25
    
Réussite d'une évaluation :

((expression))   réussit si expression est vraie (c'est-à-dire non nulle).

Par exemple, ((4 == 5 || 3 <= 7)) réussit ($? est 0).

On peut donc écrire :

((expression)) && commande
if ((expression)); then .. ; fi
while ((expression)); do .. ; done

Le shell bash autorise enfin l'emploi de la boucle for du C :

for ((expression1; expression2; expression3)); do .. ; done

Par exemple :

for ((i=0; i<10; i++)); do echo "$i" ; done

8.7. Découpage des mots sur IFS

La variable spéciale IFS (pour Internal Field Separator) contient la liste des caractères de séparation pour découper les chaînes en mots.

Elle vaut par défaut ␣\t\n (espace tabulation retour chariot).

Elle est utilisée par

  • la commande read (vu plus tard)
  • et lors de l'expansion pour découper en mots

mais pas lors du premier découpage de la commande, toujours effectué sur les blancs.

On peut le voir par exemple avec for, qui expanse puis découpe selon IFS :

$ liste="ga:bu zo:meu"
$ for mot in $liste ; do echo "$mot" ; done
ga:bu                                           # ça a découpé sur " "
zo:meu
$ OLDIFS=$IFS ; IFS=":"                         # on sauve IFS avant
$ for mot in $liste ; do echo "$mot" ; done
ga
bu zo                                           # ça a découpé sur :
meu
$ IFS=$OLDIFS                                   # rétablit IFS

8.8. Développement des noms de fichiers

Les motifs ont été abordés au cours n°6, section 7.4.2.

Les fichiers existants, correspondant au motifs, sont substitués au motif, séparés par un blanc, dans l'ordre lexicographique.

Exception : le motif * ou ? n'est pas substitué pour les fichiers cachés :

$ touch {ga,bu,.zo}.{txt,sh}
$ ls -a
./  ../  bu.sh bu.txt ga.sh  ga.txt  .zo.sh  .zo.txt
$ ls *.txt
bu.txt ga.txt
$ ls ?*.txt
bu.txt ga.txt
$ ls *.txt .*.txt
bu.txt ga.txt .zo.txt

sauf si on active l'option dotglob :

$ shopt -s dotglob
$ ls *.txt
bu.txt ga.txt .zo.txt

Si aucun fichier ne correspond, le motif est laissé inchangé, sauf si l'option nullglob est activée (cf TP n°3, exercice 2.a)).

Lorsqu'un motif est appliqué pour un chemin, les / doivent être donnés explicitement (c'est-à-dire qu'il n'y aura pas de correspondance avec * ? []) :

$ touch foo.bar foolbaz ; mkdir -p foo/bam
$ ls foo*ba?
foo.bar  foolbaz                # foo/bam non trouvé
$ ls foo*ba? foo/ba?
foo.bar  foolbaz
foo/bam:                        # foo/bam trouvé
$ ls foo[.l]ba?
foo.bar  foolbaz                # foo/bam non trouvé
$ ls foo[.l/]ba?
ls: impossible d'accéder à 'foo[.l/]ba?': Aucun fichier ou dossier de ce type
$ ls foo{*,/}ba?                # expansé en : ls foo*ba? foo/ba?
foo.bar  foolbaz
foo/bam:                        # foo/bam trouvé

Remarque finale sur l'ordre des expansions : {} puis $ puis *?

$ a={.bar,lbaz} ; echo foo$a
foo{.bar,lbaz}                  # expansion de $a, mais pas {}
$ a="*ba?" ; echo foo$a
foo.bar foolbaz                 # expansion de $a puis *?