Tableaux et chaînes de caractères#
Les tableaux et la manipulation de chaînes constituent deux piliers essentiels de la programmation en Bash. Si les scripts les plus simples se contentent de variables scalaires, les scripts professionnels font abondamment usage des tableaux pour stocker des listes de fichiers, des options de configuration ou des résultats de commandes. La manipulation de chaînes, quant à elle, permet d’extraire, transformer et valider des données textuelles sans faire appel à des outils externes comme sed ou awk, ce qui améliore sensiblement les performances des scripts.
Ce chapitre couvre les tableaux indexés et associatifs, l’ensemble des opérations sur les chaînes de caractères proposées nativement par Bash, le mécanisme de globbing pour la sélection de fichiers, ainsi que les valeurs par défaut de variables. Ces fonctionnalités, souvent méconnues des débutants, distinguent les scripts rudimentaires des scripts robustes et idiomatiques.
Tableaux indexés#
Un tableau indexé (indexed array) est une collection ordonnée d’éléments accessibles par leur position numérique, à partir de zéro. Bash supporte les tableaux indexés depuis la version 2.0 ; ils constituent la forme la plus courante de tableau dans les scripts.
Déclaration et initialisation#
Il existe plusieurs façons de déclarer un tableau indexé en Bash. La forme la plus explicite utilise declare -a, mais on peut aussi créer un tableau en lui assignant directement des valeurs.
# Déclaration explicite (optionnelle mais claire)
declare -a fruits
# Initialisation directe avec une liste
fruits=("pomme" "banane" "cerise" "datte" "figue")
# Assignation élément par élément
couleurs[0]="rouge"
couleurs[1]="vert"
couleurs[2]="bleu"
# On peut sauter des indices (tableau creux)
nombres[0]=10
nombres[5]=50
nombres[9]=90
# Création depuis la sortie d'une commande
fichiers=($(ls /etc/*.conf 2>/dev/null))
# Forme recommandée avec mapfile (évite le word splitting)
mapfile -t lignes < /etc/passwd
La forme ($(commande)) présente un risque de word splitting si les noms de fichiers contiennent des espaces ; la commande mapfile (ou son synonyme readarray) est à préférer pour lire des lignes depuis un fichier ou une commande.
Définition 33 (Tableau indexé Bash)
Un tableau indexé en Bash est une variable contenant une séquence d’éléments auxquels on accède par un indice entier, à partir de zéro. Les indices ne sont pas nécessairement contigus : Bash permet des tableaux creux où certains indices n’existent pas. Chaque élément est une chaîne de caractères (Bash ne distingue pas les types).
Accès aux éléments#
L’accès à un élément individuel nécessite la syntaxe ${tableau[indice]}. L’oubli des accolades est une erreur fréquente : $tableau[0] est interprété comme ${tableau}[0], soit la valeur scalaire de la variable suivie de la chaîne littérale [0].
fruits=("pomme" "banane" "cerise" "datte" "figue")
# Accès à un élément par son indice
echo "${fruits[0]}" # pomme
echo "${fruits[2]}" # cerise
echo "${fruits[-1]}" # figue (indice négatif : depuis la fin)
echo "${fruits[-2]}" # datte
# Tous les éléments (formes équivalentes pour les tableaux sans espaces)
echo "${fruits[@]}" # pomme banane cerise datte figue
echo "${fruits[*]}" # pomme banane cerise datte figue
# Différence cruciale entre @ et * en présence de guillemets
for item in "${fruits[@]}"; do
echo "Élément : $item" # Chaque élément séparé, même avec des espaces
done
for item in "${fruits[*]}"; do
echo "Élément : $item" # Tout en un seul mot, séparé par $IFS
done
La distinction entre "${tableau[@]}" et "${tableau[*]}" est analogue à celle entre "$@" et "$*" pour les paramètres positionnels : avec @ et des guillemets doubles, chaque élément reste un mot distinct, ce qui est indispensable si les éléments peuvent contenir des espaces.
Longueur et indices#
fruits=("pomme" "banane" "cerise" "datte" "figue")
# Nombre d'éléments dans le tableau
echo "${#fruits[@]}" # 5
# Longueur d'un élément spécifique
echo "${#fruits[1]}" # 6 (longueur de "banane")
# Liste des indices définis
echo "${!fruits[@]}" # 0 1 2 3 4
# Avec un tableau creux
sparse[0]="a"
sparse[3]="b"
sparse[7]="c"
echo "${#sparse[@]}" # 3 (nombre d'éléments, pas l'indice max)
echo "${!sparse[@]}" # 0 3 7
Manipulation des tableaux#
fruits=("pomme" "banane" "cerise")
# Ajouter un élément en fin de tableau
fruits+=("datte")
fruits+=("figue" "kiwi") # Ajouter plusieurs éléments
# Supprimer un élément (le rend indéfini, crée un trou)
unset 'fruits[1]'
echo "${fruits[@]}" # pomme cerise datte figue kiwi
echo "${!fruits[@]}" # 0 2 3 4 5 (indice 1 absent)
# Retasser le tableau (réindexation)
fruits=("${fruits[@]}")
echo "${!fruits[@]}" # 0 1 2 3 4
# Slice : sous-tableau à partir d'un offset
echo "${fruits[@]:1:3}" # du 2e au 4e élément (offset 1, longueur 3)
# Copier un tableau
copie=("${fruits[@]}")
# Vider complètement un tableau
unset fruits
Remarque 28
La suppression d’un élément avec unset 'fruits[i]' ne réindexe pas le tableau : elle crée simplement un trou. Pour obtenir un tableau sans trous après suppression, il faut réaffecter le tableau à lui-même : fruits=("${fruits[@]}"). Cette opération reconstruit le tableau en compactant les éléments existants avec de nouveaux indices consécutifs à partir de zéro.
Exemple 20 (Parcourir un tableau avec sa valeur et son indice)
langages=("Python" "Bash" "Rust" "Go" "TypeScript")
for i in "${!langages[@]}"; do
printf "%d) %s\n" "$i" "${langages[$i]}"
done
Résultat :
0) Python
1) Bash
2) Rust
3) Go
4) TypeScript
## Tableaux associatifs
Les **tableaux associatifs** (*associative arrays*), introduits dans Bash 4.0, permettent d'indexer les éléments par des clés textuelles arbitraires plutôt que par des entiers. Ils correspondent aux *hashmaps* ou *dictionnaires* d'autres langages.
```{prf:definition} Tableau associatif Bash
:label: definition-11-02
Un **tableau associatif** est une collection de paires clé-valeur où les clés sont des chaînes de caractères. Contrairement aux tableaux indexés, les tableaux associatifs doivent être déclarés explicitement avec `declare -A` avant leur utilisation. Ils ne préservent pas l'ordre d'insertion.
Déclaration et utilisation#
# Déclaration OBLIGATOIRE avec -A
declare -A capitales
# Assignation de paires clé-valeur
capitales["France"]="Paris"
capitales["Espagne"]="Madrid"
capitales["Italie"]="Rome"
capitales["Allemagne"]="Berlin"
# Initialisation en une seule ligne
declare -A couleurs_hex=(
["rouge"]="#FF0000"
["vert"]="#00FF00"
["bleu"]="#0000FF"
["blanc"]="#FFFFFF"
["noir"]="#000000"
)
# Accès par clé
echo "${capitales["France"]}" # Paris
echo "${couleurs_hex["rouge"]}" # #FF0000
# Vérifier si une clé existe
if [[ -v capitales["Japon"] ]]; then
echo "La clé Japon existe"
else
echo "La clé Japon n'existe pas"
fi
# Nombre d'éléments
echo "${#capitales[@]}" # 4
# Liste des clés
echo "${!capitales[@]}" # (ordre non garanti)
# Liste des valeurs
echo "${capitales[@]}"
Itération sur clés et valeurs#
declare -A scores=(
["Alice"]=95
["Bob"]=78
["Charlie"]=88
["Diana"]=92
)
# Itérer sur les clés
echo "=== Classement ==="
for nom in "${!scores[@]}"; do
echo "$nom : ${scores[$nom]} points"
done
# Itérer et trier par valeur (nécessite un traitement externe)
echo "=== Trié par score décroissant ==="
for nom in "${!scores[@]}"; do
echo "${scores[$nom]} $nom"
done | sort -rn | while read score nom; do
echo "$nom : $score points"
done
# Ajouter ou mettre à jour une entrée
scores["Eve"]=85
scores["Alice"]=97 # Mise à jour
# Supprimer une entrée
unset 'scores["Bob"]'
Exemple 21 (Compter les occurrences avec un tableau associatif)
Le tableau associatif est l’outil idéal pour compter des occurrences dans un flux de texte :
declare -A compteur
while IFS= read -r ligne; do
extension="${ligne##*.}"
(( compteur["$extension"]++ ))
done < <(find /home -type f -name "*.*" 2>/dev/null)
for ext in "${!compteur[@]}"; do
printf "%-10s : %d fichiers\n" "$ext" "${compteur[$ext]}"
done | sort -t: -k2 -rn
## Manipulation des chaînes de caractères
Bash propose un ensemble riche d'opérations sur les chaînes directement dans la syntaxe d'expansion de variables, sans avoir à invoquer `sed`, `awk` ou `tr`. Ces opérations sont plus rapides (pas de processus externe) et plus lisibles dans des scripts intensifs.
### Longueur d'une chaîne
```bash
texte="Bonjour, le monde !"
# Longueur en caractères
echo "${#texte}" # 20
# Fonctionne aussi avec des variables non définies
echo "${#variable_inexistante}" # 0
Extraction de sous-chaîne#
La syntaxe ${variable:offset:longueur} extrait une sous-chaîne à partir de la position offset (base zéro) sur longueur caractères.
Définition 34 (Extraction de sous-chaîne)
La syntaxe ${var:offset:longueur} extrait une sous-chaîne de var. Si offset est négatif (avec un espace obligatoire avant le moins : ${var: -n}), le décompte s’effectue depuis la fin de la chaîne. Si longueur est omise, l’extraction va jusqu’à la fin de la chaîne. Si longueur est négative, elle indique le nombre de caractères à omettre en fin de chaîne.
chaine="Programmation Bash"
# Extraction à partir de l'offset 0
echo "${chaine:0:13}" # Programmation
# À partir de l'offset 14
echo "${chaine:14}" # Bash
# Offset négatif : depuis la fin (noter l'espace)
echo "${chaine: -4}" # Bash
echo "${chaine: -4:2}" # Ba
# Longueur négative : tout sauf les N derniers caractères
echo "${chaine:0:-5}" # Programmation
# Application : extraire l'extension d'un fichier
fichier="rapport_final.pdf"
echo "${fichier: -3}" # pdf
# Extraire les 4 premiers caractères d'une date ISO
date_iso="2024-03-15"
annee="${date_iso:0:4}" # 2024
mois="${date_iso:5:2}" # 03
jour="${date_iso:8:2}" # 15
Suppression de préfixe et de suffixe#
Les opérations #, ##, % et %% permettent de supprimer des motifs en début ou en fin de chaîne. Ces opérations utilisent les patterns de globbing (et non les expressions régulières).
Définition 35 (Suppression de motif)
Bash propose quatre opérateurs de suppression de motif dans les expansions de variables :
${var#motif}— supprime le préfixe le plus court correspondant àmotif${var##motif}— supprime le préfixe le plus long correspondant àmotif${var%motif}— supprime le suffixe le plus court correspondant àmotif${var%%motif}— supprime le suffixe le plus long correspondant àmotif
Le motif utilise la syntaxe de globbing (*, ?, [...]), pas les expressions régulières.
chemin="/home/alice/documents/rapport.txt"
# Supprimer le préfixe le plus court jusqu'au premier /
echo "${chemin#*/}" # home/alice/documents/rapport.txt
# Supprimer le préfixe le plus long jusqu'au dernier /
echo "${chemin##*/}" # rapport.txt (équivalent de basename)
# Supprimer le suffixe le plus court après le dernier .
echo "${chemin%.*}" # /home/alice/documents/rapport
# Supprimer le suffixe le plus long après le premier .
echo "${chemin%%.*}" # /home/alice/documents/rapport
# Cas pratique : changer l'extension d'un fichier
fichier="image.jpg"
nouveau="${fichier%.jpg}.png"
echo "$nouveau" # image.png
# Extraire le répertoire parent (équivalent de dirname)
echo "${chemin%/*}" # /home/alice/documents
# Supprimer un préfixe fixe
url="https://exemple.fr/page"
echo "${url#https://}" # exemple.fr/page
# Nettoyer les espaces en début de chaîne
texte=" Bonjour "
echo "${texte#"${texte%%[! ]*}"}" # "Bonjour " (trim gauche)
Remarque 29
La règle mnémotechnique pour # et % est la suivante : sur un clavier standard, # se trouve à gauche (début de chaîne) et % à droite (fin de chaîne). La double répétition (##, %%) indique la correspondance la plus longue (greedy), la simple répétition indique la plus courte (lazy). Ces opérations sont extrêmement fréquentes en Bash pour manipuler des chemins de fichiers, des URLs ou des noms avec extension.
Substitution dans une chaîne#
La syntaxe ${var/motif/remplacement} et ${var//motif/remplacement} permet de remplacer des occurrences d’un motif dans une chaîne.
phrase="le chat est un animal, le chat est gris"
# Remplacer la PREMIÈRE occurrence
echo "${phrase/chat/chien}" # le chien est un animal, le chat est gris
# Remplacer TOUTES les occurrences
echo "${phrase//chat/chien}" # le chien est un animal, le chien est gris
# Remplacer au début de la chaîne (ancrage ^)
echo "${phrase/#le/un}" # un chat est un animal, le chat est gris
# Remplacer à la fin de la chaîne (ancrage %)
echo "${phrase/%gris/blanc}" # le chat est un animal, le chat est blanc
# Supprimer un motif (remplacement vide)
echo "${phrase// /}" # lechatestunanimaL,lechatesrtgris (supprime les espaces)
# Remplacer un caractère spécial
chemin="/home/alice/mon fichier.txt"
chemin_safe="${chemin// /_}" # /home/alice/mon_fichier.txt
# Application : normaliser des séparateurs
csv="valeur1;valeur2;valeur3"
echo "${csv//;/,}" # valeur1,valeur2,valeur3
Conversion de casse#
Bash 4.0 a introduit des opérateurs de conversion de casse directement dans les expansions de paramètres.
texte="Bonjour Le Monde"
# Tout en minuscules
echo "${texte,,}" # bonjour le monde
# Tout en majuscules
echo "${texte^^}" # BONJOUR LE MONDE
# Inverser la casse
echo "${texte~~}" # bONJOUR lE mONDE
# Minuscule pour le premier caractère seulement
echo "${texte,}" # bonjour Le Monde
# Majuscule pour le premier caractère seulement
echo "${texte^}" # Bonjour Le Monde
# Appliquer uniquement sur les lettres correspondant à un motif
echo "${texte^^[aeiou]}" # BOnjOUr lE mOndE (majuscules sur les voyelles)
# Application pratique : normalisation d'entrée utilisateur
read -p "Entrez votre réponse (oui/non) : " reponse
reponse="${reponse,,}" # Normaliser en minuscules
if [[ "$reponse" == "oui" ]]; then
echo "Vous avez répondu oui."
fi
Valeurs par défaut et substitution conditionnelle#
Bash offre une famille d’opérateurs permettant de définir des valeurs par défaut, d’initialiser des variables ou de signaler des erreurs en cas de variable non définie.
Définition 36 (Opérateurs de valeur par défaut)
Bash propose quatre formes de substitution conditionnelle basées sur l’état d’une variable (définie, non définie, vide ou non vide) :
Syntaxe |
Condition |
Comportement |
|---|---|---|
|
|
Retourne |
|
|
Assigne |
|
|
Affiche |
|
|
Retourne |
Sans le :, la condition porte uniquement sur le fait que la variable soit non définie (une variable vide est alors considérée comme définie).
# ${var:-défaut} : valeur de secours sans modification
fichier=""
echo "${fichier:-/etc/default.conf}" # /etc/default.conf
echo "$fichier" # (toujours vide)
# ${var:=défaut} : initialisation si absent ou vide
echo "${configuration:=/etc/app.conf}" # /etc/app.conf
echo "$configuration" # /etc/app.conf (maintenant défini)
# ${var:?message} : erreur si absent ou vide
# Exemple d'utilisation dans un script :
: "${DATABASE_URL:?La variable DATABASE_URL doit être définie}"
# Si DATABASE_URL est vide, le script s'arrête avec l'erreur
# ${var:+autre} : retourner une valeur alternative si var est définie
debug=""
log_level="DEBUG"
echo "Mode verbeux ${debug:+activé}" # Mode verbeux (debug est vide)
echo "Niveau ${log_level:+: $log_level}" # Niveau : DEBUG
# Combinaison dans un script réel
PORT="${PORT:-8080}" # Port par défaut
HOST="${HOST:-localhost}" # Hôte par défaut
LOG_FILE="${LOG_FILE:-/var/log/app.log}"
# Différence avec et sans : (sans : = seulement si non défini)
unset maVar
echo "${maVar-défaut}" # défaut (non défini → retourne défaut)
maVar=""
echo "${maVar-défaut}" # (vide, mais défini → ne retourne pas défaut)
echo "${maVar:-défaut}" # défaut (vide → retourne défaut)
Exemple 22 (Script de démarrage avec validation des variables)
Un pattern courant dans les scripts de déploiement est de valider toutes les variables d’environnement nécessaires dès le début :
#!/usr/bin/env bash
set -euo pipefail
# Variables obligatoires : le script échoue si elles sont absentes
: "${APP_NAME:?Variable APP_NAME obligatoire}"
: "${DEPLOY_ENV:?Variable DEPLOY_ENV obligatoire (dev/staging/prod)}"
: "${DATABASE_URL:?Variable DATABASE_URL obligatoire}"
# Variables optionnelles avec valeurs par défaut
PORT="${PORT:-3000}"
LOG_LEVEL="${LOG_LEVEL:-info}"
MAX_WORKERS="${MAX_WORKERS:-4}"
echo "Démarrage de $APP_NAME en environnement $DEPLOY_ENV"
echo "Port: $PORT | Workers: $MAX_WORKERS | Log: $LOG_LEVEL"
## Globbing : sélection de fichiers par motif
Le **globbing** (ou *pathname expansion*) est le mécanisme par lequel Bash remplace les motifs contenant des caractères spéciaux (`*`, `?`, `[...]`) par la liste des fichiers correspondants. Il s'agit d'une fonctionnalité fondamentale, utilisée dans presque tous les scripts pour itérer sur des fichiers.
```{prf:definition} Globbing
:label: definition-11-06
Le **globbing** est l'expansion des métacaractères de chemins effectuée par le shell avant l'exécution d'une commande. Les métacaractères de base sont :
- `*` — correspond à n'importe quelle séquence de caractères (sauf `/`)
- `?` — correspond à exactement un caractère quelconque (sauf `/`)
- `[abc]` — correspond à l'un des caractères listés
- `[a-z]` — correspond à un caractère dans l'intervalle
- `[!abc]` ou `[^abc]` — correspond à tout caractère **sauf** ceux listés
Métacaractères de base#
# * : zéro ou plusieurs caractères
ls *.txt # Tous les fichiers .txt
ls rapport* # Tous les fichiers commençant par "rapport"
ls *2024* # Tous les fichiers contenant "2024"
# ? : exactement un caractère
ls fichier?.txt # fichier1.txt, fichierA.txt, etc.
ls ??? # Fichiers dont le nom fait exactement 3 caractères
# [abc] : un des caractères de la liste
ls [abc]*.md # Fichiers commençant par a, b ou c
ls *.[ch] # Fichiers .c et .h (code C)
# [a-z] : plage de caractères
ls [0-9]*.log # Fichiers de log commençant par un chiffre
ls [A-Z]*.py # Fichiers Python commençant par une majuscule
# [!...] ou [^...] : négation
ls *.[!p]* # Fichiers dont l'extension ne commence pas par p
Expansion d’accolades#
L’expansion d’accolades ({a,b,c}) est distincte du globbing : elle génère des chaînes sans vérifier l’existence des fichiers.
# Générer des noms de fichiers
echo fichier{1,2,3}.txt # fichier1.txt fichier2.txt fichier3.txt
echo {lundi,mardi,mercredi} # lundi mardi mercredi
# Plages numériques et alphabétiques
echo {1..5} # 1 2 3 4 5
echo {01..10} # 01 02 03 04 05 06 07 08 09 10
echo {a..f} # a b c d e f
# Avec incrément
echo {0..20..5} # 0 5 10 15 20
echo {z..a..3} # z w t q n k h e b
# Création de répertoires en masse
mkdir -p projet/{src,tests,docs,assets/{images,styles}}
# Sauvegarder un fichier avec son ancien nom
cp config.yml{,.bak} # Crée config.yml.bak
# Renommer une extension (avec brace expansion)
for f in *.jpeg; do mv "$f" "${f%.jpeg}.jpg"; done
Globstar et options de globbing#
Bash propose des options qui étendent le comportement du globbing, activables avec shopt.
# Activer globstar pour la correspondance récursive **
shopt -s globstar
# ** : tous les fichiers récursivement
ls **/*.py # Tous les fichiers .py dans l'arborescence
# Itérer sur tous les fichiers d'un type
for fichier in /home/**/*.log; do
echo "Log trouvé : $fichier"
done
# Autres options utiles
shopt -s nullglob # Si aucun fichier ne correspond, retourne chaîne vide
# (sans cette option, le motif est retourné tel quel)
shopt -s dotglob # Inclure les fichiers cachés (commençant par .)
shopt -s nocaseglob # Correspondance insensible à la casse
shopt -s extglob # Activer les patterns étendus (voir ci-dessous)
# Désactiver une option
shopt -u globstar
# Vérifier l'état des options
shopt globstar
Patterns étendus (extglob)#
Avec shopt -s extglob, Bash active un ensemble de patterns plus puissants :
shopt -s extglob
# ?(pattern) : zéro ou une occurrence
# *(pattern) : zéro ou plusieurs occurrences
# +(pattern) : une ou plusieurs occurrences
# @(pattern) : exactement une occurrence
# !(pattern) : tout sauf ce pattern
# Fichiers qui ne sont pas des images
ls !(*.jpg|*.png|*.gif)
# Fichiers commençant par zéro ou un trait d'union
ls ?(-)*
# Supprimer tous les fichiers sauf les .sh
rm !(*.sh)
# Fichiers dont l'extension est jpg ou jpeg
ls *.@(jpg|jpeg)
Le tilde (~)#
L’expansion du tilde est une forme spéciale de globbing qui concerne les répertoires personnels.
echo ~ # /home/alice (répertoire home de l'utilisateur courant)
echo ~root # /root (répertoire home de root)
echo ~alice # /home/alice (répertoire home d'alice)
echo ~/Documents # /home/alice/Documents
# Utilisation dans les commandes
cd ~/projets
cp ~/config.txt /etc/
Remarque 30
Le tilde n’est développé que lorsqu’il apparaît au début d’un mot et n’est pas entre guillemets. Ainsi, "~/Documents" n’est pas développé (reste ~/Documents), tandis que ~/Documents l’est (devient /home/alice/Documents). Il faut donc ne pas mettre le tilde entre guillemets, tout en s’assurant que le chemin résultant ne contient pas d’espaces — sinon, il faut utiliser "$HOME/Documents" à la place.
Visualisation : opérations sur les chaînes#
Résumé#
Ce chapitre a couvert les mécanismes de manipulation de données textuelles et structurées natifs de Bash :
Les tableaux indexés (
declare -a) permettent de stocker des listes ordonnées accessibles par indice entier. Les syntaxes${arr[@]},${#arr[@]}et${!arr[@]}permettent respectivement d’accéder à tous les éléments, d’obtenir leur nombre et de lister les indices.Les tableaux associatifs (
declare -A) sont des dictionnaires clé-valeur, déclarés obligatoirement avec-A, parcourus via${!tableau[@]}pour les clés.La manipulation de chaînes nativement dans Bash évite les appels à
sedouawk: longueur (${#var}), extraction (${var:offset:longueur}), suppression de préfixe/suffixe (#,##,%,%%), substitution (/,//) et conversion de casse (,,,^^).Les valeurs par défaut (
:-,:=,:?,:+) permettent d’écrire des scripts robustes qui gèrent élégamment les variables absentes ou vides.Le globbing (
*,?,[...],{a,b},**avecglobstar) est le mécanisme d’expansion de chemins du shell, indispensable pour itérer sur des fichiers.
Le chapitre suivant aborde la gestion des processus et jobs : comment surveiller, contrôler et interagir avec les processus en cours d’exécution sur le système.