¿Es seguro evaluar $ BASH_COMMAND?

11

Estoy trabajando en un script de shell que construye un comando complejo a partir de variables, por ejemplo, de esta manera (con una técnica que aprendí de las preguntas frecuentes de Bash ):

#!/bin/bash

SOME_ARG="abc"
ANOTHER_ARG="def"

some_complex_command \
  ${SOME_ARG:+--do-something "$SOME_ARG"} \
  ${ANOTHER_ARG:+--with "$ANOTHER_ARG"}

Esta secuencia de comandos agrega dinámicamente los parámetros --do-something "$SOME_ARG"y --with "$ANOTHER_ARG"de some_complex_commandsi se han definido estas variables. Hasta ahora esto está funcionando bien.

Pero ahora también quiero poder imprimir o registrar el comando cuando lo estoy ejecutando, por ejemplo, cuando mi script se ejecuta en modo de depuración. Entonces, cuando se ejecuta mi script some_complex_command --do-something abc --with def, también quiero tener este comando dentro de una variable para poder, por ejemplo, iniciar sesión en el syslog.

Las preguntas frecuentes de Bash demuestran una técnica para usar la DEBUGtrampa y la $BASH_COMMANDvariable (por ejemplo, con fines de depuración) para este propósito. Lo intenté con el siguiente código:

#!/bin/bash

ARG="test string"

trap 'COMMAND="$BASH_COMMAND"; trap - DEBUG' DEBUG
echo "$ARG"

echo "Command was: ${COMMAND}"

Esto funciona, pero no expande las variables en el comando:

host ~ # ./test.sh
test string
Command was: echo "$ARG"

Yo supongo que tengo que usar eval para ampliar echo "$ARG"a echo test string(al menos no he encontrado una manera sin evalembargo). Lo siguiente funciona:

eval echo "Command was: ${COMMAND}"

Produce el siguiente resultado:

host ~ # ./test.sh
test string
Command was: echo "$ARG"
Command was: echo test string

Pero no estoy realmente seguro si puedo usarlo de evalmanera segura de esta manera. He intentado explotar sin éxito algunas cosas:

#!/bin/bash

ARG="test string; touch /x"
DANGER='$(touch /y; cat /etc/shadow)'

trap 'COMMAND="$BASH_COMMAND"; trap - DEBUG' DEBUG
echo "$ARG" $DANGER

echo "Command was: ${COMMAND}"
eval echo "Command was: ${COMMAND}"

Parece manejar esto bien, pero tengo curiosidad si alguien más ve un problema que me he perdido.

Martin von Wittich
fuente
Esto es lo que he estado usando durante años en una envoltura de sudo, nunca he notado un problema.
w00t

Respuestas:

7

Una posibilidad es hacer una función de envoltura que al mismo tiempo imprima el comando y lo ejecute, de la siguiente manera:

debug() {
    # This function prints (to stdout) its arguments and executes them
    local args=() idx=0 IFS=' ' c
    for c; do printf -v args[idx++] '%q' "$c"; done
    printf "%s\n" "${args[*]}"
    # Execute!
    "$@"
}

Para que en tu script puedas hacer:

debug echo "$ARG"

No hay necesidad de jugar con la trampa. El inconveniente es que agrega algunas debugpalabras clave en todo el código (pero eso debería estar bien, es común tener esas cosas, como afirmaciones, etc.).

Incluso puede agregar una variable global DEBUGy modificar la debugfunción de esta manera:

debug() {
    # This function prints (to stdout) its arguments if DEBUG is non-null
    # and executes them
    if [[ $DEBUG ]]; then
        local args=() idx=0 IFS=' ' c
        for c; do printf -v args[idx++] '%q' "$c"; done
        printf "%s\n" "${args[*]}"
    fi
    # Execute!
    "$@"
}

Entonces puedes llamar a tu script como:

$ DEBUG=yes ./myscript

o

$ DEBUG= ./myscript

o solo

$ ./myscript

dependiendo de si desea tener la información de depuración o no.

Capitalicé la DEBUGvariable porque debería tratarse como una variable de entorno. DEBUGes un nombre trivial y común, por lo que podría chocar con otros comandos. Quizás llamarlo GNIOURF_DEBUGo MARTIN_VON_WITTICH_DEBUGo UNICORN_DEBUGsi como los unicornios (y entonces es probable que como ponis también).

Nota. En la debugfunción, he formateado cuidadosamente cada argumento con el printf '%q'fin de que la salida se escape y se cite correctamente para que sea reutilizable textualmente con una copia directa y pegar. También le mostrará exactamente lo que vio el shell, ya que podrá descubrir cada argumento (en el caso de espacios u otros símbolos divertidos). Esta función también utiliza la asignación directa con el -vinterruptor de printfpara evitar subcapas innecesarias.

gniourf_gniourf
fuente
1
La función funciona muy bien, pero desafortunadamente mi comando contiene redirecciones y el operador de control "excute in background" &. Tuve que moverlos a la función para que funcione: no es muy agradable, pero no creo que haya una mejor manera. Pero no más eval, así que tengo eso para mí, lo cual es bueno :)
Martin von Wittich
5

eval "$BASH_COMMAND" ejecuta el comando

printf '%s\n' "$BASH_COMMAND" imprime el comando exacto especificado, más una nueva línea.

Si el comando contiene variables (es decir, si es algo así cat "$foo"), imprimir el comando imprime el texto de la variable. Es imposible imprimir el valor de la variable sin ejecutar el comando; piense en comandos como variable=$(some_function) other_variable=$variable.

La forma más sencilla de obtener un seguimiento de la ejecución de un script de shell es establecer la xtraceopción de shell ejecutando el script como bash -x /path/to/scripto invocando set -xdentro del shell. La traza se imprime con error estándar.

Gilles 'SO- deja de ser malvado'
fuente
1
Lo sé xtrace, pero eso no me da mucho control. Intenté "set -x; ...; set + x", pero: 1) el comando "set + x" para deshabilitar xtrace también se imprime 2) No puedo prefijar la salida, por ejemplo, con una marca de tiempo 3) Puedo No lo registres en syslog.
Martin von Wittich
2
"Es imposible imprimir el valor de la variable sin ejecutar el comando" - buen ejemplo, no había considerado ese caso. Sin embargo, sería bueno si bash tuviera una variable adicional al lado BASH_COMMANDque contenga el comando expandido, porque en algún momento tiene que hacer una expansión variable en el comando de todos modos cuando lo está ejecutando :)
Martin von Wittich