Ho un comando complesso di cui vorrei fare uno script shell/bash. 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 al loro interno.
Usate "$@"
per rappresentare tutti gli argomenti:
for var in "$@"
do
echo "$var"
done
Questo itererà su ogni argomento e lo stamperà su una linea separata. $@ si comporta come $*, eccetto che quando viene citato gli argomenti vengono spezzati correttamente se ci sono spazi al loro interno:
sh test.sh 1 2 '3 4'
1
2
3 4
Riscrivere una risposta ormai cancellata da VonC._Riscrivere una risposta ormai cancellata da VonC.
La succinta risposta di Robert Gamble'affronta direttamente la domanda.
Questa amplifica alcuni problemi con nomi di file contenenti spazi.
Vedere 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, anche "$*"
va bene, ma "$@"
di 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 legati a 'tutti gli argomenti della shell', ma fanno cose diverse. Quando non sono quotati, $*
e $@
fanno la stessa cosa. Trattano ogni 'parola' (sequenza di spazi non bianchi) come un argomento separato. Le forme quotate sono abbastanza diverse, tuttavia: "$*"
tratta la lista di argomenti come una singola stringa separata da spazi, mentre "$@"
tratta gli argomenti quasi esattamente come erano quando sono stati specificati sulla linea di comando.
"$@"
si espande a nulla quando non ci sono argomenti posizionali; "$*"
si espande a una stringa vuota — e sì, c'è una differenza, anche se può essere difficile da percepire.
Vedi maggiori informazioni qui sotto, dopo l'introduzione del comando (non standard) al
.
Tesi secondaria: se hai bisogno di elaborare argomenti con spazi e poi
passarli ad altri comandi, allora a volte avete bisogno di strumenti non standard
strumenti non standard per assistere. (Oppure dovreste usare gli array, con attenzione: "${array[@]}"
si comporta analogamente 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 processa le virgolette prima di espandere
le variabili.
Quindi, per fare in modo che la shell presti attenzione agli apici incorporati in $var
,
dovete 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 doppi apici 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
roba, quindi (abbastanza divertente) molti programmi Unix non fanno un buon lavoro nel
gestirli.
Su Unix, un nome di file (singolo componente) può contenere qualsiasi carattere tranne
slash e NUL '0'
.
Tuttavia, le shell incoraggiano fortemente l'assenza di spazi, newline o tabulazioni
in nessun punto di un nome di 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
fastidiosi, bisogna stare molto attenti, e ho scoperto
tempo fa che avevo bisogno di un programma che non è standard su Unix.
L'ho chiamato escape
(la versione 1.1 è datata 1989-08-23T16:01:45Z).
Ecco un esempio di escape
in uso - con il sistema di controllo SCCS.
È uno script di copertura che fa sia un delta
(pensa check-in) che un
get
(pensate a check-out).
Vari argomenti, specialmente -y
(il motivo per cui hai fatto la modifica)
conterrebbero spazi vuoti e linee nuove.
Notate che lo script risale al 1992, quindi usa i back-tick invece di
$(cmd ...)
notazione e non usa #!/bin/sh
sulla prima linea.
: "@(#)$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 l'escape così accuratamente in questi giorni - non è
non è necessario con l'argomento -e
, per esempio - ma nel complesso, questo è
uno dei miei script più semplici con l'uso di escape
).
Il programma escape
emette semplicemente i suoi argomenti, un po' come fa echo ma si assicura che gli argomenti siano protetti per l'uso con
eval(un livello di
eval; ho un programma che esegue una shell remota e che aveva bisogno di 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 al
che elenca i suoi argomenti uno per riga
(ed è ancora più antico: versione 1.1 datata 1987-01-27T14:35:49).
È molto utile quando si fa il debug degli script, poiché può essere inserito in una
linea di comando per vedere quali argomenti sono 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
$
Notate che nulla preserva gli spazi vuoti originali tra le notazioni *
e `/`` sulla linea di comando. Inoltre, si noti che si possono cambiare gli 'argomenti della linea di comando' nella shell usando:
set -- -new -opt and "arg with space"
Questo imposta 4 opzioni, '-new
', '-opt
', 'e
', e 'arg con spazio
'.
br>
]
Hmm, è una risposta piuttosto lunga - forse esegesi è il termine migliore.
Il codice sorgente di escape
è disponibile su richiesta (email a firstname dot
cognome at gmail dot com).
Il codice sorgente per al
è incredibilmente semplice:
#include <stdio.h>
int main(int argc, char **argv)
{
while (*++argv != 0)
puts(*argv);
return(0);
}
Questo è tutto. È equivalente allo script test.sh
che Robert Gamble ha mostrato, e potrebbe essere scritto come una funzione di shell (ma le funzioni di shell non esistevano nella versione locale di Bourne shell quando ho scritto al
).
Notate anche che potete scrivere al
come un semplice script di shell:
[ $# != 0 ] && printf "%s\n" "$@"
Il condizionale è necessario affinché non produca alcun output quando non vengono passati argomenti. Il comando printf
produrrà una riga vuota con solo l'argomento format string, ma il programma C non produce nulla.
Si noti che la risposta di Robert è corretta, e funziona anche in sh
. Si può (portabilmente) semplificare ancora di più:
for i in "$@"
è equivalente a:
for i
Cioè, non hai bisogno di niente!
Prova ($
è il 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
Ho letto per la prima volta di questo in Unix Programming Environment di Kernighan e Pike.
In bash
, help for
lo documenta:
for NAME [in WORDS ... ;] do COMMANDS; done
Se
'in WORDS ...;'
non è presente, allora'in "$@"'
viene assunto.