21. Sous-shells

Exécuter un script shell lance un nouveau processus, un sous-shell.

Un sous-shell est une instance séparée du gestionnaire de commande -- le shell qui vous donne l'invite sur la console ou dans une fenêtre xterm. De la même façon que vos commandes sont interprétées sur l'invite de commandes, un script traite en flot une liste de commandes. Chaque script shelle en cours d'exécution est un sous-processus (processus enfant) du shell parent shell.

Un script shell peut lancer lui-même des sous-processus. Ces sous-shells permettent au script de faire de l'exécution en parallèle, donc d'exécuter différentes tâches simultanément.

#!/bin/bash
# subshell-test.sh

(
# Parenthèses à l'intérieur, donc un sous-shell...
while [ 1 ]   # Boucle sans fin.
do
  echo "Sous-shell en cours d'exécution..."
done
)

#  Le script s'exécutera sans jamais s'arrêter,
#+ au moins tant que vous ne ferez pas Ctl-C.

exit $?  # Fin du script (mais l'exécution ne parviendra jamais jusqu'ici).



Maintenant, exécutez le script :
sh subshell-test.sh

et tant que le script s'exécute, à partir d'un autre terminal X :
ps -ef | grep subshell-test.sh

UID       PID   PPID  C STIME TTY      TIME     CMD
500       2698  2502  0 14:26 pts/4    00:00:00 sh subshell-test.sh
500       2699  2698 21 14:26 pts/4    00:00:24 sh subshell-test.sh

          ^^^^

Analyse :
PID 2698, le script, a lancé le sous-shell de PID 2699.

Note : la ligne "UID ..." peut être filtrée par la commande grep
mais elle est laissée ici dans un but démonstratif.

En général, une commande externe dans un script lance un sous-processus [103] alors qu'une commande intégrée Bash ne le fait pas. Pour cette raison, les commandes intégrées s'exécutent plus rapidement et utilisent moins de ressources que leurs commandes externes équivalentes.

Liste de commandes entre parenthèses

( commande1; commande2; commande3; ... commandeN; )

Une liste de commandes placées entre parenthèses est exécutée sous forme de sous-shells

Les variables utilisées dans un sous shell ne sont pas visibles en dehors du code du sous-shell. Elles ne sont pas utilisables par le processus parent, le shell qui a lancé le sous-shell. En fait, ce sont des variables locales au processus enfant.

Exemple 21.1. Étendue des variables dans un sous-shell

#!/bin/bash
# subshell.sh

echo

echo "Nous sommes à l'extérieur du sous-shell."
echo "Niveau de sous-shell À L'EXTÉRIEUR DU sous-shell = $BASH_SUBSHELL"
# Bash, version 3, ajoute la nouvelle variable           $BASH_SUBSELL.
echo; echo

variable_externe=externe
variable_globale=
#  Définit une variable globale pour le stockage
#+ de la valeur d'une variable d'un sous-shell.

(
echo "Nous sommes à l'intérieur du sous-shell."
echo "Niveau de sous-shell À L'INTÉRIEUR DU sous-shell = $BASH_SUBSHELL"
variable_interne=interne
echo "À partir du sous-shell interne, \"variable_interne\" = $variable_interne"
echo "À partir du sous-shell interne, \"externe\" = $variable_externe"

variable_globale="$variable_interne" #  Est-ce que ceci permet l'export
                                     #+ d'une variable d'un sous-shell ?
)

echo; echo
echo "Nous sommes à l'extérieur du sous-shell."
echo
echo "Niveau de sous-shell À L'EXTÉRIEUR DU sous-shell = $BASH_SUBSHELL"
echo


if [ -z "$variable_interne" ]
then
  echo "variable_interne non défini dans le corps principal du shell"
else
  echo "variable_interne défini dans le corps principal du shell"
fi

echo "À partir du code principal du shell, \"variable_interne\" = $variable_interne"
#  $variable_interne s'affichera comme non initialisée parce que les variables
#+ définies dans un sous-shell sont des "variables locales".
# Existe-t'il un remède pour ceci ?
echo "variable_globale = "$variable_globale""  # Pourquoi ceci ne fonctionne pas ?


echo

# =======================================================================

# De plus...

echo "-----------------"; echo

var=41                                                 # Variable globale.

( let "var+=1"; echo "\$var À L'INTÉRIEUR D'UN sous-shell = $var" )  # 42

echo "\$var EN DEHORS DU sous-hell = $var"                   # 41
#  Les opérations sur des variabledans un sous-shell, même dans une variable
#+ globale, n'affectent pas la valeur de la variable en dehors du sous-shell !

exit 0

#  Question :
#  ---------
#  Une fois le sous-shell quitté,
#+ existe-il un moyen d'entrer de nouveau dans le même sous-shell
#+ pour modifier ou accéder aux variables du sous-shell ?

Voir aussi les $BASHPID et Exemple 34.2, « Problèmes des sous-shell ».

[Note]

Note

Alors que la variable interne the $BASH_SUBSHELL indique le niveau d'imbrication des sous-shells, la variable $SHLVL ne montre aucune modification dans un sous-shell.

echo " \$BASH_SUBSHELL en dehors du sous-shell  = $BASH_SUBSHELL"           # 0
  ( echo " \$BASH_SUBSHELL dans le sous-shell = $BASH_SUBSHELL" )     # 1
  ( ( echo " \$BASH_SUBSHELL à l'intérieur du sous-shell imbriqué = $BASH_SUBSHELL" ) ) # 2
# ^ ^                           *** imbriquée ***                        ^ ^

echo

echo " \$SHLVL en dehors du sous-shell = $SHLVL"       # 3
( echo " \$SHLVL à l'intérieur du sous-shell  = $SHLVL" )   # 3 (aucun changement !)

Le changement de répertoire effectué dans un sous-shell n'a pas d'incidence sur le shell parent.

Exemple 21.2. Lister les profils utilisateurs

#!/bin/bash
# allprofs.sh : affiche tous les profils utilisateur.

# Ce script a été écrit par Heiner Steven et modifié par l'auteur du document.

FICHIER=.bashrc  #  Fichier contenant le profil utilisateur,
                 #+ était ".profile" dans le script original.

for home in `awk -F: '{print $6}' /etc/passwd`
do
  [ -d "$home" ] || continue    #  Si pas de répertoire personnel, passez au
                                #+ suivant.
  [ -r "$home" ] || continue    # Si non lisible, passez au suivant.
  (cd $home; [ -e $FICHIER ] && less $FICHIER)
done

#  Quand le script se termine, il n'y a pas de besoin de retourner dans le
#+ répertoire de départ parce que 'cd $home' prend place dans un sous-shell.

exit 0

Un sous-shell peut être utilisé pour mettre en place un « environnement dédié » à un groupe de commandes.

COMMANDE1
COMMANDE2
COMMANDE3
(
  IFS=:
  PATH=/bin
  unset TERMINFO
  set -C
  shift 5
  COMMANDE4
  COMMANDE5
  exit 3 # Sortie du seul sous-shell !
)
#  Le shell parent n'a pas été affecté et son environnement est préservé (ex :
#+ pas de modification de $PATH).
COMMANDE6
COMMANDE7

Comme vous le voyez, la commande exit termine seulement le sous-shell dans lequel il s'exécute, mais il ne termine pas le shell ou le script parent.

L'intérêt peut être par exemple de tester si une variable est définie ou pas.

if (set -u; : $variable) 2> /dev/null
then
  echo "La variable est définie."
fi     #  La variable a été initialisée dans le script en cours,
       #+ ou est une variable interne de Bash,
       #+ ou est présente dans l'environnement (a été exportée).

# Peut également s'écrire [[ ${variable-x} != x || ${variable-y} != y ]]
# ou                      [[ ${variable-x} != x$variable ]]
# ou                      [[ ${variable+x} = x ]]
# ou                      [[ ${variable-x} != x ]]

Une autre application est de vérifier si un fichier est marqué comme verrouillé :

if (set -C; : > fichier_verrou) 2> /dev/null
then
  :   # fichier_verrou n'existe pas : aucun utilisateur n'exécute ce script
else
  echo "Un autre utilisateur exécute déjà ce script."
  exit 65
fi

#  Code de Stéphane Chazelas,
#+ avec des modifications de Paulo Marcel Coelho Aragao.

+

Des processus peuvent être exécutés en parallèle dans différents sous-shells. Cela permet de séparer des tâches complexes en plusieurs sous-composants exécutés simultanément.

Exemple 21.3. Exécuter des processus en parallèle dans les sous-shells

(cat liste1 liste2 liste3 | sort | uniq > liste123) &
(cat liste4 liste5 liste6 | sort | uniq > liste456) &
# Concatène et trie les 2 groupes de listes simultanément.
# Lancer en arrière-plan assure une exécution en parallèle.
#
# Peut également être écrit :
#   cat liste1 liste2 liste3 | sort | uniq > liste123 &
#   cat liste4 liste5 liste6 | sort | uniq > liste456 &

wait   # Ne pas exécuter la commande suivante tant que les sous-shells
       # n'ont pas terminé

diff liste123 liste456

Redirection des entrées/sorties (I/O) dans un sous-shell en utilisant « | », l'opérateur tube (pipe en anglais), par exemple ls -al | (commande).

[Note]

Note

Un bloc de commandes entre accolades ne lance pas de sous-shell.

{ commande1; commande2; commande3; ... commandeN; }

var1=23
echo "$var1"   # 23

{ var1=76; }
echo "$var1"   # 76


[103] Une commande externe appelée avec un exec ne lance pas (en général) un sous-processus / sous-shell.