Questo è stato discusso in una serie di domande su unix.SE, cercherò di raccogliere tutti i problemi che posso venire qui. Riferimenti alla fine.
Perché fallisce
Il motivo per cui si affrontano questi problemi è la divisione delle parole e il fatto che le virgolette espanse dalle variabili non fungono da virgolette, ma sono solo caratteri ordinari.
I casi presentati nella domanda:
$ abc='ls -l "/tmp/test/my dir"'
Qui, $abc
è diviso e ls
ottiene i due argomenti "/tmp/test/my
e dir"
(con le virgolette nella parte anteriore del primo e nella parte posteriore del secondo):
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
Qui, l'espansione è quotata, quindi è mantenuta come una sola parola. La shell cerca di trovare un programma chiamato ls -l "/tmp/test/my dir"
, spazi e virgolette inclusi.
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
E qui, solo la prima parola o $abc
viene presa come argomento -c
, quindi Bash viene eseguito ls
nella directory corrente. Le altre parole sono argomenti di bash, e vengono utilizzati per riempire $0
, $1
e così via
$ bash -c $abc
'my dir'
Con bash -c "$abc"
, e eval "$abc"
, c'è un'ulteriore fase di elaborazione della shell, che fa funzionare le virgolette, ma provoca anche l'elaborazione di tutte le espansioni della shell , quindi c'è il rischio di eseguire accidentalmente un'espansione dei comandi dai dati forniti dall'utente, a meno che tu non sia molto attento a citare.
Modi migliori per farlo
I due modi migliori per memorizzare un comando sono a) utilizzare invece una funzione, b) utilizzare una variabile di matrice (o i parametri posizionali).
Utilizzando una funzione:
Dichiarare semplicemente una funzione con il comando all'interno ed eseguire la funzione come se fosse un comando. Le espansioni nei comandi all'interno della funzione vengono elaborate solo quando il comando viene eseguito, non quando è definito, e non è necessario citare i singoli comandi.
# define it
myls() {
ls -l "/tmp/test/my dir"
}
# run it
myls
Utilizzando un array:
Le matrici consentono di creare variabili multi-parola in cui le singole parole contengono spazi bianchi. Qui, le singole parole sono memorizzate come elementi array distinti e l' "${array[@]}"
espansione espande ogni elemento come parole shell separate:
# define the array
mycmd=(ls -l "/tmp/test/my dir")
# run the command
"${mycmd[@]}"
La sintassi è leggermente orribile, ma le matrici consentono anche di costruire la riga di comando pezzo per pezzo. Per esempio:
mycmd=(ls) # initial command
if [ "$want_detail" = 1 ]; then
mycmd+=(-l) # optional flag
fi
mycmd+=("$targetdir") # the filename
"${mycmd[@]}"
oppure mantieni costanti parti della riga di comando e usa l'array fill solo una parte di essa, opzioni o nomi di file:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
Il rovescio della medaglia degli array è che non sono una funzionalità standard, quindi le semplici shell POSIX (come dash
quelle predefinite /bin/sh
in Debian / Ubuntu) non le supportano (ma vedi sotto). Bash, ksh e zsh lo fanno, tuttavia, quindi è probabile che il tuo sistema abbia delle shell che supportano gli array.
utilizzando "$@"
Nelle shell senza supporto per array denominati, è ancora possibile utilizzare i parametri posizionali (lo pseudo-array "$@"
) per contenere gli argomenti di un comando.
I seguenti dovrebbero essere bit di script portatili che fanno l'equivalente dei bit di codice nella sezione precedente. L'array viene sostituito con "$@"
l'elenco dei parametri posizionali. L'impostazione "$@"
viene eseguita set
e le doppie virgolette intorno "$@"
sono importanti (causano la citazione individuale degli elementi dell'elenco).
Innanzitutto, è sufficiente archiviare un comando con argomenti "$@"
ed eseguirlo:
set -- ls -l "/tmp/test/my dir"
"$@"
Impostazione condizionale di parti delle opzioni della riga di comando per un comando:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "$@" -l
fi
set -- "$@" "$targetdir"
"$@"
Usando solo "$@"
per opzioni e operandi:
set -- -x -v
set -- "$@" file1 "file name with whitespace"
set -- "$@" /somedir
transmutate "$@"
(Naturalmente, di "$@"
solito è pieno degli argomenti dello script stesso, quindi dovrai salvarli da qualche parte prima di ri-proposire "$@"
.)
Stai attento eval
!
Poiché eval
introduce un ulteriore livello di elaborazione di preventivi ed espansioni, è necessario prestare attenzione all'input dell'utente. Ad esempio, questo funziona finché l'utente non digita alcuna virgoletta singola:
read -r filename
cmd="ls -l '$filename'"
eval "$cmd";
Ma se danno l'input '$(uname)'.txt
, il tuo script esegue felicemente la sostituzione del comando.
Una versione con array è immune da ciò dato che le parole sono tenute separate per tutto il tempo, non ci sono citazioni o altre elaborazioni per il contenuto di filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
Riferimenti