Python: Wie ruft man ein Programm oder einen Systembefehl auf?

Früher oder später muss ein Python-Skript etwas anderes aufrufen — git, ffmpeg, pg_dump. Dafür gibt es subprocess.run(). Und es gibt einen Parameter, vor dem überall gewarnt wird, ohne dass jemand zeigt warum.

Der richtige Weg

import subprocess

ergebnis = subprocess.run(["echo", "hallo"], capture_output=True, text=True)
returncode : 0
stdout     : 'hello\n'
stderr     : ''
type       : CompletedProcess

subprocess.run() ist seit Python 3.5 die empfohlene Schnittstelle. Drei Dinge an diesem Aufruf solltest du kennen, bevor es weitergeht.

Übergib eine Liste, keinen String. Ein Listenelement pro Argument. Warum, gleich.

capture_output=True liefert dir die Ausgabe. Ohne diesen Parameter geht die Ausgabe direkt auf dein Terminal und ergebnis.stdout ist None:

the next line is written by echo itself, not by python:
  ...straight through
and r.stdout is        : None

Das ist in Ordnung, wenn das Programm einfach nur laufen soll — und verwirrend, wenn du einen String erwartet hast.

text=True liefert str statt bytes. Ohne den Parameter bekommst du b'hello\n' und rufst zwei Zeilen weiter unten ohnehin .decode() auf.

Ein Fehlschlag ist keine Exception

Das überrascht, wenn man aus anderen Sprachen kommt:

subprocess.run(['false']).returncode : 1
-> no exception was raised, the script just carried on

Der Befehl ist fehlgeschlagen. Dein Skript hat es nicht gemerkt. Wenn ein fehlgeschlagener Befehl dein Programm anhalten soll, musst du das verlangen:

with check=True : CalledProcessError: Command '['false']' returned non-zero exit status 1.

check=True wirft CalledProcessError. Die Exception trägt returncode, stdout und stderr mit sich, sodass du etwas Brauchbares protokollieren kannst.

Warum die Liste, Teil eins: Tippfehler

Hier ist ein Unterschied, den man leicht übersieht. Ruf ein Programm auf, das es nicht gibt:

list form  : FileNotFoundError: [Errno 2] No such file or directory: 'definitely-not-a-program'
shell=True : no exception, returncode=127
             stderr='/bin/sh: 1: definitely-not-a-program: not found'

Die Listenform wirft sofort. Mit shell=True passiert überhaupt nichts — die Shell startet, findet das Programm nicht und beendet sich mit 127. Solange du den Rückgabewert nicht prüfst, läuft dein Skript weiter, als wäre alles in Ordnung.

So schafft es ein Tippfehler in einem Programmnamen bis auf das Produktivsystem.

Warum die Liste, Teil zwei: die Sicherheit

Jetzt der Grund, aus dem dir alle von shell=True abraten. Angenommen ein Dateiname kommt von einer Stelle, die du nicht kontrollierst — ein Upload-Formular, eine Konfigurationsdatei, eine Nachricht aus einer Queue:

filename from a user : 'nonexistent; echo INJECTED >&2'

Durch eine Shell geschickt:

subprocess.run(f"ls {dateiname}", shell=True, ...)
with shell=True, stderr contains : 'INJECTED'
-> the part after the semicolon ran as a second command

Das Semikolon hat den ls-Befehl beendet und einen neuen begonnen. Was danach stand, lief mit den Rechten deines Skripts. Ersetz echo INJECTED durch etwas weniger Freundliches und du hast das ganze Problem.

Die Listenform:

with the list form, stderr : "ls: cannot access 'nonexistent; echo INJECTED >&2': No such file or directory"
-> treated as one (missing) filename, nothing was executed

Der String wurde dem Betriebssystem als ein einziges Argument übergeben. Es ist gar keine Shell beteiligt, also gibt es auch nichts, was das Semikolon interpretieren könnte. Das ist nicht “gut escaped” — es ist ein anderer Mechanismus, bei dem es nichts zu escapen gibt.

Was die Shell für dich getan hat

Fairerweise: Manchmal ist shell=True seinen Preis wert.

list form does not glob : '*\n'
shell=True does glob    : '/etc/hostname\n'

Platzhalter, Pipes, &&, das Auflösen von Umgebungsvariablen und Umleitungen sind allesamt Funktionen der Shell. Ohne Shell hast du nichts davon.

Die Antwort ist meistens nicht, doch zu shell=True zu greifen, sondern die Aufgabe in Python zu erledigen. Für Platzhalter gibt es glob, und Pipes kannst du direkt bauen:

p1 = subprocess.Popen(["printf", "b\\na\\nc\\n"], stdout=subprocess.PIPE)
p2 = subprocess.Popen(["sort"], stdin=p1.stdout, stdout=subprocess.PIPE, text=True)
p1.stdout.close()
out = p2.communicate()[0]
printf | sort -> 'a\nb\nc\n'

Das p1.stdout.close() ist keine Zierde — es sorgt dafür, dass der erste Prozess ein SIGPIPE bekommt, falls sich der zweite vorzeitig beendet.

Wenn du wirklich eine Shell brauchst, dann schick die Argumente wenigstens vorher durch shlex.quote().

Wenn du den Befehl nur als String hast

Manchmal kommt der Befehl als ein String an und du brauchst die Liste. shlex.split() macht das richtig:

shlex.split("echo 'one argument' two") -> ['echo', 'one argument', 'two']
naive .split()                         -> ['echo', "'one", "argument'", 'two']

Am Leerzeichen zu trennen geht in dem Moment schief, in dem ein Argument selbst ein Leerzeichen enthält — auf den meisten Systemen also in dem Moment, in dem ein Pfad eines enthält.

Die älteren Wege

In älterem Code triffst du auf diese hier:

os.system returned    : 0  (an exit status, not the output)
os.popen().read()     : 'popen-output\n'

os.system() kann dir die Ausgabe überhaupt nicht geben. Es liefert einen Exit-Status, schreibt auf das Terminal und schickt alles durch eine Shell — trägt das Injection-Problem also schon von der Bauart her mit sich. os.popen() gibt dir die Ausgabe, aber sonst nichts: keinen Rückgabewert und kein stderr.

subprocess.call() und subprocess.check_output() funktionieren weiterhin und sind nicht veraltet. run() kann aber alles, was sie können:

check_output(['echo','x']) : 'x\n'
on failure                 : CalledProcessError (returncode 1)

Zwei Parameter, die man sich merken sollte

Einem Programm etwas auf stdin geben:

subprocess.run(["cat"], input="ueber stdin hineingegeben\n", capture_output=True, text=True)
input= : 'fed in through stdin\n'

Und nicht ewig warten:

timeout=0.3 on 'sleep 5' : TimeoutExpired after 0.3s

Ein timeout gehört an alles, was mit einem Netzwerk oder mit Hardware redet. Ohne ihn hängt ein hängender Befehl dein Skript so lange auf, bis es jemandem auffällt.

Hinweis zu Netcup (Werbung)

Der deutsche Hoster Netcup bietet unter anderem günstige und zugleich leistungsstarke Webhosting Pakete, KVM-basierte Root Server und dezidierte Server an. Mit unseren Gutscheincodes kannst du noch mehr Geld sparen (6€ bei deiner ersten Bestellung, 30% Rabatt auf alle KVM-basierten Root Server, ...).