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, ...).