Introduction aux scripts shell

Objectifs du cours

À l’issue de ce cours, vous devez être capable de …​ :

  • d’énoncer les grands principes utilisés dans les scripts

  • d’exécuter un shell script

  • d’écrire un script shell basique :

    • combinaison de commandes (enchaînements, redirections)

    • utilisation de variables

    • utilisation de structures de contrôle

Qu’est-ce qu’un shell ?

  • De manière générale, c’est un programme qui constitue une interface vers le système d’exploitation

    Le système d’exploitation est lui-même une interface vers le matériel.

  • 2 formes :

    1. graphique → GUI : Graphical User Interface

    2. en ligne de commande → CLI : Command Line Interface

  • Sur un système, plusieurs shells peuvent être disponibles simultanément. Exemples :

    • CLI : bash (Bourne Again Shell ⇒ le plus répandu sur Linux), sh (Bourne shell ⇒ le shell ``ancestral''), ksh (Korn shell), tcsh …​

    • GUI : KDE, Gnome …​

Qu’est-ce qu’un script shell ?

script sample
  • Un fichier texte éditable avec n’importe quel éditeur de texte (vi, Kate, KWrite…​)

  • Contient essentiellement :

    • des commandes Linux traditionnelles (ls, mkdir, chmod …​) qui seront exécutées les unes après les autres et automatiquement par le shell

    • des structures de contrôle pour faire des tests (→ exécution conditionnelle de commandes) , des boucles (→ exécution répétitive de commandes)

    • des variables pour mémoriser des valeurs

  • Est directement interprétable par le shell pour lequel il a été conçu (→ pas de compilation).

À quoi ça sert ?

  • À automatiser certaines tâches :

    • Lancer régulièrement des commandes disposant de nombreuses options sans avoir à les saisir à chaque fois et/ou se plonger dans la documentation pour déterminer lesquelles il faut employer

    • Assurer le suivi et la maintenance de machines ou services dans le cadre de la supervision d’un parc informatique (→ surveillance des espaces disque, analyse de fichiers de journalisation…​)

    • Coder des outils : traitement automatique de fichiers texte, calcul

    • Lancer l’exécution de logiciels qui nécessitent un paramétrage complexe.

      Le système Linux utilise lui-même abondamment les scripts shell dans sa phase d’initialisation (→ init System V, systemd).

    • …​

  • En résumé, les scripts shell servent à être plus productif et notamment dans l’administration de systèmes informatiques.

Un 1ier script

  • fichier hello.sh (l’extension .sh est arbitraire mais couramment utilisée) :

    # Afficher un message d accueil (1)
    echo "Bonjour monde !" (2)
    1 un commentaire qui débute par `#'
    2 la commande qui permet d’afficher un message à l’écran
  • Exécution du script par le shell sh (Bourne shell):

    $ sh hello.sh
    Bonjour monde !

Le “Shebang”

Le “Shebang” est une séquence de 2 caractères à la suite desquels on indique le programme qui doit interpréter le script.

Rappel

Sous Linux, il existe plusieurs interpréteurs de commande.

  • Intégration du “shebang” dans le fichier hello.sh :

    #! /bin/sh (1)
    # Afficher un message d accueil
    echo "Bonjour monde !"
    1 on indique le shell pour lequel le script est conçu à l’aide de la séquence spéciale #! nommée shebang

    Le shebang doit impérativement constituer les 2 premiers caractères du fichier.

  • Exécution du script :

    $ chmod +x hello.sh (1)
    $ ./hello.sh  (2)
    Bonjour monde !
    1 on rend le script exécutable
    2 on exécute le script simplement en tapant son nom dans le shell ⇒ celui-ci sera automatiquement interprété par le shell indiqué par le shebang (pas forcément le même que celui depuis lequel le script a été lancé)

Rappel : Interaction avec un programme

program interaction

L’exécution de tout programme Linux suit toujours les mêmes règles :

  • Débute son exécution après l’avoir invoqué depuis le shell en tapant son nom suivi ou non d’arguments (→ correspond au contenu du char * argv[] dans un programme en langage C)

  • Lit les saisies de l’utilisateur depuis le flux d’entrée standard (→ par défaut : le clavier)

  • Affiche des messages sur le flux de sortie standard (→ par défaut : l’écran) pour informer l’utilisateur

  • Alerte l’utilisateur en envoyant les messages d’erreur sur le flux d’erreur standard (→ par défaut : l’écran)

  • Retourne au système un compte-rendu d’exécution (exit code) sous forme d’entier dès qu’il se termine

Exemple :
$ ./hello.sh
Bonjour monde !
$ echo $?
0
  • argv : {"./hello.sh"}

  • stdin : ""

  • stdout : "Bonjour monde !\n"

  • stderr : ""

  • exit code : 0 (← on l’affiche avec echo $?)

    Un 0 indique que tout s’est bien passé.

    Tout autre valeur indique une erreur.

Autre exemple :
$ mkdir /sbin/bidon
mkdir: /sbin/bidon: Permission denied
$ echo $?
1
  • argv : {"mkdir", "/sbin/bidon"}

  • stdin : ""

  • stdout : ""

  • stderr : "mkdir: /sbin/bidon: Permission denied"

  • exit code : 1

Dernier exemple :
$ read -p "Votre choix ? "
Votre choix ? pizza
$ echo $?
0
  • argv : {"read", "-p", "Votre Choix ?"}

  • stdin : "pizza\n"

  • stdout : "Votre choix ? " puis "pizza\n"

  • stderr : ""

  • exit code : 0

Composition de commandes

  • Le principe d’exécution des programmes vu précédemment va permettre 2 fonctionnalités intéressantes :

    1. l’enchainement conditionnel de commandes

      • exécution ou non d’une commande en fonction du code de retour de la précédente commande

    2. la redirection

      • redirection des flux d’entrée, de sortie et d’erreur standard (stdin, stdout, stderr) vers/depuis des fichiers,

      • utilisation de la sortie standard d’une commande en tant qu’entrée standard pour la commande suivante (→ tube ou pipe)

Enchaînement conditionnel de commandes

  • Séquence de commandes pouvant être interrompue selon la réussite ou l’échec de l’une d’entre elles

  • Mécanisme reposant sur la valeur des codes de sortie des programmes (0 = OK, <>0 = NOK)

$ cmd1 && cmd2 && cmd3 && ...

⇒ Exécute cmd2 si cmd1 réussit puis cmd3 si cmd2 réussit …​

$ cmd1 || cmd2 || cmd3 || ...

⇒ Exécute cmd2 si cmd1 échoue puis cmd3 si cmd2 échoue …​

$ cmd1 ; cmd2 ; cmd3 ; ...

⇒ Exécute cmd1 puis cmd2 puis cmd3 …​ quel que soient leurs résultats d’exécution (réussite ou échec)

Enchaînement conditionnel de commandes

Question

Sous Linux il existe 2 commandes false et true qui ne font rien mais …​ :

  • qui échoue systématiquement pour false (→ exit code = 1)

  • qui réussit toujours pour true (→ exit code = 0)

Qu’affichent les commandes suivantes sachant que la commande echo réussit toujours ?

$ echo -n 'Hello ' ; false ; echo 'world !'
$ echo -n 'Hello ' && false ; echo 'world !'
$ echo -n 'Hello ' && false || echo 'world !'
$ echo -n 'Hello ' && false && echo 'world !'
$ echo -n 'Hello ' && true || echo 'world !'

L’option -n spécifie à la commande echo de ne pas retourner à la ligne.

Réponse :

$ echo -n 'Hello ' ; false ; echo 'world !'
Hello world !
$ echo -n 'Hello ' && false ; echo 'world !'
Hello world !
$ echo -n 'Hello ' && false || echo 'world !'
Hello world !
$ echo -n 'Hello ' && false && echo 'world !'
Hello $ echo -n 'Hello ' && true || echo 'world !'
Hello $
Attention à la priorité des opérateurs (→ précédence)
$ echo -n 'Hello ' || false && echo 'world !'
Hello world !
$ (echo -n 'Hello ' || false) && echo 'world !'
Hello world !
$ echo -n 'Hello ' || (false && echo 'world !')
Hello

cmd1 || cmd2 && cmd3 = (cmd1 || cmd2) && cmd3

cmd1 || cmd2 && cmd3 cmd1 || (cmd2 && cmd3)

Redirection vers/depuis un fichier : ‘>’, ‘>>’, ‘<’

$ cmd > out.txt (1)
$ cmd >> out.txt (2)
$ cmd < in.txt (3)
1 Exécute cmd et envoie sa sortie standard dans le fichier out.txt au lieu de l’écran.
  • Si le fichier n’existe pas, il est créé

  • Si le fichier existe, son contenu d’origine est perdu

2 idem sauf que la sortie standard est ajoutée au contenu du fichier out.txt s’il existe (→ on ne perd pas son contenu d’origine).
3 l’entrée standard de cmd est lue depuis le fichier in.txt et non depuis le clavier
Exemple : Rediriger la sortie du script hello.sh dans le fichier out.txt
$ cat hello.sh
#! /bin/sh
# Afficher un message d'accueil
echo "Bonjour monde !"
$ ./hello.sh > out.txt
$ cat out.txt
Bonjour monde !
Question

Donner la séquence de commandes à exécuter pour lister dans un même fichier :

  • les noms des fichiers contenu dans le répertoire /home/bonnie

  • les noms des fichiers contenu dans le répertoire /home/clyde

Réponse :

$ ls /home/bonnie >catalog.txt ; ls /home/clyde >>catalog.txt

La commande tee de Linux peut être très utile : elle copie son entrée standard (stdin) dans sa sortie standard (stdout) et dans un fichier que l’on spécifie en argument de la commande.

Cela permet, par exemple, de garder dans un fichier une trace de la sortie d’une commande tout en permettant de la relayer à une autre commande (cf. Redirection vers une autre commande : les tubes).

Pour plus d’informations sur la commande tee, consulter par exemple Linux and Unix tee command tutorial with examples link.

Redirection vers/depuis un fichier : ‘<<’

  • L’opérateur << possède une signification sensiblement différente : il est utilisé dans le cadre de ce qu’on appelle les here documents.

    Un here document permet de ``simuler'' un fichier en indiquant son contenu en ligne.

  • Exemple : Afficher le nombre de lignes du here document dont le contenu est délimité par la chaîne arbitraire FIN'' (FIN'' doit impérativement être `collé'' à `<<).

    $ wc -l <<FIN
    > ligne n°1
    > ligne n°2
    > ligne n°3
    > FIN
           3
    $

Redirection vers/depuis un fichier : ‘2>&1’

  • Il est possible de rediriger la sortie d’erreur et la sortie standard dans un même fichier.

  • Dans de nombreux scripts, on utilise la séquence 2>&1 à cet effet.

  • Exemple : compter les lignes de 2 fichiers dont 1 n’existe pas

    $ wc -l hello.sh fichier_inexistant > errout.txt 2>&1
    $ cat errout.txt
    wc: fichier_inexistant: open: No such file or directory
           3 hello.sh
           3 total
    • La commande wc ne provoque aucun affichage

    • le fichier errout.txt contient à la fois l’erreur (1ière ligne du fichier) et le résultat de la commande wc sur le fichier hello.sh

La page de manuel de bash suggère une autre syntaxe pour réaliser cette commande :

$ wc -l hello.sh fichier_inexistant &> errout.txt

Malgré une meilleure lisibilité, la 1ière syntaxe est encore largement répandue.

  • Utilisation fréquente de la redirection 2>&1 : rendre ``muet'' un programme

    $ cmd > /dev/null 2>&1
    • On envoie la sortie standard et d’erreur de la commande cmd sur le fichier /dev/null qui offre la particularité d’ignorer et de perdre tout ce qu’on écrit dedans

      ⇒ ceci permet de réduire à néant tout message émis sur le flux standard ou d’erreur :

      • aucun affichage

      • aucune utilisation de l’espace disque

  • Interprétation de cmd > /dev/null 2>&1 :

    • on redirige d’abord la sortie standard vers /dev/null

    • on redirige ensuite la sortie d’erreur (représentée par le 2 dans 2>&1) vers l’endroit où est redirigé la sortie standard (représentée par le 1).

Attention à l’emplacement de 2>&1

cmd 2>&1 > /dev/null ne redirige que la sortie standard vers /dev/null. En effet, on redirige stderr vers stdout avant que celui-ci ne soit redirigé sur /dev/null.

La preuve en image :

$ wc -l fichier_inexistant 2>&1 > /dev/null
wc: fichier_inexistant: open: No such file or directory

Redirection vers une autre commande : les tubes

$ cmd1 | cmd2

⇒ On exécute cmd1 puis cmd2 en redirigeant la sortie standard de cmd1 sur l’entrée standard de cmd2.

Exemple : Compter le nombre de fichiers dans le répertoire /var/log/apache2.
$ ls /var/log/apache2/ | wc -l
       3
# Sensiblement équivalent à :
$ ls /var/log/apache2/ > fichier.txt
$ wc -l < fichier.txt
       3
# ...mais le résultat intermédiaire n'est pas stocké
# (-> on gagne 36 octets sur le disque)
$ ls -l fichier.txt
-rw-r--r--@ 1 claude  staff  36  7 avr 14:47 fichier.txt
Question

Soit le contenu de répertoire suivant :

$ ls -l *.txt
-rw-r--r--@ 1 claude  staff  1024 31 mar 21:40 a.txt
-rw-r--r--@ 1 claude  staff   512 31 mar 21:40 b.txt
-rw-r--r--@ 1 claude  staff  1536 31 mar 21:40 c.txt

Sachant que :

  • head -n 1 affiche la 1ière ligne de son entrée standard

  • sort affiche par ordre lexicographique ce qui lui est fourni

quelle commande parmi celles indiquées ci-dessous permet d’afficher uniquement le fichier le plus petit :

$ sort | head -n 1 | ls -l *.txt
$ head -n 1 | ls -l *.txt | sort
$ ls -l *.txt | head -n 1 | sort
$ head -n 1 | sort | ls -l *.txt
$ ls -l *.txt | sort | head -n 1
$ sort | ls -l *.txt | head -n 1

Réponse :

$ ls -l *.txt | sort | head -n 1 # c'est celle-ci !!
# la preuve :
$ ls -l *.txt | sort | head -n 1
-rw-r--r--@ 1 claude  staff   512 31 mar 21:40 b.txt

L’utilisation des tubes n’est possible qu’avec les commandes de type “filtre” c.-à-d. les commandes qui acceptent de prendre leur entrée standard depuis la sortie d’un tube (ex. sort, head mais pas ls).

Il existe néanmoins une solution qui consiste à utiliser la commande xargs. Cette commande capture son entrée standard et la redistribue à la commande qu’on lui donne en argument.

Exemple :
$ echo "/home\n /var/tmp" | xargs ls (1)
1 Liste le contenu des répertoires /home et /var/tmp

Consulter internet pour des exemples plus complets. Ex. : Linux commands: xargs link.

Substitution

  • Un shell Linux est capable d’évaluer un certain nombre d’expressions au cours de l’exécution d’un script.

    ⇒ Il remplace/substitue alors l’expression par le résultat de son évaluation puis reprend l’interprétation du script

  • Parmi les expressions candidates, on trouve :

    • les variables,

    • les caractères génériques ou jokers (→ wildcards)

    • les expressions arithmétiques et bit-à-bit

    • les commandes

Substitution de variables

  • Variable : nom auquel on associe une valeur

  • Définition d'1 variable : <nom_variable>=<valeur>

  • Utilisation d'1 variable : $<nom_variable> ou ${<nom_variable>}

  • Plusieurs types de variables :

  1. variables utilisateurs

    ⇒ variables définies par l’utilisateur. ex. : $mon_age

  2. variables d’environnement

    ⇒ variables définies par le système. ex. : $PATH

  3. variables internes

    ⇒ variables définies par le shell. ex. : $@

Variables utilisateurs

Exemple avec variable utilisateur :
#! /bin/sh
# script : uservariable.sh
question= "Tu fais koi ?"
option=IR
formation="BTS SN"
echo $question
echo "J'suis en $formation_$option ...euh... ${formation}_$option)"
$ chmod +x uservariable.sh
$ ./uservariable.sh
Tu fais koi ?
J'suis en IR ...euh... BTS SN_IR

Noter la différence entre $formation_$option et ${formation}_$option.

Dans la 1ière forme, le shell cherche une variable nommée formation_ (← le `_' est un caractère autorisé dans le nom d’une variable), ne la trouve pas et la remplace donc par une chaîne vide.

Variables d’environnement

Exemple avec variable d’environnement :
$ printenv  (1)
TERM_PROGRAM=Apple_Terminal
SHELL=/bin/bash
[...]
HOME=/Users/claude
LOGNAME=claude
$ echo "Mon nom d'utilisateur est $LOGNAME et mon shell est $SHELL."
Mon nom d'utilisateur est claude et mon shell est /bin/bash.
1 : la commande printenv affiche les variables d’environnement

Variables internes

  • Le shell définit un certain nombre de variables spéciales très utiles pour écrire les scripts :

    • $0, $1, $2, …​ : les constituants de la ligne de commande qui a invoqué le script

      • $0 : le nom du script

      • $1, $2, …​ : les arguments du script

    • $@ : la liste des arguments ( $1 + $2 + …​)

    • $# : le nombre d’arguments fournis au script

    • $? : le code de retour du dernier programme/script exécuté

    • …​

  • Exemple :

#! /bin/sh
# script : internalvariables.sh
echo "Hello from script $0 -> $# argument(s) provided"
echo "1st arg : $1"
echo "2nd arg : $2"
$ ./internalvariable.sh A
Hello from script ./internalvariable.sh -> 1 argument(s) provided
1st arg : A
2nd arg :
$ ./internalvariable.sh A B
Hello from script ./internalvariable.sh -> 2 argument(s) provided
1st arg : A
2nd arg : B
$ ./internalvariable.sh A B C
Hello from script ./internalvariable.sh -> 3 argument(s) provided
1st arg : A
2nd arg : B
$ ./internalvariable.sh "A B" C
Hello from script ./internalvariable.sh -> 2 argument(s) provided
1st arg : A B
2nd arg : C

Caractères génériques

  • Certains caractères prennent une signification particulière pour le shell (cf. `<', `>', ’|', …​)

    Méta-caractères

  • Les méta-caractères suivants sont utilisés comme `"joker`" pour les noms de fichier ou de répertoire : ?, *, !, [ et ].

    • ? → remplace n’importe quel caractère individuel

    • * → remplace un ensemble de caractères consécutifs

    • [ <plage_valeurs> ] → remplace un caractère parmi ceux spécifiés dans <plage_valeurs>

      • ! <plage_valeurs> → inverse la signification de <plage_valeurs>

  • Exemples :

# Liste les dossiers débutant par 'D'
$ ls -d D*/
Desktop/  Documents/  Downloads/
# Liste les dossiers débutant par 'D' et se terminant par 'p'
$ ls -d D*p/
Desktop/
# Liste les dossiers dont la 2ième lettre est 'o' et la dernière est 's'
$ ls -d ?o*s/
Documents/  Downloads/
# Liste les dossiers débutant par 'D' et dont la 3ième lettre est 's' ou 'w'
$ ls -d D?[sw]*/
Desktop/  Downloads/
# Liste les dossiers débutant par 'D' et dont la 3ième lettre n'est ni 's' ni 'w'
$ ls -d D?[!sw]*/
Documents/

Expressions arithmétiques & bit-à-bit

  • Le shell est capable d’évaluer des expressions arithmétiques simples et bit-à-bit avec la notation suivante :

    $<expression>

  • Exemple :

# Division entière
$ echo $((10/6))
1
# Reste de la division entière (modulo)
$ echo $((10%6))
4
# Décalage d'un rang vers la droite
$ echo $((10>>1))
5
# OU bit à bit
$ echo $((10|7))
15

Substitution de commandes

  • Le shell sait capturer la sortie standard d’une commande pour l’utiliser par la suite.

  • Syntaxe : $( <commande> )

    • une autre syntaxe est possible mais n’est pas recommandée : ` <commande> `

  • Exemple :

# On mémorise dans 'annee' la sortie standard de la commande :
# date "+%Y" -> affichage de l'année courante
$ annee=$(date "+%Y")
$ echo $annee
2015
# On utilise ensuite la variable
$ echo "L'an prochain, nous serons en $((annee+1))."
L'an prochain, nous serons en 2016.

Protections des expressions

  • On a vu que certains caractères étaient interprétés par le shell.

  • Pour éviter cette interprétation, 3 moyens :

    1. l’antislash ou échappement (\) → annule le sens particulier du méta-caractère qui le suit.

      • Exception : l’antislash suivi d’un retour à la ligne

        ⇒ utilisé, par ex., pour continuer la saisie d’une commande à la ligne suivante

    2. les apostophes ('...') → tous les caractères compris entre les apostrophes perdent leur signification spéciale.

    3. les doubles guillemets ("…​") → annule le sens particulier des caractères qu’ils renferment à l’exception de $, ` (backquote) et, dans certaines conditions, de | puis !

Exécution contionnelle : if/then/else/fi

  • On retrouve une syntaxe familière :

    if cmd1
    then
       cmd2
    else
       cmd3
    fi
    # OU de manière plus condensée
    if cmd1 ; then cmd2 ; else cmd3 ; fi
    • cmd2 exécutée si cmd1 exécutée avec succès

    • cmd3 exécutée si cmd1 a échoué

  • cmd1 est généralement la commande test qui permet de tester une condition

    • tellement commun, qu’une abbréviation existe pour test

      test <condition> peut s’écrire [ <condition> ]

Exécution contionnelle : la commande test

  • Exemples :

    • test -f <fichier> : VRAI (code de retour 0) si le fichier existe

    • test -z <chaîne> : VRAI si la chaîne de caractères est vide

    • test <chaîne1> = <chaîne2> : VRAI les 2 chaînes sont identiques

    • test <valeur1> -lt <valeur2> : VRAI si <valeur1> est plus petite (less than) que <valeur2>

  • Mise en oeuvre : Déterminer le siècle de l’année courante

$ cat getcentury.sh
#! /bin/sh
annee=$(date "+%Y")
if test "$annee" -lt 2001 # ou [ "$annee" -lt 2001 ]
then
   echo "$annee -> 20ième siècle"
else
   echo "$annee -> 21ième siècle"
fi
$ ./getcentury
2015 -> 21ième siècle
  • toujours entourer les variables par des double côtes lorsqu’elles sont employées dans des conditions (→ gestion correcte des variables vides)

    if [ "$annee" -lt 2001 ]

    plutôt que : if [ $annee -lt 2001 ]

  • il est possible de combiner plusieurs conditions :

    Ex. : Combinaison de 2 conditions avec un OU logique

    if [ cond1 ] || [ cond2 ] ; then cmd1 ; fi

Exécution en boucle : for/do/done

  • Structure utilisée lorsque le nombre d’itérations est connu à l’avance

  • 2 formes :

    1. for <variable> in <list> ; do <cmds> …​ ; done

      <variable> prend successivement les valeurs présentes dans <list> et la séquence de commandes <cmds> …​ est exécutée suite à chaque affectation.

    2. for (( <expr1> ; <expr2> ; <expr3> )) ; do <cmds> …​ ; done

      <expr1> est évaluée 1 fois puis <expr2> est évaluée de manière répétitive jusqu’à ce qu’elle prenne la valeur 0. A chaque fois que <expr2> est évaluée comme différente de 0, la séquence de commandes <cmds> …​ est exécutée et <expr3> est évaluée.

  • Exemple 1 :

    $ cat comptine1.sh
    #! /bin/bash
    for i in {1..3} ; do echo -n "$i, "; done
    echo "... nous irons au bois"
    $ ./comptine1.sh
    1, 2, 3, ... nous irons au bois
  • Exemple 2 :

    $ cat comptine2.sh
    #! /bin/bash
    for ((i=4; i < 7; i++)) ; do echo -n "$i, "; done
    echo "... cueillir des cerises"
    $ ./comptine2.sh
    4, 5, 6, ... cueillir des cerises

Pour finir

Ce cours a permis d’aborder les mécanismes de base proposés par bash pour réaliser des sripts shell.

La puissance des scripts shell ne réside cependant pas uniquement sur ces mécanismes mais aussi sur la multitude et la cohérence des commandes mises à disposition par Linux.

La découverte de ces commandes dépasse le cadre de ce cours.

Il est donc recommandé de d’abord se renseigner sur les commandes “incontournables” de Linux avant d’implémenter des scripts shell “sérieux”.

Par exemple, des commandes comme tee, xargs, sed, grep, find (notamment avec son option -exec → voir Using the find -exec Command Option link), awk…​ peuvent être d’une grande utilité.

🞄  🞄  🞄