¿Se deben citar las variables cuando se ejecutan?

18

La regla general en las secuencias de comandos de shell es que las variables siempre se deben citar a menos que haya una razón convincente para no hacerlo. Para obtener más detalles de los que probablemente quiera saber, eche un vistazo a estas excelentes preguntas y respuestas: implicaciones de seguridad de olvidarse de citar una variable en shells bash / POSIX .

Considere, sin embargo, una función como la siguiente:

run_this(){
    $@
}

¿Debería $@ser citado allí o no? Jugué un poco con él y no pude encontrar ningún caso en el que la falta de comillas causara un problema. Por otro lado, el uso de las comillas hace que se rompa al pasar un comando que contiene espacios como una variable entre comillas:

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

Ejecutar el script anterior devuelve:

$ 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

Puedo evitar eso si lo uso en run_that $commlugar de run_that "$comm", pero como la función run_this(sin comillas) funciona con ambos, parece ser la apuesta más segura.

Entonces, en el caso específico de usar $@en una función cuyo trabajo es ejecutar $@como un comando, ¿debería $@citarse? Explique por qué debe / no debe citarse y proporcione un ejemplo de datos que puedan romperlo.

terdon
fuente
66
run_thatEl comportamiento es definitivamente lo que esperaría (¿y si hay un espacio en el camino hacia el comando?). Si quisiera el otro comportamiento, ¿seguramente lo citaría en el sitio de la llamada donde sabe cuáles son los datos? Esperaría llamar a esta función como run_that ls -l, que funciona igual en cualquier versión. ¿Hay un caso que te hizo esperar de manera diferente?
Michael Homer
@MichaelHomer Supongo que mi edición aquí provocó esto: unix.stackexchange.com/a/250985/70524
muru
@MichaelHomer por alguna razón (probablemente porque todavía no he tomado mi segunda taza de café) No había considerado espacios en los argumentos o la ruta del comando, sino solo en el comando en sí (opciones). Como suele ser el caso, esto parece muy obvio en retrospectiva.
terdon
Hay una razón por la cual los shells aún admiten funciones en lugar de simplemente insertar comandos en una matriz y ejecutarlos ${mycmd[@]}.
chepner

Respuestas:

20

El problema radica en cómo se pasa el comando a la función:

$ 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

"$@"debe usarse en el caso general donde su run_thisfunción está prefijada a un comando normalmente escrito. run_thislleva a citar el infierno:

$ 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

No estoy seguro de cómo debo pasar un nombre de archivo con espacios run_this.

muru
fuente
1
De hecho, fue su edición lo que provocó esto. Por alguna razón, no se me ocurrió probar con un nombre de archivo con espacios. No tengo ni idea de por qué no, pero ahí lo tienes. Tienes toda la razón, por supuesto, tampoco veo una manera de hacer esto correctamente run_this.
terdon
Las citas de @terdon se han convertido en un hábito que supuse que había dejado sin $@citar accidentalmente. Debería haber dejado un ejemplo. : D
muru
2
No, de hecho es un hábito que lo probé (erróneamente) y concluí que "eh, tal vez este no necesita comillas". Un procedimiento comúnmente conocido como brainfart.
terdon
1
No puede pasar un nombre de archivo con espacios a run_this. Este es básicamente el mismo problema con el que se encuentra al insertar comandos complejos en cadenas como se describe en la Pregunta frecuente 050 de Bash .
Etan Reisner
9

Es cualquiera:

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() {
  "$@"
}

Pero:

execute_the_simple_command_with_the_arguments_resulting_from_split+glob_applied_to_these_strings() {
  $@
}

No tiene mucho sentido

Si desea ejecutar el ls -lcomando (no el lscomando con lsy -lcomo argumentos), haría:

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

Pero si (más probable), es el lscomando con lsy -lcomo argumentos, ejecutarías:

interpret_this_shell_code 'ls -l'
execute_this_simple_command_with_these_arguments ls -l

Ahora, si es más que un simple comando que desea ejecutar, si desea hacer asignaciones variables, redirecciones, tuberías ..., solo interpret_this_shell_codehará:

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

aunque por supuesto siempre puedes hacer:

execute_this_simple_command_with_these_arguments eval '
  ls -l 2> /dev/null'
Stéphane Chazelas
fuente
5

Mirándolo desde la perspectiva bash / ksh / zsh, $*y $@son un caso especial de expansión de matriz general. Las expansiones de matriz no son como las expansiones variables normales:

$ 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 las $*/ ${a[*]}expansiones obtienes la matriz unida con el primer valor de IFS—que es el espacio por defecto— en una cadena gigante. Si no lo cita, se divide como lo haría una cadena normal.

Con las $@/ ${a[@]}expansiones, el comportamiento depende de si la $@/ ${a[@]}expansión se cita o no:

  1. si se cita ( "$@"o "${a[@]}"), obtienes el equivalente de "$1" "$2" "$3" #... o"${a[1]}" "${a[2]}" "${a[3]}" # ...
  2. si no se cita ( $@o ${a[@]}) obtienes el equivalente de $1 $2 $3 #... o${a[1]} ${a[2]} ${a[3]} # ...

Para envolver comandos, definitivamente desea las @ expansiones (1.) entre comillas .


Más buena información sobre las matrices bash (y bash-like): https://lukeshu.com/blog/bash-arrays.html

PSkocik
fuente
1
Me acabo de dar cuenta de que me estoy refiriendo a un enlace que comienza con Luke, mientras uso una máscara Vader. La fuerza es fuerte con esta publicación.
PSkocik
4

Como cuando no cita dos veces $@, dejó todos los problemas globales en el enlace que le dio a su función.

¿Cómo podrías ejecutar un comando llamado *? No puedes hacerlo con run_this:

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

Y ves, incluso cuando ocurrió un error, run_thatte dio un mensaje más significativo.

La única forma de expandirse $@a palabras individuales es comillas dobles. Si desea ejecutarlo como un comando, debe pasar el comando y sus parámetros como palabras separadas. Que lo que hizo en el lado de la persona que llama, no dentro de su función.

$ 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

Es una mejor opción. O si su shell admite matrices:

$ 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

Incluso cuando el shell no es compatible con la matriz, puedes jugar con él usando"$@" .

Cuonglm
fuente
3

La ejecución de variables en bashes una técnica propensa a fallas. Es simplemente imposible escribir una run_thisfunción que maneje correctamente todos los casos límite, como:

  • tuberías (por ejemplo ls | grep filename)
  • redirecciones de entrada / salida (p ls > /dev/null. ej. )
  • declaraciones de shell como if whileetc.

Si todo lo que quiere hacer es evitar la repetición del código, es mejor que use las funciones. Por ejemplo, en lugar de:

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

Deberías escribir

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

Si los comandos solo están disponibles en tiempo de ejecución, debe usarlo eval, que está específicamente diseñado para manejar todas las peculiaridades que harán que run_thisfalle:

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

Tenga en cuenta que evales conocido por problemas de seguridad, pero si pasa variables de fuentes no confiables a run_this, también enfrentará la ejecución de código arbitrario.

Dmitry Grigoryev
fuente
1

La decisión es tuya. Si no cita $@ninguno de sus valores, se someterá a una expansión e interpretación adicionales. Si lo cita, todos los argumentos pasados ​​la función se reproducen literalmente en su expansión. Nunca podrá manejar de manera confiable tokens de sintaxis de shell como, &>|etc. de cualquier manera sin analizar los argumentos usted mismo de todos modos, por lo que le quedan las opciones más razonables de administrar su función:

  1. Exactamente las palabras utilizadas en la ejecución de un solo comando simple con "$@".

...o...

  1. Una versión más amplia e interpretada de sus argumentos que solo se aplican juntos como un comando simple con $@.

De ninguna manera está mal si es intencional y si los efectos de lo que eliges se comprenden bien. Ambas formas tienen ventajas una sobre la otra, aunque las ventajas de la segunda rara vez son particularmente útiles. Todavía...

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

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

... no es inútil , rara vez es muy útil . Y en un bashshell, porque bashpor defecto no adhiere una definición variable a su entorno, incluso cuando dicha definición se antepone a la línea de comando de una función incorporada especial o a una función, el valor global de $IFSno se ve afectado y su declaración es local solo a la run_this()llamada.

Similar:

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

ls: cannot access *: No such file or directory

... el globbing también es configurable. Las citas tienen un propósito: no son para nada. Sin ellos, la expansión de shell experimenta una interpretación adicional: interpretación configurable . Solía ​​ser, con algunos shells muy antiguos , que $IFSse aplicaba globalmente a todas las entradas, y no solo a las expansiones. De hecho, dichos shells se comportaron de la misma manera run_this()que rompieron todas las palabras de entrada sobre el valor de $IFS. Entonces, si lo que estás buscando es ese comportamiento de shell muy antiguo, entonces deberías usarlo run_this().

No lo estoy buscando, y en este momento me cuesta bastante encontrar un ejemplo útil para ello. Generalmente prefiero que los comandos que ejecuta mi shell sean los que escribo en él. Y así, dada la opción, casi siempre lo haría run_that(). Excepto eso...

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

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

Se puede citar casi cualquier cosa. Los comandos se ejecutarán entre comillas. Funciona porque para cuando el comando se ejecuta realmente, todas las palabras de entrada ya se han eliminado entre comillas , que es la última etapa del proceso de interpretación de entrada del shell. Entonces, la diferencia entre 'ls'y lssolo puede importar mientras el shell está interpretando, y es por eso que las citas lsaseguran que cualquier alias nombrado lsno sea sustituido por mi lspalabra de comando citada . Aparte de eso, las únicas cosas que afectan las citas son la delimitación de palabras (que es cómo y por qué funciona la cita variable / input-whitespace) , y la interpretación de metacaracteres y palabras reservadas.

Entonces:

'for' f in ...
 do   :
 done

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

Nunca vas a ser capaz de hacer eso con cualquiera de run_this()o run_that().

Pero los nombres de funciones, o $PATHlos comandos 'd', o los builtins se ejecutarán entre comillas o sin comillas, y eso es exactamente cómo run_this()y cómo run_that()funcionan en primer lugar. No podrás hacer nada útil con $<>|&(){}ninguno de esos. Corto de eval, es.

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

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

Pero sin él, estás limitado a los límites de un comando simple en virtud de las comillas que utilizas (incluso cuando no lo haces porque $@actúa como una comilla al comienzo del proceso cuando el comando se analiza para metacaracteres) . La misma restricción se aplica a las asignaciones y redirecciones de la línea de comandos, que se limitan a la línea de comandos de la función. Pero eso no es gran cosa:

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

hey

Podría tener una <entrada o >salida tan fácilmente redirigida allí como si abriera la tubería.

De todos modos, en una forma circular, no hay una forma correcta o incorrecta aquí: cada forma tiene sus usos. Es solo que deberías escribirlo mientras piensas usarlo, y debes saber lo que quieres hacer. Omitiendo las cotizaciones pueden tener un propósito - de lo contrario no habría ser cotizaciones en absoluto - pero si los omite por razones no relacionadas con su propósito, estas a escribir código malo. Haz lo que quieres decir; Intento de todos modos.

mikeserv
fuente