Aller au contenu

Administration Unix - CM séance 05

6. Les scripts bash

Un script est une succession de commandes, placées dans un fichier texte, exécutées dans un processus fils.

Un script shell est donc exécuté dans un shell fils (= un sous-shell), ce qui signifie que les variables du script ne seront pas visible du shell appelant le script.

6.1. Premier script

Créons un script hello.sh, en utilisant par exemple l'éditeur vi :

#! /bin/bash
echo "Hello World!"

Après l'avoir enregistré, on rend le script exécutable avec chmod :

$ chmod +x hello.sh
$ ls -l hello.sh
-rwxrwxr-x ... hello.sh*

On peut maintenant exécuter le script de cette façon (./ indique au système qu'il est situé dans le répertoire courant) :

$ ./hello.sh
Hello World!

Le shebang

Sous Unix, tous les scripts commencent par les deux caractères #!, appelé le shebang (ou encore shabang, de sharp # bang !).

Le shebang est un mot magique, un mot placé au tout début du fichier, qui permet au système d'identifier le type du fichier par son contenu.

Il est utilisé par la famille de fonctions C exec pour distinguer les scripts des exécutables binaires.

La commande file utilise aussi le mot magique (ainsi que d'autres propriétés : encodage, droits, extension...), en s'appuyant sur une base de données de critères (les fichiers /etc/magic et /usr/share/misc/magic.mgc).

$ file hello.sh 
hello.sh: Bourne-Again shell script, ASCII text executable

L'interpréteur

Après le shebang il y a le chemin absolu de l'interpréteur ; exemples :

#! /bin/bash
#! /bin/sh
#! /bin/dash
#! /bin/tcsh
#! /bin/zsh
#! /usr/bin/env python3
#! /usr/bin/env php
#! /bin/false
...

Lorsqu'on exécute un script, le système va exécuter l'interpréteur dans un processus fils en lui passant le chemin du fichier en premier argument.

Ainsi lorsqu'on tape ./hello.sh, le système va exécuter : /bin/bash ./hello.sh

⟶ bash interprète les commandes du fichier, la première ligne étant prise comme un commentaire (#...).

= mécanisme extensible, car on peut rajouter des interpréteurs dans un système ; introduit en 1980 par Dennis Ritchie.

Certains interpréteurs sont situés dans un répertoire connu (bash, sh, ...). D'autres peuvent être situés dans un répertoire qui dépend du système (python3, php, ...), c'est pourquoi on les lance avec la commande /usr/bin/env, dont le but est de trouver leur chemin (à partir de $PATH).

Si on commence le fichier par /bin/false, le script échoue immédiatement : c'est un moyen d'interdire l'exécution d'un script.

Remarques :

  • l'extension .sh du fichier n'est pas utilisée par le système, on peut l'omettre ;
  • on peut aussi accoler shebang et interpréteur : #!/bin/bash

6.2. Débogage

Pour savoir ce que fait un script, on peut utiliser set -x (voir le cours n°4 sur le prompt PS4) :

$ cat ex1.sh
#! /bin/bash
set -x                  # active le débogage ; set +x pour le désactiver
dest="resultat.txt"
echo "Création du fichier $dest..."
echo -n "# Fichier créé le " >| "$dest"
date >> "$dest"

$ ./ex1.sh 
+ dest=resultat.txt
+ echo 'Création du fichier resultat.txt...'
Création du fichier resultat.txt...
+ echo -n 'Fichier créé le '
+ date

$ cat resultat.txt
# Fichier créé le mardi 20 octobre 2021, 12:48:51 (UTC+0200)

ou encore invoquer bash -x au shebang :

$ cat ex1.sh
#! /bin/bash -x
#set -x
...

ou encore, invoquer bash -x dans le terminal :

$ bash -x ex1.sh 

6.3. Code de terminaison

Tout processus qui se termine émet un code de terminaison (appel C exit) qui sera reçu par le processus père (appel C wait).

Ce code peut être :
0   signifie que l'opération a réussi
1..255   signifie un échec

Certains programmes donnent des codes différents selon l'erreur rencontrée, et les documentent dans la page du man (ex: man ls, /exit status).

Lorsqu'un shell ou un script exécute une commande en avant plan, à sa terminaison il recevra le code de terminaison dans la variable spéciale ?.

$ ls bidon
ls: impossible d'accéder à 'bidon': Aucun fichier ou dossier de ce type
$ echo $?
2           # ls a échoué
$ echo $?
0           # echo a réussi

⚠ chaque commande appelée modifie $?, y compris echo.

Dans un script, le code de terminaison à la sortie du script sera celui de la dernière commande exécutée.

$ cat essai-ls.sh
#! /bin/bash
echo "listing de bidon :"
ls bidon

$ ./essai-ls.sh
listing de bidon :
ls: impossible d'accéder à 'bidon': Aucun fichier ou dossier de ce type
$ echo $?
2               # code de la dernière commande exécutée, ici ls

On peut aussi sortir immédiatement d'un script avec un code de terminaison n en faisant : exit n

$ cat essai-exit.sh
#! /bin/bash
echo 1
exit 5          # ls script s'arrête ici
echo 2

$ ./essai-exit.sh
1
$ echo $?
5

Les commandes true et false ne font rien, mais renvoient un code de terminaison de succès (0) ou d'échec (1).

$ false
$ echo $?
1

Les séparateurs && et || exploitent le code de terminaison :

$ cmd1 && cmd2      # exécute cmd1 ; si cmd1 réussi, exécute ensuite cmd2
$ cmd1 || cmd2      # exécute cmd1 ; si cmd1 échoue, exécute ensuite cmd2

Exemple :

$ mkdir toto && cd toto && touch truc && ls -l truc

⚠  éviter d'utiliser && et || ensemble, ce n'est pas un if then else :

$ true && false || echo "raté"
raté

On peut inverser le résultat d'une commande :

$ true || echo bingo
$ ! true || echo bingo      # true réussit, donc ! true échoue
bingo

6.4. Arguments

Lorsqu'on invoque un script avec des arguments, par exemple

$ ./monscript.sh bonjour les amis

les arguments du script (ici : "bonjour", "les", "amis") sont accessibles dans le script par les paramètres positionnels $1, $2, ... :

  • $0   le nom du script avec son chemin d'appel
  • $1   le premier argument ("bonjour")
  • $2   le deuxième argument ("les")
  • ...
  • ${10}   le dixième
  • ...

On a aussi

  • $#   le nombre d'arguments (entier)
  • ${!#}   le dernier argument

La liste complète des arguments peut être obtenue de deux manières :

  • $*   s'expanse en : $1 $2 $3 ...
  • "$@"   s'expanse en : "$1" "$2" "$3" ...

autrement dit, "$@" protège les arguments d'un redécoupage sur les blancs (on y reviendra plus tard).

Exemple :

$ cat ex2.sh
#! /bin/bash
echo "Je suis $0"
echo "J'ai reçu $# arguments."
echo "Le premier est : $1"
echo "Le deuxième est : $2"
echo "La liste complète est : $*"

$ ./ex2.sh le canal du midi
Je suis ./ex2.sh
J'ai reçu 4 arguments.
Le premier est : le
Le deuxième est : canal
La liste complète est : le canal du midi

La commande shift

shift   décale les arguments de 1 rang vers la gauche (le 1er est perdu).
shift n   décale les arguments de n rangs vers la gauche (les n 1ers perdus).

Exemple :

$ cat ex3.sh
#! /bin/bash
echo "Mes arguments sont : $*"
shift
echo "Après shift, mes arguments sont : $*"
shift 3
echo "Après shift 3, mes arguments sont : $*"
echo "Le premier argument est maintenant $1, et leur nombre est $#"

$ ./ex3.sh do ré mi fa sol la si
Mes arguments sont : do ré mi fa sol la si
Après shift, mes arguments sont : ré mi fa sol la si
Après shift 3, mes arguments sont : sol la si
Le premier argument est maintenant sol, et leur nombre est 3

La commande set (encore !)

La commande set permet de modifier les arguments, y compris dans le shell interactif :

$ set la Provence verte
$ echo $#
3
$ echo $*
la Provence verte
$ shift
$ echo $1
Provence

Pour éviter que set interprète un argument (commençant par un -) comme une de ses options, il suffit de placer avant -- :

$ set -- -a -b -c
$ echo $*
-a -b -c

7. Structures de contrôle du shell

Le langage bash fournit des structures de contrôle :

  • des tests :

    test ..
    [ .. ]
    [[ .. ]]
    (( .. ))
    
  • des branchements : (syntaxe sh héritée du langage Algol 68)

    if .. then .. fi
    if .. then .. else .. fi
    if .. then .. elif .. else .. fi
    case .. in .. esac
    select .. in .. do .. done
    
  • des boucles :

    for .. do .. done
    for .. in .. do .. done
    for ((..)) do .. done
    while .. do .. done
    until .. do .. done
    

Comme tout ce que l'on voit ici, elles sont utilisables dans un script comme dans un shell interactif.

7.1. Le branchement if

Syntaxe :

if cmd1 ; then bloc1 ; fi

ou encore, en remplaçant les ; par des retours chariots :

if cmd1 
then bloc1
fi

if cmd1 ; then      # variante
    bloc1
fi

La commande cmd1 est exécutée ; si elle réussit, le bloc de commandes bloc1 est exécuté. C'est équivalent à :

cmd1 && bloc1

Exemple :

if mkdir toto ; then 
    echo "Création de toto/ réussie"
    cd toto
fi

Variantes : (avec ; ou retour chariot avant then, else, elif et fi)

if cmd1 ; then
    bloc1               # exécuté si cmd1 a réussi
else
    bloc2               # exécuté si cmd1 a échoué
fi

if cmd1 ; then
    bloc1
elif cmd2 ; then        # elif signifie else if
    bloc2
elif ...
...
else 
    bloc_n
fi

(voir exemples section suivante)

7.2. Les tests

Le shell bash dispose d'au moins trois opérateurs de test :

  • la commande interne test
  • la commande [, clôturée par un ]
  • la commande [[, clôturée par un ]]

Ces opérateurs effectuent un test selon des arguments, puis réussissent ou échouent (code de terminaison 0 ou != 0). Ils sont donc utilisables dans un if, avec && ou ||.

Au niveau de la syntaxe, il faut toujours

  • insérer des espaces entre les arguments, et à l'intérieur des crochets ;
  • protéger les chaînes de caractères avec des "".

La commande test

La commande test opère sur (voir : help test)

  • des fichiers :

    test -e file            # file existe
    test -f file            # file est un fichier régulier
    test -d file            # file est un répertoire
    test -L file            # file est un lien
    test -r file            # l'utilisateur a le droit de lire file
    test -w file            #                          d'écrire file
    test -x file            #                          d'exécuter file
    test -N file            # file modifié depuis sa dernière lecture
    test file1 -nt file2    # file 1 est plus récent que file2
    ...
    

    Exemple :

    g="toto"
    if test -f "$g" ; then
      echo "$g est un fichier régulier"
    elif test -d "$g" ; then
      echo "$g est un répertoire"
    else
      echo "$g n'est ni un fichier régulier ni un répertoire"
    fi
    
  • des chaînes de caractère :

    test -z str             # str est vide
    test -n str             # str est non vide
    test str1 = str2        # chaînes égales (bash accepte "==" mais pas sh)
    test str1 != str2       # chaînes différentes
    test str1 \< str2       # str1 avant str2 dans l'ordre lexicographique
    test str1 \> str2       # str1 après str2
    
  • des entiers :

    test x -eq y            # x == y (equal)
    test x -ne y            # x != y (not equal)
    test x -lt y            # x <  y (less than)
    test x -le y            # x <= y (less or equal)
    test x -gt y            # x >  y (greater than)
    test x -ge y            # x >= y (greater or equal)
    

    Exemple :

    if test $# -eq 0 ; then
      echo "il n'y a pas d'argument"
    fi
    
  • divers :

    test -v var             # la variable var existe
    test -o opt             # l'option du shell est définie
    test ! ...              # inverse le résultat du test
    test expr1 -a expr2     # et logique entre les 2 expressions
    test expr1 -o expr2     # ou logique entre les 2 expressions
    test \( expr \) ..      # parenthèses de priorité
    

L'opérateur [ ]

L'opérateur crochets [ .. ] est identique à la commande test .., et vient de sh (voir : help [) ; les lignes suivantes sont équivalentes :

$ test -z "$a" && echo "a est vide"
$ [ -z "$a" ] && echo "a est vide"
$ if test -z "$a" ; then echo "a est vide" ; fi
$ if [ -z "$a" ] ; then echo "a est vide" ; fi

Remarque :

$ type -a [
[ est une primitive du shell
[ est /usr/bin/[

L'opérateur [[ ]]

L'opérateur double crochets [[ .. ]] est propre à bash : même syntaxe que test et [ .. ], avec en plus (voir : help [[) :

  • des tests sur des motifs (à droite, sans "") :

    $ [[ "bonjour" == bon* ]] ; echo $?
    0
    
  • des tests sur des expressions régulières (à droite, sans "") :

    $ [[ "bonjour" =~ ^bon.*$ ]] ; echo $?
    0
    
  • utilisation des opérateurs && (au lieu de -a), || (au lieu de -o), des parenthèses ( et ) (au lieu de \( et \)).