pipes - Template di pipeline di comandi della shell Unix

Scopo Crea pipeline di comandi della shell Unix ripetibili
Versione Python 1.4

Il modulo pipes implementa una classe per gestire pipeline di comandi Unix arbitrariamente complesse, Gli input ed output dei comandi possono essere concatenati come con l'operatore  |  della shell; anche se i singoli comandi devono scrivere su o leggere da file invece che da stdin/stdout.

Passare Attraverso una Pipe lo Standard I/O

Un esempio molto semplice, passare lo standard input attraverso una pipe e ricevere i risultati in un file potrebbe essere il seguente:

import pipes
import tempfile

# Si predispone una pipeline molto semplice usando stdio
p = pipes.Template()
p.append('cat -', '--')
p.debug(True)

# Si passa del testo attraverso la pipeline,
# salvando l'output in un file temporaneo
t = tempfile.NamedTemporaryFile(mode='r')
f = p.open(t.name, 'w')
try:
    f.write('Porzione di testo')
finally:
    f.close()

# Ritorno ad inizio file e lettura del testo scritto
# nel file temporaneo
t.seek(0)
print t.read()
t.close()

Viene creato l'oggetto pipelines.Template, quindi viene aggiunto un singolo comando, cat -. Il comando legge lo standard input e lo scrive allo standard output, senza modifiche. Il secondo parametro per append() codifica le sorgenti di input ed output per il comando in due caratteri (input, quindi output). L'uso di  -  significa che il comando usa lo standard I/O. L'uso di  f  significa che il comando deve leggere da un file (come potrebbe essere nel caso di una pipeline per l'elaborazione di un'immagine).

Il metodo debug() attiva/disattiva l'output di debug. Quando il debug è abilitato, i comandi che sono eseguiti vengono stampati ed alla shell viene passato set -x in modo che venga eseguito in modalità dettagliata ("verbose").

Dopo che è stata impostata la pipeline, viene creato un NamedTemporaryFile per fornire alla pipeline un file su cui scrivere il suo output. Occorre sempre specificare un file come parametro per open(), sia in lettura che scrittura.

$ python pipes_simple_write.py
cat - >/tmp/tmpqdcQW4
+ cat -
Porzione di testo

Leggere da una pipeline funziona praticamente allo stesso modo, con poche modifiche ai parametri. Per questo esempio, occorre impostare il contenuto del file di input prima di aprire la pipeline. Quindi si può passare quel nome di file come input di open().

import pipes
import tempfile

# Si predispone una pipeline molto semplice usando stdio
p = pipes.Template()
p.append('cat -', '--')
p.debug(True)

# Si imposta un file di input
t = tempfile.NamedTemporaryFile(mode='w')
t.write('Porzione di testo')
t.flush()

# Si passa del testo attraverso la pipeline,
# salvando l'output in un file temporaneo
f = p.open(t.name, 'r')
try:
    contents = f.read()
finally:
    f.close()

print contents

Si può leggere il risultato dalla pipeline direttamente

python pipes_simple_read.py
cat - </tmp/tmps_ABth
+ cat -
Porzione di testo

Usare i File invece che i Flussi

Alcuni comandi devono lavorare sui file in un filesystem invece che con flussi di input. Comandi che elaborano un grande numero di dati potrebbero fornire prestazioni migliori in questo modo, visto che non si bloccheranno sul prossimo comando che sta leggendo il loro output. Qualsiasi cosa che funzioni su dati non basati su flussi deve avere questa capacità (es. i database od altri strumenti di manipolazione di file binari). Per supportare questa modalità operativa, append() consente di specificare un tipo di  f , ed il codice della pipeline creerà i necessari file temporanei. I nomi di file sono passati alla shell come e , quindi questi nomi di variabili devono comparire nella propria stringa di comandi.

import pipes
import tempfile

p = pipes.Template()
p.append('cat $IN > $OUT', 'ff')
p.debug(True)

t = tempfile.NamedTemporaryFile('r')
f = p.open(t.name, 'w')
try:
    f.write('Porzione di testo')
finally:
    f.close()

t.seek(0)
print t.read()
t.close()

Così si vede, sono stati creati parecchi file temporanei intermedi per mantenere l'input e l'output di ogni passo.

$ python pipes_file_kind.py
trap 'rm -f /tmp/tmpDWnC9N; exit' 1 2 3 13 14 15
cat >/tmp/tmpDWnC9N
IN=/tmp/tmpDWnC9N; OUT=/tmp/tmp79Dd6d; cat  > 
rm -f /tmp/tmpDWnC9N
+ trap rm -f /tmp/tmpDWnC9N; exit 1 2 3 13 14 15
+ cat
+ IN=/tmp/tmpDWnC9N
+ OUT=/tmp/tmp79Dd6d
+ cat /tmp/tmpDWnC9N
+ rm -f /tmp/tmpDWnC9N
Porzione di testo

I valori kind di input ed output possono essere mescolati, in modo che diversi passi della pipeline possano usare a piacimento i file o lo standard I/O.

import pipes
import tempfile

p = pipes.Template()
p.append('cat $IN', 'f-')
p.append('cat - > $OUT', '-f')
p.debug(True)

t = tempfile.NamedTemporaryFile('r')
f = p.open(t.name, 'w')
try:
    f.write('Porzione di testo')
finally:
    f.close()

t.seek(0)
print t.read()
t.close()

Le istruzioni trap che si vedono nell'output assicurano che i file temporanei creati dalla pipeline siano puliti anche se una attività nel mezzo fallisce oppure se la shell viene chiusa.

$ python pipes_mixed_kinds.py
trap 'rm -f /tmp/tmpN_eiEw; exit' 1 2 3 13 14 15
cat >/tmp/tmpN_eiEw
IN=/tmp/tmpN_eiEw; cat  |
{ OUT=/tmp/tmp1BUupk; cat - > ; }
rm -f /tmp/tmpN_eiEw
+ trap rm -f /tmp/tmpN_eiEw; exit 1 2 3 13 14 15
+ cat
+ IN=/tmp/tmpN_eiEw
+ cat /tmp/tmpN_eiEw
+ OUT=/tmp/tmp1BUupk
+ cat -
+ rm -f /tmp/tmpN_eiEw
Porzione di testo

Un Esempio più Complesso

Tutti gli esempi fino a qui sono stati piuttosto rozzi. Sono stati concepiti per illustrare l'uso di pipes.Template() senza dovere dipendere da una profonda conoscenza della programmazione della shell in generale. Questo esempio è più complesso, e mostra come parecchi comandi possano essere combinati per manipolare i dati prima di portarli all'interno di Python.

Lo script virtualwrapper comprende una funzione di shell per elencare tutti gli ambienti virtuali che si sono creati. La funzione viene usata per il completamento con il tasto "tab" e può essere chiamata direttamente per elencare gli ambienti, nel caso ci si dimentichi un nome. Il cuore di quella funzione è una piccola pipeline che cerca in le directory che potrebbero sembrare degli ambienti virtuali (es. hanno uno script activate). Quella pipeline é:

(cd ""; for f in */bin/activate; do echo ; done) \
    | sed 's|^\./||' \
    | sed 's|/bin/activate||' \
    | sort

Implementata usando pipes la pipeline diventa:

import pipes
import pprint
import tempfile

p = pipes.Template()
p.append('cd "$WORKON_HOME"; for f in */bin/activate; do echo $f; done', '--')
p.append(r"sed 's|^\./||'", '--')
p.append("sed 's|/bin/activate||'", '--')
p.append('sort', '--')

t = tempfile.NamedTemporaryFile('r')

f = p.open(t.name, 'r')
try:
    sandboxes = [ l.strip() for l in f.readlines() ]
finally:
    f.close()

print 'SANDBOXES:'
pprint.pprint(sandboxes)

Visto che ogni nome di sandbox viene scritto in una riga a parte, l'analisi dell'output è semplice:

$ python pipes_multistep.py
SANDBOXES:
['CastSampler',
 'bbtest',
 'bill',
 'blogbackup',
 'cursive.tools',
 'docket',
 'docket-new',
 'doughellmann',
 'email_recovery',
 'ical2org',
 'mytweets',
 'nose',
 'odtwritertest',
 'paramiko',
 'personal',
 'psf-sprints',
 'psfboard',
 'pulse',
 'pyatl',
 'pymag',
 'pymotw',
 'pymotw-ja',
 'pymotw26',
 'python-dev',
 'racemi',
 'racemi_status',
 'reporting_server',
 'rst2marsedit',
 'sphinx-contrib',
 'sphinx-contrib.old',
 'virtualenvwrapper']

Passare File Attraverso le Pipeline

Se l'input della propria pipeline esiste già in un file su disco, non c'è necessità di leggerlo dentro Python semplicemente per passarlo alla pipeline. Si può usare il metodo copy() per passare il file direttamente attraverso la pipeline e creare un file di output per la lettura.

import pipes
import tempfile

p = pipes.Template()
p.debug(True)
p.append('grep -n tortor $IN', 'f-')

t = tempfile.NamedTemporaryFile('r')

p.copy('lorem.txt', t.name)

t.seek(0)
print t.read()
t.close()
$ python pipes_copy.py
+ IN=lorem.txt
+ grep -n tortor lorem.txt
IN=lorem.txt; grep -n tortor  >/var/folders/9R/9R1t+tR02Raxzk+F71Q50U+++Uw/-Tmp-/tmplLJu9t
3:elementum elit tortor eu quam. Duis tincidunt nisi ut ante. Nulla
6:lacus. Praesent placerat tortor sed nisl. Nunc blandit diam egestas
11:eget velit auctor tortor blandit sollicitudin. Suspendisse imperdiet

Clonare Template

Una volta che si ha un template di pipeline, si potrebbe volerlo usare per diverse volte o creare varianti senza ricostruire l'intero oggetto. Il metodo clone() rende semplici entrambe le operazioni. Questo esempio costruisce una semplice pipeline per contare le parole, quindi antepone i comandi ad un paio di cloni per farli cercare parole diverse.

import pipes
import tempfile

count_word_substring = pipes.Template()
#count_word_substring.debug(True)
count_word_substring.append('grep -f - /usr/share/dict/words', '--')
count_word_substring.append('wc -l', '--')

count_py = count_word_substring.clone()
count_py.prepend('echo "py"', '--')
f = count_py.open('/dev/null', 'r')
try:
    print '  "py": %5s' % f.read().strip()
finally:
    f.close()

count_perl = count_word_substring.clone()
count_perl.prepend('echo "perl"', '--')
f = count_perl.open('/dev/null', 'r')
try:
    print '"perl": %5s' % f.read().strip()
finally:
    f.close()

Anteponendo un comando personalizzato per ogni clone, si possono creare pipeline separate che eseguono la stessa funzione base con piccole variazioni.

$ python pipes_clone.py
  "py":  1379
"perl":    71

Vedere anche:

pipes
La documentazione della libreria standard per questo modulo.
tempfile
Il modulo tempfile comprende delle classi per gestire i file temporanei
subprocess
Anche il modulo subprocess supporta la concatenazione degli input ed output dei processi.