Leçon 1 sur 12 · Le langage en profondeur
Variables, expansions et quoting : découper, remplacer, calculer sans jamais casser un nom de fichier
Objectif
Traiter les chaînes dans bash lui-même (nom de fichier, extension, nom d'hôte, casse, valeurs par défaut, calculs), puis savoir exactement ce que bash fait d'une ligne avant de lancer la commande, pour choisir le bon type de guillemets à chaque fois. C'est la base de tout le reste du cours, et la première cause de bugs dans les scripts d'exploitation.
L'essentiel
#/##retirent un motif au début (le plus court / le plus long),%/%%à la fin ;${v/a/b}remplace,${v^^}et${v,,}changent la casse,${#v}donne la longueur.${v:-défaut},${v:=défaut},${v:?message},${v:+autre}: avec les deux-points, une variable vide compte comme absente.$(( … ))calcule en entiers ;printf -v varformate dans une variable sans sous-shell.- Bash transforme une ligne dans un ordre fixe : expansions, puis découpage en mots, puis jokers. Les guillemets doubles bloquent les deux dernières étapes.
"$@"transmet chaque argument intact ;printf %qprotège une chaîne qui sera relue par un autre shell (ssh, journal rejouable).
Cette leçon suppose acquis les variables simples, ${1:-défaut} et les jokers vus dans le cours « Fondamentaux » (leçons 1.11, 1.13 et 1.17). On va beaucoup plus loin.
1. Pourquoi se passer de basename, cut ou sed ?
Dans une boucle qui traite 20 000 fichiers de journaux, chaque $(basename "$f") lance un nouveau processus. L'expansion ${f##*/} fait le même travail à l'intérieur de bash, des centaines de fois plus vite.
f=/var/backups/ml-web-01-2026-10-20.tar.gz
echo "${#f}" # 40 longueur en caractères
echo "${f:13:9}" # ml-web-01 9 caractères à partir de la position 13 (on compte depuis 0)
echo "${f: -6}" # tar.gz les 6 derniers (l'espace avant - est obligatoire)
2. Retirer un motif au début ou à la fin
Les motifs sont ceux des jokers du shell (*, ?, […]), pas des expressions régulières.
| Expansion | Retire… | Moyen mnémotechnique |
|---|---|---|
${v#motif} |
le plus court préfixe qui correspond | # est à gauche du $ sur le clavier |
${v##motif} |
le plus long préfixe | |
${v%motif} |
le plus court suffixe | % est à droite |
${v%%motif} |
le plus long suffixe |
f=/var/backups/ml-web-01-2026-10-20.tar.gz
echo "${f##*/}" # ml-web-01-2026-10-20.tar.gz (= basename)
echo "${f%/*}" # /var/backups (= dirname)
nom=${f##*/}
echo "${nom%.tar.gz}" # ml-web-01-2026-10-20 extension précise retirée
echo "${nom#*.}" # tar.gz l'extension « complète »
echo "${nom##*.}" # gz la dernière extension seulement
fqdn=ml-db-01.masterlinux.lab
echo "${fqdn%%.*}" # ml-db-01 nom court
echo "${fqdn#*.}" # masterlinux.lab domaine
for f in /srv/exports/*.CSV; do
mv -- "$f" "${f%.CSV}.csv" # renommer en masse, sans rename ni sed
done
3. Remplacer et changer la casse
ligne="agence de fes;agence de rabat;agence de oujda"
echo "${ligne/agence/AGENCE}" # AGENCE de fes;agence de rabat;... première seulement
echo "${ligne//;/ | }" # agence de fes | agence de rabat | ... toutes
echo "${ligne//agence de /}" # fes;rabat;oujda supprimer = remplacer par rien
chemin=/srv/site/
echo "${chemin/#\/srv/\/data}" # /data/site/ # = ancré au DÉBUT
echo "${chemin/%\//}" # /srv/site % = ancré à la FIN
role=web ; ENV=PROD
echo "${role^} ${role^^} ${ENV,,}" # Web WEB prod
Les changements de casse sont propres à bash 4 et plus : toutes les distributions actuelles l'ont, mais pas le sh de Debian (dash).
4. Valeurs par défaut, variables obligatoires
| Expansion | Si v est absente |
Si v est vide |
Si v a une valeur |
|---|---|---|---|
${v-d} |
d |
vide | la valeur |
${v:-d} |
d |
d |
la valeur |
${v:=d} |
d, et v reçoit d |
d, et v reçoit d |
la valeur |
${v:?msg} |
erreur, le script s'arrête | erreur | la valeur |
${v:+x} |
vide | vide | x |
#!/bin/bash
# /usr/local/bin/ml-purge — configuration par variables d'environnement
: "${RETENTION_JOURS:=14}" # « : » ne fait rien : seule l'affectation compte
CIBLE=${1:?Usage : ml-purge HOTE (exemple : ml-app-01)}
echo "Purge sur $CIBLE au-delà de $RETENTION_JOURS jours${DRY_RUN:+ (simulation)}"
${v:+…} ajoute une option seulement quand elle a un sens : rsync -a ${VERBOSE:+-v} …. Et l'indirection ${!nom} lit la variable dont le nom est dans nom (nom=seuil_db; echo "${!nom}") ; dès que les cas se multiplient, un tableau associatif (leçon 2.2) est plus propre.
5. Calculer : $(( )) et printf -v
echo $(( 45 * 100 / 60 )) # 75 division ENTIÈRE (tronquée)
echo $(( 2 * 100 / 3 )) # 66 alors que 66,67 s'arrondit à 67
echo $(( (2 * 100 + 3 / 2) / 3 )) # 67 arrondi : on ajoute la moitié du diviseur
echo $(( 17 % 5 )) $(( 2 ** 10 )) # 2 1024
(( compteur += 1 )) # pas besoin de $ dans (( ))
printf -v jour '%(%F)T' -1 # date du jour, sans lancer la commande date
printf -v hote 'ml-web-%02d' 3 # hote=ml-web-03
Bash ne connaît que les entiers : pour des montants, calculez en centimes (12350 pour 123,50 DH) ou confiez le calcul à awk.
6. Ce que bash fait d'une ligne
Prenons une variable contenant une espace et une étoile, dans un dossier qui contient rapport de juin.txt et simple.txt :
motif="ventes *"
echo $motif # ventes rapport de juin.txt simple.txt ← découpée PUIS l'étoile remplacée
echo "$motif" # ventes * ← intacte
Sans guillemets, bash a : 1) remplacé $motif par sa valeur ; 2) découpé sur les espaces (les deux espaces ont disparu) ; 3) remplacé * par la liste des fichiers. Les guillemets doubles empêchent les étapes 2 et 3. Règle d'or : toute expansion entre guillemets doubles, sauf si vous voulez précisément le découpage.
f="rapport de juin.txt"
wc -c $f # wc: rapport: No such file or directory (3 fichiers demandés)
wc -c "$f" # 4 rapport de juin.txt
7. Les trois sortes de guillemets
hote=ml-web-01
echo 'Serveur : $hote' # Serveur : $hote rien n'est interprété
echo "Serveur : $hote" # Serveur : ml-web-01 $ est interprété
echo "Prix : 120 \$" # Prix : 120 $ \ protège un caractère dans "…"
echo $'Colonne1\tColonne2' # $'…' comprend \t, \n, \e
echo 'l'\''agence' # l'agence (fermer, \' , rouvrir : seule façon d'avoir ' entre '…')
| Écriture | $v, $( ) |
\n |
Usage typique |
|---|---|---|---|
'…' |
non | non | regex, programmes awk, motifs sed |
"…" |
oui | non | presque tout le reste |
$'…' |
non | oui | caractères spéciaux (IFS=$'\n', tabulations) |
8. "$@", "$*" et IFS
# Le script reçoit trois arguments : "Rabat centre" Fes Agadir
for a in "$@"; do echo "[$a]"; done # [Rabat centre] [Fes] [Agadir] 3 tours
for a in "$*"; do echo "[$a]"; done # [Rabat centre Fes Agadir] 1 tour
for a in $@; do echo "[$a]"; done # [Rabat] [centre] [Fes] [Agadir] 4 tours : cassé
Un script enveloppe (wrapper) qui ajoute des options puis passe la main écrit exec rsync -a --exclude='*.tmp' "$@" : arguments intacts, et le code de retour est directement celui de rsync.
IFS (espace, tabulation, retour à la ligne par défaut) sert au découpage des mots et à read. Modifiez-le pour une seule commande :
IFS=: read -r nom ip ville <<< "ml-db-01:10.0.0.20:casablanca"
echo "$nom / $ip / $ville" # ml-db-01 / 10.0.0.20 / casablanca
9. Les jokers sous contrôle, et les noms qui commencent par -
Par défaut, un joker qui ne trouve rien reste tel quel : for f in /srv/vide/* tourne une fois avec la valeur littérale /srv/vide/*.
| Option | Effet |
|---|---|
shopt -s nullglob |
un joker sans résultat disparaît : la boucle ne tourne pas |
shopt -s failglob |
un joker sans résultat est une erreur |
shopt -s dotglob |
* inclut les fichiers cachés |
shopt -s globstar |
**/*.log descend dans tous les sous-dossiers |
shopt -s extglob |
motifs étendus : !(*.gz), `@(web |
Un fichier nommé -rf est pris pour une option par rm *. Parades : rm -- * (-- = fin des options) ou rm ./*.
10. printf %q : réinjecter une chaîne sans risque
ssh recolle ses arguments et les fait relire par le shell distant :
chemin="/srv/partage/compta 2026;reboot"
printf '%q\n' "$chemin" # /srv/partage/compta\ 2026\;reboot
ssh ml-web-01 rm -f "$chemin" # DANGER : le shell distant exécute « reboot »
ssh ml-web-01 "rm -f $(printf '%q' "$chemin")" # sûr
Un nom de fichier fourni par un utilisateur et passé tel quel à ssh permet d'exécuter une commande sur le serveur distant : c'est une vraie faille.
11. Cas réel : le script de purge qui a vidé le mauvais dossier
Sur un serveur de fichiers MasterLinux, un script de nettoyage contenait rm -rf $DOSSIER/*. Lancé un jour sans argument par une tâche planifiée mal écrite, $DOSSIER était vide : la commande est devenue rm -rf /*. Trois protections, chacune suffisante :
DOSSIER=${1:?Usage : purge.sh DOSSIER} # 1. argument obligatoire
[ -d "$DOSSIER" ] || exit 1 # 2. il doit exister
rm -rf -- "${DOSSIER:?}"/* # 3. ${v:?} refuse une valeur vide, guillemets partout
Pièges fréquents
- Confondre
#et%, ou oublier les deux-points :${PORT-8080}laisse passerPORT="". ${v: -3}sans espace :${v:-3}est une valeur par défaut, pas « les trois derniers caractères ».- Variables sans guillemets :
[ $v = oui ]avecvvide devient[ = oui ];for f in $(ls)casse les noms. Toujours"$v", etfor f in ./*. - Des guillemets rangés dans une variable ne servent à rien :
opts='-a "mon dossier"'; cmd $optstransmet"monetdossier". Pour une liste d'arguments, un tableau (leçon 2.2).
Lab
Sur Rocky Linux 9, vous écrivez six petits scripts dans /root/bash :
nom-archive.sh: le nom d'une archive sans dossier ni extension, sans commande externe ;court.sh: le nom court d'une machine, en majuscules ;conf.sh: des valeurs par défaut qui résistent aux variables vides ;pourcent.sh: un pourcentage entier correctement arrondi ;compte-args.sh: les arguments reçus, intacts grâce à"$@";cmd-distante.sh: une commandesshsûre avecprintf %q.
Quiz
- Avec
f=/srv/www/index.old.html, que donnent${f##*/},${f%.*}et${f%%.*}? - Quelle différence entre
${PORT-80}et${PORT:-80}? - Dans quel ordre bash applique-t-il expansion des variables, découpage en mots et jokers ?
- Quelle différence entre
"$@"et"$*"? - Pourquoi
ssh hote rm -f "$f"est-il dangereux si$fvient d'un utilisateur ?
Réponses
index.old.html;/srv/www/index.old;/srv/www/index.- La première ne remplace que si
PORTest absente ; la seconde remplace aussi quandPORTest vide. - D'abord les expansions (
$v,$( ),$(( ))), puis le découpage en mots selonIFS, puis les jokers. Les guillemets doubles empêchent les deux dernières étapes. "$@"donne un mot par argument, intact ;"$*"colle tous les arguments en une seule chaîne séparée par le premier caractère d'IFS.- ssh recolle les arguments et les fait réinterpréter par le shell distant : un nom contenant
;ou$( )y devient une commande. Il faut protéger la valeur avecprintf %q.