Hvordan iterere over argumenter i et Bash-skript

Jeg har en kompleks kommando som jeg ønsker å lage et shell/bash-skript av. Jeg kan skrive det i form av $ 1 enkelt:

foo $1 args -o $1.ext

Jeg vil være i stand til å sende flere inndatanavn til skriptet. Hva er den riktige måten å gjøre det på?

Og selvfølgelig vil jeg håndtere filnavn med mellomrom i dem.

Løsning

Bruk "$@" for å representere alle argumentene:

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

Dette vil iterere over hvert argument og skrive det ut på en egen linje. $@ oppfører seg som $* bortsett fra at når det er sitert, blir argumentene delt opp riktig hvis det er mellomrom i dem:

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

Omskriving av et nå slettet svar av VonC._ Robert Gamble&# 39s kortfattede svar omhandler direkte spørsmålet. Denne utdyper noen problemer med filnavn som inneholder mellomrom. Se også: ${1:+"$@"} i /bin/sh Grunnleggende tese: "$@" er riktig, og $* (uten anførselstegn) er nesten alltid feil. Dette er fordi "$@" fungerer fint når argumentene inneholder mellomrom, og fungerer på samme måte som $* når de ikke gjør det. I noen tilfeller er "$*" også OK, men "$@" fungerer vanligvis (men ikke alltid) på de samme stedene. alltid) fungerer på de samme stedene. Uten anførselstegn er $@ og $* likeverdige (og nesten alltid feil). Så hva er forskjellen mellom $*, $@, "$*" og "$@"? De er alle relatert til 'alle argumentene til skallet', men de gjør forskjellige ting. Når de ikke er sitert, gjør $* og $@ det samme. De behandler hvert 'ord' (sekvens av ikke-whitespace) som et eget argument. De siterte formene er imidlertid ganske forskjellige: "$*" behandler argumentlisten som en enkelt mellomromseparert streng, mens "$@" behandler argumentene nesten nøyaktig slik de var da de ble spesifisert på kommandolinjen. "$@" ekspanderer til ingenting i det hele tatt når det ikke er noen posisjonsargumenter; "$*" ekspanderer til en tom streng; og ja, det er en forskjell, selv om det kan være vanskelig å oppfatte det. Se mer informasjon nedenfor, etter introduksjonen av (ikke-standard) kommandoen al. Sekundær avhandling: hvis du trenger å behandle argumenter med mellomrom og deretter deretter sende dem videre til andre kommandoer, trenger du noen ganger ikke-standardiserte verktøy for å hjelpe deg. (Eller du bør bruke arrays, forsiktig: "${array[@]}" oppfører seg analogt med "$@"). Eksempel:

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

Hvorfor fungerer det ikke? Det fungerer ikke fordi skallet behandler anførselstegn før det ekspanderer variabler. Så, for å få skallet til å ta hensyn til anførselstegnene innebygd i $var, må du bruke 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
    $ 

Dette blir virkelig vanskelig når du har filnavn som " `Han sa, "Don't do this!&quot" (med anførselstegn og doble anførselstegn og mellomrom).

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

Skallene (alle sammen) gjør det ikke spesielt enkelt å håndtere slikt ting, så (morsomt nok) mange Unix-programmer gjør ikke en god jobb med å håndtere dem håndtere dem. På Unix kan et filnavn (enkeltkomponent) inneholde alle tegn unntatt skråstrek og NUL '\0'. Imidlertid oppmuntrer skallene sterkt til ingen mellomrom eller nye linjer eller tabulatorer hvor som helst i et banenavn. Det er også grunnen til at standard Unix-filnavn ikke inneholder mellomrom osv. Når du arbeider med filnavn som kan inneholde mellomrom og annet plagsomme tegn, må du være ekstremt forsiktig, og jeg fant for lenge siden ut at jeg trengte et program som lenge siden at jeg trengte et program som ikke er standard på Unix. Jeg kaller det escape (versjon 1.1 ble datert 1989-08-23T16:01:45Z). Her er et eksempel på escape i bruk - med SCCS-kontrollsystemet. Det er et cover-skript som gjør både en delta (tenk check-in) og en get (tenk check-out). Forskjellige argumenter, spesielt -y (grunnen til at du gjorde endringen) vil inneholde blanktegn og nye linjer. Merk at skriptet er fra 1992, så det bruker back-ticks i stedet for $(cmd ...) notasjon og bruker ikke #!/bin/sh på første linje.

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

(Jeg ville sannsynligvis ikke bruke escape fullt så grundig i disse dager - det er ikke nødvendig med -e argumentet, for eksempel - men totalt sett er dette et av mine enklere et av mine enklere skript som bruker escape). Programmet escape sender ganske enkelt ut argumentene sine, ganske likt echo``. gjør, men det sørger for at argumentene er beskyttet for bruk med eval(ett nivå aveval; jeg har et program som gjorde ekstern shell-kjøring, og som trengte å unnslippe kjøring, og som trengte å unnslippe utdataene fraescape`).

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

Jeg har et annet program som heter al som lister opp argumentene sine ett per linje (og det er enda eldre: versjon 1.1 datert 1987-01-27T14:35:49). Det er mest nyttig ved feilsøking av skript, da det kan plugges inn i en kommandolinje for å se hvilke argumenter som faktisk sendes til kommandoen.

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

[ Lagt til: Og nå for å vise forskjellen mellom de forskjellige "$@" notasjonene, her er et eksempel til:

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

Legg merke til at ingenting bevarer de opprinnelige mellomrommene mellom * og */* på kommandolinjen. Legg også merke til at du kan endre 'kommandolinjeargumentene' i skallet ved å bruke:

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

Dette angir 4 alternativer, '-new', '-opt', 'and', og 'arg med mellomrom'. &#;br> ] Hmm, det er et ganske langt svar - kanskje eksegese er et bedre begrep. Kildekode for escape tilgjengelig på forespørsel (e-post til fornavn dot etternavn at gmail dot com). Kildekoden for al er utrolig enkel:

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

Det er alt. Det tilsvarer test.sh-skriptet som Robert Gamble viste, og kan skrives som en shell-funksjon (men shell-funksjoner eksisterte ikke i den lokale versjonen av Bourne-shell da jeg først skrev al). Legg også merke til at du kan skrive al som et enkelt shell-skript:

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

Det betingede er nødvendig slik at det ikke produserer noen utdata når det ikke sendes noen argumenter. Kommandoen printf vil produsere en tom linje med bare formatstrengen som argument, men C-programmet produserer ingenting.

Kommentarer (1)

Legg merke til at Roberts svar er riktig, og det fungerer også i sh. Du kan (portabelt) forenkle det ytterligere:

for i in "$@"

er ekvivalent med:

for i

Det vil si at du ikke trenger noe!

Testing ($ er ledetekst):

$ 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

Jeg leste først om dette i Unix Programming Environment av Kernighan og Pike.

I bash dokumenterer help for dette:

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

Hvis 'in WORDS ...;' ikke er til stede, antas 'in "$@"'.

Kommentarer (10)