Programmation en bash

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.

Invocation

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

Commandes

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.

Variables (aussi appelées paramètres)

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}.

Valeurs de vérité

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.

Listes (enchaînement de commandes)

On peut composer les commandes en listes. Les opérateurs de composition sont:

Exemple:

rm blabla >& /dev/null || echo rm a échoué, erreur $?

La commande test

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 ].

Commandes complexes

Jusqu'à maintenant, nous n'avons vu que les commandes simples. Or le shell dispose de nombreuses commandes complexes qui lui donnent toute sa flexibilité.

Évaluation arithmétique

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 **.

Fonctions

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.

Test de condition

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

Itération

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

Boucle

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"\!

Sélection de cas

La syntaxe est:

case word in [ pattern [ | pattern ] ... ) list;; ] ... esac

Exemple:

case $x in

  allo) echo francais
  ;;

  hello|hi) echo anglais
  ;;

  *) echo inconnu
  ;;

esac

Expansions

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:

NomAvantAprè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

Expansion de chemins

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, ...

Pattern matching général

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 ]].

Pour finir

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...

Références

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.