Wie man über Argumente in einem Bash-Skript iteriert

Ich habe einen komplexen Befehl, den ich gerne in ein Shell/Bash-Skript umwandeln würde. Ich kann ihn leicht in Form von "$1" schreiben:

foo $1 args -o $1.ext

Ich möchte in der Lage sein, mehrere Eingaben an das Skript zu übergeben. Was ist der richtige Weg, dies zu tun?

Und natürlich möchte ich Dateinamen mit Leerzeichen in ihnen zu behandeln.

Lösung

Verwenden Sie "$@", um alle Argumente darzustellen:

for var in "$@"
do
    echo "$var"
done

Damit wird jedes Argument durchlaufen und in einer eigenen Zeile ausgedruckt. $@ verhält sich wie $*, mit dem Unterschied, dass die Argumente bei Anführungszeichen richtig aufgeteilt werden, wenn sie Leerzeichen enthalten:

sh test.sh 1 2 '3 4'
1
2
3 4
Kommentare (7)

Neuauflage einer inzwischen gelöschten Antwort von VonC. Robert Gamble's prägnante Antwort geht direkt auf die Frage ein. In dieser wird auf einige Probleme mit Dateinamen, die Leerzeichen enthalten, eingegangen. Siehe auch: ${1:+"$@"} in /bin/sh Grundthese: "$@" ist richtig, und $* (ohne Anführungszeichen) ist fast immer falsch. Das liegt daran, dass "$@" gut funktioniert, wenn Argumente Leerzeichen enthalten, und genauso funktioniert wie $*, wenn sie keine Leerzeichen enthalten. Unter bestimmten Umständen ist "$*" auch OK, aber "$@" funktioniert normalerweise (aber nicht immer) an denselben Stellen. Unquotiert sind "$@" und "$" gleichwertig (und fast immer falsch). Was ist also der Unterschied zwischen `$,$@,"$"und"$@"? Sie beziehen sich alle auf 'alle Argumente für die Shell', aber sie tun unterschiedliche Dinge. Wenn sie nicht in Anführungszeichen gesetzt sind, tun$und$@das Gleiche. Sie behandeln jedes 'Wort' (eine Folge von Nicht-Leerzeichen) als ein separates Argument. Die zitierten Formen sind jedoch recht unterschiedlich:"$"behandelt die Argumentliste als eine einzige durch Leerzeichen getrennte Zeichenkette, während"$@"die Argumente fast genau so behandelt, wie sie in der Befehlszeile angegeben wurden. "$@"expandiert zu nichts, wenn keine Positionsargumente vorhanden sind;"$"expandiert zu einer leeren Zeichenkette — und ja, es gibt einen Unterschied, auch wenn er schwer zu erkennen sein kann. Weitere Informationen finden Sie weiter unten, nach der Einführung des (nicht standardisierten) Befehlsal. **Zweitrangige These:** Wenn Sie Argumente mit Leerzeichen verarbeiten und dann an andere Befehle weitergeben müssen, dann brauchen Sie manchmal Nicht-Standard Werkzeuge zur Unterstützung. (Oder Sie sollten Arrays verwenden, vorsichtig:"${array[@]}"verhält sich analog zu"$@"`.) Beispiel:

    $ mkdir "my dir" anotherdir
    $ ls
    anotherdir      my dir
    $ cp /dev/null "my dir/my file"
    $ cp /dev/null "anotherdir/myfile"
    $ ls -Fltr
    total 0
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 my dir/
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 anotherdir/
    $ ls -Fltr *
    my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ ls -Fltr "./my dir" "./anotherdir"
    ./my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    ./anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ var='"./my dir" "./anotherdir"' && echo $var
    "./my dir" "./anotherdir"
    $ ls -Fltr $var
    ls: "./anotherdir": No such file or directory
    ls: "./my: No such file or directory
    ls: dir": No such file or directory
    $

Warum funktioniert das nicht? Es funktioniert nicht, weil die Shell Anführungszeichen verarbeitet, bevor sie die Variablen. Um also die Shell dazu zu bringen, die in $var eingebetteten Anführungszeichen zu beachten, müssen Sie eval verwenden:

    $ eval ls -Fltr $var
    ./my dir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 my file

    ./anotherdir:
    total 0
    -rw-r--r--   1 jleffler  staff  0 Nov  1 14:55 myfile
    $ 

Richtig knifflig wird es, wenn man Dateinamen wie "Er sagte, "Don't do this!"" (mit Anführungszeichen und doppelten Anführungszeichen und Leerzeichen).

    $ cp /dev/null "He said, \"Don't do this!\""
    $ ls
    He said, "Don't do this!"       anotherdir                      my dir
    $ ls -l
    total 0
    -rw-r--r--   1 jleffler  staff    0 Nov  1 15:54 He said, "Don't do this!"
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 anotherdir
    drwxr-xr-x   3 jleffler  staff  102 Nov  1 14:55 my dir
    $ 

Die Shells (alle) machen es einem nicht gerade leicht, mit so etwas umzugehen zu handhaben, so dass (lustigerweise) viele Unix-Programme keine gute Arbeit bei der mit ihnen umgehen. Unter Unix kann ein Dateiname (eine einzelne Komponente) alle Zeichen enthalten außer Schrägstrich und NUL '\0' enthalten. Die Shells empfehlen jedoch dringend, keine Leerzeichen, Zeilenumbrüche oder Tabulatoren in einem Pfadnamen zu vermeiden. Das ist auch der Grund, warum Standard-Unix-Dateinamen keine Leerzeichen usw. enthalten. Beim Umgang mit Dateinamen, die Leerzeichen und andere problematische Zeichen enthalten können und andere problematische Zeichen enthalten, muss man extrem vorsichtig sein, und ich habe Ich habe vor langer Zeit herausgefunden, dass ich ein Programm brauche, das unter Unix nicht Standard ist. Ich nenne es escape (Version 1.1 wurde 1989-08-23T16:01:45Z datiert). Hier ist ein Beispiel für escape im Einsatz - mit dem SCCS-Kontrollsystem. Es ist ein Cover-Skript, das sowohl ein delta (man denke an einchecken) als auch ein get (denke Auschecken) durchführt. Verschiedene Argumente, insbesondere -y (der Grund, warum Sie die Änderung vorgenommen haben) würden Leerzeichen und Zeilenumbrüche enthalten. Beachten Sie, dass das Skript aus dem Jahr 1992 stammt und daher Back-Ticks anstelle von $(cmd ...) und verwendet nicht #!/bin/sh in der ersten Zeile.

:   "@(#)$Id: delget.sh,v 1.8 1992/12/29 10:46:21 jl Exp $"
#
#   Delta and get files
#   Uses escape to allow for all weird combinations of quotes in arguments

case `basename $0 .sh` in
deledit)    eflag="-e";;
esac

sflag="-s"
for arg in "$@"
do
    case "$arg" in
    -r*)    gargs="$gargs `escape \"$arg\"`"
            dargs="$dargs `escape \"$arg\"`"
            ;;
    -e)     gargs="$gargs `escape \"$arg\"`"
            sflag=""
            eflag=""
            ;;
    -*)     dargs="$dargs `escape \"$arg\"`"
            ;;
    *)      gargs="$gargs `escape \"$arg\"`"
            dargs="$dargs `escape \"$arg\"`"
            ;;
    esac
done

eval delta "$dargs" && eval get $eflag $sflag "$gargs"

(Heutzutage wuerde ich wahrscheinlich nicht mehr so ausfuehrlich Escape verwenden - es ist zum Beispiel nicht mit dem Argument -e benötigt - aber insgesamt ist dies eines meiner einfacheren Skripte, das escape verwendet). Das Programm escape gibt seine Argumente einfach aus, ähnlich wie echo tut, aber es stellt sicher, dass die Argumente für die Verwendung mit eval geschützt sind (eine Ebene von eval; ich habe ein Programm, das eine entfernte Shell ausführt, und das musste die Ausgabe von escape escapen).

    $ escape $var
    '"./my' 'dir"' '"./anotherdir"'
    $ escape "$var"
    '"./my dir" "./anotherdir"'
    $ escape x y z
    x y z
    $ 

Ich habe ein anderes Programm namens al, das seine Argumente in einer Zeile auflistet (und es ist noch viel älter: Version 1.1 vom 1987-01-27T14:35:49). Es ist sehr nützlich beim Debuggen von Skripten, da es in eine Befehlszeile eingefügt werden kann, um zu sehen, welche Argumente tatsächlich an den Befehl übergeben werden.

    $ echo "$var"
    "./my dir" "./anotherdir"
    $ al $var
    "./my
    dir"
    "./anotherdir"
    $ al "$var"
    "./my dir" "./anotherdir"
    $

[Hinzufügen: Und nun, um den Unterschied zwischen den verschiedenen "$@"-Notationen zu zeigen, hier ein weiteres Beispiel:

$ cat xx.sh
set -x
al $@
al $*
al "$*"
al "$@"
$ sh xx.sh     *      */*
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al He said, '"Don'\''t' do 'this!"' anotherdir my dir xx.sh anotherdir/myfile my dir/my file
He
said,
"Don't
do
this!"
anotherdir
my
dir
xx.sh
anotherdir/myfile
my
dir/my
file
+ al 'He said, "Don'\''t do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file'
He said, "Don't do this!" anotherdir my dir xx.sh anotherdir/myfile my dir/my file
+ al 'He said, "Don'\''t do this!"' anotherdir 'my dir' xx.sh anotherdir/myfile 'my dir/my file'
He said, "Don't do this!"
anotherdir
my dir
xx.sh
anotherdir/myfile
my dir/my file
$

Beachten Sie, dass die ursprünglichen Leerzeichen zwischen * und */* in der Befehlszeile nicht erhalten bleiben. Beachten Sie auch, dass Sie die 'Kommandozeilenargumente' in der Shell ändern können, indem Sie verwenden:

set -- -new -opt and "arg with space"

Dies setzt 4 Optionen, '-neu', '-opt', 'und', und 'arg mit Leerzeichen'.
] Hmm, das ist eine ziemlich lange Antwort - vielleicht ist Exegese der bessere Begriff. Quellcode für escape auf Anfrage erhältlich (email to firstname dot nachname at gmail dot com). Der Quellcode für al ist denkbar einfach:

#include 
int main(int argc, char **argv)
{
    while (*++argv != 0)
        puts(*argv);
    return(0);
}

Das'ist alles. Es ist äquivalent zu dem Skript test.sh, das Robert Gamble gezeigt hat, und könnte als Shell-Funktion geschrieben werden (aber Shell-Funktionen gab es in der lokalen Version der Bourne-Shell nicht, als ich al schrieb). Beachten Sie auch, dass Sie al als einfaches Shell-Skript schreiben können:

[ $# != 0 ] && printf "%s\n" "$@"

Die Bedingung ist notwendig, damit es keine Ausgabe erzeugt, wenn keine Argumente übergeben werden. Der Befehl printf erzeugt eine leere Zeile mit nur dem Argument format string, aber das C-Programm erzeugt nichts.

Kommentare (1)

Beachten Sie, dass Robert's Antwort korrekt ist, und es funktioniert auch in sh. Sie können es (portabel) noch weiter vereinfachen:

for i in "$@"

ist äquivalent zu:

for i

D.h., Sie brauchen nichts!

Testen ($ ist die Eingabeaufforderung):

$ set a b "spaces here" d
$ for i; do echo "$i"; done
a
b
spaces here
d
$ for i in "$@"; do echo "$i"; done
a
b
spaces here
d

Das erste Mal habe ich darüber in Unix Programming Environment von Kernighan und Pike gelesen.

In bash ist dies in help for dokumentiert:

for NAME [in WORDS ... ;] do COMMANDS; done

Wenn 'in WORDS ...;' nicht vorhanden ist, wird 'in "$@"' angenommen.

Kommentare (10)