Comment puis-je faire en sorte que les programmes python se comportent comme des outils Unix appropriés?

24

J'ai quelques scripts Python qui traînent et je travaille à les réécrire. J'ai le même problème avec chacun d'eux.

Il n'est pas évident pour moi d'écrire les programmes pour qu'ils se comportent comme des outils Unix appropriés.

Car ce

$ cat characters | progname

et ça

$ progname characters

devrait produire la même sortie.

La chose la plus proche que j'ai pu trouver en Python était la bibliothèque d'entrée de fichiers. Malheureusement, je ne vois pas vraiment comment réécrire mes scripts Python, qui ressemblent tous à ceci:

#!/usr/bin/env python 
# coding=UTF-8

import sys, re

for file in sys.argv[1:]:
    f = open(file)
    fs = f.read()
    regexnl = re.compile('[^\s\w.,?!:;-]')
    rstuff = regexnl.sub('', fs)
    f.close()
    print rstuff

La bibliothèque fileinput traite stdin s'il y a un stdin et traite un fichier s'il y a un fichier. Mais il itère sur des lignes simples.

import fileinput
for line in fileinput.input():
    process(line)

Je ne comprends vraiment pas cela. Je suppose que si vous traitez de petits fichiers ou si vous ne faites pas grand-chose avec les fichiers, cela peut sembler évident. Mais, pour mes besoins, cela est beaucoup plus lent que d'ouvrir simplement le fichier entier et de le lire dans une chaîne, comme ci-dessus.

Actuellement, je lance le script ci-dessus comme

$ pythonscript textfilename1 > textfilename2

Mais je veux pouvoir le faire fonctionner (et ses frères) dans des tuyaux, comme

$ grep pattern textfile1 | pythonscript | pythonscript | pythonscript > textfile2
ixtmixilix
la source

Réponses:

9

Pourquoi pas

files = sys.argv[1:]
if not files:
    files = ["/dev/stdin"]

for file in files:
    f = open(file)
    ...
Mikel
la source
12
sys.stdindevrait être utilisé à la place car il est plus portable que le chemin d'accès au fichier en dur.
Piotr Dobrogost
sys.stdindevrait être utilisé à la place, comme le dit Piotr
smci
Mais sys.stdinc'est un fichier, et il est déjà ouvert, et ne doit pas être fermé. Impossible de gérer comme un argument de fichier sans sauter à travers des cerceaux.
alexis
@alexis Bien sûr, si vous souhaitez fermer fou utiliser un gestionnaire de contexte, vous avez besoin de quelque chose de plus complexe. Voir ma nouvelle réponse comme une alternative.
Mikel
12

Vérifiez si un nom de fichier est donné comme argument, ou bien lisez sys.stdin.

Quelque chose comme ça:

if sys.argv[1]:
   f = open(sys.argv[1])
else:
   f = sys.stdin 

C'est similaire à la réponse de Mikel, sauf qu'il utilise le sysmodule. Je pense que s'ils l'ont, ça doit être pour une raison ...

rahmu
la source
Que faire si deux noms de fichier sont spécifiés sur la ligne de commande?
Mikel
3
Oh absolument! Je n'ai pas pris la peine de le montrer, car cela figurait déjà dans votre réponse. À un moment donné, vous devez faire confiance à l'utilisateur pour décider de ses besoins. Mais n'hésitez pas à modifier si vous pensez que c'est le meilleur. Mon point est seulement de remplacer "open(/dev/stdin")par sys.stdin.
rahmu
2
vous voudrez peut-être vérifier if len(sys.argv)>1:au lieu de if sys.argv[1]:sinon vous obtenez une erreur d'index hors plage
Yibo Yang
3

Ma façon préférée de le faire se révèle être ... (et cela est tiré d'un joli petit blog Linux appelé Harbinger's Hollow )

#!/usr/bin/env python

import argparse, sys

parser = argparse.ArgumentParser()
parser.add_argument('filename', nargs='?')
args = parser.parse_args()
if args.filename:
    string = open(args.filename).read()
elif not sys.stdin.isatty():
    string = sys.stdin.read()
else:
    parser.print_help()

La raison pour laquelle j'ai préféré cela, c'est que, comme le dit le blogueur, il émet simplement un message stupide s'il est accidentellement appelé sans entrée. Il s'intègre également si bien dans tous mes scripts Python existants que je les ai tous modifiés pour l'inclure.

ixtmixilix
la source
3
Parfois, vous ne voulez saisir l'entrée de manière interactive qu'à partir d'un terminal; la vérification isattyet le renflouement ne sont pas conformes à la philosophie des filtres Unix.
musiphil
En dehors de la isattyverrue, cela couvre un terrain utile et important qui ne se trouve pas dans les autres réponses, donc cela obtient mon vote positif.
tripleee
3
files=sys.argv[1:]

for f in files or [sys.stdin]:
   if isinstance(f, file):
      txt = f.read()
   else:
      txt = open(f).read()

   process(txt)
JJoao
la source
C'est ainsi que je l'aurais écrit s'il /dev/stdinn'était pas disponible sur tous mes systèmes.
Mikel
0

J'utilise cette solution et cela fonctionne comme un charme. En fait, j'utilise dans un script calle unaccent qui minuscule et supprime les accents d'une chaîne donnée

argument = sys.argv[1:] if len(sys.argv) > 1 else sys.stdin.read()

Je suppose que la première fois que j'ai vu cette solution était ici .

SergioAraujo
la source
0

Si votre système n'en a pas /dev/stdin, ou si vous voulez une solution plus générale, vous pouvez essayer quelque chose de plus compliqué comme:

class Stdin(object):
    def __getattr__(self, attr):
        return getattr(sys.stdin, attr)

    def __enter__(self):
        return self

def myopen(path):
    if path == "-":
        return Stdin()
    return open(path)

for n in sys.argv[1:] or ["-"]:
    with myopen(n) as f:
            ...
Mikel
la source
Pourquoi déplacez-vous le pointeur de fichier à la sortie? Mauvaise idée. Si l'entrée a été redirigée à partir d'un fichier, le programme suivant la relira. (Et si stdin est un terminal, la recherche ne fait généralement rien, non?) Laissez-le tranquille.
alexis
Ouais, c'est fait. Je pensais juste que c'était mignon à utiliser -plusieurs fois. :)
Mikel