Parameter per Kommandozeile

Wenn Programme andere Programme starten sollen, stellt sich die Frage:

Wie?

Die C Standard Bibliothek beantwortet das leider sehr unzufriedenstellend, doch trotzdem basieren viele Lösungen auf dem alten
system("myprog arg1 arg2") Schema.

Und da kann man leider auch viel falsch machen.


Windows vs. Unix vs. C

Das aus CP/M und DOS geborene Windows verfolgt einen vom C-Standard abweichenden Weg mit “Befehlszeilenargumenten” umzugehen.
Denn während Unix Programme für erfahrene Profis gemacht wurden, die alles über eine Vielzahl von Parameter konfigurieren wollten, waren DOS und Windows Programme für Anfänger in Büros gedacht.

Ein DOS/Windows Programm braucht keine Argumente, sondern startet ein UI, in welchem der Benutzer seine Daten über eine “schöne” Maske findet.
Die “Profis” unter diesen Anfängern lernten dann, dass man den Start und das Laden eines Programmes vereinen konnte, in dem man nach der Programmdatei noch den Pfad zur Datendatei angab, wie:
edit c:\my_diary.txt

Hier unterscheiden sich DOS/Windows und Unix gewaltig, denn Windows leitet diesen String einfach an das Programm weiter, während das Unix Programm von seinem Elternprozess ein Array von Strings für jeden Parameter erhält.
Man könnte auch sagen:

Windows ist auf genau einen Parameter optimiert bzw. eingeschränkt.

Bei DOS/Windows Programmen müssen also die Parameter im Prozess selbst auseinanderdividiert werden, während auf Unix-Systemen bereits der Elternprozess festlegt, wie viele Parameter übergeben werden.

Problematisch wird das dann bei Leerzeichen, die es in DOS ursprünglich in Dateinamen nicht gab, womit man den einen Befehlszeilen-String einfach durch seine Leerzeichen aufspalten konnte.
Im Nachhinein wurde dann mit CommandLineToArgv() eine API eingeführt, damit dieses Parsen auch unter Windows einheitlich wurde und C-Runtime darauf aufsetzen konnte. Hier werden nun nämlich Anführungszeichen (double quotes) benutzt, um einen zusammengehörenden Parameter zu markieren.
z.B.: my_app first_arg "seconds arg" "and a third arg"

Einfaches und doppeltes Anführungszeichen

Die C Funktion system(command_string) ruft die OS-Shell mit dem übergebenen Kommando auf, was unter Windows zu einen CreateProcess() mit cmd.exe /c + command_string wird. Die Linux C-Runtime nutzt ein fork() / exec() Paar mit dem Array
[ "/bin/sh", "-c", command_string ].

Das bedeutet also, wenn man Leerzeichen in Argumenten eines Zielprogramms braucht (z.B.: für das Auflisten des Ordners my folder), dann muss der Aufruf den Leerzeichen-Parameter auch unter Anführungszeichen setzen.

1system("dir \"my folder\"");

wird am Ende zu:

  • Windows: CreateProcess("cmd.exe /c dir \"my folder\"")
  • Linux: exec([ "/bin/sh", "-c" "dir \"my folder\"" ] )

Man könnte glauben, doppelte Anführungszeichen lösen somit alle Probleme und sind plattformunabhängig.

Leider falsch!

Jede Shell hat die Eigenschaft, dass sie Umgebungsvariablen expandieren und Escapezeichen umwanden kann. Das trifft sowohl unter Windows wie auch unter Linux zu (wenn auch ganz unterschiedlich).

Linux führte daher die einfachen Anführungszeichen ein (Hochkomma, single quotes), mit denen die Variablen und Zeichenauswertung verhindert wird.

dir "my folder" und dir 'my folder' liefern beide unter Linux das gleiche Ergebnis, doch unter Windows wird der zweite Aufruf fehlschlagen, da die Hochkommata nicht interpretiert werden können.

echo $HOME und echo "$HOME" liefern unter Linux beide Male das tatsächliche Verzeichnis, das in der HOME Variable gespeichert wird.
Doch der Aufruf echo '$HOME' führt zur Ausgabe $HOME, da der Variablenverweis nicht durch den Inhalt ersetzt wird.

An dieser Stelle unterscheidet sich also zusätzlich das Verhalten der Shells von Windows und Linux deutlich, denn um zu verhindern, dass in manche Argumente versehentlich Umgebungsvariablen eingeschleust werden, wird das Hochkomma ' in vielen Anwendungen bevorzugt.

Beispielprogramm system_cmd

Ich habe im BLOG Classroom das Beispielprogramm system_cmd hinzugefügt, mit dem system() Aufrufe per Kommandozeile ausgeführt werden können. Hier kann man mitverfolgen, welche Kommandos wie im Prozess ankommen, und was der Prozess selbst wieder an die Shell weitergibt, und was diese am Ende daraus macht.

Hintergrund dieser Analyse war ein Bug in einer Software, wo es um die Weitergabe von Passwort-Token ging. Hier ist die Abschaltung der Variablen- und Sonderzeichenauflösung sehr sinnvoll, denn ein einzelnes Dollarzeichen kann schnell zu falschen Daten führen.

Ich sehe im Besonderen die Notwendigkeit, dass die Parameterverwaltung für Windows und Linux unterschiedlich durchgeführt werden muss und Sonderzeichen manuell so umgestaltet werden müssen, dass die Shell daraus nichts Ungewolltes macht.

Der Aufruf von Programmen, die sowohl unter Windows also auch unter Linux verfügbar sind (z.B.: curl), liefern beim Start durch system() nicht immer die gleichen Ergebnisse.

Fazit

Oh Mann! Lieber nicht system() benutzen!

Es gab schon früher einige Meldungen, dass system() zum Hacken missbraucht werden konnte. Hat man die Möglichkeit Teile des Strings zu manipulieren, kann man Schlimmste Sachen damit anstellen.

Dass aber die Art der Anführungszeichen zu unterschiedlichem Handling in der Shell führen können, hatte ich so deutlich bisher aber auch nicht auf dem Zeiger.

Generell nutze ich selbst wenn möglich direkt die APIs des OS, also exec() oder CreateProcess() und nie system().
Damit bleiben mir “Interpretationsspielräume” der Shell erspart. Leider machen es sich viele Projekte leicht und bauen “mal schnell” einen String zusammen. Der Grund liegt in der Einfachheit des Aufrufs … sicher ist das aber nicht.

Das Gleiche gilt natürlich auch für popen().

Nun geht für mich die Analyse weiter, in welchen anderen Projekten ich noch solche system() Zeitbomben finde.