Capitolo 20. Subshell

L'esecuzione di uno script di shell avvia un nuovo processo, una subshell.

Una subshell è un'istanza separata del processore dei comandi. -- la shell, quella che fornisce il prompt alla console o in una finestra xterm. Proprio come i comandi vengono interpretati al prompt della riga di comando, così fa uno script che ne elabora in modalità batch una serie. Ogni script di shell in esecuzione è, in realtà, un sottoprocesso (processo figlio) della shell genitore.

Anche uno script di shell può mettere in esecuzione dei sottoprocessi. Queste subshell consentono allo script l'elaborazione in parallelo, vale a dire, l'esecuzione simultanea di più compiti di livello inferiore.

#!/bin/bash
# subshell-test.sh
		
(
# Dentro le parentesi, quindi una subshell . . .
while [ 1 ]   # Ciclo infinito.
do
  echo "Subshell in esecuzione . . ."
done
)
		
#  Lo script continuerà a funzionare,
#+ almeno finché non verrà terminato con un Ctl-C.
		
exit $?  # Fine dello script (ma non si arriverà mai a questo punto).
		
		
		
Ora eseguiamo lo script:
sh subshell-test.sh
		
E, mentre lo script è in esecuzione, da un diverso xterm:
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
		
	  ^^^^
		
Analisi:
PID 2698, lo script, lancia il PID 2699, la subshell.
		
Nota: La riga "UID ..." dovrebbe essere eliminata tramite il comando "grep".
Qui viene mostrata a scopo illustrativo.

Generalmente, un comando esterno presente in uno script genera un sottoprocesso, [1] al contrario di un builtin di Bash. È per questa ragione che i builtin vengono eseguiti più velocemente dei loro equivalenti comandi esterni.

Elenco di comandi tra parentesi

( comando1; comando2; comando3; ... )

Una lista di comandi tra parentesi dà luogo ad una subshell.

Le variabili presenti in una subshell non sono visibili al di fuori del suo blocco di codice. Non sono accessibili al processo genitore, la shell che ha lanciato la subshell. Sono, a tutti gli effetti, variabili locali.

Esempio 20-1. Ambito di una variabile in una subshell

#!/bin/bash
# subshell.sh

echo

echo "Siamo all'esterno della subshell."
echo "Livello della subshell all'ESTERNO della subshell = $BASH_SUBSHELL"
# Bash, versione 3, adotta la nuova variabile             $BASH_SUBSHELL.
echo; echo

variabile_esterna=Esterna
variabile_globale=
#  Definiamo una variabile globale in cui verrà "conservato"
#+ il valore della variabile di subshell.

(
echo "Siamo dentro alla subshell."
echo "Livello della subshell all'INTERNO della subshell = $BASH_SUBSHELL"
variabile_interna=Interna

echo "All'interno della subshell, \"variabile_interna\" = $variabile_interna"
echo "All'interno della subshell, \"variabile_esterna\" = $variabile_esterna"

variabile_globale="$variabile_interna"   #  Questo ci consetirà di "esportare"
                                         #+ una variabile di subshell?
)

echo; echo
echo "Siamo fuori dalla subshell."
echo "Livello della subshell all'ESTERNO della subshell = $BASH_SUBSHELL"
echo

if [ -z "$variabile_interna" ]
then
  echo "variabile_interna non definita nel corpo principale della shell"
else
  echo "variabile_interna definita nel corpo principale della shell"
fi

echo "Nel corpo principale della shell,\
\"variabile_interna\" = $variabile_interna"
#  $variabile_interna viene visualizzata come uno spazio (non inizializzata)
#+ perché le variabili definite in una subshell sono "variabili locali".
#  Esiste un rimedio a ciò?
echo "variabile_globale = "$variabile_globale""  # Perché non funziona?


echo

exit 0

#  Domanda:
#  --------
#  Una volta usciti da una subshell,
#+ esiste un qualche modo per rientrare proprio nella stessa subshell
#+ per accedere alle, o modificare le, variabili della stessa?

Vedi anche Esempio 31-2.

Nota

Mentre la variabile interna $BASH_SUBSHELL indica il livello di annidamento di una subshell, la variabile $SHLVL non mostrerà nessun cambiamento all'interno della subshell.

echo " \$BASH_SUBSHELL fuori dalla subshell       = $BASH_SUBSHELL"          # 0
  ( echo " \$BASH_SUBSHELL dentro la subshell        = $BASH_SUBSHELL" )     # 1
  ( ( echo " \$BASH_SUBSHELL dentro la subshell annidata = $BASH_SUBSHELL" ) ) # 2
# ^ ^                                       *** annidata ***               ^ ^
		
echo
		
echo " \$SHLVL fuori dalla subshell = $SHLVL"       # 3
( echo " \$SHLVL dentro la subshell  = $SHLVL" )    # 3 (Nessun cambiamento!)

I cambiamenti di directory effettuati in una subshell non si ripercuotono sulla shell genitore.

Esempio 20-2. Elenco dei profili utente

#!/bin/bash
# allprofs.sh: visualizza i profili di tutti gli utenti

# Script di Heiner Steven modificato dall'autore di questo documento.

FILE=.bashrc  #  Il file contenente il profilo utente
              #+ nello script originale era ".profile".

for home in `awk -F: '{print $6}' /etc/passwd`
do
  [ -d "$home" ] || continue    #  Se non vi è la directory home, 
                                #+ va al successivo.
  [ -r "$home" ] || continue    #  Se non ha i permessi di lettura, va 
                                #+ al successivo.

  (cd $home; [ -e $FILE ] && less $FILE)
done

#  Quando lo script termina, non è necessario un 'cd' alla directory 
#+ originaria, perché 'cd $home' è stato eseguito in una subshell.

exit 0

Una subshell può essere usata per impostare un "ambiente dedicato" per un gruppo di comandi.

COMANDO1
COMANDO2
COMANDO3
(
  IFS=:
  PATH=/bin
  unset TERMINFO
  set -C
  shift 5
  COMANDO4
  COMANDO5
  exit 3 # Esce solo dalla subshell!
)
# La shell genitore non è stata toccata ed il suo ambiente è preservato.
COMANDO6
COMANDO7
Come qui si è visto, il comando exit termina solamente la subshell in cui è stato eseguito, non la shell o lo script genitore.

Una sua applicazione permette di verificare se una variabile è stata definita.

if (set -u; : $variabile) 2> /dev/null
then
  echo "La variabile è impostata."
fi     #  La variabile potrebbe essere stata impostata nello script stesso,
       #+ oppure essere una variabile interna di Bash,
       #+ oppure trattarsi di una variabile d'ambiente (che è stata esportata).

#  Si sarebbe anche potuto scrivere 
#                   [[ ${variabile-x} != x || ${variabile-y} !=y ]]
# oppure            [[ ${variabile-x} != x$variabile ]]
# oppure            [[ ${variabile+x} = x ]]
# oppure            [[ ${variabile-x} != x ]]

Un'altra applicazione è quella di verificare la presenza di un file lock:

if (set -C; : > file_lock) 2> /dev/null
then
  :   # il file_lock non esiste: nessun utente sta eseguendo lo script
else
  echo "C'è già un altro utente che sta eseguendo quello script."
exit 65
fi

#  Frammento di codice di Stéphane Chazelas,
#+ con modifiche effettuate da Paulo Marcel Coelho Aragao.

+

È possibile eseguire processi in parallelo per mezzo di differenti subshell. Questo permette di suddividere un compito complesso in sottocomponenti elaborate contemporaneamente.

Esempio 20-3. Eseguire processi paralleli tramite le subshell

        (cat lista1 lista2 lista3 | sort | uniq > lista123) &
        (cat lista4 lista5 lista6 | sort | uniq > lista456) &
        # Unisce ed ordina entrambe le serie di liste simultaneamente.
        # L'esecuzione in background assicura l'esecuzione parallela.
        #
        # Stesso effetto di
        #   cat lista1 lista2 lista3 | sort | uniq > lista123 &
        #   cat lista4 lista5 lista6 | sort | uniq > lista456 &

        wait   #  Il comando successivo non viene eseguito finché le subshell
               #+ non sono terminate.

        diff lista123 lista456

Per la redirezione I/O a una subshell si utilizza l'operatore di pipe "|", come in ls -al | (comando).

Nota

Un elenco di comandi tra parentesi graffe non esegue una subshell.

{ comando1; comando2; comando3; . . . comandoN; }

Note

[1]

Un comando esterno invocato con exec (di solito) non genera un/a sottoprocesso / subshell.