Hoe itereren over argumenten in een Bash script

Ik heb een complex commando waar ik een shell/bash script van wil maken. Ik kan het gemakkelijk in termen van $1 schrijven:

foo $1 args -o $1.ext

Ik wil meerdere inputnamen aan het script kunnen doorgeven. Wat'is de juiste manier om dit te doen?

En, natuurlijk, wil ik bestandsnamen met spaties erin kunnen verwerken.

Oplossing

Gebruik "$@" om alle argumenten weer te geven:

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

Dit zal over elk argument itereren en het op een aparte regel uitprinten. $@ gedraagt zich als $*, behalve dat wanneer de argumenten geciteerd worden, ze netjes opgesplitst worden als er spaties in staan:

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

Herschrijving van een nu verwijderd antwoord door VonC. Het beknopte antwoord van Robert Gamble's gaat direct in op de vraag. Dit antwoord gaat dieper in op enkele problemen met bestandsnamen die spaties bevatten. Zie ook: ${1:+"$@"} in /bin/sh Basis stelling: "$@" is correct, en $* (ongeciteerd) is bijna altijd fout. Dit komt omdat "$@" prima werkt als argumenten spaties bevatten, en hetzelfde werkt als $* als ze geen spaties bevatten. In sommige omstandigheden, "$*" is ook OK, maar "$@" werkt meestal (maar niet altijd) op dezelfde plaatsen. Ongeciteerd, $@ en $* zijn gelijkwaardig (en bijna altijd fout). Dus, wat is het verschil tussen $*, $@, "$*", en "$@"? Ze zijn allemaal gerelateerd aan 'alle argumenten voor de shell', maar ze doen verschillende dingen. Als ze niet aangehaald worden, doen $* en $@ hetzelfde. Ze behandelen elk 'woord' (opeenvolging van niet-whitespace) als een afzonderlijk argument. De geciteerde vormen zijn echter heel verschillend: "$*" behandelt de argumentenlijst als een enkele door spaties gescheiden tekenreeks, terwijl "$@" de argumenten bijna precies zo behandelt als ze waren toen ze op de opdrachtregel werden opgegeven. "$@" breidt zich uit tot helemaal niets als er geen positionele argumenten zijn; "$*" breidt zich uit tot een lege string — en ja, er'is een verschil, hoewel het moeilijk kan zijn om het te zien. Zie meer informatie hieronder, na de introductie van het (niet-standaard) commando al. Tweede stelling: als je argumenten met spaties moet verwerken en dan doorgeven aan andere commando's, dan heb je soms niet-standaard hulpmiddelen nodig om te helpen. (Of je moet arrays gebruiken, voorzichtig: "${array[@]}" gedraagt zich analoog aan "$@"). Voorbeeld:

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

Waarom werkt dat niet? Het werkt niet omdat de shell eerst aanhalingstekens verwerkt voordat het variabelen. Dus, om de shell aandacht te laten besteden aan de aanhalingstekens in $var, moet je eval gebruiken:

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

Dit wordt echt lastig als je bestandsnamen hebt zoals "He said, "Don't do this!"" (met aanhalingstekens en dubbele aanhalingstekens en spaties).

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

De shells (allemaal) maken het niet bijzonder gemakkelijk om met zulke dingen, dus (grappig genoeg) kunnen veel Unix programma's er niet goed mee ze te verwerken. Op Unix kan een bestandsnaam (enkele component) alle tekens bevatten behalve schuine streep en NUL '\0'. De shells raden echter sterk aan om geen spaties, nieuwe regels of tabs overal in een padnaam. Dit is ook de reden waarom standaard Unix bestandsnamen geen spaties, etc. bevatten. Wanneer je te maken hebt met bestandsnamen die spaties en andere lastige karakters bevatten, moet je uiterst voorzichtig zijn, en ik vond lang geleden dat ik een programma nodig had dat niet standaard is op Unix. Ik noem het escape (versie 1.1 dateert van 1989-08-23T16:01:45Z). Hier is een voorbeeld van escape in gebruik - met het SCCS besturingssysteem. Het is een cover script dat zowel een delta (denk check-in) als een get (denk check-out). Verschillende argumenten, vooral -y (de reden waarom je de wijziging hebt gemaakt) zouden spaties en nieuwe regels bevatten. Merk op dat het script dateert uit 1992, dus het gebruikt back-ticks in plaats van $(cmd ...) notatie en gebruikt niet #!/bin/sh op de eerste regel.

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

(Ik zou escape tegenwoordig waarschijnlijk niet meer zo grondig gebruiken - het is het is niet nodig met het -e argument, bijvoorbeeld - maar over het algemeen is dit een van mijn eenvoudigere scripts met escape). Het escape programma voert simpelweg zijn argumenten uit, net zoals echo doet, maar het zorgt ervoor dat de argumenten beschermd zijn voor gebruik met eval (één niveau van eval; ik heb een programma dat op afstand shell en dat moest ontsnappen aan de uitvoer van escape).

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

Ik heb een ander programma, al, dat zijn argumenten één voor één opsomt (en het is nog ouder: versie 1.1 uit 1987-01-27T14:35:49). Het is vooral nuttig bij het debuggen van scripts, omdat het kan worden aangesloten op een commandoregel om te zien welke argumenten daadwerkelijk aan het commando worden doorgegeven.

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

[Aangevuld: En om het verschil te laten zien tussen de verschillende "$@" notaties, volgt hier nog een voorbeeld:

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

Merk op dat niets de originele spaties tussen de * en */* op de commandoregel behoudt. Merk ook op dat je de 'command line arguments' in de shell kunt veranderen door te gebruiken:

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

Dit stelt 4 opties in, '-new', '-opt', 'and', en 'arg met spatie'.
] Hmm, dat's nogal een lang antwoord - misschien is exegese de betere term. Broncode voor escape beschikbaar op aanvraag (email naar firstname dot achternaam at gmail dot com). De broncode voor al is ongelooflijk eenvoudig:

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

Dat's alles. Het is gelijkwaardig aan het test.sh script dat Robert Gamble liet zien, en zou geschreven kunnen worden als een shell functie (maar shell functies bestonden nog niet in de lokale versie van Bourne shell toen ik al voor het eerst schreef). Merk ook op dat je al kunt schrijven als een eenvoudig shell script:

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

De voorwaarde is nodig om geen uitvoer te produceren als er geen argumenten worden doorgegeven. Het printf commando zal een lege regel produceren met alleen het format string argument, maar het C programma produceert niets.

Commentaren (1)

Merk op dat Robert's antwoord correct is, en het werkt ook in sh. Je kunt het (portabel) nog verder vereenvoudigen:

for i in "$@"

is gelijkwaardig aan:

for i

Ik bedoel, je hebt niets nodig!

Testen ($ is commando prompt):

$ 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

Ik heb hier voor het eerst over gelezen in Unix Programming Environment van Kernighan en Pike.

In bash, help for wordt dit gedocumenteerd:

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

Als 'in WORDS ...;' niet aanwezig is, dan wordt 'in "$@"' aangenomen.

Commentaren (10)