Come scorrere gli argomenti in uno script Bash


900

Ho un comando complesso di cui vorrei creare uno script shell / bash. Posso scriverlo in termini di $1facilmente:

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 dei file con spazi all'interno.


67
Cordiali saluti, è una buona idea mettere sempre i parametri tra virgolette. Non si sa mai quando un parm potrebbe contenere uno spazio incorporato.
David R Tribble,

Risposte:


1482

Utilizzare "$@"per rappresentare tutti gli argomenti:

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

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

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

38
Per inciso, uno degli altri meriti di "$@"è che non si espande nel nulla quando non ci sono parametri posizionali mentre si "$*"espande nella stringa vuota - e sì, c'è una differenza tra nessun argomento e un argomento vuoto. Vedi ${1:+"$@"} in / bin / sh` .
Jonathan Leffler,

9
Notare che questa notazione dovrebbe essere usata anche nelle funzioni shell per accedere a tutti gli argomenti della funzione.
Jonathan Leffler,

Eliminando le doppie virgolette: $varsono stato in grado di eseguire gli argomenti.
eonista,

come posso iniziare dal secondo argomento? Voglio dire, ho bisogno di questo, ma inizia sempre dal secondo argomento, ovvero salta $ 1.
m4l490n,

4
@ m4l490n: shiftbutta via $1e sposta verso il basso tutti gli elementi successivi.
MSalters

239

Riscrivi una risposta ora cancellata da VonC .

La risposta sintetica di Robert Gamble si occupa direttamente della domanda. Questo amplifica alcuni problemi con nomi di file contenenti spazi.

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

Tesi di base: "$@" è corretta e $*(non quotata) è quasi sempre sbagliata. Questo perché "$@"funziona bene quando gli argomenti contengono spazi e funziona come $*quando non lo fanno. In alcune circostanze, "$*"va bene lo stesso, ma di "$@"solito (ma non sempre) funziona negli stessi posti. Non quotati $@e $*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 quotate, $*e $@fare la stessa cosa. Trattano ogni "parola" (sequenza di non spazi bianchi) come un argomento separato. Le forme tra virgolette sono piuttosto diverse, però: "$*"tratta l'elenco degli argomenti come una singola stringa separata da spazi, mentre "$@"tratta gli argomenti quasi esattamente come quando erano specificati sulla riga di comando. "$@"non si espande per niente quando non ci sono argomenti posizionali; "$*"si 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 aiutarti. (O dovresti usare le matrici, con attenzione: "${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 di espandere le variabili. Quindi, per fare in modo che la shell presti attenzione alle virgolette incorporate $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 hai nomi di file come " He said, "Don't do this!"" (con virgolette, virgolette doppie 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 (abbastanza stranamente) molti programmi Unix non fanno un buon lavoro nel gestirli. Su Unix, un nome file (singolo componente) può contenere qualsiasi carattere tranne barra e NUL '\0'. Tuttavia, le shell incoraggiano fortemente nessuno spazio o newline o tab ovunque nei nomi di un percorso. È anche il motivo per cui i nomi di file Unix standard non contengono spazi, ecc.

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

Ecco un esempio di escapein uso - con il sistema di controllo SCCS. È uno script di copertina che esegue 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 righe. Si noti che lo script risale al 1992, quindi utilizza back-tick anziché $(cmd ...)notazione e non utilizza #!/bin/shsulla 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 escape così accuratamente in questi giorni - non è necessario con l' -eargomento, ad esempio - ma nel complesso, questo è uno dei miei script più semplici usando escape.)

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

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

Ho un altro programma chiamato alche elenca i suoi argomenti uno per riga (ed è ancora più antico: versione 1.1 del 1987-01-27T14: 35: 49). È molto utile durante il debug degli script, in quanto 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
$

Notare che nulla conserva gli spazi vuoti 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 escapedisponibile su richiesta (e-mail al nome punto punto cognome su gmail punto com). Il codice sorgente per alè incredibilmente semplice:

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

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

Si noti inoltre che è possibile scrivere alcome un semplice script shell:

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

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


132

Nota che la risposta di Robert è corretta e funziona shanche. Puoi (portabilmente) semplificarlo ulteriormente:

for i in "$@"

è equivalente a:

for i

Cioè, non hai bisogno di niente!

Test ( $è prompt dei comandi):

$ 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

L'ho letto per la prima volta in Unix Programming Environment di Kernighan e Pike.

In bash, help fordocumenta questo:

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

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


4
Non sono d'accordo. Bisogna sapere cosa "$@"significa criptico , e una volta che sai cosa for isignifica, non è meno leggibile di for i in "$@".
Alok Singhal,

10
Non ho affermato che for iè meglio perché consente di risparmiare i tasti. Ho confrontato la leggibilità di for ie for i in "$@".
Alok Singhal,

questo è quello che stavo cercando - come si chiama questo presupposto di $ @ nei cicli in cui non è necessario fare riferimento esplicitamente? C'è un aspetto negativo nel non usare $ @ reference?
qodeninja,

58

Per casi semplici puoi anche usare shift. Tratta l'elenco degli argomenti come una coda. Ognuno shiftbutta fuori il primo argomento e l'indice di ciascuno degli argomenti rimanenti viene diminuito.

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

Penso che questa risposta sia migliore perché se lo fai shiftin un ciclo for, quell'elemento fa ancora parte dell'array, mentre con questo shiftfunziona come previsto.
DavidG,

2
Si noti che è necessario utilizzare echo "$1"per preservare la spaziatura nel valore della stringa dell'argomento. Inoltre, (un problema minore) questo è distruttivo: non puoi riutilizzare gli argomenti se li hai spostati tutti via. Spesso, questo non è un problema: in ogni caso elabori l'elenco degli argomenti solo una volta.
Jonathan Leffler,

1
Concordi sul fatto che shift sia meglio, perché in questo modo hai un modo semplice di prendere due argomenti in un caso switch per elaborare argomenti flag come "-o outputfile".
Baxissimo,

D'altra parte, se stai iniziando a analizzare gli argomenti flag, potresti prendere in considerazione un linguaggio di script più potente di bash :)
nuoritoveri,

4
Questa soluzione è migliore se hai bisogno di un paio di parametri-valore, ad esempio --file myfile.txt $ 1 è il parametro, $ 2 è il valore e chiami il turno due volte quando devi saltare un altro argomento
Daniele Licitra,

16

Puoi anche accedervi come elementi di un array, ad esempio se non vuoi iterarli tutti

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

for (( j=0; j<argc; j++ )); do
    echo "${argv[j]}"
done

5
Credo che la riga argv = ($ @) dovrebbe essere argv = ("$ @") altri argomenti saggi con spazi non sono gestiti correttamente
kdubs,

Confermo che la sintassi corretta è argv = ("$ @"). a meno che non otterrai gli ultimi valori se hai un parametro tra virgolette. Ad esempio, se usi qualcosa come ./my-script.sh param1 "param2 is a quoted param": - usando argv = ($ @) otterrai [param1, param2], - usando argv = ("$ @"), otterrai [param1, param2 è un parametro citato]
jseguillon

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

aparse "$@"

1
Come indicato di seguito, [ $# != 0 ]è meglio. #$$#while [[ $# > 0 ]] ...
Quanto

1

Amplificando la risposta di baz, se è necessario enumerare l'elenco degli argomenti con un indice (ad esempio per cercare una parola specifica), è possibile farlo senza copiare l'elenco o modificarlo.

Supponi di voler dividere un elenco di argomenti in un doppio trattino ("-") e passare gli argomenti prima dei trattini a un comando e gli argomenti dopo i trattini a un altro:

 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:$#}"
 }

I risultati dovrebbero apparire così:

 $ 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

1

getopt Usa il comando nei tuoi script per formattare qualsiasi opzione o parametro della riga di comando.

#!/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

Uno complicato, lo farei ricerche approfondite. Esempio: file: ///usr/share/doc/util-linux/examples/getopt-parse.bash, Manuale: man.jimmylandstudios.xyz/?man=getopt
JimmyLandStudios
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.