Kann ein Programm die Anzahl der Leerzeichen zwischen Befehlszeilenargumenten in POSIX ermitteln?

23

Sagen Sie, wenn ich ein Programm mit der folgenden Zeile geschrieben habe:

int main(int argc, char** argv)

Jetzt weiß es, welche Befehlszeilenargumente durch Überprüfen des Inhalts von an es übergeben werden argv.

Kann das Programm feststellen, wie viele Leerzeichen zwischen Argumenten stehen? Zum Beispiel, wenn ich diese in bash eingebe:

ibug@linux:~ $ ./myprog aaa bbb
ibug@linux:~ $ ./myprog       aaa      bbb

Environment ist ein modernes Linux (wie Ubuntu 16.04), aber ich denke, die Antwort sollte für alle POSIX-kompatiblen Systeme gelten.

iBug
quelle
22
Warum sollte Ihr Programm das aus Neugier wissen müssen?
nxnev
2
@nxnev Ich habe einige Windows-Programme geschrieben und weiß, dass es dort möglich ist. Daher frage ich mich, ob es unter Linux (oder Unix) etwas Ähnliches gibt.
iBug
9
Ich erinnere mich vage an CP / M, dass Programme ihre eigenen Befehlszeilen parsen mussten - dies bedeutete, dass jede C-Laufzeit einen Shell-Parser implementieren musste. Und alle haben es etwas anders gemacht.
Toby Speight
3
@iBug Gibt es, aber Sie müssen die Argumente beim Aufrufen des Befehls in Anführungszeichen setzen. So wird das auf POSIX (und ähnlichen) Shells gemacht.
Konrad Rudolph
3
@iBug, ... Windows hat das gleiche Design, das Toby von CP / M oben erwähnt. UNIX macht das nicht - aus Sicht des aufgerufenen Prozesses ist keine Befehlszeile erforderlich, um ihn auszuführen .
Charles Duffy

Antworten:

39

Es ist nicht sinnvoll, von "Räumen zwischen Argumenten" zu sprechen. Das ist ein Shell-Konzept.

Die Aufgabe einer Shell besteht darin, ganze Eingabezeilen zu Argumenten zusammenzufassen, mit denen Befehle gestartet werden sollen. Dies kann das Parsen von Strings in Anführungszeichen, das Erweitern von Variablen, das Einfügen von Platzhaltern und Tilde-Ausdrücken usw. beinhalten. Der Befehl wird mit einem Standardsystemaufruf gestartet exec, der einen Vektor von Zeichenfolgen akzeptiert.

Es gibt andere Möglichkeiten, einen Vektor aus Zeichenfolgen zu erstellen. Viele Programme verzweigen sich und führen ihre eigenen Unterprozesse mit vorgegebenen Befehlsaufrufen aus - in diesem Fall gibt es nie so etwas wie eine "Befehlszeile". Ebenso kann eine grafische (Desktop-) Shell einen Prozess starten, wenn ein Benutzer ein Dateisymbol zieht und es auf einem Befehls-Widget ablegt. Auch hier gibt es keine Textzeile, in der Zeichen "zwischen" Argumenten stehen.

Was den aufgerufenen Befehl betrifft, ist das, was in einer Shell oder einem anderen übergeordneten / vorläufigen Prozess vor sich geht, privat und verborgen - wir sehen nur das Array von Zeichenfolgen, das Standard C main()akzeptiert.

Toby Speight
quelle
Gute Antwort - es ist wichtig, dies auf Unix-Neulinge hinzuweisen, die häufig davon ausgehen, dass tar cf texts.tar *.txtdas tar-Programm beim Ausführen zwei Argumente erhält und das zweite ( *.txt) selbst erweitern muss. Viele Leute wissen erst, wie es wirklich funktioniert, wenn sie ihre eigenen Skripte / Programme schreiben, die mit Argumenten umgehen.
Laurence Renshaw
58

Im Allgemeinen nicht. Die Befehlszeilenanalyse wird von der Shell durchgeführt, die die nicht analysierte Zeile dem aufgerufenen Programm nicht zur Verfügung stellt. Tatsächlich kann Ihr Programm von einem anderen Programm ausgeführt werden, das das Argument nicht durch Analysieren eines Strings, sondern durch programmgesteuertes Erstellen eines Arrays von Argumenten erstellt hat.

Hans-Martin Mosner
quelle
9
Sie möchten vielleicht erwähnen execve(2).
iBug
3
Du hast recht, als lahme Ausrede kann ich sagen, dass ich gerade telefoniere und das
Nachschlagen von Manpages
1
Dies ist der relevante Abschnitt von POSIX.
Stephen Kitt
1
@ Hans-MartinMosner: Termux ...? ;-)
DevSolar
9
"im Allgemeinen" war als Schutzmaßnahme gegen das Zitieren eines speziellen verschachtelten Falls gedacht, in dem dies möglich ist. Beispielsweise kann ein suid-Root-Prozess den Speicher der aufrufenden Shell untersuchen und die nicht analysierte Befehlszeilenzeichenfolge finden.
Hans-Martin Mosner
16

Nein, dies ist nur möglich, wenn die Leerzeichen Teil eines Arguments sind.

Der Befehl greift über ein Array auf die einzelnen Argumente zu (je nach Programmiersprache in der einen oder anderen Form), und die tatsächliche Befehlszeile kann in einer Verlaufsdatei gespeichert werden (sofern sie an einer interaktiven Eingabeaufforderung in einer Shell mit Verlaufsdateien eingegeben wird) niemals in irgendeiner Form an den Befehl weitergegeben.

Alle Befehle unter Unix werden am Ende von einer exec()der Funktionsfamilien ausgeführt. Diese enthalten den Befehlsnamen und eine Liste oder ein Array von Argumenten. Keiner von ihnen verwendet eine Befehlszeile, wie sie an der Shell-Eingabeaufforderung eingegeben wurde. Die system()Funktion funktioniert, aber ihr String-Argument wird später von ausgeführt execve(), das wiederum ein Array von Argumenten anstelle eines Befehlszeilen-Strings verwendet.

Kusalananda
quelle
2
@LightnessRacesinOrbit Ich habe das hier nur für den Fall eingefügt, dass Verwirrung über "Leerzeichen zwischen Argumenten" herrscht. Das Setzen von Leerzeichen in Anführungszeichen zwischen hellound worldist buchstäblich ein Leerzeichen zwischen den beiden Argumenten.
Kusalananda
5
@Kusalananda - Nun, nein ... Leerzeichen eingeben in Anführungszeichen zwischen hellound worldist buchstäblich die zweite von drei Argumente zu liefern.
Jeremy
@ Jeremy Wie ich schon sagte, für den Fall, dass es Verwirrung darüber gab, was mit "zwischen den Argumenten" gemeint war. Ja, als zweites Argument zwischen den beiden anderen, wenn Sie so wollen.
Kusalananda
Ihre Beispiele waren gut und lehrreich.
Jeremy
1
Nun, Leute, die Beispiele waren eine offensichtliche Quelle für Verwirrung und Missverständnisse. Ich habe sie gelöscht, da sie den Wert der Antwort nicht erhöht haben.
Kusalananda
9

Im Allgemeinen ist es nicht möglich, wie mehrere andere Antworten erklären.

Allerdings Unix - Shells sind gewöhnliche Programme (und sie interpretieren die Befehlszeile und Globbing es, dh Erweiterung des Befehls , bevor Sie fork& execvefür sie). Siehe diese Erklärung zu bashShell-Operationen . Sie könnten Ihre eigene Shell schreiben (oder eine vorhandene freie Software- Shell, z. B. GNU bash, patchen ) und diese als Ihre Shell verwenden (oder sogar Ihre Login-Shell, siehe passwd (5) & shells (5) ).

Beispielsweise können Sie von Ihrem eigenen Shell-Programm die vollständige Befehlszeile in eine Umgebungsvariable schreiben lassen (stellen Sie sich MY_COMMAND_LINEzum Beispiel vor) - oder Sie verwenden eine andere Art der Kommunikation zwischen Prozessen, um die Befehlszeile von der Shell zum untergeordneten Prozess zu übertragen -.

Ich verstehe nicht, warum Sie das tun möchten, aber Sie könnten eine Shell programmieren, die sich so verhält (aber ich empfehle, dies nicht zu tun).

BTW, könnte ein Programm durch ein Programm gestartet , das ist nicht eine Schale ( die aber tut Gabel (2) dann execve (2) , oder ein nur execveein Programm in seinem aktuellen Prozess zu starten). In diesem Fall gibt es überhaupt keine Befehlszeile und Ihr Programm könnte ohne einen Befehl gestartet werden ...

Beachten Sie, dass Sie möglicherweise ein (spezialisiertes) Linux-System ohne installierte Shell haben. Das ist seltsam und ungewöhnlich, aber möglich. Sie müssen dann ein spezielles Init- Programm schreiben, das nach Bedarf andere Programme startet - ohne Verwendung einer Shell, sondern durch Ausführen von fork& execveSystemaufrufen.

Lesen Sie auch Betriebssysteme: Drei einfache Teile und vergessen Sie nicht, dass dies execvepraktisch immer ein Systemaufruf ist (unter Linux werden sie in syscalls (2) aufgelistet , siehe auch intro (2) ), die den virtuellen Adressraum (und einige andere ) neu initialisieren Dinge) des Prozesses , der es tut.

Basile Starynkevitch
quelle
Das ist die beste Antwort. Ich gehe davon aus (ohne das nachgeschlagen zu haben), dass argv[0] der Programmname und die übrigen Elemente für die Argumente POSIX-Spezifikationen sind und nicht geändert werden können. Eine Laufzeitumgebung könnte argv[-1]für die Kommandozeile angegeben werden, nehme ich an ...
Peter - Reinstate Monica
Nein, das konnte es nicht. Lesen Sie die execveDokumentation genauer durch . Sie können nicht verwenden argv[-1], es ist undefiniertes Verhalten, um es zu verwenden.
Basile Starynkevitch
Ja, guter Punkt (auch der Hinweis, dass wir einen Systemanruf haben) - die Idee ist ein bisschen erfunden. Alle drei Komponenten der Laufzeit (Shell, Stdlib und OS) müssten zusammenarbeiten. Die Shell muss eine spezielle Nicht-POSIX- execvepluscmdFunktion mit einem zusätzlichen Parameter (oder einer Argv-Konvention) aufrufen , der Syscall erstellt einen Argumentvektor für main, der einen Zeiger auf die Befehlszeile vor dem Zeiger auf den Programmnamen enthält, und übergibt dann die Adresse des Zeigers auf den Programmnamen wie argvbeim Aufrufen des Programms main...
Peter - Reinstate Monica
Die Shell muss nicht neu geschrieben werden. Verwenden Sie einfach die Anführungszeichen. Diese Funktion war in der Bourn Shell verfügbar sh. Ist also nicht neu.
ctrl-alt-delor
Bei der Verwendung von Anführungszeichen muss die Befehlszeile geändert werden. Und OP will das nicht
Basile Starynkevitch
3

Sie können Ihrer Shell jederzeit mitteilen, welcher Shell-Code zu ihrer Ausführung geführt hat. Zum Beispiel mit zsh, indem Sie diese Informationen in der $SHELL_CODEUmgebungsvariablen mit dem preexec()Hook übergeben ( printenvals Beispiel, würden Sie getenv("SHELL_CODE")in Ihrem Programm verwenden):

$ preexec() export SHELL_CODE=$1
$ printenv SHELL_CODE
printenv SHELL_CODE
$ printenv  SHELL_CODE
printenv  CODE
$ $(echo printenv SHELL_CODE)
$(echo printenv SHELL_CODE)
$ for i in SHELL_CODE; do printenv "$i"; done
for i in SHELL_CODE; do printenv "$i"; done
$ printenv SHELL_CODE; : other command
printenv SHELL_CODE; : other command
$ f() printenv SHELL_CODE
$ f
f

Alle diese würden ausführen printenvals:

execve("/usr/bin/printenv", ["printenv", "SHELL_CODE"], 
       ["PATH=...", ..., "SHELL_CODE=..."]);

Ermöglicht printenvdas Abrufen des zsh-Codes, der zur Ausführung printenvdieser Argumente führt. Was Sie mit diesen Informationen anfangen möchten, ist mir nicht klar.

Mit bashdem Merkmal am nächsten zshist preexec()mit seiner würden $BASH_COMMANDin einer DEBUGFalle, aber beachten Sie, dass ein bashgewisses Maß tut der in das Umschreiben (und insbesondere refactors einige der Leerzeichen als Trennzeichen verwendet wird ) , und das ist auf jeden angewandt (na ja, etwas) Befehl Führen Sie nicht die gesamte Befehlszeile aus, wie an der Eingabeaufforderung angegeben (siehe auch die functraceOption).

$ trap 'export SHELL_CODE="$BASH_COMMAND"' DEBUG
$ printenv SHELL_CODE
printenv SHELL_CODE
$ printenv $(echo 'SHELL_CODE')
printenv $(echo 'SHELL_CODE')
$ for i in SHELL_CODE; do printenv "$i"; done; : other command
printenv "$i"
$ printf '%s\n' "$(printenv "SHELL_CODE")"
printf '%s\n' "$(printenv "SHELL_CODE")"
$ set -o functrace
$ printf '%s\n' "$(printenv "SHELL_CODE")"
printenv "SHELL_CODE"
$ print${-+env  }    $(echo     'SHELL_CODE')
print${-+env  } $(echo     'SHELL_CODE')

Sehen Sie, wie einige der Leerzeichen, die Trennzeichen in der Shell-Sprachensyntax sind, in 1 gequetscht wurden und wie nicht immer die vollständige Befehlszeile an den Befehl übergeben wird. Also in deinem Fall wohl nicht sinnvoll.

Beachten Sie, dass ich Ihnen davon abraten würde, da Sie potenziell vertrauliche Informationen an jeden Befehl weitergeben, wie in:

echo very_secret | wc -c | untrustedcmd

würde dieses Geheimnis an beide wcund weitergeben untrustedcmd.

Natürlich können Sie so etwas auch für andere Sprachen als die Shell tun. In C könnten Sie beispielsweise einige Makros verwenden, die den C-Code, der einen Befehl ausführt, in die Umgebung exportieren:

#include <unistd.h>
#include <stdlib.h>
#include <sys/wait.h>
#define WRAP(x) (setenv("C_CODE", #x, 1), x)

int main(int argc, char *argv[])
{
  if (!fork()) WRAP(execlp("printenv", "printenv", "C_CODE", NULL));
  wait(NULL);
  if (!fork()) WRAP(0 + execlp("printenv",   "printenv", "C_CODE", NULL));
  wait(NULL);
  if (argc > 1 && !fork()) WRAP(execvp(argv[1], &argv[1]));
  wait(NULL);
  return 0;
}

Beispiel:

$ ./a.out printenv C_CODE
execlp("printenv", "printenv", "C_CODE", NULL)
0 + execlp("printenv", "printenv", "C_CODE", NULL)
execvp(argv[1], &argv[1])

Sehen Sie, wie einige Leerzeichen vom C-Preprozessor komprimiert wurden, wie im Bash-Fall. In den meisten, wenn nicht allen Sprachen, spielt der in Begrenzungszeichen verwendete Speicherplatz keine Rolle. Daher ist es nicht verwunderlich, dass der Compiler / Interpreter hier etwas Freiheit mit sich nimmt.

Stéphane Chazelas
quelle
Als ich dies testete, BASH_COMMANDenthielt es nicht die ursprünglichen Leerzeichen, die die Argumente trennten, so dass dies für die wörtliche Anforderung des OP nicht verwendbar war. Enthält diese Antwort eine Demonstration für diesen speziellen Anwendungsfall?
Charles Duffy
@CharlesDuffy, ich wollte nur das nächste Äquivalent von zshs preexec () in bash angeben (da dies die Shell ist, auf die sich das OP bezieht) und darauf hinweisen, dass es für diesen speziellen Anwendungsfall nicht verwendet werden kann, aber ich stimme dem zu sehr deutlich. Siehe Bearbeiten. Diese Antwort soll allgemeiner sein, wie der Quellcode (hier in zsh / bash / C), der die Ausführung des Befehls verursacht hat, ausgeführt werden soll (nicht nützlich, aber ich hoffe, dass dies währenddessen geschieht, und insbesondere mit den Beispielen zeige ich, dass es nicht sehr nützlich ist)
Stéphane Chazelas
0

Ich werde nur hinzufügen, was in den anderen Antworten fehlt.

Nein

Siehe andere Antworten

Vielleicht irgendwie

Es gibt nichts, was im Programm getan werden kann, aber es gibt etwas, was in der Shell getan werden kann, wenn Sie das Programm ausführen.

Sie müssen Anführungszeichen verwenden. Also statt

./myprog      aaa      bbb

Sie müssen eine davon tun

./myprog "     aaa      bbb"
./myprog '     aaa      bbb'

Dies übergibt ein einzelnes Argument mit allen Leerzeichen an das Programm. Es gibt einen Unterschied zwischen den beiden, die zweite ist wörtlich, genau die Zeichenfolge, wie sie angezeigt wird (mit der Ausnahme, dass 'als eingegeben werden muss \'). Der erste interpretiert einige Zeichen, ist jedoch in mehrere Argumente unterteilt. Weitere Informationen finden Sie unter Shell-Anführungszeichen. Die Shell muss also nicht neu geschrieben werden, darüber haben die Shell-Designer bereits nachgedacht. Da es sich jetzt jedoch um ein Argument handelt, müssen Sie mehr innerhalb des Programms übergeben.

Option 2

Gib die Daten über stdin weiter. Dies ist der normale Weg, um große Datenmengen in einen Befehl zu bekommen. z.B

./myprog << EOF
    aaa      bbb
EOF

oder

./myprog
Tell me what you want to tell me:
aaaa bbb
ctrl-d

(Kursivschrift wird vom Programm ausgegeben)

Strg-Alt-Delor
quelle
Technisch gesehen ist der Shell - Code: ./myprog␣"␣␣␣␣␣aaa␣␣␣␣␣␣bbb"ausführt ( im allgemeinen in einem untergeordneten Prozess) die Datei gespeichert in ./myprogund übergibt sie zwei Argumente: ./myprogund ␣␣␣␣␣aaa␣␣␣␣␣␣bbb( argv[0]und argc[1], argcwobei 2) und wie in den OPs, der Raum, der die beiden Argumente trennt , ist nicht in irgendeiner Weise weitergegeben zu myprog.
Stéphane Chazelas
Aber Sie ändern das Kommando, und OP will es nicht ändern
Basile Starynkevitch
@BasileStarynkevitch Nach Ihrem Kommentar habe ich die Frage noch einmal gelesen. Sie machen eine Annahme. Nirgendwo sagt das OP, dass sie die Art und Weise, wie das Programm ausgeführt wird, nicht ändern wollen. Vielleicht stimmt das, aber sie hatten nichts zu sagen. Daher könnte diese Antwort genau das sein, was sie braucht.
Strg-Alt-Delor
OP fragen explizit nach Leerzeichen zwischen Argumenten, nicht nach einem einzigen Argument, das Leerzeichen enthält
Basile Starynkevitch