mercredi 29 octobre 2014

Vacances au sous-shell


Bon. J'ai raté la fin des vacances et la rentrée... Mais je suis juste à temps pour la rentrée des vacances de la Toussaint, non ? 

Jour de la rentrée dans la cour de récréation...

  • Octet Touffu : Alors, ces vacances?
  • Octet Foutu  : M'en parle pas, je me suis perdu dans les sous-shell.

Égarement

Le temps des vacances. Flânerie, on écrit quelques lignes de shell-script... Non, je n'ai pas dit avoir codé du shell script pendant mes vacances... mais pendant le temps des vacances... des autres. Nuance.

Et hop, une ligne en appelle une autre, du coup voilà une fonction... qui doit faire une boucle et récupérer un résultat facile... Je simplifie et vous épargne les 1 500 lignes de script environnantes !
Oui, je sais, quand on atteint ce volume, on s'est trompé de langage. Python serait probablement plus adapté. Vous ne faites jamais d'erreur vous?

 Et une fonction shell, une.

function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
         FS_ARRAY[$CNT]=$FS
         CNT=$((CNT + 1))
    done

Facile. On récupère le premier champ de champ de chaque ligne utile imprimée par df, et on le stocke dans un tableau. Du coup, après on espère pouvoir utiliser le tableau!

Essai

fa# get_fs
fa# echo ${FS_ARRAY[0]}


fa# Euh, y'a rien là au dessus?

Pardon?  Eh? Où est la valeur de mon tableau?
Oui, je vous vois, tordus, pliés, rigolards.

N'empêche, persévérons

On met donc des traces dans la fonction et on réessaye ! Pour plus de sûreté, on trace l'entrée du tableau après son initialisation et le compteur après son incrémentation.

fa# function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
         FS_ARRAY[$CNT]=$FS
         echo ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))
         echo $CNT
    done


fa# get_fs
/dev/sda1
1
udev
2
tmpfs
3

fa# echo ${FS_ARRAY[0]}

fa# Euh, y'a toujours rien là au dessus?

Souvenirs, souvenirs

Et là, se profile l'idée que peut-être  la fonction procéderait au moyen d'un sous-shell, qui initialiserait sa copie de FS_ARRAY... mais que cette copie serait somme toute perdue quand on quitte le sous-shell. Et donc, il faudrait trouver un autre moyen pour remplir ce tableau. Ah, oui le shell, les sous-shells.

Mais, vexé, on voudrait quand même savoir si c'est bien  un sous-shell la cause de notre perplexité, sans parler de la déception. 

Et donc on vérifie!

Imprimons donc le PID de nos shells et sous-shells avec la variable $$ .

fa# function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
         FS_ARRAY[$CNT]=$FS
         echo shell $$ sets ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))
         echo $CNT
    done


fa# get_fs
shell 2130 sets /dev/sda1
shell 2130 sets udev
shell 2130 sets tmpfs

fa# echo $$
2130
fa# Euh, Le shell et le sous-shell auraient le même pid? Ou il n'y a pas de sous-shell? Qu'est-ce que cette embrouille?

Je ne crois même pas ce que je vois !

Comme on subodore une entourloupe et qu'on connaît un peu son système, si on allait chercher nous même le pid du processus? Il faut quand même une bonne dose de paranoïa pour ne plus faire confiance au shell pour quelque chose d'aussi simple que echo $$ !
 
fa# function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
         read -r MYPID GARBAGE
        FS_ARRAY[$CNT]=$FS
         echo shell $MYPID $$ sets ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))
         echo $CNT
    done


fa# get_fs
shell 5707 2130 sets /dev/sda1
shell 5707 2130 sets udev
shell 5707 2130 sets tmpfs

fa# read -r MYPID GARBAGE
fa# echo $MYPID $$
2130 2130
fa# Ah! Ah! Aurais-je trouvé un bug grossièrement trivial? Pourquoi $$ donne-t-il 2130 dans les 2 cas, alors que /proc donne 2130 pour l'un et 5707 pour l'autre?
       
Donc, le pid du sous-shell diffère de $$ ? Des certitudes s'effondrent. Retour aux sources: RTFM.

RTFM ! Et le manuel nous dit:

$   Expands to the process ID of the shell. In a () subshell, it expands to the process ID of the current shell, not the subshell.

En clair, le manuel nous dit, apprend à lire. En fait, je ne sais pas si j'ai jamais relu cette partie depuis la page manuel d'un bon vieux shell Bourne d'un Unix V7.

 Vérification! Un coup d’œil dans un bon vieux manuel V7 trouvé là http://plan9.bell-labs.com/7thEdMan/v7vol1.pdf
$ The process number of this shell.

Ah flûte, le monde évolue ! On a modifié la sémantique de de $$ sans m'avertir ! Serait-il nécessaire de se tenir au courant  ?

Simplifions avec BASHPID et BASH_SUBSHELL

fa# function get_fs()
{
    df  | sed -e 1d | awk '{print $1}' | while  read FS
    do
        FS_ARRAY[$CNT]=$FS
         echo shell $BASHPID $BASH_SUBSHELL $$ sets ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))
         echo $CNT
    done


fa# get_fs
shell 5715 1 2130 sets /dev/sda1
shell 5715 1 2130 sets udev
shell 5715 1 2130 sets tmpfs

fa# echo $BASHPID $BASH_SUBSHELL $$
2130 0 2130
fa# 

 Voilà! $BASHPID est la variable qui indique réellement le pid du processus shell courant. et $BASH_SUBSHELL est une indication d'un niveau de sous-shell. A utiliser la prochaine fois qu'un cas douteux se présentera.

Tatillons

Je vous connais, lecteurs tatillons ! D'accord, il sait maintenant déterminer si le code est exécuté dans un sous-shell ou non, mais il ne peut toujours pas récupérer le tableau initialisé par la fonction dans le shell principal. Voilà un moyen comme un autre.


fa# function get_fs()
{
    for FS in $(df  | sed -e 1d | awk '{print $1}')
    do
        FS_ARRAY[$CNT]=$FS
         echo shell $BASHPID $BASH_SUBSHELL $$ sets ${FS_ARRAY[$CNT]}
         CNT=$((CNT + 1))

    done


fa# get_fs
shell 6432 0 6432 sets /dev/sda1
shell 6432 0 6432 sets udev
shell 6432 0 6432 sets tmpfs

fa# echo$CNT ${FS_ARRAY[0]}
7 /dev/sda1


fa# Ah, enfin!

Bref

Le shell (et spécialement les sous-shells) contrairement au vélo, ça s'oublie.
Il faut toujours lire les manuels des nouvelles versions.