¿Cómo podemos ejecutar 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 

Comentarios

Respuesta

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


Por qué falla

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

El 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 principio del segundo y otra al final 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 para encontrar un programa llamado literalmente ls -l "/tmp/test/my dir", con espacios y comillas incluidos.

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

Y aquí, $abc está dividido y solo la primera palabra resultante se toma como argumento para -c, por lo que Bash solo ejecuta ls en el directorio actual. Las otras palabras son argumentos para bash, y se utilizan para completar $0, $1, etc.

$ bash -c $abc "my dir" 

Con bash -c "$abc" y eval "$abc", hay un paso de procesamiento de shell, que hace que las cotizaciones funcionen, pero también hace que todas las expansiones de shell se procesen nuevamente , por lo que existe el riesgo de ejecutar accidentalmente, por ejemplo, una sustitución de comando de los datos proporcionados por el usuario, a menos que » eres muy cuidadoso acerca de las comillas.


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 el comando se ejecuta, no cuando está definido, y no es necesario 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 blanco espacio. Aquí, las palabras individuales se almacenan como elementos de matriz distintos y la expansión "${array[@]}" expande cada elemento como palabras de shell independientes:

# 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 te permiten construir la línea de comandos 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 constantes partes de la línea de comando 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 los arreglos es que «no son una característica estándar, por lo que los shells POSIX simples (como dash, el /bin/sh en Debian / Ubuntu) no los soporta (pero vea más abajo). Sin embargo, Bash, ksh y zsh sí, por lo que es probable que su sistema tenga algún shell que admita matrices.

Usando "$@"

En shells sin soporte para matrices con nombre, aún se pueden usar los parámetros posicionales (la pseudo-matriz "$@") 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 por "$@", la lista de parámetros posicionales. La configuración de "$@" se realiza con set y las comillas dobles alrededor de "$@" son importantes (estos hacen que los elementos de la lista se citen individualmente).

Primero, simplemente almacenar un comando con argumentos en "$@" y ejecutarlo:

set -- ls -l "/tmp/test/my dir" "$@" 

Establecer condicionalmente partes de las opciones de la línea de comandos para un comando:

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

Solo se usa "$@" para opciones y operandos :

set -- -x -v set -- "$@" file1 "file name with whitespace" set -- "$@" /somedir transmutate "$@" 

(Por supuesto, "$@" suele llenarse con los argumentos del script en sí, así que » Tendré que guardarlos en algún lugar antes de reutilizar "$@").


Tenga cuidado con eval !

Dado que eval introduce un nivel adicional de procesamiento de cotizaciones y expansiones, debe tener cuidado con la entrada del usuario. Por ejemplo, esto funciona siempre que el usuario no escribe entre comillas simples:

read -r filename cmd="ls -l "$filename"" eval "$cmd"; 

Pero si dan la entrada "$(uname)".txt, su script felizmente ejecuta la sustitución de comandos.

Una versión con matrices es inmune a Dado que las palabras se mantienen separadas durante todo el tiempo, no hay comillas ni ningún otro procesamiento para el contenido de filename.

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

Referencias

Comentarios

  • puede sortear la cuestión de las citas de evaluación haciendo cmd="ls -l $(printf "%q" "$filename")". no es bonito, pero si el usuario está decidido a usar un eval, ayuda. También es ‘ muy útil para enviar el comando a través de cosas similares, como ssh foohost "ls -l $(printf "%q" "$filename")", o en el espíritu de esta pregunta: ssh foohost "$cmd".
  • No está directamente relacionado, pero ¿ha codificado el directorio? En ese caso, es posible que desee mirar alias. Algo como: $ alias abc='ls -l "/tmp/test/my dir"'

Responder

La forma más segura de ejecutar un comando (no trivial) es eval. Luego puede escribir el comando como lo haría en la línea de comando y se ejecutará exactamente como si lo acabara de ingresar. Pero tienes que citar todo.

Caso simple:

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

caso no tan simple:

# command: awk "! a[$0]++ { print "foo: " $0; }" inputfile abc="awk "\""! a[$0]++ { print "foo: " $0; }"\"" inputfile" eval "$abc" 

Comentarios

  • Hola @HaukeLaging, ¿qué es \ ‘ ‘?
  • @jumping_monkey No puede tener una ' dentro de una ' entre comillas . Por lo tanto, debe (1) finalizar / interrumpir la cita, (2) escapar del siguiente ' para obtenerla literalmente, (3) escribir la cita literal y (4) en caso de una interrupción reanude la cadena entre comillas: (1)'(2)\(3)'(4)'
  • Hola @HaukeLaging, interesante, y por qué no solo abc='awk \'! a[$0]++ { print "foo: " $0; }\' inputfile' ?
  • @jumping_monkey Y mucho menos que te acabo de explicar por qué no funciona: ¿No ‘ no tendría sentido probar el código antes de publicarlo?

Responder

El segundo signo de comillas rompe el comando.

Cuando ejecuto:

abc="ls -l "/home/wattana/Desktop"" $abc 

Me dio un error.

Pero cuando ejecuto

abc="ls -l /home/wattana/Desktop" $abc 

No hay ningún error en absoluto

No hay forma de solucionar esto en este momento (para mí) pero puede evitar el error si no tiene espacio en el nombre del directorio.

Esta respuesta decía que el comando eval se puede usar para solucionar este problema, pero no funciona para mí 🙁

Comentarios

  • Sí, eso funciona siempre que ‘ no sea necesario, p. ej. nombres de archivo con espacios incrustados (o que contengan caracteres globales).

Responder

Si esto no funciona con "", entonces debería usar ``:

abc=`ls -l /tmp/test/my\ dir/` 

Actualice mejor way:

abc=$(ls -l /tmp/test/my\ dir/) 

Comentarios

  • Esto almacena el resultado del comando en una variable. El OP está (extrañamente) queriendo guardar el comando en una variable. Ah, y realmente debería comenzar a usar $( command... ) en lugar de las comillas inversas.
  • ¡Muchas gracias por las aclaraciones y los consejos!

Respuesta

¿Qué tal un resumen de python3?

bash-4.4# pwd /home bash-4.4# CUSTOM="ls -altr" bash-4.4# python3 -c "import subprocess; subprocess.call(\"$CUSTOM\", shell=True)" total 8 drwxr-xr-x 2 root root 4096 Mar 4 2019 . drwxr-xr-x 1 root root 4096 Nov 27 16:42 .. 

Comentarios

  • La pregunta es sobre BASH, no sobre Python.

Responder

Otro truco simple para ejecutar cualquier comando (trivial / no trivial) almacenado en abc variable es:

$ history -s $abc 

y presione UpArrow o Ctrl-p para traerlo a la línea de comando. A diferencia de cualquier otro método, de esta manera, puede editarlo antes de la ejecución si es necesario.

Este comando agregará el contenido de la variable como una nueva entrada al historial de Bash y puede recuperarlo UpArrow.

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *