Petit précis de shell

Posted on 2014-09-07 in Programmation Last modified on: 2014-10-05

Le shell (POSIX, ie sans les améliorations de Bash ou zsh) est un langage assez particulier qu’on ne connaît pas forcément. Le but de ce document est de rappeler (ou donner) quelques bases du langage, de rappeler les différences notables avec bash. Je propose dans un autre article quelques solutions standards afin de vous aider dans vos scripts. Ce document ne se veut en aucun cas un cours/tuto complet et exhaustif. Des bases de shell et d’un autre langage de programmation peuvent s’avérer utile. Voir le site du zéros pour un tuto orienté vers les débutants.

Initialement publié ici. Ce document est une version améliorée et corrigée.

Quelques rappels d’Unix

Le langage shell est le langage de script qui vient par défaut avec tous les Unix. En effet, même si désormais d’autres interpréteurs plus modernes sont désormais répandus (bash, csh, zsh, etc.), ils ont tous conservé la compatibilité avec le shell.

Comme pour la plupart des langages de script, il existe deux façons d’exécuter des instructions shell :

  • directement dans l’interpréteur
  • dans un script shell

Pour lancer un interpréteur shell, rien de plus simple : lancer un terminal (graphique ou tty). Et oui, le shell comprend toutes les commandes Unix. Vous pouvez donc les réutiliser telles quelles dans vos scripts et utiliser un simple terminal pour faire des boucles, des conditions et j’en passe. C'est un des très gros intérêts du langage d'ailleurs.

Les bases du shell

Il est très important de comprendre et de garder à l’esprit qu’en shell tout est :

  • chaîne de caractères (y compris les nombres) ! Entrez echo 1 + 1 dans le terminal pour vous en convaincre.
  • commande et que donc elles peuvent prendre des arguments (cela s’éclaircira plus tard)

Syntaxe de base

Les commandes s’écrivent soit :

  • les unes à la suite des autres séparées par ; (peu recommandé)
  • séparées les unes des autres par un saut de ligne

Chaque commande peut prendre des options (qui modifient la façon dont la commande se comporte comme affiche de l’aide, modifier la verbosité de la sortie, etc.) de deux types :

  • les options courtes (l, r, h pour ls par exemple) qui sont passés comme suit : CMD -ARG
  • les options longues (recursive pour rsync par exemple) qui se passent comme ceci : CMD --ARGUMENT

Il est évidement possible de passer plusieurs options à une même commande.

Certains paramètres existent sous une forme courte et une forme longue. Consulter le manuel de la commande pour plus de détails. Le manuel contient également la liste complète des arguments supportés par une commande.

Certains commandes ne respectent pas la convention énoncée ci-dessus. Leurs arguments long se passent avec un seul - (find en est un exemple).

Les commandes peuvent aussi prendre des arguments positionnels. Ils se placent après les paramètres.

Exemple : ls -R ~/Documents. Par défaut, ls ne liste que les fichiers et dossiers dans le dossier courant. L’option -R permet de descendre dans tous les dossiers. L’argument ~/Documents permet d’exécuter la commande dans le dossier ~/Documents sans s’y déplacer.

Pensez à la commande man qui prend comme argument une autre commande et affiche son manuel.

Les variables

Les variables se déclarent comme ceci : var="ping". Notez bien l'absence d'espaces entre var, = et "ping". Si vous ajoutez des espaces, le shell tentera de faire un test. Voir Conditions if … else ….

Ensuite, pour utiliser une variable, il faut mettre un $ devant le nom de la variable. Par exemple : echo $var affichera le contenu de la variable var. La variable est aussi remplacée par sa valeur dans une chaîne si elle est encadrée par des guillemets mais pas si elle est encadrée par des apostrophes. Par exemple :

  • echo "$var pong" affichera ping pong.
  • echo '$var pong' affichera $var pong.

Valeurs de retour des commandes et exceptions

Une fois qu’une commande s’est exécutée, elle renvoie une valeur de retour afin d’informer l’utilisateur. Cette valeur permet en effet de savoir si la commande s’est exécutée correctement. Voici les valeurs de retour courantes et leur signification :

  • 0 : tout va bien
  • 1 : erreur
  • 2 : erreur grave

Vous pouvez vous aussi utiliser ces valeurs de retour. Par défaut, un script qui se complète correctement retourne 0. Mais vous pouvez (par exemple si un utilisateur tente d’exécuter un script qui nécessite un argument sans) retourner un autre code d’erreur avec la commande exit. Il suffit de lui passer le code qu’elle doit retourner. Votre script s’arrêtera alors avec le code d’erreur spécifié.

Vous pouvez connaître la valeur de retour de la dernière commande exécutée avec la variable $?.

Les valeurs de retour ne sont pas limitées à 0, 1, 2. Par convention, c'est n'importe quel entier positif. À chaque entier est associé une signification, ce qui permet de comprendre ce qui s'est passé si une commande échoue.

Exécuter une commande

S’il est facile dans les cas simples d’exécuter une commande, dès lors qu’en shell tout est chaîne de caractères, si vous voulez affecter la sortie d’une commande à une variable, vous ne pouvez pas simplement faire var=CMD car var va alors valoir la chaîne CMD.

Pour obtenir le résultat souhaité vous devez placer CMS entre $(…). Par exemple : var=$(ls).

Vous pouvez aussi mettre la commande entre backquote `. Mais cette syntaxe ` est plus ancienne et ne doit plus être utilisée. La syntaxe $(…) présente l’avantage de pouvoir imbriquer les commandes sans ambiguïté.

Il est également tout à fait possible de stocker une commande dans une variable pour l'exécuter ensuite (une fois de plus, tout est chaîne de caractère). Une commande est en effet une chaîne de caractère située en position 0 sur une ligne.

mon_cd=cd
$mon_cd

Conditions et itérations

Conditions if … else …

La structure générale d’une condition est la suivante :

if QQC
then
    CMDS
else
    CMDS
fi

Le else est facultatif. Il est aussi possible de regrouper if et then en une seule ligne comme ceci : if QQC ; then. On peut également utiliser des elif.

La question que vous devriez avoir est que mettre à la place de QQC. Il y a deux possibilités :

  • la fonction test
  • une commande
La fonction test

Dans toute la suite, il faudra faire très attention aux espaces

La fonction test s’utilise en général comme suit : if [ ARGS ] (Notez les espaces).

La syntaxe if [[ ARGS ]] n’est valide qu’avec bash.

Pour faire un test, il suffit ensuite de passer les bons arguments à la commande. Par exemple, pour tester si une chaîne est vide : if [ -z "$chaine" ]. Si l’argument a besoin de deux paramètres pour fonctionner, mettre un paramètre de chaque côté de celui-ci. Par exemple, pour faire un test d’égalité de chaîne de caractères : CHAINE1 = CHAINE2.

On peut aussi combiner les arguments avec des ET et des OU avec les options -a et -o. Le caractère “!” permet de faire une négation.

On peut également faire des ET avec && et des OU avec ||. Ces opérateurs ne sont pas spécifiques à la fonction test, ils font parti du langage. Il est donc tout à fait possible de les utiliser sans la fonction test.

Voir ci-dessous pour la liste complète.

En shell, tout est chaîne de caractère. Bien faire attention au type que l’on veut tester (chaîne ou nombre).

Tests sur les chaînes de caractères
Argument Signification
= égalité
-z chaîne vide
-n chaîne non vide

Lors des tests de chaîne de caractères, entourez la variable de guillemets. Sinon, si la chaîne est vide, le test ne pourra être correctement effectué.

Tests sur les nombres
Argument Signification
-eq égalité
-ne différent
-lt strictement plus petit
-gt strictement plus grand
-ge plus grand ou égal
-le plus petit ou égal
Test avec une commande

Comme indiqué précédemment, une commande qui s’exécute correctement est considérée comme vraie. Ainsi, il est tout a fait possible, par exemple, pour savoir si on arrive à se connecter à un serveur mysql de faire simplement : if mysql -h HOST -u asso -pTATA.

Parfois vous pourrez rencontrer des problèmes. Pensez alors à donner cette commande en argument à la fonction test.

Le shell contient deux fonctions utiles: true et false. La première renvoie toujours 0 et la seconde toujours 1. Vous pouvez les trouver utiles si vous avez besoin de manipuler des booléens.

Boucles while/until

La structure générale est la suivante :

while QQC
do
    CMD
done

Il est possible de regrouper while QQC et le do en while QQC ; do. Le QQC peut être remplacer par exactement les mêmes choses que pour la condition. Se référer à cette section pour les précisions.

Le shell propose également le mot clé until QQC qui fait une action jusqu’à ce que QQC soit réalisé (équivalent de while ! QQC).

Boucle for

L’utilisation de la boucle for en shell ressemble à celle de python. La structure générale est la suivante :

for var in `CMD`
do
   CMD
done

La variable var va alors prendre une à une les valeurs données par CMD. Par exemple, for file in $(ls) la variable var va prendre tour à tour le nom de tous les fichiers et dossiers donnés par la commande ls.

Vous pouvez également utiliser for pour boucler d’un nombre à un autre avec la syntaxe : for i in $(seq [first [incr]] last)

Les tableaux

Un tableau se crée comme ceci : tab=(un deux trois). On accède à un élément grâce à son index (les index commencent à zéros) :

tab=(un deux trois)
echo ${tab[0]}
echo ${tab[1]}
echo ${tab[42]} # Ne renvoie rien, index en dehors de la plage défini.

Idem pour l'assignation :

tab[0]=zero
echo ${tab[0]} # Renvoie zero.

Les index ne sont pas obligatoirement continus :

tab[42]=42
echo ${tab[42]} # Renvoie 42.
echo ${tab[9]} # Ne renvoie rien.

On peut facilement connaître le nombre d'élément du tableau avec ${#tab[@]}. Il est également facile de parcourir tous les éléments :

for elt in ${tab[@]}; do
    echo $elt
done

Va afficher :

12
deux
trois
42

On peut également afficher tout le tableau avec ${tab[@]}.

Paramètres de scripts

Généralités

Un script peut prendre des paramètres qui ont le même but que les arguments d’une fonction : lui passer des informations. Un paramètre peut être :

  • une variable

  • une chaîne de caractère (donc un nombre, en shell on ne fait pas la distinction). Si la chaîne à passer en paramètre contient plusieurs mots séparés par des espaces, ne pas oublier de la mettre entre ‘ ou “.

    Si une variable coucou contient la chaîne ‘salut’, alors ‘$coucou toi’ sera compris $coucou toi tandis que $coucou toi sera interprétée en salut toi

    Les paramètres se passent à un script comme ceux d’une commande. Ils sont ensuite disponibles dans l’ordre avec des numéros :

    • le premier : $1
    • le deuxième : $2
    • et ainsi de suite

    Le shell ne supporte que 9 paramètres.

Les paramètres spéciaux

  • $0 : contient le nom du script

  • $# : contient le nombre d’arguments passés au script

    Le paramètre $0 est toujours passé au script. $# est donc toujours supérieur ou égal à 1.

  • $? : le code de retour de la dernière commande invoquée

  • $$ : le PID du shell qui exécute le script

  • $! : le PID du dernier processus lancé en arrière plan

  • $* : l’ensemble des paramètres en un seul argument

  • $@ : L’ensemble des arguments, un argument par paramètre

Pour bien voir la différence entre $* et $@, il suffit de regarder la sortie du script suivant :

echo 'Avec $*'
for param in "$*" ; do
  echo $param
done

echo 'Avec $@'
for param in "$@" ; do
  echo $param
done

Les commandes shift et set

La commande set permet d’affecter les paramètres. Ainsi set bonjour salut bonsoir va initialiser $1 à bonjour, $2 à salut et $3 à bonsoir. Les paramètres spéciaux $#, $* et $@ sont mis à jour.

La commande shift permet de décaler les variables. Ainsi, si après shift $1 prend la valeur de $2 et ainsi de suite. shift n décale les arguments de n.

Les fonctions

Il est tout à fait possible de définir en shell des fonctions que ce soit dans un script ou un terminal. La syntaxe est la même.

nom_de_la_fonction () {
 CMDS
}

ou encore

function nom_de_la_fonction {
 CMDS
}

Les fonctions ainsi créées s’utilisent comme les commandes classiques et leurs arguments se manipulent exactement comme ceux d’un script. Voir la section dédiée. Il faut néanmoins faire attention à deux points :

  • la portée des variables
  • la valeur de retour

Par défaut, les variables définies dans la fonction resteront accessibles une fois la fonction exécutée. De même les variables définies auparavant restent accessibles dans la fonction. Ces variables sont donc globales par défaut.

Pour qu’une variable soit locale, il faut utiliser le mot clé local lors de sa définition. Par exemple : local nom=clubdrupal (NB : local est une commande qui peut prendre des options).

Pour qu’une fonction en bash retourne une valeur comme vous pouvez en avoir l’habitude dans d’autres langages, il faut utiliser la commande echo.

Il faut alors faire très attention car si la sortie n'est pas capturée dans une variable, elle s'affiche dans le terminal. Par exemple avec la fonction suivante :

x_files () {
  top_secret=`dd if=/dev/urandom count=1 2> /dev/null | cksum | cut -f1 -d" "`
  echo $top_secret
}

Le mot clé return

Comme la plupart des langages, le shell dispose d'un mot clé return. Lorsque ce mot clé est exécuté dans une fonction, cette fonction est exécuté jusqu'à ce mot clé, puis l'exécution du programme reprend à l'instruction suivante. Ce mot clé est particulièrement utile pour que des fonctions renvoient des codes d'erreur sans pour autant arrêter le script (ce que exit aurait fait).

Les redirections de flux

Les flux sortant

Les flux représentent les sorties générées par les commandes. Par défaut, il existe deux flux :

  • le flux standard
  • le flux d’erreur

Par défaut, il s’affiche sur la sortie standard (votre écran pour être bref). Il peut s’avérer intéressant d’envoyer ces flux ailleurs (logs, le néant, etc.). Pour cela, on va les rediriger. Par exemple pour rediriger la sortie de ls dans un fichier nommé toto, on fait :

  • ls > toto
  • ou
  • ls >> toto

La première solution efface le contenu du fichier puis écrit dedans. La seconde ajoute la sortie à la fin du fichier. On a ici redirigé le flux standard. Pour rediriger les flux d’erreurs, on utilise les symboles 2> ou 2>>. On peut évidemment combiner les deux : ls -R / > mes_fichiers.txt 2> errors.log avec toutes les variantes possibles.

Pour rediriger l’erreur au même endroit que l’entrée, on peut faire ls > toto 2> toto ou plus simplement ls > toto 2>&1. Pour rediriger une sortie vers le néant, on l’envoie dans /dev/null.

Les flux entrant

Il est également possible de passer en paramètre le contenu d’un fichier. Pour cela, on utilise le symbole <.

Importer une configuration

Il est tout à fait possible d’écrire un fichier de configuration contenant les variables et les fonctions indispensables à d’autres scripts et les réutiliser facilement dans ceux-ci. Pour cela, respecter la syntaxe shell dans le fichier puis au début du script qui en a besoin, placer la ligne : . config-file.sh pour l’importer.

Mode debug

Lorsqu’un de vos scripts shell bug il peut être difficile de savoir d’où vient le problème. Heureusement, le shell propose un mode débug qui vous dit pour chaque ligne comment elle est exécuté, avec quels paramètres (les variables sont remplacées par leur contenu). Il suffit de faire : sh -x SCRIPT

Parser des arguments

Il existe deux commandes pour parser des arguments en shell : getopts, qui est une commande ”builtin" qui ne supporte que les options courtes et getopt, qui est une commande à part, pas forcément présente mais qui supporte les arguments courts et longs. Nous n’étudierons ici que getopts qui a l’avantage d’être présent partout.

La commande s’utilise comme suit :

while getopts "options" opt; do
  case "$opt" in
    option1) action;;
  esac
done

Ainsi, pour utiliser l’option -o :

while getopts "o" opt; do
  case "$opt" in
    o) echo $opt;;
  esac
done

script -o affera : o. Si on le lance avec l’option -a, il affichera :

illegal option -- a

Il est possible d’utiliser getopts en mode erreurs silencieuses en ajoutant : au début de la chaîne des options.

Si votre option a besoin d’un argument, il suffit de placer : après son nom :

while getopts "ob:" opt; do
  case "$opt" in
    o) echo $opt;;
    b) echo "$opt used with option $OPTARG";;
  esac
done

Une fois vos arguments parser, vous pouvez mettre le premier argument positionel de votre script dans $1 avec :

shift $((OPTIND-1))

Vous pouvez aussi utiliser dans le case ? pour afficher un message si l’utilisateur passe une option inconnue et : pour afficher un message si une option qui requiert un argument ne l’a pas eu.

Exemple

while getopts ":h:u:p:P" opt
do
    case "$opt" in
        h)
            host=$OPTARG; hflag=true;;
        u)
            user=$OPTARG; uflag=true;;
        p)
            passwd=$OPTARG; pflag=true;;
        P)
            Pflag=true;;
        :)
            echo "Option -$OPTARG requires an argument." >&2
            usage; exit 1;;
        \?)
            usage; exit 0;;
    esac
done

shift $((OPTIND-1)) # To get the 1st positional argument with $1

Différences notables avec bash

Variables disponibles uniquement en Bash

  • RANDOM (pour la génération de nombre aléatoire). Voir les trucs et astuces pour plus de détails.

Syntaxe

  • &> et |&. Permettent de rediriger tous les flux vers un fichier ou de passer tous les flux à une commande (pipe généralisé)
  • {2..10} pour générer des séquences de nombres
  • [[ ARGS ]] : permet de faire des tests de façon plus facile ou agréable. Exemple : [[ chaine == chaine2 ]]
  • =~ : s'utilise conjointement avec [[ ]] pour tester si la chaîne de gauche est contenu dans celle de droite
  • $((ARGS)) est un raccourcis pour la fonction let

Autres

  • Le nombre de paramètres en bash n’est pas limité à 9. Les paramètres positionnels de numéros supérieurs à 10 s’appellent comme suit : ${num}

Divers

Différences entre la sortie de ls et de find

  • ls renvoie simplement la liste des fichiers.
  • find renvoie un chemin absolu si l’argument donnée est un chemin absolu et relatif (de la forme ./fichier) si l’argument est .

ET et OU dans le langage

L’opérateur && permet de réaliser un ET paresseux entre deux commandes. Ainsi, par exemple : cmd1 && cmd2. cmd2 ne sera exécuté que si cmd1 a pu s’exécuter correctement. En outre le code de retour de l’ensemble ne sera 0 que si les deux commandes ont pu s’exécuter correctement.

L’opérateur || permet de réaliser un OU paresseux entre deux commandes. Ainsi, par exemple : cmd1 || cmd2. Si cmd1 s’exécute correctement cmd2 ne sera pas exécutée. Si cmd1 retourne une erreur, cmd2 sera exécutée. Le code de retour de l’ensemble correspond à un OU entre les deux codes de retour.

Ceci peut s’utiliser dans les conditions.

Des fonctionnalités analogues existes dans d’autres langages inspirés du shell comme Perl ou PHP.

Pour exécuter simplement deux commandes à la suite des autres sur une seule ligne, il suffit de séparer les commandes par un point virgule.