¿Cómo podemos ejecutar un comando almacenado en una variable?

35
$ 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
Tim
fuente

Respuestas:

54

Esto se ha discutido en una serie de preguntas sobre unix.SE, intentaré recopilar todos los problemas que pueda encontrar 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 a partir de variables no actúan como comillas, sino que son solo caracteres ordinarios.

Los casos presentados en la pregunta:

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

Aquí, $abcse divide, y lsobtiene los dos argumentos "/tmp/test/myy dir"(con las comillas al frente del primero y al reverso del segundo):

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

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

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

Y aquí, solo $abcse toma la primera palabra o como argumento -c, por lo que Bash solo se ejecuta lsen el directorio actual. Las otras palabras son argumentos para golpear, y se utilizan para rellenar $0, $1etc.

$ bash -c $abc
'my dir'

Con bash -c "$abc", y eval "$abc", hay un paso adicional 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 una expansión de comando a partir de datos proporcionados por el usuario, a menos que sea muy cuidado con las citas.


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 está definido, 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 la "${array[@]}"expansión expande cada elemento como palabras de 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, 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 dashel predeterminado /bin/shen Debian / Ubuntu) no los admiten (pero vea a continuación). Sin embargo, Bash, ksh y zsh sí, por lo que es probable que su sistema tenga algún shell que admita matrices.

Utilizando "$@"

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 hacen el equivalente de los bits de código en la sección anterior. La matriz se reemplaza con "$@"la lista de parámetros posicionales. La configuración "$@"se realiza con set, y las comillas dobles "$@"son importantes (esto hace que los elementos de la lista se citen individualmente).

Primero, simplemente almacenando un comando con argumentos "$@"y ejecutándolo:

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

Configuración condicional de partes de las opciones de línea de comando 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, "$@"generalmente se rellena con los argumentos del script en sí, por lo que deberá guardarlos en algún lugar antes de volver a utilizarlos "$@").


¡Cuidado con eval!

A medida que evalintroduce un nivel adicional de procesamiento de cotizaciones y expansión, debe tener cuidado con las aportaciones del usuario. Por ejemplo, esto funciona siempre que el usuario no escriba comillas simples:

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

Pero si dan la entrada '$(uname)'.txt, su script ejecuta felizmente la sustitución del comando.

Una versión con matrices es inmune a eso, ya que las palabras se mantienen separadas durante todo el tiempo, no hay presupuesto u otro procesamiento para el contenido de filename.

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

Referencias

ilkkachu
fuente
2
puede evitar la cita de evaluación haciendo cmd="ls -l $(printf "%q" "$filename")". no es bonito, pero si el usuario está empeñado en usar un eval, ayuda. También es muy útil para enviar el comando aunque las cosas similares, como por ejemplo ssh foohost "ls -l $(printf "%q" "$filename")", o en el espíritu de esta pregunta: ssh foohost "$cmd".
Patrick
No está directamente relacionado, pero ¿ha codificado el directorio? En ese caso, es posible que desee ver el alias. Algo así como: $ alias abc='ls -l "/tmp/test/my dir"'
Hopping Bunny
4

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 ejecuta exactamente como si acabara de ingresarlo. 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"
Hauke ​​Laging
fuente
3

El segundo signo de comillas rompe el comando.

Cuando corro:

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

Me dio un error.

Pero cuando corro

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

No hay ningún error en absoluto

No hay forma de solucionar esto en ese momento (para mí), pero puede evitar el error al no tener espacio en el nombre del directorio.

Esta respuesta dice que el comando eval puede usarse para arreglar esto pero no funciona para mí :(

Wattana Gaming
fuente
1
Sí, eso funciona siempre que no sea necesario, por ejemplo, nombres de archivo con espacios incrustados (o que contengan caracteres globales).
ilkkachu
0

Si esto no funciona '', entonces debe usar ``:

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

Actualizar mejor manera:

abc=$(ls -l /tmp/test/my\ dir/)
balon
fuente
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ías comenzar a usar en $( command... )lugar de backticks.
roaima
Muchas gracias por las aclaraciones y consejos!
balon
0

¿Qué tal un python3 one-liner?

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 ..
Marius Mitrofan
fuente