¿Cómo almacenar un comando en una variable en un script de shell?

113

Me gustaría almacenar un comando para usar en un período posterior en una variable (no la salida del comando, sino el comando en sí)

Tengo un script simple de la siguiente manera:

command="ls";
echo "Command: $command"; #Output is: Command: ls

b=`$command`;
echo $b; #Output is: public_html REV test... (command worked successfully)

Sin embargo, cuando intento algo un poco más complicado, falla. Por ejemplo, si hago

command="ls | grep -c '^'";

La salida es:

Command: ls | grep -c '^'
ls: cannot access |: No such file or directory
ls: cannot access grep: No such file or directory
ls: cannot access '^': No such file or directory

¿Alguna idea de cómo podría almacenar tal comando (con tuberías / múltiples comandos) en una variable para su uso posterior?

Benjamín
fuente
10
¡Usa una función!
gniourf_gniourf

Respuestas:

146

Utilice eval:

x="ls | wc"
eval "$x"
y=$(eval "$x")
echo "$y"
Erik
fuente
27
$ (...) ahora se recomienda en lugar de backticks. y = $ (eval $ x) mywiki.wooledge.org/BashFAQ/082
James Broadhead
14
evales una práctica aceptable solo si confía en el contenido de sus variables. Si está ejecutando, digamos, x="ls $name | wc"(o incluso x="ls '$name' | wc"), entonces este código es una vía rápida para las vulnerabilidades de inyección o escalada de privilegios si esa variable puede ser configurada por alguien con menos privilegios. (¿Iterando sobre todos los subdirectorios en /tmp, por ejemplo? Será mejor que confíe en cada usuario del sistema para que no haga uno llamado $'/tmp/evil-$(rm -rf $HOME)\'$(rm -rf $HOME)\'/').
Charles Duffy
9
evales un gran imán de errores que nunca debe recomendarse sin una advertencia sobre el riesgo de un comportamiento de análisis inesperado (incluso sin cadenas maliciosas, como en el ejemplo de @ CharlesDuffy). Por ejemplo, intente x='echo $(( 6 * 7 ))'y luego eval $x. Es de esperar que imprima "42", pero probablemente no lo hará. ¿Puedes explicar por qué no funciona? ¿Puedes explicar por qué dije "probablemente"? Si las respuestas a esas preguntas no son obvias para usted, nunca debe tocar eval.
Gordon Davisson
1
@ Estudiante, intente ejecutar de set -xantemano para registrar la ejecución de los comandos, lo que hará que sea más fácil ver lo que está sucediendo.
Charles Duffy
1
@ Estudiante También recomendaría shellcheck.net por señalar errores comunes (y malos hábitos que no debería adquirir).
Gordon Davisson
41

¡No lo use eval! Tiene un gran riesgo de introducir ejecución de código arbitrario.

BashFAQ-50 : estoy tratando de poner un comando en una variable, pero los casos complejos siempre fallan.

Colóquelo en una matriz y expanda todas las palabras con comillas dobles "${arr[@]}"para no permitir que IFSlas palabras se dividan debido a la división de palabras .

cmdArgs=()
cmdArgs=('date' '+%H:%M:%S')

y ver el contenido de la matriz en su interior. Le declare -ppermite ver el contenido de la matriz dentro con cada parámetro de comando en índices separados. Si uno de esos argumentos contiene espacios, citar adentro mientras se agrega a la matriz evitará que se divida debido a la división de palabras.

declare -p cmdArgs
declare -a cmdArgs='([0]="date" [1]="+%H:%M:%S")'

y ejecutar los comandos como

"${cmdArgs[@]}"
23:15:18

(o) usar una bashfunción para ejecutar el comando,

cmd() {
   date '+%H:%M:%S'
}

y llamar a la función como

cmd

POSIX shno tiene matrices, por lo que lo más cerca que puede acercarse es crear una lista de elementos en los parámetros posicionales. Aquí hay una shforma POSIX de ejecutar un programa de correo

# POSIX sh
# Usage: sendto subject address [address ...]
sendto() {
    subject=$1
    shift
    first=1
    for addr; do
        if [ "$first" = 1 ]; then set --; first=0; fi
        set -- "$@" --recipient="$addr"
    done
    if [ "$first" = 1 ]; then
        echo "usage: sendto subject address [address ...]"
        return 1
    fi
    MailTool --subject="$subject" "$@"
}

Tenga en cuenta que este enfoque solo puede manejar comandos simples sin redirecciones. No puede manejar redirecciones, canalizaciones, bucles for / while, declaraciones if, etc.

Otro caso de uso común es cuando se ejecuta curlcon múltiples campos de encabezado y carga útil. Siempre puede definir argumentos como a continuación e invocar curlen el contenido de la matriz expandida

curlArgs=('-H' "keyheader: value" '-H' "2ndkeyheader: 2ndvalue")
curl "${curlArgs[@]}"

Otro ejemplo,

payload='{}'
hostURL='http://google.com'
authToken='someToken'
authHeader='Authorization:Bearer "'"$authToken"'"'

ahora que las variables están definidas, use una matriz para almacenar sus argumentos de comando

curlCMD=(-X POST "$hostURL" --data "$payload" -H "Content-Type:application/json" -H "$authHeader")

y ahora haz una expansión cotizada adecuada

curl "${curlCMD[@]}"
Inian
fuente
Esto no me funciona, lo he intentado Command=('echo aaa | grep a')y "${Command[@]}", esperando que se ejecute literalmente el comando echo aaa | grep a. No es así. Me pregunto si hay una forma segura de reemplazar eval, pero parece que cada solución que tenga la misma fuerza que evalpodría ser peligrosa. ¿No es así?
Estudiante
En resumen, ¿cómo funciona esto si la cadena original contiene una tubería '|'?
Estudiante
@ Estudiante, si su cadena original contiene una tubería, entonces esa cadena debe pasar por las partes inseguras del analizador bash para ejecutarse como código. No use una cuerda en ese caso; use una función en su lugar: Command() { echo aaa | grep a; }- después de lo cual puede ejecutar Command, o result=$(Command), o similar.
Charles Duffy
1
@ Estudiante, a la derecha; pero eso falla intencionalmente , porque lo que estás pidiendo es inherentemente inseguro .
Charles Duffy
1
@ Estudiante: agregué una nota al final para mencionar que no funciona bajo ciertas condiciones
Inian
25
var=$(echo "asdf")
echo $var
# => asdf

Con este método, el comando se evalúa inmediatamente y se almacena su valor de retorno.

stored_date=$(date)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 10:57:16 EST 2015

Lo mismo con la comilla invertida

stored_date=`date`
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:02:19 EST 2015

Usar eval en el $(...)no hará que se evalúe más tarde

stored_date=$(eval "date")
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015
# (wait a few seconds)
echo $stored_date
# => Thu Jan 15 11:05:30 EST 2015

Usando eval, se evalúa cuando evalse usa

stored_date="date" # < storing the command itself
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:05 EST 2015
# (wait a few seconds)
echo $(eval "$stored_date")
# => Thu Jan 15 11:07:16 EST 2015
#                     ^^ Time changed

En el ejemplo anterior, si necesita ejecutar un comando con argumentos, colóquelos en la cadena que está almacenando

stored_date="date -u"
# ...

Para los scripts de bash esto rara vez es relevante, pero una última nota. Tenga cuidado con eval. Evalúe solo las cadenas que controla, nunca las que provengan de un usuario que no sea de confianza o que se generen a partir de la entrada de un usuario que no sea de confianza.

  • ¡Gracias a @CharlesDuffy por recordarme citar el comando!
Nate
fuente
Esto no resuelve el problema original donde el comando contiene una tubería '|'.
Estudiante
@Nate, tenga en cuenta que eval $stored_datepuede ser suficiente cuando stored_datesolo contiene date, pero eval "$stored_date"es mucho más confiable. Ejecute str=$'printf \' * %s\\n\' *'; eval "$str"con y sin las comillas alrededor de la final "$str"como ejemplo. :)
Charles Duffy
@CharlesDuffy Gracias, me olvidé de citar. Apuesto a que mi linter se habría quejado si me hubiera molestado en ejecutarlo.
Nate
0

Para bash, almacena tu comando de esta manera:

command="ls | grep -c '^'"

Ejecute su comando así:

echo $command | bash
Derek Hazell
fuente
1
No estoy seguro, pero quizás esta forma de ejecutar el comando tiene los mismos riesgos que tiene el uso de 'eval'.
Derek Hazell
0

Probé varios métodos diferentes:

printexec() {
  printf -- "\033[1;37m$\033[0m"
  printf -- " %q" "$@"
  printf -- "\n"
  eval -- "$@"
  eval -- "$*"
  "$@"
  "$*"
}

Salida:

$ printexec echo  -e "foo\n" bar
$ echo -e foo\\n bar
foon bar
foon bar
foo
 bar
bash: echo -e foo\n bar: command not found

Como puede ver, solo el tercero "$@"dio el resultado correcto.

mpen
fuente
0

Tenga cuidado al registrar un pedido con: X=$(Command)

Este todavía se ejecuta incluso antes de ser llamado. Para comprobar y confirmar esto, puede hacer:

echo test;
X=$(for ((c=0; c<=5; c++)); do
sleep 2;
done);
echo note the 5 seconds elapsed
Azerty
fuente
-1
#!/bin/bash
#Note: this script works only when u use Bash. So, don't remove the first line.

TUNECOUNT=$(ifconfig |grep -c -o tune0) #Some command with "Grep".
echo $TUNECOUNT                         #This will return 0 
                                    #if you don't have tune0 interface.
                                    #Or count of installed tune0 interfaces.
Silentexpert
fuente
-8

No es necesario almacenar comandos en variables, incluso si necesita usarlo más tarde. simplemente ejecútelo como de costumbre. Si almacena en variable, necesitaría algún tipo de evaldeclaración o invocar algún proceso de shell innecesario para "ejecutar su variable".

Kurumi
fuente
1
El comando que almacenaré dependerá de las opciones que envíe, por lo que en lugar de tener toneladas de declaraciones condicionales en la mayor parte de mi programa, es mucho más fácil almacenar el comando que necesito para usar más adelante.
Benjamin
1
@Benjamin, al menos almacene las opciones como variables, y no el comando. egvar='*.txt'; find . -name "$var"
kurumi