Come eseguire iterazioni su argomenti in uno script Bash

Ho un comando complesso che mi piacerebbe fare uno script shell / bash di. Posso scriverlo in termini di $1 facilmente:

 foo $1 args -o $1.ext 

Voglio essere in grado di passare più nomi di input allo script. Qual è il modo giusto per farlo?

E, naturalmente, voglio gestire i nomi di file con spazi in essi.

Usa "$@" per rappresentare tutti gli argomenti:

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

Questo itererà su ogni argomento e lo stamperà su una riga separata. $ @ si comporta come $ * tranne che quando vengono citati gli argomenti vengono suddivisi correttamente se ci sono degli spazi in essi:

 sh test.sh 1 2 '3 4' 1 2 3 4 

Riscrivi di una risposta cancellata da VonC .

La succinta risposta di Robert Gamble tratta direttamente la domanda. Questo si amplifica su alcuni problemi con nomi di file contenenti spazi.

Vedi anche: $ {1: + “$ @”} in / bin / sh

Tesi di base: "$@" è corretto, e $* (non quotato) è quasi sempre sbagliato. Questo perché "$@" funziona bene quando gli argomenti contengono spazi e funziona allo stesso modo di $* quando non lo fanno. In alcune circostanze, "$*" è OK, ma "$@" solito (ma non sempre) funziona negli stessi posti. Non quotati, $@ e $* sono equivalenti (e quasi sempre sbagliati).

Quindi, qual è la differenza tra $* , $@ , "$*" e "$@" ? Sono tutti collegati a “tutti gli argomenti della shell”, ma fanno cose diverse. Quando non quotato, $* e $@ fanno la stessa cosa. Trattano ogni “parola” (sequenza di non spazi bianchi) come argomento separato. Le forms citate sono piuttosto diverse, tuttavia: "$*" considera l’elenco degli argomenti come una singola stringa separata dallo spazio, mentre "$@" tratta gli argomenti quasi esattamente come erano quando specificati sulla riga di comando. "$@" espande affatto quando non ci sono argomenti posizionali; "$*" espande in una stringa vuota – e sì, c’è una differenza, anche se può essere difficile percepirla. Vedi ulteriori informazioni di seguito, dopo l’introduzione del comando (non standard) al .

Tesi secondaria: se hai bisogno di elaborare argomenti con spazi e poi passarli ad altri comandi, a volte hai bisogno di strumenti non standard per aiutare. (Oppure dovresti usare gli array, attentamente: "${array[@]}" si comporta in modo analogo a "$@" .)

Esempio:

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

Perché non funziona? Non funziona perché la shell elabora le virgolette prima che espanda le variabili. Quindi, per fare in modo che la shell presta attenzione alle virgolette incorporate in $var , devi usare 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 $ 

Questo diventa davvero complicato quando si hanno nomi di file come ” He said, "Don't do this!" ” (Con virgolette e virgolette e spazi).

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

Le shell (tutte) non rendono particolarmente facile gestire tali cose, quindi (stranamente) molti programmi Unix non fanno un buon lavoro nel gestirli. Su Unix, un nome file (singolo componente) può contenere qualsiasi carattere tranne la barra e NUL '\0' . Tuttavia, le shell incoraggiano fortemente nessuno spazio o newline o tab ovunque in un nome di percorso. È anche il motivo per cui i nomi dei file Unix standard non contengono spazi, ecc.

Quando si ha a che fare con nomi di file che possono contenere spazi e altri personaggi problematici, bisogna stare estremamente attenti, e ho scoperto molto tempo fa che avevo bisogno di un programma che non fosse standard su Unix. Lo chiamo escape (la versione 1.1 era datata 1989-08-23T16: 01: 45Z).

Ecco un esempio di escape in uso – con il sistema di controllo SCCS. È uno script di copertina che fa sia un delta (pensa al check-in ) sia un get (pensa al check-out ). Vari argomenti, in particolare -y (il motivo per cui hai apportato la modifica) conterrebbero spazi vuoti e nuove righe. Si noti che lo script risale al 1992, quindi usa notazioni back-tick invece di $(cmd ...) e non usa #!/bin/sh nella prima riga.

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

(Probabilmente non userei la fuga abbastanza completamente in questi giorni – non è necessario con l’argomento -e , ad esempio – ma nel complesso, questo è uno dei miei script più semplici che usano escape ).

Il programma di escape emette semplicemente i suoi argomenti, piuttosto come fa echo , ma assicura che gli argomenti siano protetti per l’uso con eval (un livello di eval ; ho un programma che ha fatto l’esecuzione della shell remota, e che doveva sfuggire all’output di escape ).

  $ escape $var '"./my' 'dir"' '"./anotherdir"' $ escape "$var" '"./my dir" "./anotherdir"' $ escape xyz xyz $ 

Ho un altro programma chiamato al che elenca i suoi argomenti uno per riga (ed è ancora più antico: versione 1.1 del 1987-01-27T14: 35: 49). È molto utile quando si esegue il debug degli script, poiché può essere collegato a una riga di comando per vedere quali argomenti vengono effettivamente passati al comando.

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

[ Aggiunto: E ora per mostrare la differenza tra le varie notazioni "$@" , ecco un altro esempio:

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

Si noti che nulla mantiene gli spazi bianchi originali tra * e */* sulla riga di comando. Inoltre, tieni presente che puoi modificare gli “argomenti della riga di comando” nella shell usando:

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

Questo imposta 4 opzioni, ‘ -new ‘, ‘ -opt ‘, ‘ and ‘, e ‘ arg with space ‘.
]

Hmm, questa è una risposta piuttosto lunga – forse l’ esegesi è il termine migliore. Codice sorgente per escape disponibile su richiesta (e-mail a firstname dot lastname su gmail dot com). Il codice sorgente per al è incredibilmente semplice:

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

È tutto. È equivalente allo script test.sh mostrato da Robert Gamble e potrebbe essere scritto come una funzione di shell (ma le funzioni di shell non esistevano nella versione locale di Bourne shell quando scrissi per la prima volta al ).

Si noti inoltre che è ansible scrivere al come uno script di shell semplice:

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

Il condizionale è necessario in modo che non produca output quando non viene passato alcun argomento. Il comando printf produrrà una riga vuota con solo l’argomento stringa formato, ma il programma C non produce nulla.

Nota che la risposta di Robert è corretta, e funziona anche in sh . Puoi (portabile) semplificarlo ulteriormente:

 for i in "$@" 

è equivalente a:

 for i 

Cioè, non hai bisogno di niente!

Test ( $ è il prompt dei comandi):

 $ set ab "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 

Ho letto per la prima volta su questo in ambiente di programmazione Unix di Kernighan e Pike.

In bash , help for documenti questo:

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

Se 'in WORDS ...;' non è presente, quindi si assume 'in "$@"' .

Per i casi più semplici puoi anche usare shift . Tratta la lista degli argomenti come una coda, ogni shift lancia il primo argomento, il numero di ogni argomento che viene lasciato viene decrementato.

 #this prints all arguments while test $# -gt 0 do echo $1 shift done 

Puoi anche accedervi come elementi di un array, ad esempio se non vuoi iterare attraverso tutti loro

 argc=$# argv=($@) for (( j=0; j 
 aparse() { while [[ $# > 0 ]] ; do case "$1" in --arg1) varg1=${2} shift ;; --arg2) varg2=true ;; esac shift done } aparse "$@"