$ ls -l /tmp/test/my dir/
total 0
Mi chiedevo perché i seguenti modi per eseguire il comando precedente non riescono o hanno esito positivo?
$ abc='ls -l "/tmp/test/my dir"'
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory
$ bash -c $abc
'my dir'
$ bash -c "$abc"
total 0
$ eval $abc
total 0
$ eval "$abc"
total 0
Risposta accettata:
Questo è stato discusso in una serie di domande su unix.SE, cercherò di raccogliere tutti i problemi che posso trovare qui. Riferimenti alla fine.
Perché non riesce
Il motivo per cui affronti questi problemi è la divisione delle parole e il fatto che le virgolette espanse dalle variabili non agiscono come virgolette, ma sono solo caratteri normali.
I casi presentati nella domanda:
L'assegnazione qui assegna la singola stringa ls -l "/tmp/test/my dir"
a abc
:
$ abc='ls -l "/tmp/test/my dir"'
Sotto, $abc
è diviso su spazi bianchi e ls
ottiene i tre argomenti -l
, "/tmp/test/my
e dir"
(con una citazione davanti alla seconda e un'altra dietro alla terza). L'opzione funziona, ma il percorso viene elaborato in modo errato:
$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory
Qui, l'espansione è citata, quindi è mantenuta come una singola parola. La shell cerca di trovare un programma chiamato letteralmente 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, $abc
è diviso e solo la prima parola risultante viene presa come argomento per -c
, quindi Bash esegue semplicemente ls
nella directory corrente. Le altre parole sono argomenti per bash e sono usate per riempire $0
, $1
, ecc.
$ bash -c $abc
'my dir'
Con bash -c "$abc"
e eval "$abc"
, c'è un ulteriore passaggio di elaborazione della shell, che fa funzionare le virgolette, ma fa anche rielaborare tutte le espansioni della shell , quindi c'è il rischio di correre accidentalmente, ad es. una sostituzione di comando dai dati forniti dall'utente, a meno che tu non stia 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 array (o i parametri posizionali).
Utilizzo di una funzione:
Dichiara semplicemente una funzione con il comando all'interno ed esegui 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
Utilizzo di un array:
Gli array consentono di creare variabili multi-parola in cui le singole parole contengono spazi bianchi. Qui, le singole parole vengono memorizzate come elementi distinti dell'array e il "${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 gli array consentono anche di costruire la riga di comando pezzo per pezzo. Ad 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 il riempimento dell'array solo per una parte di esso, come opzioni o nomi di file:
options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir
transmutate "${options[@]}" "${files[@]}" "$target"
Lo svantaggio degli array è che non sono una funzionalità standard, quindi semplici shell POSIX (come dash
, il /bin/sh
predefinito in Debian/Ubuntu) non li supportano (ma vedi sotto). Bash, ksh e zsh lo fanno, tuttavia, quindi è probabile che il tuo sistema abbia una shell che supporta gli array.
Utilizzo di "[email protected]"
Nelle shell senza supporto per array con nome, è comunque possibile utilizzare i parametri posizionali (lo pseudo-array "[email protected]"
) per contenere gli argomenti di un comando.
I seguenti dovrebbero essere bit di script portatili che eseguono l'equivalente dei bit di codice nella sezione precedente. L'array viene sostituito con "[email protected]"
, l'elenco dei parametri posizionali. Impostazione di "[email protected]"
è fatto con set
e le virgolette intorno a "[email protected]"
sono importanti (questi fanno sì che gli elementi della lista siano citati individualmente).
Innanzitutto, archiviando semplicemente un comando con argomenti in "[email protected]"
ed eseguirlo:
set -- ls -l "/tmp/test/my dir"
"[email protected]"
Impostazione condizionale di parti delle opzioni della riga di comando per un comando:
set -- ls
if [ "$want_detail" = 1 ]; then
set -- "[email protected]" -l
fi
set -- "[email protected]" "$targetdir"
"[email protected]"
Solo utilizzando "[email protected]"
per opzioni e operandi:
set -- -x -v
set -- "[email protected]" file1 "file name with whitespace"
set -- "[email protected]" /somedir
transmutate "[email protected]"
(Ovviamente, "[email protected]"
di solito è riempito con gli argomenti dello script stesso, quindi dovrai salvarli da qualche parte prima di riproporli "[email protected]"
.)
Utilizzo di eval
(attenzione qui!)
eval
prende una stringa e la esegue come comando, proprio come se fosse stata inserita nella riga di comando della shell. Ciò include tutte le elaborazioni di preventivi ed espansioni, utili e pericolose.
Nel caso semplice, permette di fare proprio quello che vogliamo:
cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"
Con eval
, le virgolette vengono elaborate, quindi ls
alla fine vede solo i due argomenti -l
e /tmp/test/my dir
, come vogliamo. eval
è anche abbastanza intelligente da concatenare tutti gli argomenti che ottiene, quindi eval $cmd
potrebbe anche funzionare in alcuni casi, ma ad es. tutte le serie di spazi bianchi verrebbero modificate in spazi singoli. È ancora meglio citare la variabile lì in quanto ciò assicurerà che non venga modificata in eval
.
Tuttavia, è pericoloso includere l'input dell'utente nella stringa di comando per eval
. Ad esempio, questo sembra funzionare:
read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";
Ma se l'utente fornisce un input che contiene virgolette singole, può uscire dalle virgolette ed eseguire comandi arbitrari! Per esempio. con l'input '$(whatever)'.txt
, il tuo script esegue felicemente la sostituzione del comando. Che potrebbe essere rm -rf
(o peggio) invece.
Il problema è che il valore di $filename
è stato incorporato nella riga di comando che eval
corre. È stato ampliato prima di eval
, che ha visto ad es. il comando ls -l ''$(whatever)'.txt'
. Dovresti pre-elaborare l'input per essere sicuro.
Se lo facciamo nell'altro modo, mantenendo il nome del file nella variabile e lasciando che eval
comando espandilo, è di nuovo più sicuro:
read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";
Nota che le virgolette esterne ora sono virgolette singole, quindi le espansioni all'interno non si verificano. Quindi, eval
vede il comando ls -l "$filename"
ed espande il nome del file in modo sicuro.
Ma non è molto diverso dalla semplice memorizzazione del comando in una funzione o in un array. Con funzioni o array, non ci sono problemi del genere poiché le parole vengono mantenute separate per tutto il tempo e non ci sono citazioni o altre elaborazioni per il contenuto di filename
.
read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"
Praticamente l'unico motivo per usare eval
è quello in cui la parte variabile coinvolge elementi della sintassi della shell che non possono essere inseriti tramite variabili (pipeline, reindirizzamenti, ecc.). Anche in questo caso, assicurati di non incorporare l'input dell'utente in eval
comando!
Riferimenti
- Scomposizione delle parole in BashGuide
- BashFAQ/050 o "Sto cercando di inserire un comando in una variabile, ma i casi complessi falliscono sempre!"
- La domanda Perché il mio script di shell si blocca su spazi bianchi o altri caratteri speciali?, che discute una serie di problemi relativi alle virgolette e agli spazi bianchi, inclusa la memorizzazione dei comandi.