So iterieren Sie über Argumente in einem Bash-Skript

900

Ich habe einen komplexen Befehl, aus dem ich ein Shell / Bash-Skript erstellen möchte. Ich kann es in Bezug auf $1leicht schreiben :

foo $1 args -o $1.ext

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

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

Thelema
quelle
67
Zu Ihrer Information, es ist eine gute Idee, Parameter immer in Anführungszeichen zu setzen. Sie wissen nie, wann ein Parameter einen eingebetteten Bereich enthalten könnte.
David R Tribble

Antworten:

1482

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

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

Dadurch wird jedes Argument durchlaufen und in einer separaten Zeile ausgedruckt. $ @ verhält sich wie $ *, außer dass die Argumente beim Zitieren richtig aufgeteilt werden, wenn sie Leerzeichen enthalten:

sh test.sh 1 2 '3 4'
1
2
3 4
Robert Gamble
quelle
38
Übrigens ist einer der anderen Vorteile, "$@"dass es sich zu nichts erweitert, wenn es keine Positionsparameter gibt, während es "$*"sich zu einer leeren Zeichenfolge erweitert - und ja, es gibt einen Unterschied zwischen keinen Argumenten und einem leeren Argument. Siehe ${1:+"$@"} in / bin / sh` .
Jonathan Leffler
9
Beachten Sie, dass diese Notation auch in Shell-Funktionen verwendet werden sollte, um auf alle Argumente für die Funktion zuzugreifen.
Jonathan Leffler
Durch Löschen der doppelten Anführungszeichen: $varIch konnte die Argumente ausführen.
Eonist
Wie beginne ich mit dem zweiten Argument? Ich meine, ich brauche das, aber beginne immer mit dem zweiten Argument, das heißt, überspringe $ 1.
m4l490n
4
@ m4l490n: shiftwirft weg $1und verschiebt alle nachfolgenden Elemente nach unten.
MSalters
239

Umschreiben einer jetzt gelöschten Antwort von VonC .

Die prägnante Antwort von Robert Gamble befasst sich direkt mit der Frage. Dieser wird bei einigen Problemen mit Dateinamen, die Leerzeichen enthalten, erweitert.

Siehe auch: $ {1: + "$ @"} in / bin / sh

Grundlegende These: "$@" ist richtig und $*(nicht zitiert) ist fast immer falsch. Dies liegt daran, dass es gut "$@"funktioniert, wenn Argumente Leerzeichen enthalten, und genauso funktioniert, wie $*wenn dies nicht der Fall ist. Unter bestimmten Umständen "$*"ist das auch in Ordnung, "$@"funktioniert aber normalerweise (aber nicht immer) an denselben Orten. Nicht zitiert $@und $*gleichwertig (und fast immer falsch).

Also, was ist der Unterschied zwischen $*, $@, "$*"und "$@"? Sie beziehen sich alle auf 'alle Argumente zur Shell', aber sie machen verschiedene Dinge. Wenn nicht zitiert, $*und $@machen Sie das Gleiche. Sie behandeln jedes 'Wort' (Folge von Nicht-Leerzeichen) als separates Argument. Die Anführungszeichen in Anführungszeichen sind jedoch sehr unterschiedlich: "$*"Behandelt die Argumentliste als einzelne durch Leerzeichen getrennte Zeichenfolge, während "$@"die Argumente fast genau so behandelt werden, wie sie in der Befehlszeile angegeben wurden. "$@"erweitert sich zu nichts, wenn es keine Positionsargumente gibt; "$*"erweitert sich zu einer leeren Zeichenfolge - und ja, es gibt einen Unterschied, obwohl es schwierig sein kann, ihn wahrzunehmen. Weitere Informationen finden Sie unten nach der Einführung des Befehls (nicht Standard) al.

Sekundärthese: Wenn Sie Argumente mit Leerzeichen verarbeiten und dann an andere Befehle weitergeben müssen, benötigen Sie manchmal nicht standardmäßige Tools zur Unterstützung. (Oder Sie sollten Arrays vorsichtig verwenden: "${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 Variablen erweitert. Um die Shell dazu zu bringen, auf die darin eingebetteten Anführungszeichen zu achten $var, müssen Sie Folgendes verwenden eval:

    $ 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
    $ 

Dies wird sehr schwierig, wenn Sie Dateinamen wie " He said, "Don't do this!"" (mit Anführungszeichen und doppelten Anführungszeichen und Leerzeichen) haben.

    $ 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 nicht besonders einfach, mit solchen Dingen umzugehen, so dass (komischerweise) viele Unix-Programme nicht gut damit umgehen können. Unter Unix kann ein Dateiname (einzelne Komponente) beliebige Zeichen außer Schrägstrich und NUL enthalten '\0'. Die Shells empfehlen jedoch nachdrücklich keine Leerzeichen, Zeilenumbrüche oder Tabulatoren in Pfadnamen. Aus diesem Grund enthalten Standard-Unix-Dateinamen keine Leerzeichen usw.

Wenn Sie mit Dateinamen arbeiten, die Leerzeichen und andere störende Zeichen enthalten können, müssen Sie äußerst vorsichtig sein, und ich habe vor langer Zeit festgestellt, dass ich ein Programm benötige, das unter Unix nicht Standard ist. Ich nenne es escape(Version 1.1 war vom 1989-08-23T16: 01: 45Z datiert).

Hier ist ein Beispiel für die escapeVerwendung - mit dem SCCS-Steuerungssystem. Es ist ein Coverskript, das sowohl ein delta(Think Check-In ) als auch ein get(Think Check-Out ) ausführt . Insbesondere verschiedene Argumente -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 der $(cmd ...)Notation verwendet und nicht #!/bin/shin der ersten Zeile verwendet wird.

:   "@(#)$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"

(Ich würde Escape heutzutage wahrscheinlich nicht mehr so ​​gründlich verwenden - es wird -ezum Beispiel für das Argument nicht benötigt -, aber insgesamt ist dies eines meiner einfacheren Skripte escape.)

Das escapeProgramm gibt einfach seine Argumente aus, ähnlich wie dies der echo Fall ist, stellt jedoch sicher, dass die Argumente für die Verwendung mit geschützt sind eval(eine Ebene von eval; Ich habe ein Programm, das eine Remote-Shell-Ausführung durchgeführt hat und das der Ausgabe von entkommen muss escape).

    $ 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 eins pro Zeile auflistet (und es ist noch älter: Version 1.1 vom 1987-01-27T14: 35: 49). Dies ist am nützlichsten 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"
    $

[ Hinzugefügt: Um nun den Unterschied zwischen den verschiedenen "$@"Notationen zu zeigen , hier noch ein 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 durch nichts die ursprünglichen Leerzeichen zwischen *und */*in der Befehlszeile erhalten bleiben . Beachten Sie außerdem, dass Sie die 'Befehlszeilenargumente' in der Shell ändern können, indem Sie Folgendes verwenden:

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

Dies setzt 4 Optionen, ' -new', ' -opt', ' and' und ' arg with space'.
]]

Hmm, das ist eine ziemlich lange Antwort - vielleicht ist Exegese der bessere Begriff. Quellcode für escapeauf Anfrage erhältlich (E-Mail an Vorname und Nachname bei gmail dot com). Der Quellcode für alist unglaublich einfach:

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

Das ist alles. Es entspricht dem test.shSkript, das Robert Gamble gezeigt hat, und kann als Shell-Funktion geschrieben werden (aber Shell-Funktionen waren in der lokalen Version der Bourne-Shell beim ersten Schreiben nicht vorhanden al).

Beachten Sie auch, dass Sie alals einfaches Shell-Skript schreiben können :

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

Die Bedingung wird benötigt, damit sie keine Ausgabe erzeugt, wenn keine Argumente übergeben werden. Der printfBefehl erzeugt eine leere Zeile nur mit dem Formatzeichenfolgenargument, aber das C-Programm erzeugt nichts.

Jonathan Leffler
quelle
132

Beachten Sie, dass Roberts Antwort richtig ist und auch funktioniert sh. Sie können es (portabel) noch weiter vereinfachen:

for i in "$@"

ist äquivalent zu:

for i

Dh du brauchst nichts!

Testen ( $ist 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

Ich habe dies zuerst in der Unix-Programmierumgebung von Kernighan und Pike gelesen .

In bash, help fordokumentiert diese:

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

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

Alok Singhal
quelle
4
Ich stimme dir nicht zu. Man muss wissen, was das Kryptische "$@"bedeutet, und wenn man weiß, was es for ibedeutet, ist es nicht weniger lesbar als for i in "$@".
Alok Singhal
10
Ich habe nicht behauptet, dass dies for ibesser ist, weil es Tastenanschläge spart. Ich habe die Lesbarkeit von for iund verglichen for i in "$@".
Alok Singhal
das ist, wonach ich gesucht habe - wie nennen sie diese Annahme von $ @ in Schleifen, auf die Sie nicht explizit verweisen müssen? Gibt es einen Nachteil, wenn Sie $ @ reference nicht verwenden?
Qodeninja
58

Für einfache Fälle können Sie auch verwenden shift. Die Argumentliste wird wie eine Warteschlange behandelt. Jedes shiftwirft das erste Argument aus und der Index jedes der verbleibenden Argumente wird dekrementiert.

#this prints all arguments
while test $# -gt 0
do
    echo "$1"
    shift
done
nuoritoveri
quelle
Ich denke, diese Antwort ist besser, denn wenn Sie dies shiftin einer for-Schleife tun , ist dieses Element immer noch Teil des Arrays, während dies shiftwie erwartet funktioniert.
DavidG
2
Beachten Sie, dass Sie echo "$1"den Abstand im Wert der Argumentzeichenfolge beibehalten sollten . Außerdem (ein kleines Problem) ist dies destruktiv: Sie können die Argumente nicht wiederverwenden, wenn Sie sie alle verschoben haben. Oft ist das kein Problem - Sie verarbeiten die Argumentliste ohnehin nur einmal.
Jonathan Leffler
1
Stimmen Sie zu, dass eine Verschiebung besser ist, da Sie dann auf einfache Weise zwei Argumente in einem Switch-Fall erfassen können, um Flag-Argumente wie "-o Ausgabedatei" zu verarbeiten.
Baxissimo
Auf der anderen Seite, wenn Sie anfangen, Flag-Argumente zu analysieren, sollten Sie eine leistungsfähigere Skriptsprache als bash in Betracht ziehen :)
nuoritoveri
4
Diese Lösung ist besser, wenn Sie ein paar Parameterwerte benötigen, zum Beispiel --file myfile.txt $ 1 ist der Parameter, $ 2 ist der Wert und Sie rufen Shift zweimal auf, wenn Sie ein anderes Argument überspringen müssen
Daniele Licitra
16

Sie können auch als Array-Elemente auf sie zugreifen, wenn Sie beispielsweise nicht alle Elemente durchlaufen möchten

argc=$#
argv=("$@")

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done
baz
quelle
5
Ich glaube, die Zeile argv = ($ @) sollte argv = ("$ @") sein. Andere weise Argumente mit Leerzeichen werden nicht richtig behandelt
kdubs
Ich bestätige, dass die korrekte Syntax argv = ("$ @") ist. es sei denn, Sie erhalten keine letzten Werte, wenn Sie einen Parameter in Anführungszeichen haben. Wenn Sie beispielsweise ./my-script.sh param1 "param2 is a quoted param"Folgendes verwenden : - Wenn Sie argv = ($ @) verwenden, erhalten Sie [param1, param2]. - Wenn Sie argv = ("$ @") verwenden, erhalten Sie [param1, param2 ist ein zitierter Parameter]
jseguillon
2
aparse() {
while [[ $# > 0 ]] ; do
  case "$1" in
    --arg1)
      varg1=${2}
      shift
      ;;
    --arg2)
      varg2=true
      ;;
  esac
  shift
done
}

aparse "$@"
g24l
quelle
1
Wie unten angegeben [ $# != 0 ]ist das besser. Oben #$sollte sein, $#wie inwhile [[ $# > 0 ]] ...
hute37
1

Wenn Sie die Argumentliste mit einem Index auflisten müssen (z. B. um nach einem bestimmten Wort zu suchen), können Sie dies tun, ohne die Liste zu kopieren oder zu mutieren.

Angenommen, Sie möchten eine Argumentliste mit einem Doppelstrich ("-") teilen und die Argumente vor den Bindestrichen an einen Befehl und die Argumente nach den Bindestrichen an einen anderen Befehl übergeben:

 toolwrapper() {
   for i in $(seq 1 $#); do
     [[ "${!i}" == "--" ]] && break
   done || return $? # returns error status if we don't "break"

   echo "dashes at $i"
   echo "Before dashes: ${@:1:i-1}"
   echo "After dashes: ${@:i+1:$#}"
 }

Die Ergebnisse sollten folgendermaßen aussehen:

 $ toolwrapper args for first tool -- and these are for the second
 dashes at 5
 Before dashes: args for first tool
 After dashes: and these are for the second
Rich Kadel
quelle
1

getopt Verwenden Sie den Befehl in Ihren Skripten, um alle Befehlszeilenoptionen oder -parameter zu formatieren.

#!/bin/bash
# Extract command line options & values with getopt
#
set -- $(getopt -q ab:cd "$@")
#
echo
while [ -n "$1" ]
do
case "$1" in
-a) echo "Found the -a option" ;;
-b) param="$2"
echo "Found the -b option, with parameter value $param"
shift ;;
-c) echo "Found the -c option" ;;
--) shift
break ;;
*) echo "$1 is not an option";;
esac
shift
JimmyLandStudios
quelle
Knifflig, ich würde das gründlich recherchieren. Beispiel: Datei: ///usr/share/doc/util-linux/examples/getopt-parse.bash, Handbuch: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios