Le shell

Présentation

Le mot shell signifie « coquille » en anglais. Il s'agit du programme que vous utilisez régulièrement à l'ENS, et qui interprète les commandes. Par exemple, vous y tapez pine ou mutt, forum, cc, mozilla, etc.

Mais quel rapport avec une coquille ? Eh bien, dans une coquille vous pouvez mettre de l'eau, pour la porter ensuite à votre bouche ; vous pouvez y mettre du sable avant de le verser dans des bocaux ; en somme, une coquille est un récipient qui permet de manipuler toutes sortes de contenus. Il en va de même du shell. C'est un outil en mode texte qui permet l'exploitation d'un grand nombre de ressources de l'ordinateur.

Cette page vous donnera les rudiments pour exploiter les deux principales fonctionnalités du shell :

Les deux états du shell

Le shell, comme le normalien, ne connaît que deux états :

Le shell, une fois lancé, est inactif : il attend qu'on lui donne des ordres. Quand on lui en donne un, il l'exécute ; et quand il a terminé, il retourne à son état d'inactivité, en attente d'un nouveau commandement.

Quand le shell est inactif, il affiche une invite (prompt en anglais), qui ressemble à cela :

chaland ~ $

Un curseur, parfois clignotant, indique que le shell attend que vous lui tapiez des instructions.

Comment le shell trouve-t-il les commandes ?

J'ai l'habitude de taper des commandes dans le shell, et je vois qu'il réagit. Mais comment comprend-il ce que je veux faire ?

Prenons un cas simple. Je tape la commande bonjour à l'invite (prompt) du shell. Il va chercher à plusieurs endroits ce que j'entends par là :

  1. d'abord, il va se demander si bonjour n'est pas une de ses commandes intégrées ; si c'est le cas, il l'exécute directement, sinon il passe à l'étape suivante ;
  2. ensuite, il va lire le contenu d'une variable, qui s'appelle PATH, et qui indique le « chemin » où trouver les commandes que l'on appelle. Par exemple, si la variable PATH contient les répertoires : alors le shell va chercher successivement les commandes :
  3. enfin, s'il ne trouve la commande dans aucun des répertoires référencés par le PATH, il va renvoyer un message d'erreur en disant que désolé, il ne voit pas ce que l'on entend par bonjour. Exemple :
    chaland ~ $ bonjour
    bonjour: Command not found

La variable PATH consiste en une liste de répertoires séparés par des « : ». Si vous voulez voir à quoi ressemble votre PATH, tapez :

chaland ~ $ echo $PATH

Commandes internes

Certaines commandes du shell ne sont pas des programmes mais des commandes internes (builtins functions). Comme nous l'avons vu, elles sont directement reconnues et exécutées par le shell. Un exemple de commande interne est cd ; elle modifie le répertoire courant du shell.

Attention : si vous créez un script (c'est-à-dire un programme écrit en langage shell) qui utilise cd, il ne modifie pas le répertoire courant du shell qui lance ce script, mais celui d'un shell qui est créé à l'occasion de l'exécution de ce script, et qui meurt à la fin de cette exécution.

Exemple : je crée un script aller qui contient les lignes suivantes :

#! /bin/sh
cd $*

Nous aurons alors :

chaland ~ $ aller toto
chaland ~ $ cd toto
chaland ~/toto $

Quels programmes utilisent le langage du shell ?

La répétition de commandes complexes en ligne de commande du shell est rapidement fastidieuse ; aussi est-il très pratique de connaître les bases de la programmation de scripts shell. Les scripts servent à automatiser ou systématiser des tâches.

Il existe un script spécial, qui est exécuté au moment où on se connecte. Ce script est contenu dans le fichier $HOME/.profile. C'est ce fichier qui vous dit s'il y a de nouveaux messages dans forum, si vous avez du courrier, etc.

Ce fichier est normalement mis à jour automatiquement par les scripts de la config conscrits. Il est néanmoins possible de le modifier pour changer des options.

Il existe encore le script .xinitrc, qui lance X ; X est le gestionnaire de fenêtres classique sous Unix.

Le nombre de scripts possibles est illimité ; vous pouvez en créer autant que vous voulez, selon vos besoins : c'est ainsi que l'on personnalise son système et qu'on l'adapte à ses exigences, plutôt que l'inverse.

La ligne de commande

Raccourcis pour les noms de fichiers : les jokers

Il est parfois ennuyeux d'avoir à taper un nom complet de fichier comme nabuchodonosor. Il est encore plus ennuyeux d'avoir à taper une liste de fichier pour les donner en arguments à une commande, comme :

cc -o foo bar.c gee.c buz.c gog.c

Pour éviter ces problèmes, on peut utiliser des jokers (wildcards en anglais).

L'étoile : *

Une étoile * dans un nom de fichier est interprétée par le shell comme « n'importe quelle séquence de caractères » (mais ça ignore les fichiers dont le nom commence par un point). Exemple :

cc -o foo *.c

Pour interpréter l'étoile, le shell va faire la liste de tous les noms de fichiers du répertoire courant qui ne commencent pas par . et qui finissent par .c. Ensuite, il remplace *.c par cette liste (triée par ordre alphabétique) dans la ligne de commande, et exécute le résultat, c'est-à-dire par exemple :

cc -o foo bar.c buz.c foo.c gee.c gog.c

Le point d'interrogation : ?

On a aussi le point d'interrogation ?, qui remplace un (et exactement un) caractère quelconque (sauf un point en début de nom). Par exemple, ls *.? liste tous les dont l'extension ne comporte qu'un caractère (.c, .h...).

Les crochets : []

La forme [abcd] remplace un caractère quelconque parmi a, b, c, d. Enfin, [^abcd] remplace un caractère quelconque qui ne se trouve pas parmi a, b, c, d.

Exemple

echo /users/*

affiche à peu près la même chose que

ls /users

(La commande echo se contente d'afficher ses arguments.)

&icone.attention; Attention :

Interlude: comment effacer un fichier nommé ?* ? On ne peut pas taper rm ?* car le shell remplace ?* par la liste de tous les fichiers du répertoire courant. On peut taper rm -i * qui supprime tous les fichiers, mais en demandant confirmation à chaque fichier. On répond n à toutes les questions sauf rm: remove  ?*. Autre méthode: utiliser les mécanismes de quotation.

Quotation

Avec tous ces caractères spéciaux, comment faire pour passer des arguments bizarres à une commande ? Par exemple, comment faire afficher un point d'interrogation suivi d'une étoile et d'un dollar par echo ? Le shell fournit des mécanismes pour ce faire. Ce sont les quotations.

Le backslash (\)

Il suffit de précéder un caractère spécial d'un backslash, et le shell remplace ces deux caractères par le caractère spécial seul. Évidemment, le backslash est lui-même un caractère spécial.

Exemples :

chaland ~ $ echo \?\*\$
?*$
chaland ~ $ echo \\\?\\\*\\\$
\?\*\$

Les apostrophes ou simples quotes (')

Un autre moyen est d'inclure une chaîne de caractères entre apostrophes (simples quotes) '. Tout ce qui se trouve entre deux apostrophes sera passé tel quel par le shell à la commande. Exemple :

chaland ~ $ echo '$?*'
$?*

Les guillemets doubles ou doubles quotes (")

Les guillemets se comportent comme les apostrophes, à une exception près: les dollars et les backslashes sont interprétés entre les guillemets. Exemple :

chaland ~ $ echo "$HOME/*"
/users/87/maths/doligez/*

Une technique utile: Quand on juxtapose deux chaînes de caractères quotées, le shell les concatène, et elles ne forment qu'un argument. Exemple :

chaland ~ $ echo "'"'"'
'"

Quant aux interactions plus compliquées (backslashes à l'intérieur des guillemets, guillemets à l'intérieur des apostrophes, etc.), le meilleur moyen de savoir si ça donne bien le résultat attendu est d'essayer. La commande echo est bien utile dans ce cas.

Les backquotes (`)

Dernière forme de quotation: `commande`. Le shell exécute la commande indiquée entre backquotes, lit la sortie de la commande mot par mot, et remplace ` commande ` par la liste de ces mots. Exemple :

chaland ~ $ echo `ls`
Mail News bin foo lib misc mur notes.aux notes.dvi notes.log
notes.tex planar text
chaland ~ $ ls -l `which emacs`
-rwxr-xr-t  1 root   taff   978944 Jul 16  1991 /usr/local/bin/emacs

La commande which cmd employée ci-dessus affiche sur sa sortie le nom absolu du fichier exécuté par le shell quand on lance la commande it cmd :

chaland ~ $ which emacs
/usr/local/bin/emacs

Entrée, sortie et redirection

Entrée et sortie

Un programme consiste à traiter des données, et à renvoyer des données transformées : il transforme des informations, et c'est pourquoi l'on parle d'informatique.

Prenons le programme hachoir, par exemple : on y fait entrer des choses, elles sortent sous une autre forme, et dans l'intervalle, elles subissent des transformations régulières. Par exemple, on fait entrer une vache, il en ressort du steak haché ; on y fait entrer des carottes, il en ressort des carottes rapées.

Deux concepts permettent de modéliser cette transformation d'informations : les concepts d'entrée et de sortie. L'entrée, c'est la vache ou les carottes ; la sortie, c'est le steak haché ou les carottes rapées.

Sortie standard et sortie d'erreur

Mais cette première distinction entre entrée et sortie ne suffit pas, car la sortie d'un programme, c'est-à-dire les messages qu'il renvoie, sont de deux ordres : il y a les messages normaux relevant de la transformation d'informations (par exemple le steak haché ou les carottes rapées), mais il y a aussi des messages d'erreur.

Par exemple, si vous mettez, sur le tapis roulant qui mène au hachoir, un objet trop gros pour y rentrer, le hachoir, s'il est bien conçu, devra vous prévenir qu'il n'arrive pas à hacher un objet si gros. Et ce message, quoiqu'il sorte aussi bien du hachoir que le steak haché, ne doit pas être traité de la même façon à sa sortie, et n'est pas suivi des mêmes conséquences. C'est pourquoi l'on distingue la sortie standard et la sortie d'erreur.

Pour résumer, chaque commande a donc :

Comportement par défaut

Par défaut, l'entrée standard est le clavier, la sortie standard est l'écran, et la sortie d'erreur est aussi l'écran.

C'est sur le clavier que vous tapez ; ce que vous tapez et ce que renvoient les programmes s'inscrit à l'écran ; les messages d'erreur renvoyés par les programmes s'affichent à l'écran.

Les redirections

Mais il ne s'agit là que du comportement par défaut, et pas d'un comportement obligatoire. Vous pouvez tout à fait orienter différemment vos programmes.

Par exemple, si vous donnez une vache comme entrée au hachoir, vous pouvez orienter la sortie vers une imprimante (au lieu de l'écran, proposé par défaut), et vous imprimerez ainsi du steak haché.

Ou encore, vous pouvez donner un plant de carottes comme entrée au programme cueillette, et envoyer la sortie (c'est-à-dire les carottes cueillies) au programme hachoir.

Rediriger la sortie dans un fichier : >

On peut rediriger la sortie standard d'une commande vers un fichier (caractère « > »). Le résultat de la commande sera placé dans le fichier au lieu de s'afficher sur l'écran. Exemple :

chaland ~ $ ls -l > foo

Le résultat de ls -l ne s'affiche pas à l'écran, mais il est placé dans le fichier foo. On peut alors taper

chaland ~ $ less foo

(ou more foo) pour lire le fichier page par page.

Ajouter la sortie à un fichier : >>

On veut parfois ajouter la sortie d'un programme à un fichier, sans effacer ce qui précède. Or, par défaut, si l'on tape plusieurs fois

chaland ~ $ ls -l > foo

à chaque fois, le contenu antérieur du fichier foo est écrasé par le contenu ultérieur.

Pour éviter cela, il existe l'outil de redirection >>. Ainsi, si vous tapez plusieurs fois

chaland ~ $ ls -l >> foo

le fichier foo contiendra à la suite tout ce que vous a renvoyé la commande.

Rediriger l'entrée : <

On peut aussi rediriger l'entrée standard d'une commande (caractère « < »). La commande lira alors le fichier au lieu du clavier. Exemple :

chaland ~ $ elm leroy < foo

envoie par mail à Xavier Leroy le résultat de la commande ls -l de tout à l'heure.

On peut aussi taper more < foo qui est équivalent à more foo car more sans argument lit son entrée standard et l'affiche page par page sur le terminal.

Connecter la sortie d'une commande sur l'entrée d'une autre : |

Il devient rapidement ennuyeux de taper :

chaland ~ $ ls -l > foo
chaland ~ $ less < foo
chaland ~ $ rm foo

On peut se passer du fichier intermédiaire (foo dans notre exemple) grâce à un pipe (caractère « | »). Un pipe connecte directement la sortie standard d'une commande sur l'entrée standard d'une autre commande. Exemple : pour afficher page par page la liste des fichiers du répertoire courant, faire

chaland ~ $ ls -l | less

Le pipe, ou tube, est d'un usage très courant, et rend beaucoup de services.

Succès et erreur

On a parfois besoin de savoir si une commande a réussi ou non avant d'en lancer une autre. Les indicateurs && et || permettent, respectivement, de lancer une commande si (et seulement si) la précédente a réussi ou échoué.

Par exemple, si j'ai un fichier foo, j'obtiens :

chaland ~ $ ls foo && echo "J'ai un fichier foo."
foo
J'ai un fichier foo.

Si je n'ai pas de fichier foo, le message ne s'affiche pas. En revanche, si je tape alors :

chaland ~ $ ls foo || echo "Je n'ai pas de fichier foo."
ls: foo: No such file or directory
Je n'ai pas de fichier foo.

Récapitulatif

La panoplie complète des redirections est la suivante (le tableau indique les redirections qui diffèrent selon les shells) :

Redirection des sorties standard et d'erreur

Il est parfois utile de rediriger la sortie standard et la sortie d'erreur vers un même endroit. Pour cela, on utilise le motif 2>&1 avant la redirection.

Fonction Bourne shell (sh, bash) Z-shell (zsh)
Redirige la sortie d'erreur (2) et la sortie standard (1) sur l'entrée de la commande suivante 2>&1 | |&
Redirige la sortie d'erreur et la sortie standard dans un fichier 2>&1 > >&
Redirige la sortie d'erreur et la sortie standard à la fin d'un fichier existant 2>&1 >> >>&

Remarques

Normalement, une redirection avec > sur un fichier qui existe déjà efface le contenu du fichier avant d'y placer le résultat de la commande. Les shells ont des options pour demander confirmation, ou refuser d'effacer le fichier.

Une ligne de commandes contenant des | s'appelle un pipe-line. Quelques commandes souvent utilisées dans les pipe-lines sont:

Exemples

cat glop buz > toto

Concatène les fichiers glop et buz et place le résultat dans toto.

wc -w /usr/dict/words

Affiche le nombre de mots du dictionnaire Unix.

grep gag /usr/dict/words | tail

Affiche les 10 derniers mots du dictionnaire qui contiennent la chaîne gag.

Variables

Qu'est-ce qu'une variable ?

Une variable est l'assignation d'une étiquette à un contenu ; ce contenu, comme l'indique le mot « variable », peut changer autant que l'on veut ; l'assignation de l'étiquette à ce contenu, elle, est fixe, aussi longtemps que l'on ne dissout pas la variable.

La notion de variable est commune à presque tous les langages informatiques, et en particulier aux langages de programmation. Ceux qui ont déjà manipulé des langages sont donc familiers avec cette notion. Pour les autres, un petit exemple les aidera peut-être à la saisir.

Mettons que vous programmiez le hachoir dont nous parlions plus haut. Un hachoir est un instrument dangereux, à ne pas mettre à la portée des enfants. Robert le jardinier, qui a conçu ce hachoir, veut être le seul à pouvoir l'utiliser, sans quoi le petit Émile pourrait se blesser en y mettant le doigt.

Ainsi, il va dire au programme hachoir de vérifier la variable USER, qui contient le nom de l'utilisateur. Si le nom « Robert » est associé à l'étiquette USER, alors le programme se met en route ; sinon, il dit à l'utilisateur de ne pas utiliser cet instrument sans la présence d'un adulte, et de bien regarder des deux côtés avant de traverser la rue.

Certaines variables sont prédéfinies, par exemple USER ; mais on peut en créer autant que l'on veut. Par exemple, si Robert veut autoriser d'autres adultes que lui à utiliser son hachoir, il peut faire que le programme demande à l'utilisateur quel âge il a ; la réponse est enregistrée dans la variable age ; ensuite, le programme va examiner le contenu de cette variable. Si age >= 18, alors le hachoir peut se mettre en route ; mais si age < 18, le hachoir refuse de se mettre en marche.

Les variables en shell

En shell, pour désigner le contenu d'une variable, on écrit le nom de la variable précédé du signe dollar. Exemple : echo $HOME affiche le nom du répertoire personnel de l'utilisateur, mémorisé par la variable HOME.

Les noms de variables

Par convention, les variables relevant du système, comme HOME, USER et beaucoup d'autres, sont en majuscules, tandis que l'on recommande d'écrire en minuscules les variables que l'on se crée soi-même. On évite ainsi la désagréable surprise de remplacer une variable importante et de casser tout ou partie de son système.

Les noms de variables sont en effet sensibles à la casse : USER, user, User, uSeR etc. sont des variables différentes.

Définir une variable

La façon de donner une valeur à une variable varie selon le type de shell utilisé :

C-Shell (csh, tcsh, lcsh) : on utilise la commande setenv :

 
chaland ~ $ setenv foo bar
chaland ~ $ echo $foo
bar

Famille des Bourne Shell (sh, bash, zsh, ksh) : on utilise export :

chaland ~ $ foo=bar
chaland ~ $ export foo
chaland ~ $ echo $foo
bar
Les variables d'environnement

Les valeurs des variables sont accessibles aux commandes lancées par le shell. L'ensemble de ces valeurs constitue l'environnement. On peut aussi supprimer une variable de l'environnement avec unsetenv (C-Shell) ou unset (Bourne Shell).

Quelques variables d'environnement:

Exercice : Assurez-vous que /usr/local/games/bin se trouve bien dans votre PATH.

Programmation du shell sh

Un shell, quel qu'il soit, peut exécuter des commandes prises dans un fichier. Un fichier contenant des commandes pour le shell est appelé un script. C'est en fait un programme écrit dans le langage du shell. Ce langage comprend non seulement les commandes que nous avons déjà vues, mais aussi des structures de contrôle (constructions conditionnelles et boucles).

Pour la programmation du shell, nous allons utiliser le shell sh, qui est le plus répandu et standard. Ce que nous avons vu jusqu'ici s'applique aussi bien à sh qu'à zsh et aussi à csh, à l'exception de setenv et de certaines redirections signalées.

Rendre un script exécutable

Pour être un script, un fichier doit commencer par la ligne:

#!/bin/sh

Il doit aussi avoir être exécutable (bit x). Le #!/bin/sh sur la première ligne indique que ce script doit être exécuté par le shell sh dont on indique le chemin d'accès. Pour rendre un fichier exécutable, tapez :

chaland ~ chmod u+x fichier

(pour en savoir plus sur les droits attachés à un fichier, consultez la page sur les droits d'accès).

Structures de contrôle

C'est avec les structures de contrôle qu'un programme peut commencer à devenir sérieux. Le principe général de ces structures est le suivant : adapter le comportement du programme selon les réponses apportées à certaines questions.

Nous avons déjà vu une application possible des structures de contrôle en parlant des variables. Le programme hachoir conçu par Robert le jardinier pose la question suivante : est-ce que $USER vaut « Robert » ?

La boucle if

La structure de contrôle if est la plus courante de toutes, et la plus élémentaire. Elle est constituée de quatre termes :

  1. if (si), qui marque la condition à remplir ;
  2. then (alors), qui marque les conséquences si la condition est remplie ;
  3. else (sinon), qui est facultatif et marque le comportement à adopter si la condition n'est pas remplie ;
  4. fi (if à l'envers), qui marque la fin de la boucle.
if commande ; then commandes ; else commandes ; fi

ou bien (car le point et virgule équivaut à un saut de ligne) :

if commande  
then commandes 
else commandes  
fi

Par exemple, pour Robert le jardinier, on aura :

if test $USER = Robert
    then hacher_menu_comme_chair_à_pâté $*
    else echo "Quand tu seras grand, $USER." 
         echo "Et fais bien attention en traversant la rue."
fi

La boucle for

La boucle for affecte successivement une variable chaque chaîne de caractères trouvée dans une liste de chaînes, et exécute les commandes une fois pour chaque chaîne.

for var in liste de chaînes ; do commandes ; done

ou bien :

for var in liste de chaînes 
do commandes 
done

Rappel : $var accède à la valeur courante de var. La partie commandes est une suite de commandes, séparées par des points et virgules (;) ou des retours à la ligne.

Exemple :

for i in *; do echo "$i"; done

affiche les noms de tous les fichiers du répertoire courant, un par ligne.

Remarque : plus un programme grossit, plus il importe que son contenu soit lisible. Les noms de variable doivent donc être le plus lisibles possible, pour permettre à d'autres gens, ou bien à vous-même dans quelques mois, de comprendre rapidement votre script. Ainsi, il peut être plus explicite d'écrire, au lieu de « for i in *; do echo "$i"; done » :

for fichier in *; do echo "$fichier"; done

En outre, pour des raisons de lisibilité, n'hésitez pas à gaspiller des lignes de code en sautant des lignes ; les lignes de code ne sont pas une ressource rare :

for fichier in * 
do echo "$fichier" 
done

La boucle while

while commande  
do commande  
done

La boucle while exécute les commandes de manière répétée tant que la première commande réussit.

Par exemple : tant que le mot de passe correct n'a pas été donné, refuser l'accès et redemander le mot de passe.

La boucle case

case chaîne in
  pattern) commande ;;
  pattern) commande ;;
esac

La boucle case exécute la première commande telle que la chaîne est de la forme pattern. Un pattern (motif) est un mot contenant éventuellement les constructions *, ?, [a-d], avec la même signification que pour les raccourcis dans les noms de fichiers. Exemple :

case $var in
  [0-9]*) echo 'Nombre';;
  [a-zA-Z]*) echo 'Mot';;
  *) echo 'Autre chose';;
esac

Vous observerez que l'on clôt la boucle case avec esac, qui est le mot case à l'envers, de même que l'on clôt if avec fi.

Code de retour

On remarque que la condition des commandes if et while est une commande. Chaque commande renvoie un code de retour (qui est ignoré en utilisation normale). Si le code est 0, la commande a réussi ; sinon, la commande a échoué. Par exemple, le compilateur cc renvoie un code d'erreur non nul si le fichier compilé contient des erreurs, ou s'il n'existe pas.

Les commandes if et while considèrent donc le code de retour 0 comme « vrai », et tout autre code comme « faux ».

Il existe une commande test, qui évalue des expressions booléennes (c'est-à-dire dont la valeur ne peut être que vraie ou fausse, 1 ou 0) passées en argument, et renvoie un code de retour en fonction du résultat. Elle est bien utile pour les scripts. Exemple :

if test $var = foo
then echo 'La variable vaut foo'
else echo 'La variable ne vaut pas foo'
fi

Variables

Dans les scripts, on peut utiliser des variables définies à l'extérieur (avec setenv ou export), mais aussi définir ses variables locales propres au script. On donne une valeur à une variable avec une commande de la forme nom-de-variable=valeur. Les variables sont utilisées pour stocker des informations.

On a aussi des variables spéciales, initialisées automatiquement au début du script:

$0 Le nom de la commande (i.e. : du script)
$1, $2, etc. Le premier, deuxième, etc, argument passés au script.
$* La liste de tous les arguments passés au script.
$# Le nombre d'arguments passés au script.
$? Le code de retour de la dernière commande lancée.
$! Le numéro de process de la dernière commande lancée en tâche de fond.
$$ Le numéro de process du shell lui-même.

Vous êtes maintenant en mesure de faire ces exercices pour vous entraîner.

Basé sur un polycopié de Roberto Di Cosmo, Xavier Leroy et Damien Doligez. Ajustements : Nicolas George. Compléments : Baptiste Mélès. Dernière modification le .