Administration Unix - CM séance 09
9. Approfondissements du langage bash (suite)
9.4. Commande declare
La commande interne declare (voir : help declare) permet de
- demander au shell des informations sur les variables et les fonctions (on appelle cette capacité l'introspection) ;
- mémoriser des attributs sur des variables ou fonctions (variable exportée, entière, liste, read-only...).
9.4.1. Introspection
$ toto="bonjour"
$ declare -p toto
declare -- toto="bonjour" # -- signifie fin des options
$ export toto
$ declare -p toto
declare -x toto="bonjour" # -x = attribut eXport
En fait, la commande declare affiche ce qu'il faudrait taper pour créer
directement la variable :
$ declare -- titi="coucou"
$ echo "$titi"
coucou
$ declare -x tutu="byebye"
$ printenv tutu
byebye
On peut demander à declare la liste des variables :
$ declare -p | tail -n 3
declare -- titi="coucou"
declare -x toto="bonjour"
declare -x tutu="byebye"
Ceci permet d'enregistrer toutes les variables :
$ declare -p >| sauvegarde.txt
et de les réutiliser plus tard :
$ source sauvegarde.txt
Grâce à declare, on peut demander des informations sur des fonctions :
$ bingo() { echo "Vous avez gagné 1 million !" ;}
$ declare -f bingo # affiche le nom et le corps de la fonction
bingo ()
{
echo "Vous avez gagné 1 million !"
}
$ declare -F bingo # affiche uniquement le nom si c'est 1 fct
bingo
$ declare -F | tail -n 10 # affiche toutes les fonctions
...
declare -f bingo
...
On peut aussi exporter une fonction, avec export -f ou declare -f -x.
9.4.2. Attributs
La commande declare permet d'activer une conversion automatique :
declare -u variable # convertit en majuscule lors de l'affectation
declare -l variable # idem en minuscule
Pour défaire, rappeler declare avec +u ou +l.
$ declare -u paf
$ paf=boum
$ echo $paf
BOUM
La commande declare permet de donner un "type" à une variable :
- type entier :
$ n=5
$ declare -p n
declare -- n="5" # variable ordinaire (chaîne de caractères)
$ n+=1 # à quoi sert += ?
$ declare -p n
declare -- n="51" # += a fait la concaténation : n="${n}1"
$ declare -i n # on donne le type entier (Integer)
$ n+=1
$ declare -p n
declare -i n="52" # += a fait l'addition : ((n+=1))
$ declare +i n # pour enlever un attribut on utilise +
$ n+=1
$ declare -p n
declare -- n="521" # c'est de nouveau une chaîne ⟶ concaténation
- types tableau - vus plus bas :
$ declare -a liste # tableau indexé
$ declare -A dict # tableau associatif
La commande declare permet aussi de créer une référence sur un nom de variable
avec l'attribut nameref (c'est l'option -n) :
$ a=flip
$ declare -n b=a # crée une référence b sur a
$ echo $b
flip
$ a=flop
$ echo $b
flop
$ b=flup
$ echo $a
flup
On peut obtenir le nom de la variable référencée :
$ echo ${!b} # "déréférence" ou "indirection"
a
On peut créer une référence vers une variable de tout type (y compris les tableaux indexés et associatifs).
À noter :
-
dans une fonction, on peut déclarer une variable locale de tout type, y compris une référence, en utilisant avec
localles mêmes options quedeclare:par exemple :local [option] nom[=valeur] ...$ echanger() # a b { local -n a=$1 b=$2 # a et b sont des références local tmp tmp=$a ; a=$b; b=$tmp } $ c=ga ; d=bu $ echanger c d # on passe le nom des variables $ echo $c $d bu ga # ça les a bien échangéça ne marche que si les variables référencées sont accessibles (donc avec des noms différents).
-
on peut détruire une variable ou une fonction avec
unset:unset variable unset -f fonction unset -n référence
9.5. Les tableaux
Caractéristiques :
- tableau à 1 dimension, de taille illimitée ;
- chaque case est une chaîne de caractère, de taille illimitée (on ne peut donc pas faire de tableau de tableaux).
bash gère 2 sortes de tableaux :
9.5.1. Les tableaux indexés
Particularités :
- cases indexées par des entiers
- tableau creux : les cases peuvent être non contigues
- peuvent être utilisés comme des listes
- déclaration optionnelle par :
declare -a
declare -a tab # crée un tableau tab
declare -a tab[$n] # idem, taille ignorée
tab[$i]=valeur # affectation, et création automatique de tab
${tab[$i]} # substitution par la valeur, "" si non affectée
Syntaxe : dans la substitution, les {} sont obligatoires :
$ t[0]=ga ; t[1]=bu
$ echo $t # incorrect
ga
$ echo $t[1] # incorrect
ga[1]
$ echo ${t[1]} # ok
bu
Expansion arithmétique de l'indice entre [] :
même syntaxe que (()), et on peut omettre les $ :
$ t[0]=ga ; t[1]=bu ; t[2]=zo ; t[3]=meu
$ i=1; echo "${t[i+1]}"
zo
On peut exprimer les éléments d'un tableau par une liste :
tab=() # vide le tableau
tab=(val1 val2 .. valn) # écrase le tableau, indices 0..n-1
tab=([2]=val1 [5]=val2 ..) # idem, pour les indices donnés
tab+=(valx ..) # concatène la liste à la fin de tab
Exemple :
$ t=(ba bu)
$ t+=(zo meu)
$ declare -p t
declare -a t='([0]="ba" [1]="bu" [2]="zo" [3]="meu")'
Récupération de la liste des valeurs : ${tab[*]} ou "${tab[@]}"
⟶ même usage que $* et "$@", pour itérer avec un for.
$ t=(ga bu zo meu)
$ echo "${t[1]}"
bu
$ echo "${t[*]}"
ga bu zo meu
$ echo "${t[*]:2}"
zo meu
$ echo "${t[*]:1:2}"
bu zo
$ notes=(do "ré mi" fa)
$ for son in ${notes[*]} ; do echo "$son" ; done
do
ré
mi
fa
$ for son in "${notes[@]}" ; do echo "$son" ; done
do
ré mi
fa
Taille :
${#tab[i]} # longueur de la case i
${#tab[*]} # nombre de cases affectées
$ t=([2]=ga [8]=bu [5]=zo)
$ echo "${#t[*]}"
3
dans un tableau creux, taille du tableau != indice+1 du dernier élément
Liste des indices : ${!tab[*]}
$ t=([2]=ga [8]=bu [5]=zo)
$ echo ${!t[*]}
2 5 8
Boucler sur les indices d'un tableau creux :
$ for i in ${!t[*]}; do
echo "t[$i] = ${t[i]}"
done
t[2] = ga
t[5] = zo
t[8] = bu
Compléments :
unset tab # suppression du tableau tab
unset tab[i] # suppression de la case i
local -a tab # déclaration locale d'un tableau
read -a tab # lecture de l'entrée standard, découpe en mots,
# mémorisation des mots dans le tableau
args=("$@") # mémorise les arguments dans un tableau
b=("${a[@]}") # recopie le tableau a en le tassant
c=("${a[@]}" "${b[@]}") # concatène 2 tableaux en les tassant
9.5.2. Les tableaux associatifs
Les tableaux associatifs sont encore appelés hash table ou dictionnaire.
Permet de représenter des données structurées, des tables de bases de données... ⟶ outil très puissant (Javascript, Php, Python, ...)
Particularités :
- cases indexées par un mot clé
- déclaration obligatoire par :
declare -A - même syntaxe que pour les tableaux indexés
Exemple :
$ declare -A dico
$ dico=([lundi]=poisson [mardi]=viande)
$ echo ${dico[mardi]}
viande
$ echo ${!dico[*]}
lundi mardi # clés
$ echo ${dico[*]}
poisson viande # valeurs
$ for cle in "${!dico[@]}"; do
echo "dico[$cle] = '${dico[$cle]}'"
done
dico[lundi] = 'poisson'
dico[mardi] = 'viande'
9.5.3. Passage de tableaux par référence de nom
Possible en déclarant une référence de nom (nameref) avec declare -n ou
local -n (depuis la version 4.3 de bash en 2014).
afficher_case () # tableau cle
{
local -n tab="$1" # par référence
local cle="$2"
echo "${tab[$cle]}"
}
$ shadoks=(ga bu zo) # tableau indexé
$ afficher_case shadoks 1
bu
$ declare -A menu=([lundi]=poisson [mardi]=viande) # tableau associatif
$ afficher_case menu mardi
viande
modifier_case () # tableau cle valeur
{
local -n tab="$1" # par référence
local cle="$2" val="$3"
tab[$cle]="$val"
}
$ modifier_case shadoks 3 meu
$ declare -p shadoks
declare -a shadoks='([0]="" [1]="bu" [2]="zo" [3]="meu")'
$ modifier_case menu jeudi œuf
$ declare -p menu
declare -A menu='([lundi]="poisson" [jeudi]="œuf" [mardi]="viande" )'
le nom du tableau passé en référence doit accessible, et doit
être différent du nom local :
$ tab=()
$ afficher_case tab 0
bash: avertissement : tab: circular name reference
9.6. 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⟶var1mot2⟶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.