Kaip "Bash" scenarijuje iteruoti argumentus

Turiu sudėtingą komandą, iš kurios norėčiau sukurti shell/bash scenarijų. Ją galiu lengvai užrašyti kaip $1:

foo $1 args -o $1.ext

Noriu, kad scenarijui būtų galima perduoti kelis įvesties vardus. Kaip teisingai tai padaryti?

Ir, žinoma, noriu tvarkyti failų pavadinimus su tarpais.

Sprendimas

Naudokite "$@" visiems argumentams reikšti:

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

Taip bus iteruojamas kiekvienas argumentas ir spausdinamas atskiroje eilutėje. $@ elgiasi kaip $*, išskyrus tai, kad cituojant argumentai tinkamai suskaidomi, jei juose yra tarpų:

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

Perrašytas dabar jau ištrintas atsakymas pagal VonC. Robert Gamble's glaustame atsakyme tiesiogiai nagrinėjamas klausimas. Šiame atsakyme plačiau aptariami kai kurie klausimai, susiję su failų pavadinimais, kuriuose yra tarpų. Taip pat žr: ${1:+"$@"} in /bin/sh Pagrindinė tezė: "$@" yra teisinga, o $* (be kabučių) beveik visada klaidinga. Taip yra todėl, kad "$@" veikia gerai, kai argumentuose yra tarpai, o veikia taip pat, kaip $*, kai jų nėra. Tam tikromis aplinkybėmis "$*" taip pat tinka, bet "$@" paprastai (bet ne visada) veikia tose pačiose vietose. Neįrašyti $@ ir $* yra lygiaverčiai (ir beveik visada neteisingi). Taigi, kuo skiriasi $*, $@, "$*" ir "$@"? Visi jie susiję su 'visais apvalkalo argumentais', tačiau atlieka skirtingus veiksmus. Kai $* ir $@ nėra cituojami, jie atlieka tą patį. Jie kiekvieną 'žodį' (ne baltųjų ženklų seką) traktuoja kaip atskirą argumentą. Tačiau cituojamos formos yra visiškai skirtingos: "$*" traktuoja argumentų sąrašą kaip vieną tarpeliais atskirtą eilutę, o "$@" traktuoja argumentus beveik taip pat, kaip jie buvo nurodyti komandinėje eilutėje. "$@" išsiplečia į nieką, kai nėra pozicionuojamųjų argumentų; "$*" išsiplečia į tuščią eilutę — ir taip, yra skirtumas, nors jį gali būti sunku pastebėti. Daugiau informacijos rasite toliau, po (nestandartinės) komandos al įvedimo. Antroji tezė: jei reikia apdoroti argumentus su tarpais ir tada juos perduoti kitoms komandoms, tuomet kartais reikia nestandartinių pagalbos priemonių. (Arba atsargiai naudokite masyvus: "${Masyvas[@]}" elgiasi analogiškai kaip "$@".) Pavyzdys:

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

Kodėl tai neveikia? Neveikia, nes apvalkalas apdoroja kabutes prieš jas išplėsdamas kintamuosius. Taigi, kad apvalkalas atkreiptų dėmesį į kabutes, įterptas į $var, turite naudoti 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
    $ 

Tai tampa labai sudėtinga, kai turite tokius failų pavadinimus kaip "Jis sakė, "Nedarykite to!"" (su kabutėmis, dvigubomis kabutėmis ir tarpais).

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

Kriauklės (visos) ne itin palengvina tokių todėl (kaip bebūtų keista) daugelis "Unix" programų nelabai gerai atlieka šį darbą. juos apdoroti. "Unix" sistemoje failo pavadinime (viename komponente) gali būti bet kokie simboliai, išskyrus pasvirąjį brūkšnį ir NUL '\0'. Tačiau apvalkalai primygtinai rekomenduoja nenaudoti tarpų, naujų eilučių ar skirtukų niekur kelio pavadinime. Dėl to ir standartiniuose Unix failų varduose nėra tarpų ir pan. Kai susiduriama su failų vardais, kuriuose gali būti tarpų ir kt. simbolių, turite būti labai atsargūs, ir aš pastebėjau, kad seniai, kad man prireikė programos, kuri nėra standartinė "Unix" sistemoje. Pavadinau ją escape (1.1 versijos data - 1989-08-23T16:01:45Z). Štai escape naudojimo pavyzdys - su SCCS valdymo sistema. Tai yra viršelio scenarijus, kuris atlieka ir delta (pagalvokite check-in), ir Get (pagalvokite check-out). Įvairūs argumentai, ypač -y (priežastis, dėl kurios atlikote pakeitimą) yra tuščių ir naujų eilučių. Atkreipkite dėmesį, kad scenarijus sukurtas 1992 m., todėl jame naudojamos atgalinės žymės, o ne $(cmd ...) ir pirmoje eilutėje nenaudoja #!/bin/sh.

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

(Tikriausiai šiais laikais taip kruopščiai nenaudočiau escape - tai yra pavyzdžiui, nereikia su -e argumentu, bet apskritai tai yra vienas iš mano paprastesnių skriptų, naudojančių escape.) Programa escape tiesiog išveda savo argumentus, panašiai kaip echo tačiau ji užtikrina, kad argumentai būtų apsaugoti, kad juos būtų galima naudoti su eval (vienas iš eval lygių; turiu programą, kuri atliko nuotolinį shell vykdymą, ir jai reikėjo apsaugoti escape išvestį).

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

Turiu kitą programą, pavadintą al, kurios argumentai išvardijami po vieną eilutėje (ir ji dar senesnė: 1987-01-27T14:35:49 versija 1.1). Ji naudingiausia derinant scenarijus, nes ją galima prijungti prie į komandų eilutę, kad pamatytumėte, kokie argumentai iš tikrųjų perduodami komandai.

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

[Pridėta: O dabar, norėdami parodyti skirtumą tarp įvairių "$@" užrašų, pateikiame dar vieną pavyzdį:

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

Atkreipkite dėmesį, kad niekas neišsaugo originalių tuščių vietų tarp * ir */* komandinėje eilutėje. Taip pat atkreipkite dėmesį, kad galite pakeisti 'komandinės eilutės argumentus' apvalkale naudodami:

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

Taip nustatomos 4 parinktys: 'naujas', 'opt', 'ir' ir 'arg su tarpais'.
] Hmm, tai'gana ilgas atsakymas - gal geriau tinka terminas egzegezė. Pirminį escape kodą galima gauti paprašius (el. paštu firstname dot lastname at gmail dot com). al šaltinio kodas yra neįtikėtinai paprastas:

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

Štai ir viskas. Tai yra lygiavertis test.sh scenarijus, kurį parodė Robertas Gamble'as, ir galėtų būti parašytas kaip apvalkalo funkcija (tačiau apvalkalo funkcijos neegzistavo vietinėje Bourne'o apvalkalo versijoje, kai pirmą kartą parašiau al). Taip pat atkreipkite dėmesį, kad al galite parašyti kaip paprastą apvalkalo scenarijų:

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

Sąlyga reikalinga tam, kad jis nesukurtų išvesties, kai jam neperduodami jokie argumentai. Komanda printf išves tuščią eilutę tik su formato eilutės argumentu, tačiau C programa nieko neišves.

Komentarai (1)

Atkreipkite dėmesį, kad Roberto atsakymas yra teisingas, jis veikia ir sh. Galite jį dar labiau supaprastinti:

for i in "$@"

yra lygiavertis:

for i

T.y., jums nieko nereikia!

Testavimas ($ yra komandų eilutė):

$ 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

Pirmą kartą apie tai perskaičiau Kernighan ir Pike knygoje Unix programavimo aplinka.

bash, help for tai dokumentuoja:

... ;] do COMMANDS; done`

Jei 'in WORDS ...;' nėra, tuomet manoma, kad 'in "$@"'.

Komentarai (10)