Automatisation et Shell avancé
Composition
Sous Linux, chaque commande est supposée ne faire qu'une seule chose, mais la faire bien (philosophie Unix : "Write programs that do one thing and do it well."). Les commandes sont ainsi simples à écrire, à tester, et à maintenir, augmentant significativement sa fiabilité. Notamment en évitant l'explosion combinatoire des cas à supporter et à tester.
Les comportements complexes émergent alors de l'interaction des différentes commandes entre elles. Il est ainsi possible de combiner une commande pour obtenir une liste de fichiers, une pour en obtenir la taille de chaque fichiers, une pour trier la liste, et enfin une pour reformater la liste. Individuellement, chaque commande est simple, mais leur combinaison permet des traitements plus complexes.
Entrée et sortie standard.
Sous Linux, les commandes communiquent principalement via leurs :
- Entrée standard (stdin - standard input) : ce que la commande lit, e.g. via en Python.
- Sortie standard (stdout - standard output) : ce que la commande écrit, e.g. via en Python.
- Sortie d'erreur (stderr - standard error) : pour les logs, e.g. erreurs, avertissements, informations de débogue, etc.
💡 Sous Linux, tout est fichier. L'entrée et les sorties d'une commande n'échappent pas à cette règle et sont eux aussi des fichiers spéciaux. Bien qu'on manipule usuellement des données textuelles, il est aussi possible de lire (ou d'écrire) des données binaires sur l'entrée (ou les sorties).
💡 pour fermer l'entrée standard d'une' commande en cours d'exécution.
Le Shell initialise et configure les entrées/sorties des commandes qu'il lance. Par défaut, il les redirige vers ses propres entrées et sorties. Il est cependant possible de les rediriger vers :
- : une autre commande, e.g. redirige la sortie standard de cmd1 vers l'entrée standard de cmd2.
- : un fichier (lecture), e.g. place le fichier sur l'entrée standard de la commande.
- : un fichier (écriture), e.g. redirige la sortie standard de la commande vers le fichier.
- et : une chaîne de caractère (pour l'entrée).
Il existe aussi quelques variations qui peuvent être combinées :
- : vers un fichier (concaténation), contrairement à ne supprime pas le contenu originel du fichier.
- ou : rediriger la sortie d'erreur.
- , ou : rediriger la sortie standard et la sortie d'erreur.
| Entrée | Sortie | Erreur | Sortie+Erreur | |
|---|---|---|---|---|
| Commande | ||||
| Fichier (écrasement) | ||||
| Fichier (concaténation) | NA | |||
Pour rediriger une chaîne de caractère vers l'entrée standard d'une commande, on utilise :
herestring (here string) :
heredoc (here document), si multiligne :
💡 Il est tout à fait possible de rediriger les flux après un heredoc, voire d'en utiliser plusieurs :
💡 pour insérer une tabulation lorsqu'on écrit une ligne de commande à partir du terminal.
💡 Les structures suivantes sont fréquemment utilisées :
- : enregistre les erreurs dans un fichier de logs.
- : permet de logger le flux entre c1 et c2.
Fichiers spéciaux
La redirection des sorties vers les entrées se fait via un tube anonyme (pipe), une sorte de buffer en RAM qui s'écrit et se lit comme un fichier. Les tubes suivent une politique FIFO (First In First Out), i.e. les première données écrites sont les premières à être lues.
Il est toutefois possible d'utiliser un tube nommé (named pipe), qui, à la différence d'un tube anonyme, possède un chemin sur le système de fichier (mais toujours stocké en RAM). Ainsi toute commande connaissant ce chemin (et possédant les droits nécessaires) pourra lire et écrire dans ce tube :
💡 créé un fichier (ou un dossier) unique et en retourne le chemin.
Sous Linux, les dossiers , , , et contiennent des fichiers spéciaux, notamment :
- : contient une infinité de zéros.
- : contient une infinité d'aléas.
- : écrire dedans ne fait rien.
💡 est fréquemment utilisé pour ignorer des flux de sorties, e.g. .
Groupements et substitutions
Il est possible d'exécuter une ligne de commande, de sorte à ce qu'elle soit considérée comme une unique commande :
- : dans la même instance shell.
- : dans une nouvelle instance shell.
Cela est notamment utilisé pour rediriger les flux d'une ligne de commande, et non plus seulement d'une seule commande. Ainsi redirige la sortie standard de la ligne de commande vers un même fichier. Pour le Shell, cela revient à exécuter la ligne de commande , avec cmd1_2 une commande correspondant à une autre ligne de commande.
Le Shell est aussi capable d'effectuer d'autres types de substitutions :
- : redirige la sortie standard vers un tube anonyme puis en retourne le chemin.
- : redirige l'entrée standard vers un tube anonyme puis en retourne le chemin.
- : retourne la sortie standard sous forme d'une chaîne de caractères.
- : retourne la valeur calculée (arithmétique).
⚠ Attention à bien mettre des espaces avant et après les délimiteurs.
💡 Certaines commandes prenant en paramètre un fichier acceptent parfois pour indiquer d'utiliser l'entrée ou la sortie standard au lieu d'un fichier. Cependant, ce comportement n'est pas défini par le Shell, mais par la commande elle-même.
Interactions Python-Shell
Les redirections d'E/S des commandes fonctionnent quelque soit le langage dans lequel est écrit la commande. Ainsi, il est tout à fait possible de rediriger la sortie d'un script Python, vers un script Bash, et inversement. Mais il est aussi possible d'appeler du code Python à partir d'un script Bash, et inversement.
Exécuter du Python à partir de Shell
Pour exécuter un code Python à partir d'un script Bash, il suffit de lui fournir code qu'on souhaite exécuter sur son paramètre . Cela permet d'effectuer des opérations potentiellement complexes à faire en Shell.
⚠ Pour éviter les attaques par injections, passez les données d'entrées soit par l'entrée standard, les paramètres, ou les variables d'environnement.
Exécuter du Shell à partir de Python
Pour exécuter des commandes Shell à partir de Python, un simple appel de fonction peut suffire. Cela permet d’effectuer des opérations d’administration système directement depuis Python.
⚠ Utilisez pour éviter les attaques par injections :
Code de sortie
Lorsqu'un programme se termine, il retourne s'il s'est exécuté correctement, ou un entier non-null s'il y a eu une erreur. Par défaut, un script bash retourne le code de la dernière commande exécutée. Il est cependant possible de quitter le script en précisant le code de sortie :
💡 affiche la signification du code, et liste les code standards.
Il est ainsi possible d'exécuter des commandes en fonction du code de sortie de la commande précédente :
- : exécuter cmd1 puis cmd2.
- : exécuter cmd1, puis cmd2, si pas d'erreurs (code de sortie 0).
- : exécuter cmd1, puis cmd2, si erreurs (code de sortie non-null).
- 💡 : simule un opérateur ternaire.
Paramétrisation
Variables
Comme en Python, il est possible d'utiliser des variables dans Shell. Cependant, leur utilisation diffère légèrement :
⚠ Pensez à bien déclarer vos variables avec avant de les utiliser
⚠ Comme vous le savez, le Shell ré-interprète certains caractères. Par exemple, les espaces indiquent le début d'un nouvel argument. Pour éviter cela, il convient d'utiliser des guillemets :
- ne faire aucune interprétation.
- interpréter les variables et conserver les espaces.
- interpréter les caractères échappés (e.g. \t, \n, \e).
💡 Il est possible d'utiliser des guillemets simples pour empêcher l'interprétation des variables dans les heredocs :
Variables d'environnement
Les variables d'environnement sont des variables spéciales dont une copie est donnée à chaque processus enfant du Shell, i.e. aux commandes lancées et sous-Shell créés (e.g. avec ). Elles sont utilisées pour définir des informations contextuelles ou de configurations "globales", sans avoir à les renseigner à chaque appel.
Le Shell fournit déjà de nombreuses variables d'environnements, dont :
- : l'utilisateur.
- : le dossier home.
- : le dossier de travail.
- : dossiers dans lesquels chercher les commandes.
Vous pouvez accéder à ces variables directement en Shell, ou via :
- la commande .
- en Python.
Il existe plusieurs manière de définir/modifier une variable d'environnement en Shell :
- : pour le Shell actuel.
- : pour la commande lancée.
Pour définir une variable d'environnement au niveau du système, on exporte alors les variables à partir de :
- pour un utilisateur.
- pour tous les utilisateurs.
⚠ Exporter des variables d'environnement ne modifie pas leurs valeurs dans les processus déjà lancés (e.g. shell, commandes). Pour mettre à jour les variables d'environnement dans un shell, il faut alors exécuter le bashrc dans le shell courant :
La commande permet en effet de lancer un script dans le Shell actuel, sans lancer de sous-Shell. C'est à dire que le script s'exécutera dans le contexte du Shell actuel, et aura donc accès à ses variables, et pourra ainsi les modifier.
💡 est notamment utilisé pour importer des bibliothèques Shell, initialisant des variables d'environnement, définissant des alias ou des fonctions, etc.
Lire l'entrée standard
Usuellement on utilise la commande pour lire l'entrée standard, cependant elle est très piégeuse. Nous vous fournissons donc une série de fonctions (dont vous pourrez regarder le code au besoin), pour lire :
- : une ligne.
- : un caractère.
- : un secret.
- : une liste d'arguments.
💡 pour ajouter un prompt.
⚠ Il n'est pas possible de stocker le caractère dans une variable Shell.
Afin d'éviter tout problème de parsing, nous vous conseillons de transférer vos structures de données en utilisant JSON. Pour cela nous vous fournissons deux fonctions (dont vous pourrez regarder le code au besoin) :
- (doit être déclaré avec ).
Fonctions et arguments
Les arguments passés au script sont accessibles via les variables Shell suivantes :
- : liste des arguments.
- : commande appelée.
- : N-ième argument.
- : nombre d'arguments.
En Shell, une fonction est considérée comme une commande interne (builtin). On peut ainsi aussi utiliser , , :
💡 permet de quitter une fonction en en spéficiant le code de sortie. quant à elle indique le code de retour de la dernière commande/fonction exécutée.
💡 Il est aussi possible d'utiliser ou pour lire l'entrée standard de la fonction :
Structures de contrôle
Shell fournit aussi les structures de contrôle classiques :
et prennent en arguments une ligne de commande. La condition est considérée vraie si la commande réussie (i.e. code de retour nul). Usuellement, on utilise les commandes :
- pour des comparaisons arithmétiques.
- pour des comparaisons lexicographiques.
💡 peut aussi être utilisé pour effectuer des opérations arithmétiques, e.g. .
💡 possède de nombreuses options, e.g. :
- pour des regex.
- : est un fichier.
- : est un dossier.
💡 et sont des commandes retournant respectivement 0 et 1. Ce qui est l'inverse de ce qu'on trouve usuellement dans les autres langages, où une valeur vraie est usuellement associée à 1, et une valeur fausse à 0.
💡 attend un ensemble d'arguments à parcourir. Il est alors fréquent d'utiliser afin de générer des valeurs allant de à inclus.
Structures de données
En Shell, il est possible d'utiliser des tableaux indexés, ainsi que des tableaux associatifs.
💡 transforme la liste des arguments en un tableau indexé, facilitant son parcours.
Il est aussi possible d'effectuer des manipuler de chaînes de caractères. Cependant, ces manipulations ne sont pas toujours explicites et lisibles, d'autant plus si on les chaîne :
💡 Il existe aussi de multiples commandes avec de multiples options :
- : trier.
- : sélectionner des lignes correspondant à un motif.
- : découper des lignes et en sélectionne les champs.
- (translate): remplacer des caractères
- : formatter la sortie en colonnes.
- (JSON query): requêter des données JSON.
- (stream editor) : effectuer des substitutions, sélections, ou suppressions de lignes.
Il est ainsi parfois difficile d'en comprendre le sens, sans avoir retenu la signification de chacun des symboles, structures, et options... sachant que certaines variations n'ont pas été inclues dans la liste ci-dessus. Il est ainsi recommandé d'encapsuler (et de documenter) les transformations complexes dans une fonction Shell, que vous pourrez facilement combiner avec d'autres commandes, et qui rendra le code bien plus explicite. Voire même, d'écrire le traitement en Python si la manipulation est trop complexe (ou si vous n'êtes pas suffisamment à l'aise avec les transformations Shell) :