Administration Unix - CM séance 10
9. Approfondissements du langage bash (suite)
9.6. La grammaire du shell
La ligne de commande peut être un assemblage de différents éléments :
(légende : []
signifie optionnel, ...
signifie 1 ou plusieurs)
-
Commande : par priorité décroissante
- une fonction bash
- une commande interne de bash
- un fichier exécutable (binaire ou script)
-
Commande simple :
[variable=valeur]... commande [argument]... [redirection]...
-
Pipeline de commandes :
[!] commande_simple [ | commande_simple]...
-
Liste de commandes :
pipeline [SEP pipeline]... [; ou &] # séparateur SEP parmi ; & && ||
-
Commande composée : (
list_co
désigne une liste de commande)(list_co) # liste_co exécutée dans un sous-shell { list_co ;} # liste_co exécutée dans le même shell ((expression)) [[ expression ]] if list_co ; then list_co ; [elif list_co ; then list_co ;]... [else list_co ;] fi case nom in [motif) list_co ;;]... esac select nom [in mots] ; do list_co ; done for nom [in mots] ; do list_co ; done for ((expr1;expr2;expr3)) ; do list_co ; done while list_co ; do list_co ; done until list_co ; do list_co ; done
Le code de terminaison $?
d'une commande composée est celui de la dernière
commande simple exécutée.
Une commande composée peut être redirigée ; la rediction s'applique alors à tous les éléments qui la composent.
Exemples :
{ echo "Répertoire courant :" ; pwd ; echo "date :" ; date ;} >| trace.txt
while read ligne ; do echo "J'ai lu la ligne : $ligne" ; done < trace.txt
À noter : la définition d'une fonction étant
nom_fonction() commande_composée [redirection]...
elle autorise les déclarations suivantes :
toto() { echo "Bonjour" ;} >> trace.txt # avec redirection
titi() (cd .. ; pwd) # sans {}
9.7. Lecture et découpage avec read
Revenons sur la commande interne read
: avec un seul argument,
read var1
lit une ligne sur l'entrée standard, puis la mémorise dans var1
.
Exemple :
$ read nom
Maître Yoda # entré par l'utilisateur
$ echo "$nom"
Maître Yoda
Remarque : on peut aussi fournir l'entrée standard via un here-string avec <<<
:
$ read nom <<< "Maître Yoda"
Avec plusieurs arguments,
read var1 var2 ... varn
lit une ligne sur l'entrée standard, puis découpe la ligne en mots, selon IFS
et
le nombre d'arguments :
mot1
⟶var1
mot2
⟶var2
- ...
- reste de la ligne ⟶
varn
Exemple :
$ read a b c <<< "À vos intuitions, vous fier, il faut."
$ echo "$a | $b | $c"
À | vos | intuitions, vous fier, il faut.
On peut préciser le caractère de découpe avec la variable d'environnement IFS
:
$ IFS="," read a b c <<< "À vos intuitions, vous fier, il faut."
$ echo "$a | $b | $c"
À vos intuitions | vous fier | il faut.
On peut également découper tous les mots et stocker dans un tableau :
$ read -a notes <<< "do ré mi fa sol"
$ declare -p notes
declare -a notes='([0]="do" [1]="ré" [2]="mi" [3]="fa" [4]="sol")'
$ IFS=":" read -a pif <<< "$PATH" # découpe le PATH
$ declare -p pif
declare -a pif='([0]="/home/thiel/bin" [1]="/bin" [2]="/usr/bin" ...)'
Il a d'autres options intéressantes (help read
) :
read -p message # affiche le message sans retour chariot avant de lire
read -t timeout # s'arrête si rien n'est rentré au bout du timeout
read -r # protège les \ lus en les doublant
...
On emploie souvent read
pour lire un texte ligne à ligne :
while read ligne ; do
echo "traitement de la ligne : $ligne"
done < texte_en_entree.txt
Autre exemple, dans lequel on lit ligne à ligne le fichier /etc/passwd
en les
découpant en mots sur ":"
while IFS=: read login mdp uid reste ; do
echo "Le user $login a le numéro $uid"
done < /etc/passwd
Il y a un piège fréquent lorsqu'on utilise des tubes et un read
:
commande1 | commande2 | ... | while read ligne ; do ... ; done
commande1 | commande2 | ... | read resultat
Piège
Dans un pipeline, chaque commande est exécutée dans un processus fils
⟶ le read
aussi, donc les variables affectées par read
ne seront pas
connues du père.
Exemples :
$ type bash | cut -d' ' -f 3 | read chemin # Incorrect
echo "chemin : $chemin"
chemin :
$ chemin=$(type bash | cut -d' ' -f 3) # Solution
$ echo "chemin : $chemin"
chemin : /bin/bash
$ echo 1 2 3 | read p q r # Incorrect
$ echo "$p ; $q ; $r"
; ;
$ read p q r <<< "$(echo 1 2 3)" # Solution
$ echo "$p ; $q ; $r"
1 ; 2 ; 3
$ i=0
$ ls | while read fichier ; do ((i++)) ; done # Incorrect
$ echo "$i"
0
$ while read fichier ; do ((i++)) ; done <<< "$(ls)" # Solution
$ echo "$i"
42
La solution générale s'appelle le retournement de tube : au lieu d'écrire
commande1 | commande2
où les 2 commandes sont exécutées dans des fils, on écrit
commande2 <<< "$(commande1)" # les "" sont importantes
où commande2
est exécutée dans le shell principal.
9.8. Redirections et descripteurs
On a vu la notion de descripteur de fichier (fd
) :
- 0 : entrée standard
- 1 : sortie standard
- 2 : sortie d'erreurs
On peut utiliser d'autres fd
(<= 9, voir plus loin).
$ { echo "foo" >&3 ; echo "bar" ;} 3>| trace.txt | tr a-z A-Z
BAR
$ cat trace.txt
foo
- Explications :
3>| trace.txt
écrase le fichiertrace.txt
et l'associe aufd
3
>&3
ou1>&3
redirige la sortie standard dans lefd
3
ceci permet au premier echo
de "contourner" le tube.
Application : lecture en parallèle de fichiers
$ while read ligne1 <&3 && read ligne2 <&4
do
echo "Lu '$ligne1' et '$ligne2'"
done 3< fichier1 4< fichier2
- Explications :
3< fichier1
ouvre lefichier1
en lecture et l'associe aufd
3
read ligne1 <&3
ou encore0<&3
: redirige l'entrée standard sur lefd
3
; doncread
va lire une ligne defichier1
.- Le
while read && read
s'arrête dès que la fin d'un des deux fichiers est atteinte.
On peut rediriger le shell de façon permanente avec la commande exec
:
$ exec 4>| spam.txt
$ echo "eggs" >&4
$ cat spam.txt
eggs
$ exec 4>&- # ferme le fichier, supprime le fd
$ echo "bacon" >&4
bash: 4: Mauvais descripteur de fichier
On peut demander à bash d'attribuer automatiquement un fd
(qui sera alors >= 10) ; notez la syntaxe {nom}
à gauche et $nom
à droite :
$ exec {myfd}>| gloubi.txt # crée le fd myfd et lui associe le fichier
$ echo $myfd
10
$ ls -l /proc/$$/fd # Affiche la liste des fd du shell
total 0
lrwx------ 1 ... 0 -> /dev/pts/37
lrwx------ 1 ... 1 -> /dev/pts/37
lrwx------ 1 ... 2 -> /dev/pts/37
l-wx------ 1 ... 10 -> /tmp/gloubi.txt # ouvert en écriture
lrwx------ 1 ... 255 -> /dev/pts/37 # contrôle du tty par bash
$ echo "boulga" >&$myfd # écrit dans le fd myfd
$ cat gloubi.txt
boulga
$ exec {myfd}>&- # ferme le fichier, supprime le fd
$ echo "Casimir" >&$myfd
bash: $myfd: Mauvais descripteur de fichier
9.9. Diverses choses utiles
9.9.1. Le PID $$
Le PID
du shell principal est obtenu avec $$
(alors que $BASHPID
est le PID
du
shell courant, qui peut être un fils, et changer à chaque appel).
Usage fréquent : pour créer des fichiers temporaires dans un script :
tmp1="temp-$$.txt"
commande >| "$tmp1"
rm -f "$tmp1"
Chaque fois qu'on lance le script, il a son propre $$
.
9.9.2. Substitution $'\sequence'
Est substitué par le caractère décrit par \sequence
:
$'\n'
retour chariot$'\t'
tabulation$'\xyz'
caractère dont le code octal estxyz
par exemple$'\41'
⟶!
,$'\61'
⟶1
,$'\101'
⟶A
$'...'
doit être situé en dehors des ""
pour expanser.
9.9.3. Hasard avec $RANDOM
À chaque substitution, produit un nombre au hasard entre 0 et 32767 (c'est un peu short !).
Utile par exemple pour générer des chaînes aléatoires :
generer_chaine() # tableau_caractères longueur
{
local -n tabcar=$1 # par référence
local longueur=$2
local tablen=${#tabcar[*]} i res=
for ((i=0; i<longueur; i++)); do
res+=${tabcar[RANDOM % tablen]} # mode arithmétique dans les []
done
echo "$res"
}
$ char64=({a..z} {A..Z} {0..9} - +)
$ generer_chaine char64 40
PWJUMUrnAS-KoE9WciP9HHg+A9daAx1+aScOMMOR
$ generer_chaine char64 60
npjA+Gt+BF7aNKcZVVswRggKKEmoIV7RUyjuLF5Skwejure5obwS5IHH2JPl
9.9.4. Commande printf
Permet d'afficher avec les mêmes arguments que printf
en C :
$ printf "%05d\n" 12 # affichage décimal précédé de zéros
00012
$ printf "%x\n" 63 # en base 16
3f
$ printf "%o\n" 63 # en octal
77
Attention à la locale pour les réels :
$ printf "%.2f\n" "3.14159"
bash: printf: 3.14159: nombre non valable
0,00
$ printf "%.2f\n" "3,14159" # en français il faut une ","
3,14
$ LANG=C printf "%.2f\n" "3.14159" # locale "C" = en anglais : "."
3.14
9.9.5. Flags true et false
Comme true
et false
sont des commandes, on peut écrire :
flag1=true
flag2=false
if $flag1 ; then ... ; fi # ça fera : if true ; then ..
while ! $flag2 ; do ... ; done
C'est plus court que d'écrire
if test "$flag1" = true ; then ... ; fi
while ! test "$flag2" = "true" ; do ... ; done
9.9.6. Commande trap
La commande trap
permet de capter un signal Unix :
trap CMD liste_signaux
Chaque fois qu'un signal de la liste est reçu, la commande CMD
est exécutée.
Exemple :
$ trap 'echo "signal reçu"' TERM QUIT
$ kill -QUIT $$
signal reçu
Ceci permet par exemple de rétablir un fichier modifié par un script s'il est interrompu.
Cas particuliers :
trap CMD 0 # la commande CMD est exécutée à la sortie du shell ;
# très utile pour supprimer les fichiers temporaires
trap CMD DEBUG # commande CMD executée après chaque commande simple
Exemple :
$ trap 'echo "[$BASHPID] $BASH_COMMAND"' DEBUG
$ echo salut
[15666] echo salut
salut
Valeurs spéciales pour CMD :
""
supprime letrap
-
rétablit le comportement par défaut
9.10. Quelques pièges à éviter
a) Le $
en trop :
$var1=truc
Cela affecte la variable dont le nom est dans var1
, mais pas var1
:
$ var1=toto
$ $var1=truc
$ echo "$var1"
toto
$ echo "$toto"
truc
De plus, si var1
n'existe pas, cela produit une erreur :
$ unset var1
$ $var1=truc
=truc : commande introuvable
b) Le =
non accolé :
var1 = truc
Pour bash ce n'est pas une affectation, mais une commande :
$ var1 = truc
var1 : commande introuvable
c) Oubli de la substitution de commandes $()
:
var1=fonction $*
Ceci affecte var1
comme variable d'environnement puis exécute $1
comme une
commande. Très dangereux !
$ set echo je t\'ai eu
$ hello(){ echo "bonjour" ;}
$ foo=hello $*
je t'ai eu
d) echo
avec des !!
bash réalise l'expansion des !
lorsqu'il est suivi d'un symbole, avec
l'historique des commandes (!!
⟶ dernière commande, etc).
$ echo "coucou !"
coucou !
$ echo "bye bye !!"
echo "bye bye echo "coucou !""
bye bye echo coucou !
Pour afficher !!
il faut l'isoler avec des ''
:
$ echo "bye bye "'!!'
bye bye !!
e) Indirections multiples en mode arithmétique
$ a=b ; b=c ; c=d ; d=42
$ echo $((a))
42
En mode arithmétique, lorsqu'une variable contient une chaîne, bash fait une indirection, et continue jusqu'à ce qu'il trouve une valeur entière.
Dans le cas où la variable n'est pas définie, bash l'expanse en 0 :
$ unset bidon
$ echo $((bidon))
0
f) Oubli de protection contre la découpe avec des ""
Les arguments et variables contiennent souvent des chemins, dans lesquels il
peut y avoir des blancs. Il est donc crucial de les protéger systématiquement
contre la découpe avec des ""
.
g) Boucler sur ls
Lorsqu'on veut parcourir les fichiers d'un répertoire dir
, on est parfois
tenté de boucler sur ls
:
for f in $(ls "$dir") ; do # MAUVAIS
echo "$f"
done
C'est une mauvaise pratique car ls
ne préserve pas les noms qui
contiennent des blancs.
La bonne approche est de boucler sur un motif, en plaçant les wildcards
en dehors des ""
pour qu'ils soient expansés :
for f in "$dir"/* ; do
echo "$f"
done