Patrice Ferlet
Patrice Ferlet
Créateur de ce blog.
Publié le mai 30, 2014 Temps de lecture: 8 min

Me suivre sur Mastodon

Python en mode one liner

Python, c’est cool, c’est fun, c’est lisible et en plus c’est super bien intégré aux Unix. Alors quand on peut en plus s’en servir comme “one liner”…

Bon tout le monde connait sed, awk, perl etc… ils permettent d’être utilisés en “one liner” c’est-à-dire à traiter en une ligne des données injectées (généralement depuis un fichier ou une redirection dans STDIN). Sauf que Python n’est pas en reste… ou plutôt il l’est mais on peut rapidement se créer son petit outil qui avoir le même effet que sed, awk et consorts.

Non parce que bon, sérieusement, au taff on me sort souvent “pfff mais c’est quoi ce truc” en me montrant du doigt des instructions awk que j’ai tapé… ou encore “hey mais comment tu fais pour récupérer tel élément avec sed là ?”. Car voilà, au taff, on est surtout des Pythonistes… alors quand ils voient des variables avec un dolar devant ils sont perdus (si un collègue lis mon article: te vexe pas, je taquine :) )

Il existe un projet nommé Pyped que je trouve vraiment super, mais bon il demande pas mal de deps et pis bon hein… c’est du Python 2.7. Non pas que j’aime pas Python 2.7, mais faut commencer à utiliser Python 3 un jour…

Du coup je vous montre celui que j’ai codé, et dans la foulée ça fera un petit tuto. On commence, allons-y gaiement !

Lire STDIN, tout un programme

L’idée est de pouvoir injecter des lignes dans le STDIN du script, et ce depuis pas mal de sources possible (fichier, commande shell, …).

On va utiliser “sys.stdin” (quelle suprise !). Pour lire une ligne, on utilise “sys.stdin.readline()”.

Jusque là c’est facile. Mais j’aime les piments, alors on pimente un peu: je sais qu’un jour ou l’autre je vais me prendre plusieurs centaines de mega, voir des gigas de donnée dans STDIN… alors je vais “itérer” sur les lignes de données. Et devinez quoi, y’a un built-in Python génialissime pour ça: iter().

“iter” est un petit cachotier, car on le connait pour sa forme iter(list), mais bien moins pour sa forme “iter(callable, sentinel)”. C’est très simple il va retourner ce que le callable génère jusqu’à ce que la sentinel soit trouvée. En l’occurence, nous, on veut lire sys.stdin.readline jusqu’à ce qu’elle nous retourne plus rien… hahaha on tente ?

#!/usr/bin/python3 -u
import sys

for x in iter(sys.stdin.readline, ''):
    x = x.rstrip('\n')
    print(x)

Remarquez bien que j’utilise “python 3” et l’option “-u” pour utiliser STDIN et STDOUT en mode “non bufferisé”

Bon, vous rendez ce p’tit script exécutable et vous testez:

$ chmod +x pype.py
$ dmesg | tail | pype.py

Cool hein, on vient de faire un script qui affiche la sortie standard. Voilà c’est tout pour aujourd’hui… non je déconne, on va aller plus loin :)

Bon, on sait lire de manière propre une entrée standard. Reste maintenant à faire en sorte de traiter ces lignes.

Dans Python (version 3 pour l’heure) il existe la commande “exec”. Elle permet d’une part d’éxecuter du code python, mais prend en second et troisième paramètres des dictionnaires qui représentent respectivement les variables globales et locales. On appelle souvent ces attributs des “contextes” (ceux qui utilisent Jinja connaissent le principe). Chaque clef du dict contexte est le nom d’une variable accessible dans les instructions passées à “exec”.

Donc, on va permettre de prendre en argument un truc à exécuter. Vous avez compris… sys.argv va être notre allié.

#!/usr/bin/python3 -u
    # -*- encoding: utf-8 -*-

import sys

ctx = globales()
cmd = sys.argv[1]

for x in iter(sys.stdin.readline, ''):
            # on va donner une variable "x" qui est
            # la ligne STDIN récupérée
    ctx["x"] = x.rstrip('\n')
    exec(cmd, ctx)

On va donc s’amuser, par exemple lister le répertoire “/etc” et n’afficher que les fichiers qui ont “.conf” dans leur nom:

$ ls -1 /etc | pype.py 'if x.find(".conf"): print(x)'

Pas mal hein :) on va maintenant aller un peu plus loin. J’adore “enumerate” qui retourne l’index et l’entrée d’une liste. Allez, on y va.

#!/usr/bin/python3 -u
    # -*- encoding: utf-8 -*-

import sys

ctx = globales()
cmd = sys.argv[1]

for i, x in enumerate(iter(sys.stdin.readline, '')):
            # on va donner une variable "x" qui est
            # la ligne STDIN récupérée
            # et "i" est le numéro de ligne
    ctx["x"] = x.rstrip('\n')
    ctx["i"] = i
    exec(cmd, ctx)

On peut tester:

$ dmesg | tail | pype.py "print('%d -- %s' % (i, x))"

Si tout se passe bien, vous devez avoir le numéro de la ligne, puis deux tirets et le message récupéré dans STDIN, sinon c’est que j’ai foiré un truc dans mes exemples…

Bon reste un souci…

Si j’ai besoin d’un import “lourd”, et que je le met dans la ligne d’exécution, chaque ligne rencontrée va lancer l’import du module. C’est rageant. Le mieux, tout comme le propose awk, c’est d’avoir un block “begin” qui n’est exécuté qu’au début, et pourquoi pas un un block “end”. Et comme Python est founis en outils pour gérer facilement des trucs, on va utiliser “optparse”.

Le résultat est cool:

#!/usr/bin/python3
# -*- encoding: utf8 -*-

import optparse
import sys

# prepare options
parser = optparse.OptionParser()
parser.add_option('-e', '--end', 
            action="store", 
            dest="end", 
            help="end code", 
            default=None)
parser.add_option('-b', '--begin', 
            action="store", 
            dest="begin", 
            help="begin code", 
            default=None)

options, expr = parser.parse_args()

# now, save ctx (and get imports fro BEGIN if any)
ctx = globals()

#execute BEGIN block
if options.begin is not None:
    exec(options.begin, ctx)


#joining all commands
expr = "\n".join(expr)

# for each stdin line, stop if empty
for i, x in enumerate(iter(sys.stdin.readline, '')):
    ctx['x'] = x.rstrip('\n')
    ctx['i'] = i
    exec (expr, ctx)

# execute END code
if options.end is not None:
    exec (options.end, ctx)

Donc, là, pour tout vous expliquer:

  • On propose deux options, -b (–begin) et -e (–end) qui permettent de donner des instructions à exécuter au début et à la fin du travail (import, ouverture de fichier, etc…) les variables seront accessibles dans les block d’instructions
  • on récupère touts les autres paramètre qui sont les instructions à traiter. C’est une liste, donc je les join avec “n”.join(…)
  • on intègre dans le contexte deux variables: x => la ligne capturée dans STDIN et “i” qui est le numéro de la ligne à traiter (sympa pour compter)

Comprennez bien, j’utilise le contexte “ctx” dans les 3 “exec”, du coup les variables et import créés dans le block begin sont accessible dans les blocks d’instructions et le block “end”; et par conséquent une variable créée dans le block principal sera accessible dans le block “end”. Le context est clairement l’environnement de globales qui se promène partout.

Du coup, là, on peut faire:

$ ls -1 /etc | ./main.py \
-b "import re; counter = 0" \
-e "print('Founds: %d' % counter)" \
"if re.findall('.*.conf', x):
     print('found %s at %d' % (x, i))
     counter += 1
"

(les backslash sont là parce que dans mon blog ça déborde, ça permet de revenir à la ligne sans exécuter l’instruction… si vous le souhaitez, vous pouvez ne pas les mettre et tout faire en une ligne, d’où le nom “one liner” hein)

On charge “re” au début et on initialise un compteur “counter”, puis pour chaque ligne on utilise le module.

A la fin… on acffiche le compteur.

Bref, c’est pas idiot comme script, et ça peut être (pour les Pythonistes) plus agréable que awk ou sed.

Et un zeste de Cython

Allez, on va gagner le temps d’interprétation et (dans certains cas) accélerer un chouillat le tratement de notre script. Bon ok, c’est pas super avantageux mais juste pour l’amusement… on va compiler notre script pour en faire un binaire exécutable natif:

$ cython --embed pype.py
$ gcc pype.c -o pype -O9 $(pkg-config --libs --cflags python3)

Vous avez maintenant un binaire “pype” que vous pouvez copier dans un répertoire de votre $PATH, par exemple dans “~./local/bin”. Le binaire doit faire dans les 40ko.

Alors oui, on peut améliorer le truc, on peut utiliser le projet Pyped (su-cité), mais bon sang… que c’est cool de le faire soi-même !

Un mot pour la fin

De mon coté, mon script est à peine différent que celui que je vous propse; en fait j’ai juste ajouté des “import” pour que le contexte ait accès à des modules que je trouve indispensable:

  • import re -> pour les expressions régulières, je m’en sers tout le temps
  • import json -> on ne le présente plus
  • import os -> parfois utile pour la résolution de path

Tout ce que vous avez à faire, c’est de les mettre dans pype.py au début du script. Comme nous initialisons le contexte avec “globales()”, ces modules sont accessible dans les block d’instructions. Vous pouvez le recompiler avec cython, et le tour est joué.

A vous de vous amuser, de mettre les modules qui vous intéressent, d’adapter mon script (qui est libre), de l’améliorer. Il est clair que je le fais ici en Python, mais les PHPistes pourront le faire en PHP, les Javaistes ne pourront pas (oui bha une boutade de temps en temps hein…), et je suis sûr que c’est faisable en NodeJS.

Allez à vous !

comments powered by Disqus