"builtin" : quelques subtilités

Quand j'ai créé la liste AppleScript francophone, j'ai dispensé aux nouveaux venus des petits cours sans prétention pour leur mettre le pied à l'étrier.
Je récidive ici, et en toute humilité, avec un exemple complexe disséqué je l'espère avec clarté.

Un shell est un environnement de travail, en tant que tel, il doit mettre à disposition de l'utilisateur des outils pour lui faciliter la tâche. Pour ce faire, les shells proposent un certain nombre de commandes intégrées appelées "builtin" (man builtin). On trouve parmi celles-ci : cd, alias, login, bg, echo, ...

L'avantage d'une commande intégrée face à une commande externe c'est, entre autre, la rapidité d'exécution. Prenons par exemple cette boucle, qui utilise la commande "echo" intégrée à Bash :

# on execute la boucle tant que i < 50000
# la boucle incrémente i, puis fait un echo de
# la chaîne 'toto' dans /dev/null (un trou noir)

i=0; time while [ $i -lt 50000 ]; do
let i=$i+1
echo 'toto' > /dev/null
done

elle donne ce résultat sur un FreeBSD 5.4 (bash) :

real    0m4.115s  <-- temps total d'exécution
user    0m3.258s  <-- temps passé dans la sphère "utilisateur"
sys     0m0.857s  <-- temps passé dans la sphère "système" 

La même boucle utilisant la commande /bin/echo donne ce résultat :

real    1m54.043s
user    0m11.127s
sys     1m54.990s 

Le temps passé à faire les echos reste relativement faible (user), mais le temps de chargement de la commande alourdi énormément la boucle, si bien qu'au final, on passe de 4 secondes à 1m54 quand on décide d'utiliser une commande binaire à la place de l'équivalent "builtin".
Faites pareil sur un Mac OS X, et vous verrez qu'un des points faibles du système Apple se situe justement dans les étapes de chargement de commande (fork). La différence de temps est alors effrayante.

Il est donc intéressant d'utiliser les commandes intégrées quand elles sont disponibles. D'ailleurs, le shell le fait pour vous automatiquement dès l'instant où vous appelez la commande par son nom, et pas par son chemin :

echo toto	<-- builtin
/bin/echo toto	<-- commande externe 

Vous pouvez faire une pause ici, ça devient nettement plus ardu dans la suite.

Si on s'intéresse de plus prêt aux équivalents externes des builtins, on découvre qu'ils sont de plusieurs types. il y a des exécutables binaires et des scripts shells :

...
/usr/bin/cd:       Bourne shell script text executable
/usr/bin/command:  Bourne shell script text executable
/bin/echo:         Mach-O executable ppc
... 

En prenant /usr/bin/cd, ou tout autre fichier de type Bourne shell script de la liste on découvre qu'ils sont identiques et contiennent : (j'ai numéroté les lignes)

1| #!/bin/sh
2| # $FreeBSD: src/usr.bin/alias/generic.sh,v 1.1 2002/07/16 22:16:03 wollman Exp $
3| # This file is in the public domain.
4| ${0##*/} ${1+"$@"} 

la ligne 1 annonce le début d'un script shell :

 #!	<-- le shebang.
/bin/sh	<-- le shell utilisé pour exécuter le script. 

Les lignes 2 et 3 sont du commentaire, seule la ligne 4 est active :

${0##*/} ${1+"$@"}

Elle est très complexe pour le novice, prenons les choses dans l'ordre.
Le shell utilise un environnement qui contient des variables. Parmi ces dernières, Bash renseigne la variable $0 avec le nom/chemin du programme qui est exécuté. Ainsi, si j'ai un script shell "monscript.sh" comme ceci :

#!/bin/sh
echo $0 

et que je le lance comme ça :

/Users/patpro/monscript.sh

il va retourner "/Users/patpro/monscript.sh" comme résultat. car c'est le chemin de mon programme (celui qui exécute "echo $0").
Dans le shell il est parfois important de protéger les variables, ou de les modifier à la volée. Pour ce genre d'opération, on introduit des accolades : $0 devient ${0}.
Les manipulations de variables disponibles dans Bash sont classiques : substitutions, découpe de chaîne, ...
##<expression> correspond à une suppression de la plus grande chaîne correspondant à <expression> à partir du début de la chaîne complète. Cela veut dire que pour $0 valant "/Users/patpro/monscript.sh", ${0##*/} vaudra la chaîne "/Users/patpro/monscript.sh" à laquelle on enlève */.
* signifie n'importe quel caractère, et / est simplement un "/". Donc notre expression correspond à n'importe quel caractère suivi d'un /.
Si on repart de notre chaîne initiale, cela donne :

$0		-> "/Users/patpro/monscript.sh"
${0##*/}	-> "monscript.sh"

Le bloc suivant est aussi complexe. On sait maintenant que ${1} est la version protégée de $1. Cette variable correspond au premier argument passé à une commande. Si je veux sélectionner le second argument, j'utilise $2, et ainsi de suite. Pour sélectionner tous les arguments je peux utiliser $@. Je modifie monscript.sh pour ajouter cette variable :

#!/bin/sh
echo ${0##*/} ${1} 

je l'exécute en ajoutant un argument :

/Users/patpro/monscript.sh premier_argument

Il retourne :

monscript.sh premier_argument

Le signe + figurant après le 1 n'est pas un signe d'addition, c'est un opérateur qui signifie si ma variable est définie, alors utilise l'expression qui suit, sinon, utilise une valeur nulle. Dans notre cas, l'expression qui suit est "$@", c'est à dire l'ensemble des arguments passés à la commande.
Donc mon ${1+"$@"} signifie que si j'ai au moins un argument défini, alors je récupère tous les arguments, si je n'ai pas d'argument défini, alors je retourne un chaîne nulle.

Récapitulons.

Le script de départ est ${0##*/} ${1+"$@"} (sans le echo de monscript.sh). Donc quand je le lance, il lance en fait `nom_de_la_commande arguments_éventuels`.

Qu'est ce que cela implique pour notre commande externe cd ?
Quand on lance `/usr/bin/cd /tmp`, on lance en fait un script shell qui exécute `cd /tmp`. On voit que si on appelle la commande externe, cette dernière se rabat sur la commande interne.
C'est en fait le cas pour une quinzaine d'équivalent externes des builtins. Les exceptions notables sont /bin/echo, /usr/bin/false, /bin/kill, /usr/bin/login, /usr/bin/nice, /usr/bin/nohup, /usr/bin/printenv, /bin/pwd, /bin/test, /usr/bin/time, /usr/bin/true, qui sont des binaires. Il faut consulter les sources de ces derniers pour vérifier leur comportement individuel.

Vous pouvez aller boire une bière maintenant :)

Pour aller un peu plus loin, n'hésitez pas à consulter les autres articles sur bash !

Related posts

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée. Les champs obligatoires sont indiqués avec *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.