Kommandos mit dem Subprocess-Modul aufrufen

© subbotina, 123RF

In der Pfeife

Für die meisten Anwendungen stellt Python eigene Bibliotheken zur Verfügung. Wer dennoch einmal externe Unix-Programme aufrufen oder gar verbinden will, sollte das Subprocess-Modul verwenden, das dieser Workshop näher vorstellt.
KVM etabliert sich als Standardlösung zur Virtualisierung unter Linux. Der ADMIN-Schwerpunkt hilft beim Bau virtueller Linux-Maschinen, stellt das ... (mehr)

Jede Skriptsprache stellt Mechanismen bereit, mit denen Anwender Unix-Systemprogramme aufrufen können. Bei vielen gibt es einen Aufruf namens »system()« , mit dem sich das bewerkstelligen lässt. Oder es gibt die von der Shell bekannten Backticks wie »`ls`« . Auch unter Python bietet das Modul »os« diese Funktion, aber mit Python 2.4 wurde das Subprocess-Modul eingeführt, dass für diesen Zweck einige Vorteile bietet.

Das Subprocess-Modul löst einige andere Aufrufe ab, die das OS-Modul bietet, etwa »popen()« (das seit Python 2.6 als "deprecated" also veraltet gilt) und verschiedene Varianten von »spawn()« . Weiterhin bietet das OS-Modul eine Reihe von Exec-Aufrufen wie »execl()« , »execlp()« und so weiter, die ihren Pendants der C-Bibliothek entsprechen, aber das laufende Skript komplett durch den neu gestarteten Prozess ersetzen. Dieser Artikel beschäftigt sich im Weiteren mit den Methoden, die das Subprocess-Modul bereitstellt, um in eigenen Skripten möglichst strukturiert Kommandozeilenprogramme aufzurufen und deren Ausgabe weiterzuverarbeiten.

Im einfachsten Fall lässt sich ein Befehl mit der Methode call() aufrufen, wobei Befehl und Argument als Array übergeben werden:

>>> import subprocess
>>> subprocess.call(["ls", "-l"])
0

Das führt im interaktiven Python-Modus zwar zum gewünschten Ergebnis, denn der Inhalt des Verzeichnisses wird auch angezeigt, aber das ist eigentlich nur ein Ebeneffekt, weil sich Interpreter und der Aufruf die Standardausgabe teilen. Tatsächlich gibt der Aufruf von Call nur den Rückgabewert des aufgerufenen Befehls zurück, im Erfolgsfall, wie oben, also eine Null. Der Call-Aufruf wartet, bis das aufgerufene Kommando beendet ist, blockiert also solange das Skript beziehungsweise den aktuellen Thread. Beinahe das Gleiche wie »call()« macht »check_call()« , es prüft jedoch zusätzlich den Rückgabewert und löst eine Exception aus, wenn ein Fehler aufgetreten ist.

Neu in Python-Version 2.7 ist die Methode »subprocess.check_output()« , die die Ausgabe des aufgerufenen Befehls als Bytestring zurückliefert. Wer das bei älteren Python-Versionen erreichen will, muss sich anders behelfen, dazu gleich mehr.

Möglicherweise empfindet man die Notwendigkeit, den aufgerufenen Befehl und seine Argumente als Array übergeben zu müssen als umständlich. Allerdings hält Python hierzu einen nützlichen Helfer bereit, der sich in diesem Fall als typisches Programmieridiom einsetzen lässt: Das Modul »shlex« bietet die Methode »split()« , die aus einem Aufruf-String genau das von den Subprocess-Methoden erwartete Format macht:

cmd = "convert -geometry 80x60 a.png b.png"
args = shlex.split(cmd)
subprocess.call(args)

Wenn man nicht wie in diesem Beispiel, Dateien auf der Festplatte verändert, wird man in Skripte typischerweise häufig die Ausgabe eines Befehls weiterverarbeiten wollen. Das erwähnte »subprocess.check_open()« ist hierzu perfekt geeignet, doch was kann man machen, wenn man noch auf eine Python-Version älter als 2.7 angewiesen ist? Die Lösung dafür ist leider nicht sehr elegant, denn sie verwendet eine der Popen-Methoden, die eigentlich für die Kommunikation mehrerer Prozesse gedacht ist. Den Stdout-Dateideskriptor setzt man dafür zuerst auf den Pipe-Modus und startet dann mit dem Aufruf von communicate die Kommunikation mit dem Subprozess:

output = Popen(args, stdout=PIPE).communicate()[0]

»communicate()« gibt ein Tupel von Daten für Stdout und Stderror zurück, deshalb verwendet der obige Aufruf das »[0]« -Subskript, um an die Standardausgabe zu gelangen.

Analog funktioniert es, mit Popen mehrere Befehle zu verketten, ähnlich wie bei Shell-Pipelines. Der erste Prozess bekommt als Ausgabe das Token »subprocess.PIPE« zugewiesen, das der folgende Prozess dann als Standardeingabe verwendet. Am Ende lässt sich das Ergebnis wieder mit »communicate()[0]« auffangen. Ein Beispiel zeigt Listing 1. Aus dem mit Newlines durchsetzten Ergebnisstring macht dann etwa der Aufruf »lines = output.split("\n")« ein Array mit einzelnen Zeilen.

Listing 1

Pipes

01 import shlex
02 from subprocess import Popen
03 from subprocess import PIPE
04
05 pscmd = shlex.split("ps auxw")
06 grepcmd = shlex.split("grep root")
07 ps = Popen(pscmd, stdout=PIPE)
08 grep = Popen(grepcmd, stdin=ps.stdout, stdout=PIPE)
09 output = grep.communicate()[0]

Shell=True

Einer der möglichen Aufrufparameter der Subprocess-Methoden gibt an, ob beim Ausführen des Programms eine Shell gestartet werden soll oder nicht. Aus Sicherheitsgründen rät die Dokumentation davon ab, das zu tun, jedenfalls wenn ein Skript Benutzereingaben verarbeitet, die später in einen solchen Aufruf münden. Das kann nämlich zu einer Sicherheitslücke führen, die den von Webanwendungen bekannten SQL Injections ähnelt. Sie firmieren deshalb auch unter dem Namen Shell Injections und funktionieren so: Der böswillige Anwender hängt hinter eine Eingabe mit Semikolon getrennt einen eigenen Befehl an, den das fehlerhafte Skript dann mit ausführt.

Parallel

Bei eigenen Skripten, die länger dauernde Berechnungen oder Tasks durchführen, ist es auch wichtig zu wissen, dass manche Subprocess-Methoden die Befehle im Hintergrund ausführen, ohne das Skript zu blockieren. Auf Multicore-Rechnern führen diese Methoden zum Teil zu einem erheblichen Performance-Gewinn. Als Beispiel konvertiert das Skript in Listing 2 alle Jpg-Dateien im aktuellen Verzeichnis in Thumbnails.

Listing 2

Benchmark-Code

01 import glob
02 import subprocess
03
04 files = glob.glob("*.jpg")
05
06 for file in files:
07    cmd = "convert -geometry 640x480 " + file + " tmp/" + file
08    print cmd
09    subprocess.call(cmd)

In dieser Form verwendet es die Call-Methode, die ein Bild nach dem anderen verarbeitet. Das Ganze dauert auf einem Vierkern-Rechner etwa 30 Sekunden:

$ time python call.py
real 0m32.028s
user 1m32.154s
sys 0m4.568s

Ersetzt man den Call-Aufruf in Zeile 9 durch die Methode Popen, sieht das Ergebnis gleich anders aus (Abbildung 1).

 

Die Laufzeit reduziert sich damit auf ein Drittel! Mit so einfachen Methoden lassen sich Skripte tunen, wenn man die richtigen Python-Methoden verwendet. Das setzt natürlich voraus, dass die verarbeiteten Tasks voneinander unabhängig sind. Wer möglichst fehlerfreie Skripte schreiben will, überprüft beim Start eines Skripts, ob alle aufgerufenen Shell-Kommandos auch verfügbar sind. Dabei hilft ein Python-Skript, das ausführbare Dateien im »PATH« auf einem Unix-System sucht (Listing 3). Es zerlegt die Umgebungsvariable in einzelne Elemente, verkettet sie mit dem fraglichen Befehl und prüft, ob die Datei mit dem entstehenden Pfad existiert und ausführbar ist. Falls ja, gibt die Funktion sie zurück. Findet sie keine Datei, auf die das zutrifft, ist der Rückgabewert »False« . Mit den im Artikel beschriebenen Methoden sollte es ein leichtes sein, eine Funktion zu schreiben, die das gleiche Ziel erreicht, indem sie den Unix-Befehl »which« aufruft.

Listing 3

which

01 def which(program):
02    pathlist = os.environ['PATH'].split(os.pathsep)
03    for dir in pathlist:
04        filename = os.path.join(dir, program)
05        try:
06            st = os.stat(filename)
07        except os.error:
08            continue
09        mode = S_IMODE(st.st_mode)
10        if mode & 0111:
11            return filename
12    return False
comments powered by Disqus
Mehr zum Thema

Skriptprogrammierung mit Python

Es ist nicht alles Shell, was glänzt. Auch mit Python lassen sich Systemaufgaben unkompliziert skripten. Dieser Artikel gibt eine Einführung in die moderne Skriptsprache.

Artikel der Woche

Support-Ende von SMBv1

Mit dem aktuellen Update für Windows 10 und Windows Server 2016 steht eine Änderung ins Haus, die gerade für Benutzer älterer Linux-Distributionen große Auswirkungen hat. Nachdem Microsoft es über viele Jahre schon angekündigt hat, entfernt der Konzern mit dem aktuellen Update-Release den Support für das SMB-Protokoll 1. (mehr)
Einmal pro Woche aktuelle News, kostenlose Artikel und nützliche ADMIN-Tipps.
Ich habe die Datenschutzerklärung gelesen und bin einverstanden.

Linux-Backup

Welche Backup-Lösung setzen Sie für Linux im professionellen Umfeld ein?

  • keine
  • eigene Scripts
  • rsnapshot
  • rdiff-backup
  • Bacula
  • Bareos
  • Borg
  • Duplicity
  • Amanda
  • Burp
  • Clonezilla
  • Acronis
  • Arkeia
  • SEP sesam
  • Veeam
  • Redo Backup
  • Relax-and-Recover
  • andere kommerzielle Software
  • andere Open-Source-Software

Google+

Ausgabe /2017

Microsite