-
Notifications
You must be signed in to change notification settings - Fork 8
7. Comme Shell
Tout d'abord, clarifions ce que nous entendons par : shell. Le shell est l'interpréteur de commandes de base d'un système d'exploitation. Lorsque vous utilisez cd, ls, rm, vous utilisez les instructions du shell. C'est aussi simple que cela...
Ceux qui ont développé le shell d'Unix ont fait un travail remarquable, même si on peut parfois hésiter sur la pertinence de certains noms de commandes, dont la forme cryptique est parfois difficile à interpréter pour un néophyte. Cependant, le concept principal au cœur d'Unix, le pipe a été une révolution. A une époque où les machines ne disposaient que de très peu de mémoire, la possibilité d'enchaîner simplement les traitements ligne par ligne a soudain permis de passer à l'échelle des exécutions qui nécessitaient jusqu'alors de gros programmes spécifiques. Ainsi, comme le raconte Brian Kernighan, l'inventeur de l'interpréteur awk avec Aho et Weinberg, on pouvait désormais compter la présence de mots particuliers au sein d'un document trop gros pour tenir en mémoire, en une seule ligne de code.
Ce n'est pas un hasard si les noms de Aho, Weinberger et Kernighan apparaissent ici (awk est la combinaison de leurs initiales). En effet, Unix n'a pas seulement introduit la notion de pipe, il a aussi généralisé l'utilisation des expressions régulières. Et awk a été conçu pour tirer le meilleur parti de ces expressions.
Grâce à awk, les utilisateurs ont enfin pu agir sur les données retournées par le système d'exploitation. awk a introduit des concepts assez révolutionnaires, comme les dictionnaires et les champs extraits de la ligne d'entrée. Cependant, awk a une syntaxe assez peu orthodoxe et relire un vieux script peut parfois être un défi. Je ne veux pas entrer dans une quelconque controverse à ce sujet, je suis un utilisateur enthousiaste de awk depuis longtemps, dans la douleur, mais heureux d'avoir eu un outil aussi utile sous la main.
Et pendant longtemps, je me suis demandé s'il pouvait y avoir une alternative à awk...
LispE est un interpréteur Lisp maison, écrit dans l'espoir d'implémenter un code C++ aussi élégant et lisible que possible. Il est difficile pour moi de quantifier objectivement si j'ai atteint le résultat souhaité, mais je pense avoir produit l'un des codes les plus propres de toute ma carrière. Si je ne devais avoir qu'une seule mesure, ce serait une comparaison avec mes travaux antérieurs, dont la plupart étaient assez grossiers, tant en termes d'implémentation que de conception.
L'avantage de produire votre propre interpréteur est que vous pouvez en faire ce que vous voulez... Vraiment ce que vous voulez...
Par exemple, l'utiliser comme une alternative à awk.
Je peux déjà entendre des commentaires amusés : Vraiment, vous vous plaigniez de la syntaxe exotique de awk et vous voulez utiliser Lisp à la place ?
A cela, je ne répondrais qu'une chose... Ok ! C'est vrai... Disons que Lisp est un peu saturé en parenthèses.
Mais, le langage est puissant, très flexible avec une syntaxe à la fois très simple et fonctionnelle (dans les deux sens du terme).
Il ne faut presque rien pour en faire un beau langage de shell.
LispE offre un environnement où il est possible d'éditer et d'exécuter directement des instructions Lisp. En particulier, LispE offre un interpréteur interactif qui permet, par exemple, de tester des instructions ou de vérifier la valeur de variables globales à la fin d'une exécution.
De plus, cet interpréteur interactif permet d'exécuter des instructions à partir du shell. Pour cela, il suffit de faire précéder l'instruction d'un " !".
◀▶1> !ls
Il est même possible de stocker le résultat de cette instruction dans une variable Lisp. Il suffit d'écrire : !var=instruction pour que var reçoive le contenu de l'exécution de instruction :
◀▶1> !v=ls
En fait, cette instruction est traduite en une instruction Lisp sous-jacente :
(setq v (commande "ls"))
Si vous parcourez l'historique, vous trouverez certainement cette ligne. command est une instruction fournie par LispE qui permet l'exécution d'une commande shell et renvoie le résultat de cette commande sous la forme d'une liste.
Mais LispE va plus loin. Nous avons également ajouté des options supplémentaires en ligne de commande, qui permettent d'intégrer un appel à LispE dans une séquence de pipes.
C'est l'option la plus simple. Elle est utilisée sur la ligne de commande devant un pipe.
ls -al | lispe -a
LispE lit ce que le pipe renvoie et initialise une liste : _args, où chaque élément correspond à une ligne lue sur stdin.
LispE est ensuite lancé comme interpréteur interactif, ce qui vous permet de construire le code nécessaire étape par étape, à votre propre rythme.
ls -al | lispe -a
_args contient donc :
("." ".." ".DS_Store" ".git" "Lispe" "Makefile" "Makefile.in" "README.md" "check")
Il est également possible de construire un programme capable d'examiner le contenu de cette liste et de la traiter :
ls -al | lispe program.lisp -a
Notons que si nous voulons éditer le programme dans LispE, nous devons placer la commande -e après :
ls -al | lispe -a -e programme.lisp
Cette option, en revanche, vous permet de placer réellement l'interpréteur dans une séquence de pipe.
Sa syntaxe est très simple :
ls -al | lispe -p 'instructions'.
Les instructions sont exécutées pour chaque nouvelle ligne provenant du pipe, contrairement à -a qui lit toutes les lignes à l'avance.
Ce qui rend cette option intéressante est le fait d'avoir accès à des variables particulières :
accu1,accu2,...,accu9 : Neuf accumulateurs prédéfinis initialisés à 0 au démarrage
l0 : est la ligne courante complète
l1,l2,l3... : sont des variables correspondant à un champ de 'l0'.
ln : correspond au nombre de champs trouvés dans 'l0'.
ll : est une liste composée de tous les champs.
Les champs proviennent de la division de la ligne complète le long des espaces.
ls -al | lispe -p 'l10'
# donne :
# .
# ..
# DS_Store
# .git
# Lispe
# Makefile
# Makefile.in
# README.md
# check
# checkrgx.py
# ...
# Nous calculons la taille de tous les fichiers dans le répertoire
ls -al | lispe -p '(+= accu1 l6)'.
544
800
11044
...
Notez que les champs ne contenant que des chiffres deviennent des valeurs numériques :
ls -al | lispe -p '(print (type l2) " " (type l3))'
# integer_ string_
# integer_ string_
# integer_ string_
# integer_ string_
...
Les instructions sont exécutées dans un block. La dernière instruction renvoie donc le résultat final :
ls -al | lispe -p '(type l2) (type l3)'.
# Ici, seul (type l3) est affiché, même si l'ensemble a été exécuté
# string_
# string_
# string_
...
Vous pouvez utiliser ll pour itérer sur tous les champs :
ls -al | lispe -p '(loop v ll (print (type v) " ")'
Ces options doivent être placées avant -p.
- -pb : Permet d'exécuter du code avant que -p ne soit exécuté.
- -pe : Permet d'exécuter du code à la fin de l'exécution de -p.
ls -al | lispe -pb '(setq s 0) -pe '(println "Sum=" s)' -p '(+= s l5)'
Permet de calculer la somme de la taille des fichiers présents dans un répertoire.
Cette option fonctionne sur le même principe que -p mais au lieu de prendre du code, elle utilise un fichier contenant un programme.
IMPORTANT: ce fichier doit contenir une fonction runpipe qui sert de point d'entrée au programme.
ls -al | lispe -P programme.lisp
et programme.lisp doit contenir au moins la fonction runpipe :
(defun runpipe()
(println ll)
)
Cette fonction sera appelée pour chaque nouvelle ligne provenant de stdin.
Pour s'assurer que la transformation vers un langage shell est complète, nous ajoutons les options -r/-R associées à -p/-P.
Ces options vérifient si une expression régulière correspond à la ligne d'entrée avant d'exécuter ou non le code donné avec -p/-P.
La différence entre -r et -R porte sur la nature des expressions régulières :
- -r prend les expressions régulières posix en entrée
- -R prend les expressions régulières internes de LispE (voir LispE Regular Expressions)
# La forme suivante filtre les lignes qui ne contiennent pas au moins 3 chiffres en séquence
ls -al | lispe -r "\d\d\d" -p "l0"
# ce qui donne :
# -rw-r--r-- 1 roux NLE\Domain Users 10244 26 Sep 16:42 .DS_Store
# -rwxr-xr-x@ 1 roux NLE\Domain Users 1715 30 Sep 09:18 Makefile
# -rwxr-xr-x@ 1 roux NLE\Domain Users 3297 23 Sep 14:02 checkrgx.py
# Même chose mais avec une expression régulière interne (voir documentation)
ls -al | lispe -R "%d%d%d" -p "l0"