Cours Linux / Exécution des commandes Bash : ;, &&, ||, alias et variables d’environnement

Dans Bash, ; enchaîne normalement deux commandes sans condition de réussite, && exécute la suivante seulement après un état 0 et || l’exécute après un état non nul. Un alias remplace un mot pendant la lecture de la commande, tandis qu’une variable exportée transmet une valeur aux processus enfants. Ces mécanismes expliquent pourquoi une même commande apparente peut prendre une autre forme ou suivre une autre branche.

Commencez par les états de sortie et les exemples avec true, false et printf : ils permettent d’observer le contrôle sans modifier des fichiers réels. Les parties sur les groupes, les pipes et les tâches en arrière-plan approfondissent ce modèle. Un script complet simule ensuite une construction, un test et un déploiement uniquement par des messages ; il ne réalise aucun déploiement.

Identifier ce que le shell interprète

Bash lit et analyse une ligne, développe les alias à la lecture, effectue les expansions nécessaires et les redirections, puis exécute la commande résolue. Le nom peut correspondre à une fonction, une commande interne du shell ou un exécutable externe. PATH fournit la liste des répertoires où chercher les commandes externes ; ce n’est pas la liste de tous les alias ou fonctions.

type -a printf
type -a ls
command -v ls

type -a peut décrire plusieurs correspondances pour un nom. command -v fournit une indication concise de la résolution ; sa sortie n’est pas toujours un chemin. Les résultats dépendent de votre session : ne remplacez pas PATH pour essayer d’imiter ceux d’un autre système. Les guides de type et de command détaillent ces vérifications.

Lire l’état de sortie et distinguer les opérateurs

L’état de sortie est le nombre renvoyé à la fin d’une commande. Pour le shell, 0 indique une réussite et une valeur non nulle déclenche le traitement de non-réussite. Le programme définit aussi le sens concret de ses valeurs : grep peut renvoyer 1 simplement parce qu’aucune ligne ne correspond, et diff parce que deux fichiers diffèrent.

true
printf 'status=%s\n' "$?"
false
printf 'status=%s\n' "$?"

Vous devez lire status=0 puis status=1. $? concerne la commande exécutée immédiatement avant ; une consultation intermédiaire ou un message peut remplacer la valeur que vous vouliez lire. Sauvegardez-la tout de suite si vous devez la réutiliser.

Bash utilise notamment 127 pour une commande introuvable et 126 lorsqu’elle est trouvée mais ne peut pas être exécutée. Une terminaison par signal suit encore une autre règle. Le manuel Bash des états de sortie explique ces conventions : toute non-réussite n’est pas simplement le nombre 1.

Notation Rôle Distinction utile
; Attend la fin, puis passe à la suivante Pas de condition sur l’état précédent
&& Passe à droite si l’état précédent est 0 Une étape dépend de la réussite précédente
|| Passe à droite si l’état précédent est non nul Ce n’est pas un pipe de données
& Lance en arrière-plan sans attendre la fin Le lancement n’est pas le résultat final de la tâche
| Relie stdout à stdin Transporte des données entre commandes
|& Relie aussi stderr au pipe dans Bash Fusionne résultats et diagnostics
! Inverse logiquement l’état d’une commande ou d’un pipe Ne transforme pas le texte produit

;, &, && et || construisent des listes de commandes. | et |& sont des opérateurs de pipeline ; ! est un mot réservé qui modifie l’état du pipeline. Cette classification aide à ne pas confondre transmission de données et décision d’exécuter une étape.

Choisir la commande suivante selon le résultat

Observer l’enchaînement et le court-circuit

false; printf 'always\n'
false && printf 'not run\n'
false || printf 'fallback\n'
true && printf 'next step\n'

Vous obtenez always, fallback et next step ; not run n’apparaît pas. Dans && et ||, la partie droite peut être ignorée : c’est l’évaluation en court-circuit. Le point-virgule n’impose pas une réussite préalable. Dans une liste ordinaire, un saut de ligne peut également séparer des commandes, mais il ne termine pas forcément une entrée encore incomplète à cause de guillemets, d’un pipe ou d’une construction composée.

Cette différence compte pour un déplacement : cd vers un dossier suivi de && puis du travail évite d’exécuter ce travail ailleurs si cd échoue. Évitez de remplacer ce lien par ; lorsque le chemin est une condition indispensable.

Comprendre pourquoi A && B || C n’est pas toujours un if

&& et || ont la même priorité et s’associent de gauche à droite. Si A réussit mais que B échoue, C peut donc s’exécuter :

true && false || printf 'fallback runs\n'

fallback runs apparaît. Comparez avec une branche choisie seulement sur la première condition :

if true; then
  false
else
  printf 'condition failed\n'
fi

Ce second bloc n’affiche rien. L’échec de false dans then ne renvoie pas vers else : la branche a déjà été choisie. Il reste à traiter l’échec de l’étape si votre tâche le demande. Le manuel Bash des listes précise la priorité, l’association et l’état de la dernière commande effectivement exécutée.

Distinguer un message d’échec de l’état conservé

false || printf 'failure reported\n'
printf 'list status=%s\n' "$?"

Vous lisez failure reported, puis list status=0. Le message a annoncé l’échec précédent, mais printf a lui-même réussi : la liste se termine donc sur 0. Pour signaler un échec et le renvoyer, stockez l’état avant le message puis retournez-le explicitement dans le script, comme dans l’exemple complet plus bas.

Grouper des commandes et observer leur portée

Les accolades conservent l’environnement du shell courant

Les accolades { ...; } exécutent la liste dans le même environnement de shell. Il faut séparer l’accolade ouvrante de la commande et placer un point-virgule ou un saut de ligne avant l’accolade fermante :

(
  lf15_scope=outer
  { lf15_scope=inner; printf '%s\n' "$lf15_scope"; }
  printf '%s\n' "$lf15_scope"
)

inner est affiché deux fois. La modification dans les accolades reste visible après ce groupe. Les parenthèses extérieures isolent seulement notre démonstration de votre session principale : le « shell courant » observé ici est celui de cette expérience.

Les parenthèses créent un sous-shell

(
  lf15_scope=outer
  ( lf15_scope=inner; printf '%s\n' "$lf15_scope" )
  printf '%s\n' "$lf15_scope"
)

Vous lisez inner puis outer. Les parenthèses intérieures créent un environnement de sous-shell ; leurs changements de variable ou de répertoire courant ne restent pas dans le shell qui les a lancées. Cela ne constitue pas une annulation générale : une écriture de fichier, une suppression ou une requête à un service extérieur produit toujours son effet hors du shell. Le manuel de groupement Bash définit cette différence.

Relier les flux et interpréter l’état d’un pipe

Un pipe transporte des données

printf 'alpha\nbeta\n' | grep 'alpha'

Seule alpha est affichée. | relie stdout de printf à stdin de grep. À la différence de ||, il ne signifie pas « exécuter à droite si la gauche échoue ». Les commandes d’un pipeline traitent les données reliées et peuvent s’exécuter en même temps. La leçon sur stdin, stdout, stderr et les redirections permet de suivre précisément leurs destinations.

|& transmet aussi stderr dans Bash

bash -c 'printf "normal\n"; printf "error\n" >&2' |& cat

normal et error passent tous deux par cat. Dans cet exemple, |& a l’effet de 2>&1 |. Il fusionne les diagnostics et les résultats ; ce n’est pas toujours souhaitable pour un traitement de données. Si d’autres redirections sont déjà présentes, leur ordre d’application compte : ne remplacez pas aveuglément | par |&.

! inverse l’état, pas la sortie

! false
printf 'inverted status=%s\n' "$?"

Vous obtenez inverted status=0. Une non-réussite devient une réussite pour ce test logique ; inversement, un état 0 devient non nul. Le contenu de fichiers et les messages ne sont pas « inversés ».

pipefail considère les échecs en amont

( set +o pipefail; false | cat; printf 'default=%s\n' "$?" )
( set -o pipefail; false | cat; printf 'pipefail=%s\n' "$?" )

Vous lisez default=0 puis pipefail=1. Sans pipefail, le dernier cat termine normalement et fournit l’état du pipe. Avec pipefail, l’état vaut 0 si toutes les commandes réussissent ; sinon il reprend l’état de la commande non réussie la plus à droite. Chaque changement d’option reste dans son sous-shell, sans modifier votre session principale.

Le manuel des pipelines Bash décrit aussi |& et !. pipefail ne détermine pas à votre place si, par exemple, « aucune correspondance grep » est acceptable : votre automatisation doit définir ce qu’elle attend.

Attendre le résultat réel d’une tâche en arrière-plan

& rend la main sans attendre la fin de la commande. Il ne joue pas le rôle conditionnel de &&. Lancez une courte attente et récupérez son résultat avec wait :

(
  sleep 2 &
  lf15_job=$!
  printf 'foreground continues\n'
  wait "$lf15_job"
  lf15_status=$?
  printf 'job status=%s\n' "$lf15_status"
)

foreground continues apparaît avant la fin de l’attente ; job status=0 suit lorsque sleep se termine. $! désigne le processus d’arrière-plan le plus récent. L’état lu juste après le lancement ne remplace pas l’état final récupéré par wait. Le manuel Bash des commandes de contrôle des tâches décrit cette attente.

Dans un Bash interactif, jobs liste les tâches et fg ou bg permettent des transitions entre premier plan et arrière-plan. Ajouter & ne garantit pas à lui seul qu’un travail survivra à la déconnexion. Pour un service ou une longue exécution, choisissez un mode de gestion de session ou de service adapté.

Exécuter un script complet de contrôle du flux

Créez un dossier de pratique neuf. Les commandes suivantes préparent uniquement un fichier contenant des fonctions qui affichent build, test et deploy. Elles n’effectuent aucune construction ou publication réelle.

atelier=
candidat="$HOME/linux-course-15-practice"
if mkdir -- "$candidat"; then
  atelier=$candidat
  printf 'Atelier : %s\n' "$atelier"
else
  printf '%s\n' 'Choisir un dossier neuf avant de poursuivre.' >&2
fi

Si le nom existe, choisissez-en un autre et recommencez la préparation ; ne continuez pas dans des données antérieures. Gardez le même terminal, puis créez le script complet ci-dessous. Le bloc refuse de réécrire un fichier déjà présent :

if test -n "$atelier" && test ! -e "$atelier/course15-flow.sh"; then
  cat > "$atelier/course15-flow.sh" <<'BASH'
#!/usr/bin/env bash
case "${1:-0}" in
  0|1) ;;
  *) printf '%s\n' 'Utiliser seulement 0 ou 1.' >&2; exit 2 ;;
esac

build() { printf 'build\n'; }
run_tests() { printf 'test\n'; return "${1:-0}"; }
deploy() { printf 'deploy\n'; }

build && run_tests "${1:-0}" && deploy
flow_status=$?
if (( flow_status != 0 )); then
  printf 'workflow failed: status=%s\n' "$flow_status" >&2
fi
exit "$flow_status"
BASH
else
  printf '%s\n' 'Création refusée : vérifier le dossier et utiliser un nouveau fichier.' >&2
fi

Le mot BASH de fermeture doit rester seul au début de sa ligne. Cette construction, appelée here-document, donne à cat le texte à enregistrer. Les apostrophes de 'BASH' empêchent l’expansion des variables pendant la création : ${1:-0}, $? et les autres expressions doivent rester dans le script pour son exécution ultérieure. Le manuel Bash des here-documents précise cette différence entre délimiteurs cités et non cités.

Les fonctions nomment des groupes de commandes. return renvoie un état de fonction ; exit termine ce script et renvoie son état à Bash qui l’a lancé. ${1:-0} utilise 0 si le premier argument est absent ou vide. Notre exemple accepte seulement 0 et 1, et conserve l’état de la chaîne dans flow_status avant d’écrire un diagnostic.

Exécutez le fichier avec Bash ; il n’est pas nécessaire d’ajouter un droit d’exécution :

if test -n "$atelier"; then
  if bash -- "$atelier/course15-flow.sh" 0; then
    lf15_status=0
  else
    lf15_status=$?
  fi
  printf 'script status=%s\n' "$lf15_status"
fi

Vous devez lire build, test, deploy puis script status=0. Changez seulement le dernier argument de l’appel Bash de 0 en 1 et relancez le même bloc. Cette fois, vous voyez build puis test, un diagnostic workflow failed: status=1 sur stderr et script status=1 ; deploy n’est pas exécuté. Revenez à l’argument 0 pour retrouver exactement le chemin de réussite.

La capture dans else conserve immédiatement l’état non nul de Bash. Le message qui suit ne l’efface pas dans la variable. Dans une tâche réelle, chaque fonction doit renvoyer un état qui représente effectivement son résultat, au lieu de terminer accidentellement sur un printf réussi.

Un enchaînement travail; nettoyage ne garantit pas le nettoyage si le shell s’arrête avant cette étape ou reçoit un signal. Les scripts qui en ont besoin doivent prévoir une gestion adaptée, par exemple avec trap. set -e comporte des exceptions dans les conditions et certaines listes ; il ne remplace pas une conception explicite du traitement des échecs. Le manuel de set dans Bash décrit les contextes où cette option ne provoque pas une sortie.

Observer le moment où un alias est développé

Un alias remplace un mot de commande à la lecture. Faites cette expérience dans un Bash interactif temporaire sans charger vos fichiers de configuration :

bash --noprofile --norc -i

Vous obtenez une nouvelle invite de ce Bash. Saisissez la définition suivante sur sa propre ligne, puis validez :

alias lf15_list='ls -l'

À la nouvelle invite seulement, saisissez les deux commandes suivantes, chacune validée séparément :

type lf15_list
lf15_list

type indique l’alias et lf15_list produit la liste détaillée du dossier courant. Cette liste dépend de vos fichiers ; elle n’a pas de contenu universel à reproduire. Ne réunissez pas définition et utilisation dans une seule ligne ou le même bloc composé : Bash peut avoir déjà lu les mots suivants avant que le nouvel alias existe.

Les alias ne sont normalement pas développés dans Bash non interactif, sauf activation de expand_aliases. Un script ne doit donc pas supposer qu’il hérite du raccourci de votre terminal. Pour une action qui prend et traite des arguments, une fonction est généralement plus adaptée. Le manuel Bash des alias explique cette phase de lecture.

Dans ce Bash temporaire, retirez l’alias puis revenez au shell précédent :

unalias lf15_list
exit

L’alias d’expérience et les modifications de ce sous-shell ne changent pas les définitions du shell parent. Le fichier du script précédent reste dans son dossier : quitter un shell ne supprime pas les fichiers qu’il a créés.

Distinguer une variable du shell de l’environnement enfant

Une variable du shell est disponible dans ce shell. export marque une variable pour que les futurs processus enfants reçoivent aussi sa valeur dans leur environnement. L’expérience suivante utilise un nom dédié et un sous-shell pour préserver votre session :

(
  unset lf15_mode
  lf15_mode=practice
  printf 'shell=%s\n' "$lf15_mode"
  if printenv lf15_mode; then
    printf '%s\n' 'Variable présente dans cet environnement.'
  else
    printf '%s\n' 'Variable absente de cet environnement.'
  fi
  export lf15_mode
  printenv lf15_mode
  lf15_mode=once printenv lf15_mode
  printf 'shell=%s\n' "$lf15_mode"
)

Vous lisez shell=practice, puis Variable absente de cet environnement. Après export, printenv affiche practice. L’affectation placée devant le second printenv donne seulement à ce processus la valeur once ; le dernier printf confirme shell=practice. printenv est ici une commande externe qui lit son environnement, pas la variable interne de son parent.

export ne modifie pas les autres shells déjà ouverts, ni le processus parent. La forme nom=valeur commande fournit une valeur à cette invocation ; notre exemple choisit une commande externe pour observer ce comportement sans particularités de fonction. Le manuel Bash de l’environnement décrit la transmission des paires nom-valeur.

Évitez de réutiliser HOME, PATH ou une autre variable importante pour les exercices. Si vous avez défini une variable de pratique directement dans votre session, unset permet de la retirer ; les variables de nos sous-shells disparaissent déjà à leur fin.

Localiser une exécution inattendue

  • Nom mal interprété : utilisez type -a dans le terminal concerné pour voir les alias, fonctions et commandes possibles.
  • Étape ignorée ou lancée par surprise : vérifiez chaque état et l’association gauche à droite de && et || avant de transformer la liste en condition plus explicite.
  • Pipe apparemment réussi : distinguez les données reçues, stderr et l’état choisi avec ou sans pipefail.
  • Alias absent : contrôlez le caractère interactif du shell et le moment de lecture de sa définition.
  • Valeur manquante chez un enfant : vérifiez l’export et le processus qui reçoit l’environnement ; ne supposez pas que l’export change un shell déjà lancé.

Avant de relier des commandes qui modifient réellement des fichiers ou un service, reproduisez leur logique avec true, false et printf. Définissez les conditions attendues, observez les messages et les états séparément, puis vérifiez que chaque branche termine avec le résultat que vous voulez transmettre.