Le variabili devono essere quotate quando eseguite?


18

La regola generale nello scripting della shell è che le variabili dovrebbero essere sempre quotate a meno che non ci sia un motivo convincente per non farlo. Per maggiori dettagli di quelli che probabilmente vorresti sapere, dai un'occhiata a queste fantastiche domande e risposte: implicazioni sulla sicurezza di dimenticare di citare una variabile nelle shell bash / POSIX .

Considera, tuttavia, una funzione come la seguente:

run_this(){
    $@
}

Dovrebbe $@essere citato lì o no? Ci ho giocato un po 'e non sono riuscito a trovare alcun caso in cui la mancanza di virgolette causasse un problema. D'altra parte, l'uso delle virgolette lo interrompe quando si passa un comando contenente spazi come una variabile tra virgolette:

#!/usr/bin/sh
set -x
run_this(){
    $@
}
run_that(){
    "$@"
}
comm="ls -l"
run_this "$comm"
run_that "$comm"

L'esecuzione dello script sopra restituisce:

$ a.sh
+ comm='ls -l'
+ run_this 'ls -l'
+ ls -l
total 8
-rw-r--r-- 1 terdon users  0 Dec 22 12:58 da
-rw-r--r-- 1 terdon users 45 Dec 22 13:33 file
-rw-r--r-- 1 terdon users 43 Dec 22 12:38 file~
+ run_that 'ls -l'
+ 'ls -l'
/home/terdon/scripts/a.sh: line 7: ls -l: command not found

Posso aggirarlo se lo uso run_that $comminvece di run_that "$comm", ma poiché la funzione run_this(non quotata) funziona con entrambi, sembra la scommessa più sicura.

Quindi, nel caso specifico dell'uso $@in una funzione il cui lavoro deve essere eseguito $@come comando, dovrebbe $@essere citato? Spiegare perché non dovrebbe / non dovrebbe essere quotato e fornire un esempio di dati che potrebbero romperlo.


6
run_thatIl comportamento è sicuramente quello che mi aspetterei (e se ci fosse uno spazio nel percorso del comando?). Se volessi l'altro comportamento, sicuramente lo annulleresti nel sito della chiamata dove sai quali sono i dati? Mi aspetto di chiamare questa funzione come run_that ls -l, che funziona allo stesso modo in entrambe le versioni. C'è un caso che ti ha fatto aspettare in modo diverso?
Michael Homer,

@MichaelHomer Immagino che la mia modifica qui abbia richiesto questo: unix.stackexchange.com/a/250985/70524
muru,

@MichaelHomer per qualche motivo (probabilmente perché non ho ancora avuto la mia seconda tazza di caffè) Non avevo considerato gli spazi negli argomenti o nel percorso del comando, ma solo nel comando stesso (opzioni). Come spesso accade, questo sembra molto ovvio in retrospettiva.
terdon

C'è un motivo per cui le shell supportano ancora le funzioni anziché semplicemente inserire i comandi in un array ed eseguirli ${mycmd[@]}.
Chepner,

Risposte:


20

Il problema risiede nel modo in cui il comando viene passato alla funzione:

$ run_this ls -l Untitled\ Document.pdf 
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_that ls -l Untitled\ Document.pdf 
-rw------- 1 muru muru 33879 Dec 20 11:09 Untitled Document.pdf

"$@"dovrebbe essere usato nel caso generale in cui la tua run_thisfunzione è preceduta da un comando normalmente scritto. run_thisporta a citare l'inferno:

$ run_this 'ls -l Untitled\ Document.pdf'
ls: cannot access Untitled\: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l "Untitled\ Document.pdf"'
ls: cannot access "Untitled\: No such file or directory
ls: cannot access Document.pdf": No such file or directory
$ run_this 'ls -l Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory
$ run_this 'ls -l' 'Untitled Document.pdf'
ls: cannot access Untitled: No such file or directory
ls: cannot access Document.pdf: No such file or directory

Non sono sicuro di come dovrei passare un nome file con spazi a run_this.


1
È stata davvero la tua modifica a suggerire questo. Per qualche motivo non mi è mai venuto in mente di provare con un nome di file con spazi. Non ho assolutamente idea del perché, ma il gioco è fatto. Hai perfettamente ragione, ovviamente, non vedo un modo per farlo correttamente con run_thisnessuno dei due.
terdon

La citazione di @terdon è diventata così un'abitudine che ho pensato che avresti lasciato $@accidentalmente non citato . Avrei dovuto lasciare un esempio. : D
muru,

2
Nah, è davvero un'abitudine così tanto che l'ho provato (a torto) e ho concluso che "eh, forse questo non ha bisogno di virgolette". Una procedura comunemente nota come Brainfart.
terdon

1
Non è possibile passare un nome file con spazi a run_this. Questo è fondamentalmente lo stesso problema che si verifica quando si inseriscono comandi complessi nelle stringhe, come discusso nella FAQ 050 di Bash .
Etan Reisner,

9

O è:

interpret_this_shell_code() {
  eval "$1"
}

O:

interpret_the_shell_code_resulting_from_the_concatenation_of_those_strings_with_spaces() {
  eval "$@"
}

o:

execute_this_simple_command_with_these_arguments() {
  "$@"
}

Ma:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

Non ha molto senso.

Se si desidera eseguire il ls -lcomando (non il lscomando con lse -lcome argomenti), è necessario:

interpret_this_shell_code '"ls -l"'
execute_this_simple_command_with_these_arguments 'ls -l'

Ma se (più probabilmente), è il lscomando con lse -lcome argomenti, avresti eseguito:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Ora, se è più di un semplice comando che vuoi eseguire, se vuoi fare assegnazioni variabili, reindirizzamenti, pipe ..., interpret_this_shell_codefarà solo :

interpret_this_shell_code 'ls -l 2> /dev/null'

anche se ovviamente puoi sempre fare:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'

5

Guardando dalla bash / ksh / zsh prospettiva, $*e $@sono un caso speciale di generale espansione dell'array. Le espansioni di array non sono come le normali espansioni di variabili:

$ a=("a b c" "d e" f)
$ printf ' -> %s\n' "${a[*]}"
 -> a b c d e f
$ printf ' -> %s\n' "${a[@]}"
-> a b c
-> d e
-> f
$ printf ' -> %s\n' ${a[*]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f
$ printf ' -> %s\n' ${a[@]}
 -> a
 -> b
 -> c
 -> d
 -> e
 -> f

Con le espansioni $*/ ${a[*]}ottieni l'array unito al primo valore di IFS—che è lo spazio per impostazione predefinita — in una stringa gigante. Se non lo citi, viene diviso come una normale stringa.

Con le espansioni $@/ ${a[@]}, il comportamento dipende dal fatto che l' espansione $@/ ${a[@]}sia quotata o meno:

  1. se è citato ( "$@"o "${a[@]}"), ottieni l'equivalente di "$1" "$2" "$3" #... o"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. se non è quotato ($@ o ${a[@]}) ottieni l'equivalente di $1 $2 $3 #... o${a[1]} ${a[2]} ${a[3]} # ...

Per i comandi di wrapping, sicuramente vuoi le espansioni quotate @ (1.).


Altre informazioni utili sugli array bash (e bash-like): https://lukeshu.com/blog/bash-arrays.html


1
Ho appena realizzato che mi riferisco a un link che inizia con Luke, mentre indosso una maschera di Vader. La forza è forte con questo post.
PSkocik,

4

Da quando non hai fatto una doppia citazione $@, hai lasciato tutti i problemi traballanti link che hai dato alla tua funzione.

Come hai potuto eseguire un comando chiamato *? Non puoi farlo con run_this:

$ ls
1 2
$ run_this '*'
dash: 2: 1: not found
$ run_that '*'
dash: 3: *: not found

E vedi, anche quando si è verificato un errore, run_thatti ha dato un messaggio più significativo.

L'unico modo per espandersi $@ singole parole è la doppia virgoletta. Se si desidera eseguirlo come comando, è necessario passare il comando e i parametri come parole separate. Quello che hai fatto sul lato chiamante, non all'interno della tua funzione.

$ cmd=ls
$ param1=-l
$ run_that "$cmd" "$param1"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

è una scelta migliore. O se le matrici di supporto della shell:

$ cmd=(ls -l)
$ run_that "${cmd[@]}"
total 0
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 1
-rw-r--r-- 1 cuonglm cuonglm 0 Dec 23 17:33 2

Anche quando la shell non supporta affatto l'array, puoi comunque giocarci usando"$@" .


3

L'esecuzione delle variabili in bashè una tecnica soggetta a guasti. È semplicemente impossibile scrivere arun_this funzione che gestisca correttamente tutti i casi limite, come:

  • condotte (ad es ls | grep filename )
  • reindirizzamenti input / output (es ls > /dev/null )
  • dichiarazioni shell come if whileecc.

Se tutto ciò che vuoi fare è evitare la ripetizione del codice, stai meglio usando le funzioni. Ad esempio, anziché:

run_this(){
    "$@"
}
command="ls -l"
...
run_this "$command"

Dovresti scrivere

command() {
    ls -l
}
...
command

Se i comandi sono disponibili solo in fase di esecuzione, è necessario utilizzare eval, che è specificamente progettato per gestire tutte le stranezze che run_thisfalliranno:

command="ls -l | grep filename > /dev/null"
...
eval "$command"

Si noti che evalè noto per problemi di sicurezza, ma se si passano variabili da fonti non attendibili a run_this, si dovrà affrontare anche l'esecuzione di codice arbitrario.


1

La scelta è tua. Se non citate $@nessuno dei suoi valori subite ulteriori espansioni e interpretazioni. Se lo citate, tutti gli argomenti passati vengono riprodotti alla lettera nella sua espansione. Non sarai mai in grado di gestire in modo affidabile token di sintassi della shell come &>|ed ecc. In entrambi i modi senza analizzare gli argomenti tu stesso - e quindi ti rimangono le scelte più ragionevoli di consegnare la tua funzione in uno dei seguenti modi:

  1. Esattamente le parole usate nell'esecuzione di un singolo comando semplice con "$@".

...o...

  1. Un'ulteriore versione ampliata e interpretata dei tuoi argomenti che solo successivamente vengono applicati insieme come un semplice comando $@ .

In nessun caso è sbagliato se è intenzionale e se gli effetti di ciò che si sceglie sono ben compresi. Entrambe le vie presentano vantaggi l'una rispetto all'altra, sebbene raramente i vantaggi della seconda siano particolarmente utili. Ancora...

(run_this(){ $@; }; IFS=@ run_this 'ls@-dl@/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

... non è inutile , solo raramente può essere di grande utilità . E in una bashshell, perché bashper impostazione predefinita non si attacca una definizione di variabile al suo ambiente anche quando detta definizione è anteposta alla riga di comando di un builtin speciale o a una funzione, il valore globale per$IFS non è interessato e la sua dichiarazione è locale solo per la run_this()chiamata.

Allo stesso modo:

(run_this(){ $@; }; set -f; run_this ls -l \*)

ls: cannot access *: No such file or directory

... il globbing è anche configurabile. Le citazioni servono a uno scopo - non sono per niente. Senza di essi l'espansione della shell subisce un'interpretazione extra - interpretazione configurabile . In passato, con alcune shell molto vecchie , $IFSveniva applicato a livello globale a tutti gli input e non solo alle espansioni. In effetti, le shell si sono comportate in modo molto simile run_this()a quello in cui hanno rotto tutte le parole di input sul valore di $IFS. E quindi, se quello che stai cercando è quel comportamento shell molto vecchio, allora dovresti usare run_this().

Non lo sto cercando, e al momento sono abbastanza difficile da trovare un esempio utile. In genere preferisco che i comandi eseguiti dalla mia shell siano quelli che scrivo. E così, data la scelta, quasi sempre run_that(). Salvo che...

(run_that(){ "$@"; }; IFS=l run_that 'ls' '-ld' '/tmp')

drwxrwxrwt 22 root root 660 Dec 28 19:58 /tmp

Quasi tutto può essere citato. I comandi verranno eseguiti tra virgolette. Funziona perché quando il comando viene effettivamente eseguito, tutte le parole di input sono già state sottoposte a rimozione delle virgolette, che è l'ultima fase del processo di interpretazione dell'input della shell. Quindi la differenza tra 'ls'e lspuò importare solo mentre la shell sta interpretando - ed è per questo che la citazione lsassicura che qualsiasi alias nominato lsnon sia sostituito dalla mia lsparola di comando tra virgolette . A parte questo, le uniche cose che influenzano le citazioni sono la delimitazione delle parole (che è come e perché funziona la citazione di variabili / input-spazi bianchi) e l'interpretazione di metacaratteri e parole riservate.

Così:

'for' f in ...
 do   :
 done

bash: for: command not found
bash:  do: unexpected token 'do'
bash:  do: unexpected token 'done'

Non sarai mai in grado di farlo con nessuno dei due run_this() o run_that().

Ma i nomi delle funzioni, $PATHi comandi o i builtin eseguiranno solo virgolette o non quotate, ed è esattamente come run_this()e come run_that()funziona in primo luogo. Non sarai in grado di fare nulla di utile con $<>|&(){}nessuno di questi. A corto di eval, è.

(run_that(){ "$@"; }; run_that eval printf '"%s\n"' '"$@"')

eval
printf
"%s\n"
"$@"

Ma senza di esso, sei vincolato ai limiti di un semplice comando in virtù delle virgolette che usi (anche quando non lo fai perché si $@comporta come una citazione all'inizio del processo quando il comando viene analizzato per i metacaratteri) . Lo stesso vincolo vale per le assegnazioni e i reindirizzamenti della riga di comando, che sono limitati alla riga di comando della funzione. Ma questo non è un grosso problema:

(run_that(){ "$@";}; echo hey | run_that cat)

hey

Avrei potuto facilmente < reindirizzare input o >output lì come ho aperto la pipe.

Ad ogni modo, in modo circolare, non esiste un modo giusto o sbagliato qui - ogni modo ha i suoi usi. È solo che dovresti scriverlo come intendi usarlo e dovresti sapere cosa intendi fare. Citazioni omettendo possono avere uno scopo - altrimenti non ci sarebbe essere citazioni a tutti - ma se li si omette, per motivi che non interessano al vostro scopo, sei solo a scrivere codice cattivo. Fai quello che vuoi dire; Ci provo comunque.

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.