Les langages de Shell, dont Bash n'est que la version GNU, sont de vénérables ancêtres aussi vieux que la ligne de commande elle-même. Le Korn Shell ou ksh par exemple a prés de 30 ans.
Suivant la croyance que tout ce qui est nouveau est mieux, Bash, et ses confrère, est souvent relégué au rang d'antiquité bordélique et peu puissante. Mais lorsqu'il est mieux connu, BASH est en réalité assez cohérent et permet de scripter à une vitesse étonnantes des choses très évoluées pour le nombre de lignes de code produites. Ce n'est pas un hasard si le coeur du démarrage de Linux n'est à peu prés écrit qu'en Bash.
Le but de ce tutoriel n'est pas d'expliquer comment fonctionne BASH. Il y a d'excellent tutoriels qui font cela très bien. Mon idée est plus de documenter dans la langue de Molière certains aspects plus pointus de ce langage et en l'occurrence l'utilisation des blocs de codes, des fonctions et des variables.
Tout le monde connaît les fonctions en BASH :
Une fonction fonctionne à peu prés comme une commande de base ou un sous-script. On peut lui passer des paramètres ($1, $2...), elle a une valeur de retour avec le mot clef return à la place du exit. On peut même utiliser avec elle les redirections, pipes compris comme nous le voyons dans l'exemple. En somme la seule différence entre une fonction et un script ou une commande externe, c'est qu'il n'y a pas création du nouveau processus.
Maintenant ce que l'on sait moins c'est qu'une fonction en Bash est en réalité un cas particulier d'un concept plus large, celui de bloc de code. Il est ainsi possible de créer des fonctions qui n'ont pas de nom au sein d'un script. Ainsi le code précédent peut s'écrire de la manière suivante :
Autre exemple pratique, d'utilisation d'un bloc pour conditionner l'exécution d'une série de commande au bon déroulement d'une première commande :
En somme un bloc est une fonction anonyme, qui s'exécute tout de suite, qui ne peut avoir de passage paramètre ($1, $2...) et ne peut renvoyer de code de retour (return). Tout le reste est possible. Maintenant comme une fonction est un bloc, la logique nous souffle que s'il est possible de mettre un bloc dans une fonction, il doit aussi être possible de placer une fonction dans une autre fonction :
Et effectivement cela fonctionne très bien, de même que mettre une fonction dans un bloc anonyme... Maintenant on peut se demander quelle sont les règles de visibilités sur les fonctions. Deux fonctions dans deux bloc distinct peuvent t-elles s'appeler ? La réponse à cette question passe par une petite manipulation :
Si on exécute ce script cela nous donne
Ainsi une fonction n'est rien d'autre qu'une variable. Du coup, la réponse à notre question réside dans les régles de visibilités qui leur sont associées.
Pour terminer sur les blocs de code, sachez que TOUT n'est que block de code. Et une commande seule est un bloc dont les accolades ont été rendues optionnelles. Ainsi les deux syntaxe suivantes sont équivalentes :
En Bash, les variables d'environnement font parti de l'espace de travail du processus. Par défaut, les variables d'un processus ne sont pas visible par les processus engendrés. Pour que cela puisse ce faire, il faut marquer les variables avec la commande export. En revanche, un processus fils ne peut jamais modifier les variables du processus parent. Ainsi si l'on a un premier script :
Et un second qui définit deux variables, dont une est marquée par export :
A l'exécution du second script nous voyons bien que V1 n'est pas renseigné mais que V2 l'est. En revanche, nous constatons bien que le sous-script n'a pas réussi à torpiller la valeur de la variable parent. Les données peuvent donc "descendre" mais pas "remonter".
Maintenant qu'en est-il pour une fonction ? Et bien comme c'est une variable, les mêmes règles s'appliquent : elle ne sera pas visible par les processus fils. Pour qu'elle le soit, il faut l'exporter. Si si, avec un paramètre magique en plus ça fonctionne très bien : export -f ma_fonction.
Maintenant que nous voyons bien les régles entre le processus courant et ceux engendrés, comment cela se passe-t-il pour les blocs et les fonctions ? Pour tester cela, nous pouvons modifier un peu notre ./script2.sh en supprimant la ligne export V2 et en lançant le script de manière un peu différente, par un source ./script1.sh. Et là... nos deux variables sont renseignées sans qu'aucune ne soit exportée. La "magie" réside dans le mot clef source. Lorsque l'on "source" un script, bash le charge... dans un bloc. On en déduit que les règles de visibilité changent un peu lorsque l'on reste dans le même processus. A noter avant d'aller plus loin que la syntaxe source script est équivalente à la plus connue . ./script (attention au premier point, suivi d'un espace).
La régle dans les blocs d'un même processus est donc que l'inverse de celle qui nous obligeait à utiliser export : les variables (et donc les fonctions) créées dans un bloc sont visibles partout ailleurs. Pour changer ce comportement, vous pouvez marquer, uniquement dans un bloc "fonction", les variables comme local. Attention cependant, ce changement de visibilité interdit juste la variable d'être vu des blocs parents, elle ne la cache pas des blocs enfants.
Pour terminer sur la visibilité, il nous reste une possibilité à explorer. Celle de rendre une variable accessible pour un bloc seulement. Tout le monde connais la syntaxe suivante :
L'idée sous-jacente est qu'une copie des variables est faite à l'arrivée sur cette ligne et que la variable variable y est insérée. Cette copie est ensuite passée à la commande. Ainsi la variable en question n'est visible QUE de cette commande. Et bien sur la même chose est possible avec un bloc de code, mais uniquement de type "fonction" :
Maintenant que nous avons une bonne compréhension des variables, voyons comment aller plus loin en y accédant par adressage indirecte. L'idée est de disposer d'une variable qui contient le nom d'une autre variable et de pouvoir lire et changer la valeur de cette seconde variable. Le tout est de connaître la syntaxe :
Maintenant voyons comment écrire de manière indirecte en utilisant la fonction bien pratique eval
Alors maintenant nous savons adresser des variables de manière indirecte. Vu que les fonctions sont elles aussi des variables, il doit y avoir moyen de faire avec elles la même chose. Commençons par écrire une fonction qui vérifie que la fonction dont le nom sera passé en paramètre existe. Pour cela nous utilisons la fonction type. Cette dernière utilisée avec le paramètre -t suivi d'un nom de variable, nous renvoie une chaîne indiquant le type de cette variable : alias, keyword, builtin, file ou function. C'est le dernier cas qui nous intéresse :
Ce qui nous donne à l'exécution
Grâce à cela nous pouvons savoir si une fonction existe et même récupérer la liste des fonctions existantes en parcourant la liste des variables (genre renvoyée par set). Mais cela ne servirait pas à grand chose si l'on ne pouvait pas exécuter de manière indirecte une fonction :
Alors quel est l'intérêt de cette méthode ? Simplement de pouvoir construire des "hooks". Des hooks sont des fonctions qui son exécutées pour modifier un comportement si elles existent. Nous pouvons donc faire des petits plugins en bash très facilement. Pour cela écrivons le code suivant :
Maintenant, il suffit de créer un sous dossier dossier_plugins et d'y mettre par exemple
Encore un autre pour la température cette fois :
Voilà, maintenant nous pouvons lancer notre script
En quelques lignes de bash, nous avons un "nagios du pauvre" qui s'étend de manière très simple en ajoutant des plugins dans le dossier ./dossier_plugins.
Il ne s'agit pas de construire d'énormes applications en bash plus que de l'utiliser pour ce qu'il est, à savoir le plus efficace moyen de commander un système unix. Un programme écrit en python, perl ou ruby sera sans nul doute plus robuste ou offrira plus de fonctionnalités, mais la compacité de la ligne de commande permet à bash de battre des records de vitesse pour des tâches simples et répétitives. De plus son côté "standard" fait que n'importe quelle distribution sera capable de manger du bash, ce qui est un gros plus de portabilité.
- répondre
advaya, le 11 September, 2008 - 16:32hummm.... désolé mais j'ai un problème avec bash, et je ne trouve pas la solution. Je l'écris ici, même si c'est pas forcément le lieu adapté, en espérant que quelqu'un saura me répondre.
C'est au sujet des wildcards du type ensemble : [a-z] ne devrait me sortir que les lettre minuscules en théorie ; or quand je fais un
essai.tex Essai.tex
Et ce n'est pas a priori normal qu'il me liste le second fichier, n'est-ce pas ? Plus fort encore :
essai.tex
essai.tex Essai.tex
et donc tant que l'intervalle ne dépasse pas 'e', il ne me sort bien que le premier fichier, tout en minuscules. Mais si l'intervalle dépasse, il me sort les deux (avec minuscule et majuscule). Autre tentative :
ls: ne peut accéder [A-D]ssai.tex: Aucun fichier ou répertoire de ce type
essai.tex Essai.tex
essai.tex Essai.tex
La, en majuscule, dès que l'ensemble contient 'E', il me sort systématiquement les deux.
La logique voudrait que j'en déduise que l'ordre alphabétique dans la construction des ensemble est en fait : a,A,b,B,c,C etc ..., ce qui expliquerait ce comportement. Mais c'est contraire à ce que je croyais jusqu'ici sur bash.
J'ai regardé s'il n'y avait pas d'options spécifiques mais rien trouvé ... j'avoue ne pas comprendre ... any ideas ? (et est-ce que je suis le seul dans ce cas ?)
EDIT : je viens de tenter ça :essai.tex
ce qui aurait tendance à confirmer ce que je suppose sur l'ordre alphabétique et les wildcards ... y a-t-il moyen de changer ça ?
- répondre
advaya, le 12 September, 2008 - 13:01Je me réponds à moi-même, puisqu'on m'a fait comprendre d'où venait le problème : il s'agit en fait d'un problème de locale. Si on veut utiliser les ensembles de type [a-z] [A-Z] avec uyne distinction minuscule/majuscule en bash, il suffit par exemple d'utiliser la locale par défaut (C):
Et là ça fonctionne
- répondre
advaya, le 23 September, 2008 - 18:02Désolé, encore un autre hors-sujet, enfin presque ...
Merci, ça répond à une question que je me suis posé un moment (qu'est-ce qui différencie un script d'une fonction ?). Du reste, je suis tombé par hasard (faute de frappe dans un script
) sur la variable $$, qui permet de mettre cela en évidence puisqu'elle renvoit le PID du processus d'où elle est utilisée.
Par exemple, si dans bash on tappe
6653
ps aux | grep bash | grep -v grep | awk '{ print $2 }'
6653
Cela renvoit bien le PID de bash lui-même. Maintenant, si on fait une fonction pid_func comme suit :
et un script pid_script totalement identique on aura :
PID : 6653 # puisque la fonction est dans bash, le pid reste le même
source pid_script
PID : 6653 # même PID puisque le script est "sourcé" dans bash
./pid_script
PID : 12654 # PID différent, puisque le script exécuté engendre un nouveau processus
Plus ça va, plus je trouve bash plaisant
- répondre
Emmanel , le 15 November, 2008 - 04:10En lieu et place de
ps aux | grep bash | grep -v grep | awk '{ print $2 }'on peut utiliser plus court pour le même résultat :
ps aux|awk '{if ($11~/bash/) print $2}'Poster un nouveau commentaire