Par Éric Lesage
Note: ce document a été conçu comme matériel de cours; cependant, il est mis à la disposition de tous étant donné son utilité générale.Le rôle du shell sous UNIX est d'interpréter les commandes de l'utilisateur. Comme il y a plusieurs utilisateurs avec des goûts différents, il existe sous UNIX une variété de shells. Le premier shell populaire fut sh, le Bourne Shell, créé par S. R. Bourne. Vint ensuite le C-Shell, csh, écrit à l'Université de la Californie à Berkeley (UCB) essentiellement par Bill Joy. Il est nommé ainsi parce que sa syntaxe ressemble fortement au langage C. Un peu plus tard, tcsh fut écrit pour améliorer ses capacités.
Plus tard dans l'histoire de UNIX les vendeurs de systèmes ont décidé de s'asseoir ensemble pour créer des normes d'interopérabilité. Ces normes, appelées POSIX, définissent une façon standard pour le shell de traiter les commandes (entre autres). Le projet GNU s'est alors donné l'objectif de créer une implantation libre avec code source disponible de ce shell; il a été appelé bash, pour Bourne Again Shell (en effet, le Shell POSIX est très similaire au sh original).
Nous allons donc utiliser le shell bash pour programmer.
Mais avant tout, pourquoi apprendre le langage du shell? Il y a trois raisons principales:
Note: on appelle souvent les programmes écrits en shell des scripts. Cela est dû au fait que les programmes ne sont jamais compilés: ils sont toujours interprétes directement de leur code source.
Puisque normalement sur les machines des laboratoires d'enseignement le shell par défaut est tcsh, il faut pour utiliser bash exécuter /bin/bash. De plus, la première ligne de tous vos programmes bash doit être:
#!/bin/bash
Deux sortes de commandes existent dans tout shell: les commandes internes et les commandes externes; les commandes externes entraînent l'exécution d'un programme distinct. Par exemple, pine, date, nfrm et finger sont des commandes externes. Les commandes internes appellent simplement des routines prédéfinies à l'intérieur du shell. Par exemple, cd est une commande interne.
Quelques commandes internes utiles
Pipelines et redirection
Une des forces du bash est de pouvoir contrôler précisément d'où viennent et où vont les entrées et sorties d'un programme.
Il y a trois fichier standards asssociés à un programme:
Un programme normalement prend son entrée du clavier, et écrit ses résultats à l'&ecute;cran. Si le programme rencontre des erreurs, il les écrit ègalement à l'écran.
Or, il y a moyen de changer ce comportement. Par exemple, si je veux écrire la liste des fichiers du répertoire /u/dift2880 dans un fichier appelé "liste", le shell le permet en écrivant:
ls /u/dift2880 > liste
Attention, si on a déjà un fichier "liste", cette commande risque (selon la configuration de votre shell) de tout simplement écrire par dessus. Si on veut ajouter au contenu existant de liste, (c.-à-d. écrire à la fin du fichier), on écrira plutôt:
ls /u/dift2880 >> liste
On peut aussi envoyer la sortie d'une commande à un autre programme. Par exemple, la commande ypcat passwd (Yellow Pages Concatenate Password) écrit la liste des utilisateurs du systàme (plusieurs centaines de lignes contenant chacune l'information sur un compte). Sur une même ligne est écrit le login (dift2880), le numéro d'utilisateur (3386), le nom au complet ("Demo IFT2880"), et quelques autres informations. De plus, nous avons vu la semaine dernière la commande grep qui permet de filtrer les lignes contenant quelque chose en particulier. Ainsi, on peut facilement trouver dans toute la base l'entrée qui correspond au compte dift2880 en redigigeant la sortie de ypcat vers le programme grep. Pour se faire, la syntaxe à utiliser est:
ypcat passwd | grep dift2880
Cela est équivalent à:
ypcat passwd > tempfile grep dift2880 < tempfile rm tempfile
Pour rediriger à la fois la sortie et l'erreur standard, on utilise l'opérateur >&. Pour rediriger à la fois la sortie et l'erreur standard vers un fichier x, on utilise la combinaison >> x 2>&1 (>> veut dire rediriger la sortie standard à la fin de x; la sortie standard est le fichier numéroté 1; 2> signifie de rediriger le fichier numéroté 2; &1 signifie de rediriger au même endroit que le fichier numéroté 1, soit la sortie standard, donc vers x).
Que faire si l'on désire ignorer la sortie d'une commande? Il suffit de l'envoyer sur le fichier spécial /dev/null:
rm blabla >& /dev/null
La commande précédente va effacer le fichier blabla s'il existe, mais ne produira pas de message d'erreur si le fichier blabla n'existe pas.
Comme tout langage de programmation, bash permet aux programmeurs de stocker de l'information dans des variables. Les variables en bash ne sont pas (par défaut) typées. Ainsi, sauf ordre contraire, bash considère tout comme du texte. Également, toutes les variables sont globales, sauf encore une fois si on fait une mention du contraire. Finalement, bash n'oblige pas le programmeur à déclarer ses variables.
Pour mettre quelque chose dans une variable:
nomvariable=CONTENU
Pour utiliser le contenu d'une variable:
${nomvariable}
Par exemple:
argument="-l" ls ${argument}
Les variables sont stockées dans une zone mémoire propre au shell. Cependant, tous les programmes (pas juste les shells) possèdent une zone mémoire pour stocker des variables. Cette zone, propre à chaque processus, est appelée environnement. Le shell possède donc deux zones, sa zone privée de variables et son environnement. Certains programmes sont affectés par les valeurs dans leur environnement. Par exemple, gcc est affecté par la variable TMPDIR, qui à gcc quel répertoire utiliser pour stocker ses fichiers temporaires. Ainsi, pour faire en sorte que gcc stocke ses fichiers dans un sous-répertoire appelé "tmp" dans votre compte, il faut exécuter:
TMPDIR=$HOME/tmp # assigner une valeur à TMPDIR export TMPDIR # On veut que bash transmette TMPDIR
Quelques remarques sur le fragment précédent:
Quelques variables standard maintenues automatiquement:
Par convention, les variables destinées à l'environnement sont en majuscules et les autres en minuscules.
Pour obtenir la longueur d'une chaîne stockée dans une variable, on écrit ${#VAR}.
Chaque commande retourne une valeur qui détermine si le programme a bien fonctionné ou non. C'est la raison du return 0; dans un programme C: 0 indique que tout s'est bien passé. Une commande qui retourne autre chose que 0 a rencontré un problème. La valeur de retour de la dernière commande exécutée se trouve dans la variable ?. Par exemple, pour savoir s'il y a eu un problème lorsqu'on tente d'effacer un fichier:
rm fichier echo $?
Or, si un programme n'a pas bien fonctionné, il peut y avoir une multitude de raisons. Chaque programme encode donc sa valeur de retour selon la raison de l'échec. Ainsi, 1 pourrait dire "pas assez de place sur le disque", 2 "fichier introuvable", etc.
Ainsi, logiquement, on va attribuer à 0 la valeur vrai et à toute autre nombre la valeur faux. Ceci est le contraire d'un programme C!
Pour spécifier une valeur de retour à une fonction, utiliser la commande return. Pour terminer immédiatement un script avec une valeur de retour, utiliser la commande interne exit.
On peut composer les commandes en listes. Les opérateurs de composition sont:
Exemple:
rm blabla >& /dev/null || echo rm a échoué, erreur $?
Cette commande est importante parce qu'elle va nous permettre de trouver de faire des tests de comparaison utiles:
test string1 = string2 # retourne vrai si les chaînes a et b sonti # identiques test string1 != string2 # vrai si différentes test -e fichier # vrai si fichier existe (fichier est un fichier normal, # un répertoire, un lien ou autre chose test -f fichier # vrai si le fichier existe et est un fichier normal test -r fichier # vrai si le fichier est lisible test -x fichier # vrai si le fichier est un programme exécutable test $nb1 OP $nb2 # vrai si le nombre 1 OP le nombre 2, où OP est: # -eq (equal), -ne (not equal), -lt (less than), # -le (less than or equal), -gt (greater than), ou # -ge (greater than or equal) test ! expr # vrai si expr est fausse test -n string # vrai si string est de longueur # supérieure à 0 test -z string # vrai si string est de longueur 0 (ex.: test -z "")
En général on n'écrit cependant jamais "test expr". On utilise plutô la forme condensée [ expr ].
Jusqu'à maintenant, nous n'avons vu que les commandes simples. Or le shell dispose de nombreuses commandes complexes qui lui donnent toute sa flexibilité.
Pour évaluer arithméthiquement une expression, la mettre entre parenthèses doubles.
(($a > $b)) # Retourne vrai si a plus grand que b, faux sinon sum=$(($a + $b)) # En autant que a et b contiennent des nombres
Les mêmes opérateurs sont disponibles qu'en C, avec les mêmes règles de précédence. Il y a aussi l'opérateur d'exponentiation, représenté par **.
On peut définir des fonctions en bash; cela est très utile pour structurer ses programmes et fait tr&eagrave;s plaisir à votre démonstrateur:
nom_de_fonction() { corps }
Pour appeler la fonction, simplement:
nom_de_fonction param1 param2 param3
À l'intérieur du corps de la fonction, les paramètres sont disponibles dans les variables $1, $2, $3, etc. Le nombre de paramètres est dans la variable $#.
Ne pas oublier qu'une variable utilisée à l'intérieur d'une fonction est globale! Pour éviter ce phénomène, il faut dire au shell si une variable doit être locale. Pour ce faire, utiliser la commande interne:
local nomvar
Pour sortir d'une fonction, utiliser la commande interne return; la fonction va sortir avec la valeur de retour de la dernière commande exécutée. Si ce n'est pas ce qui est désiré, utiliser return N.
La syntaxe du test de condition est
if list then list else list fi ou encore, en condensé: if list; then list; else list; fi
Quelques exemples:
if (($a > $b)); then echo $a est plus grand que $b else echo $a est plus petit ou egal a $b fi if [ -f $file ]; then echo le fichier $file existe, voici la preuve: ls -l $file elif [ -d $file]; then echo le fichier $file existe, mais est un repertoire: ls -l -d $file else echo $f n'existe pas ou n'est pas un fichier fi if rm blabla then echo le fichier blabla a ete efface else echo le fichier blabla n'a pas ete efface fi
La syntaxe est:
for name in word; do list; done
Exemples:
for x in allo bonjour hello sayonara; do echo $x done for file in *; do if [ -f $file ]; then echo $file est un fichier else echo $file n'est pas un fichier fi done
La syntaxe d'une boucle est:
while list; do list; done ou l'inverse, until list; do list; done
Exemples:
while [ $a -gt 0 ]; do echo -n "$a " a=$(($a - 1)) done echo -e "\nBoom"\! until [ $a -eq 0 ]; do echo -n "$a " a=$(($a - 1)) done echo -e "\nBoom"\!
La syntaxe est:
case word in [ pattern [ | pattern ] ... ) list;; ] ... esac
Exemple:
case $x in allo) echo francais ;; hello|hi) echo anglais ;; *) echo inconnu ;; esac
Nous avons déja vu quelques types d'expansions. Par exemple, le shell substitue aux références de variable leurs contenu: $x est remplacé par le contenu de x. Ce n'est pas la seule expansion. Voici les principales:
Nom | Avant | Après |
Substitution d'accolades | h{ello, i} | hello hi |
Substitution de tilde | ~ | votre home |
Substitution de tilde (2) | ~dift2880 | home de dift2880 (/u/dift2880) |
Substitution de paramètre | ${x} | contenu de x |
Substitution de commande | `echo joe` | première ligne de la commande echo joe; dans ce cas-ci, joe |
Substitution arithmétique | $((2 + 2)) | 4 |
Le shell peut aussi consulter les fichiers pour faire des substitutions, en utilisant pour cela une technique de "pattern-matching". Voici quelques exemples; supposons que le répertoire courant contient les fichiers hello, hi, allo, sayonara, bonjourno, bonjour
echo * donne: allo bonjour bonjourno hello hi sayonara --> notez l'ordre lexicographique echo ?? donne: hi echo h* donne: hello hi echo [ab]* donne: allo bonjour bonjourno
Autrement dit, * remplace 0 ou plus caractères, ? remplace exactement un caractère, et [liste] remplace exactement un des caractères dans la liste. Le shell retourne tous les fichiers qui correspondent à votre pattern.
Il est possible de demander au shell d'utiliser les fichiers d'un autre répertoire, par exemple: /some/dir/* liste /some/dir/file1, /some/dir/file2, ...
On peut demander de vérifier si une chaîne correspond à un pattern en particulier avec [[ chaîne == pattern ]]. Ainsi, [[ boule == bo* ]] retourne vrai. La valeur contraire peut ètre obtenue par [[ chaîne != pattern ]].
Nous avons abordé bash du point de vue du programmeur. Bash comporte une foule d'autres options et aussi de capacités pour l'utilisateur: l'historique des commandes récentes, la capacité de modifier la ligne de commande, la complétion automatique (essayer de rentrer le nom partiel d'une commande, puis appuyez deux fois sur tab), etc. Nous vous encourageons à explorer votre shell. Après tout, vous allez l'utiliser tous les jours...
La référence la plus complète qui soit est la page man du shell: man bash2. De l'aide sur chacune des constructions ou commandes internes est disponible par help [commande] à l'intérieur de bash
Une introduction au bash se trouve à http://www.tldp.org/HOWTO/Bash-Prog-Intro-HOWTO.html.
Un guide plus avancé se trouve à http://www.tldp.org/LDP/abs/html/index.html.
Par Éric Lesage, 2000-01-26, cours IFT 2880
Révision par Éric Lesage, 2000-02-17
Révision par Éric Lesage, 2001-09-20
Révision par Éric Lesage, 2004-09-01
Copyright © Éric Lesage 2001, 2004, all rights reserved.