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 : graphique → GUI : Graphical User Interface 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 ? 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 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 : l’enchainement conditionnel de commandes exécution ou non d’une commande en fonction du code de retour de la précédente commande 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 . 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 . 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 : variables utilisateurs ⇒ variables définies par l’utilisateur. ex. : $mon_age variables d’environnement ⇒ variables définies par le système. ex. : $PATH 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 : 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 les apostophes ('...') → tous les caractères compris entre les apostrophes perdent leur signification spéciale. 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 : 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. 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 ), awk… peuvent être d’une grande utilité. 🞄 🞄 🞄 Pratique du shell Linux Raspberry Pi