GNU/Linux >> Tutoriales Linux >  >> Linux

¿Cómo ejecutamos un comando almacenado en una variable?

$ ls -l /tmp/test/my dir/
total 0

Me preguntaba por qué las siguientes formas de ejecutar el comando anterior fallan o tienen éxito.

$ 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

Respuesta aceptada:

Esto se ha discutido en una serie de preguntas sobre unix.SE, intentaré recopilar todos los problemas que se me ocurran aquí. Referencias al final.

Por qué falla

La razón por la que enfrenta esos problemas es la división de palabras y el hecho de que las comillas expandidas de las variables no actúan como comillas, sino que son solo caracteres comunes.

Los casos presentados en la pregunta:

La asignación aquí asigna la cadena única ls -l "/tmp/test/my dir" a abc :

$ abc='ls -l "/tmp/test/my dir"'

Abajo, $abc se divide en espacios en blanco y ls obtiene los tres argumentos -l , "/tmp/test/my y dir" (con una cita al frente del segundo y otra al reverso del tercero). La opción funciona, pero la ruta se procesa incorrectamente:

$ $abc
ls: cannot access '"/tmp/test/my': No such file or directory
ls: cannot access 'dir"': No such file or directory

Aquí, la expansión se cita, por lo que se mantiene como una sola palabra. El shell intenta encontrar un programa llamado literalmente ls -l "/tmp/test/my dir" , espacios y comillas incluidas.

$ "$abc"
bash: ls -l "/tmp/test/my dir": No such file or directory

Y aquí, $abc se divide, y solo la primera palabra resultante se toma como argumento para -c , entonces Bash solo ejecuta ls en el directorio actual. Las otras palabras son argumentos para bash y se usan para llenar $0 , $1 , etc.

$ bash -c $abc
'my dir'

Con bash -c "$abc" y eval "$abc" , hay un paso de procesamiento de shell adicional, que hace que las comillas funcionen, pero también hace que todas las expansiones de shell se procesen de nuevo , por lo que existe el riesgo de que se ejecute accidentalmente, p. una sustitución de comando de los datos proporcionados por el usuario, a menos que tenga mucho cuidado al citar.

Mejores formas de hacerlo

Las dos mejores formas de almacenar un comando son a) usar una función en su lugar, b) usar una variable de matriz (o los parámetros posicionales).

Usando una función:

Simplemente declare una función con el comando dentro y ejecute la función como si fuera un comando. Las expansiones en los comandos dentro de la función solo se procesan cuando se ejecuta el comando, no cuando se define, y no necesita citar los comandos individuales.

# define it
myls() {
    ls -l "/tmp/test/my dir"
}

# run it
myls

Usando una matriz:

Las matrices permiten crear variables de varias palabras donde las palabras individuales contienen espacios en blanco. Aquí, las palabras individuales se almacenan como elementos de matriz distintos, y el "${array[@]}" expansión expande cada elemento como palabras shell separadas:

# define the array
mycmd=(ls -l "/tmp/test/my dir")

# run the command
"${mycmd[@]}"

La sintaxis es un poco horrible, pero las matrices también le permiten construir la línea de comando pieza por pieza. Por ejemplo:

mycmd=(ls)               # initial command
if [ "$want_detail" = 1 ]; then
    mycmd+=(-l)          # optional flag
fi
mycmd+=("$targetdir")    # the filename

"${mycmd[@]}"

o mantenga partes de la línea de comando constantes y use la matriz para llenar solo una parte, como opciones o nombres de archivo:

options=(-x -v)
files=(file1 "file name with whitespace")
target=/somedir

transmutate "${options[@]}" "${files[@]}" "$target"

La desventaja de las matrices es que no son una función estándar, por lo que los shells POSIX simples (como dash , el /bin/sh predeterminado en Debian/Ubuntu) no los admiten (pero consulte a continuación). Sin embargo, Bash, ksh y zsh sí, por lo que es probable que su sistema tenga algún shell que admita arreglos.

Relacionado:¿el comando "eval" en bash?

Uso de "[email protected]"

En shells sin soporte para matrices con nombre, aún se pueden usar los parámetros posicionales (la pseudo-matriz "[email protected]" ) para contener los argumentos de un comando.

Los siguientes deben ser bits de script portátiles que hagan el equivalente a los bits de código de la sección anterior. La matriz se reemplaza con "[email protected]" , la lista de parámetros posicionales. Configuración "[email protected]" se hace con set y las comillas dobles alrededor de "[email protected]" son importantes (estos hacen que los elementos de la lista sean citados individualmente).

Primero, simplemente almacenar un comando con argumentos en "[email protected]" y ejecutándolo:

set -- ls -l "/tmp/test/my dir"
"[email protected]"

Configuración condicional de partes de las opciones de la línea de comando para un comando:

set -- ls
if [ "$want_detail" = 1 ]; then
    set -- "[email protected]" -l
fi
set -- "[email protected]" "$targetdir"

"[email protected]"

Solo usando "[email protected]" para opciones y operandos:

set -- -x -v
set -- "[email protected]" file1 "file name with whitespace"
set -- "[email protected]" /somedir

transmutate "[email protected]"

(Por supuesto, "[email protected]" generalmente se llena con los argumentos del script en sí, por lo que deberá guardarlos en algún lugar antes de volver a utilizar "[email protected]" .)

Usando eval (¡ten cuidado aquí!)

eval toma una cadena y la ejecuta como un comando, como si se hubiera ingresado en la línea de comando del shell. Esto incluye todo el procesamiento de presupuestos y expansiones, que es tanto útil como peligroso.

En el caso simple, permite hacer justo lo que queremos:

cmd='ls -l "/tmp/test/my dir"'
eval "$cmd"

Con eval , las comillas se procesan, por lo que ls eventualmente ve solo los dos argumentos -l y /tmp/test/my dir , como queremos. eval también es lo suficientemente inteligente como para concatenar cualquier argumento que obtenga, por lo que eval $cmd también podría funcionar en algunos casos, pero p. todas las ejecuciones de espacios en blanco se cambiarían a espacios individuales. Todavía es mejor citar la variable allí, ya que eso asegurará que no se modifique a eval .

Sin embargo, es peligroso incluir la entrada del usuario en la cadena de comando para eval . Por ejemplo, esto parece funcionar:

read -r filename
cmd="ls -ld '$filename'"
eval "$cmd";

Pero si el usuario da una entrada que contiene comillas simples, ¡puede saltarse las comillas y ejecutar comandos arbitrarios! P.ej. con la entrada '$(whatever)'.txt , su secuencia de comandos ejecuta felizmente la sustitución de comandos. Que podría haber sido rm -rf (o peor) en su lugar.

El problema es que el valor de $filename estaba incrustado en la línea de comando que eval carreras. Se expandió antes de eval , que vio, p. el comando ls -l ''$(whatever)'.txt' . Necesitará procesar previamente la entrada para estar seguro.

Si lo hacemos al revés, manteniendo el nombre del archivo en la variable, y dejando que eval comando expandirlo, es más seguro de nuevo:

read -r filename
cmd='ls -ld "$filename"'
eval "$cmd";

Tenga en cuenta que las comillas externas ahora son comillas simples, por lo que no se producen expansiones internas. Por lo tanto, eval ve el comando ls -l "$filename" y expande el nombre del archivo de forma segura.

Pero eso no es muy diferente de simplemente almacenar el comando en una función o una matriz. Con funciones o matrices, no existe tal problema ya que las palabras se mantienen separadas todo el tiempo y no hay comillas u otro procesamiento para el contenido de filename .

read -r filename
cmd=(ls -ld -- "$filename")
"${cmd[@]}"

Prácticamente la única razón para usar eval es uno en el que la parte variable involucra elementos de sintaxis de shell que no se pueden traer a través de variables (tuberías, redirecciones, etc.). Incluso entonces, asegúrese de no incrustar la entrada del usuario en eval ¡comando!

Relacionado:¿Agregar una ruta de entorno al símbolo del sistema de Visual Studio?

Referencias

  • División de palabras en BashGuide
  • BashFAQ/050 o "¡Estoy tratando de poner un comando en una variable, pero los casos complejos siempre fallan!"
  • La pregunta ¿Por qué mi script de shell se ahoga con los espacios en blanco u otros caracteres especiales?, que analiza una serie de problemas relacionados con las comillas y los espacios en blanco, incluido el almacenamiento de comandos.

Linux
  1. ¿Cómo asignar la salida de un comando a una variable de shell?

  2. ¿Cómo ejecutar un comando como administrador del sistema (raíz)?

  3. ¿Cómo ejecutar un comando dentro de un contenedor Systemd en ejecución?

  4. ¿Cómo ejecutar un comando sin propiedades raíz?

  5. Cómo ejecutar un comando en un contenedor Docker en ejecución

Cómo ejecutar el comando / secuencia de comandos de Linux Shell en segundo plano

Cómo ejecutar el comando Sudo sin contraseña

Cómo ejecutar comandos de Linux en segundo plano

Cómo ejecutar un comando durante un tiempo específico en Linux

Cómo almacenar un comando de Linux como una variable en el script de Shell

Cómo ejecutar un comando periódicamente en Linux usando Watch