Errores de captura en la sustitución de comandos utilizando "-o errtrace" (es decir, establecer -E)

14

De acuerdo con este manual de referencia :

-E (también -o errtrace)

Si se establece, cualquier trampa en ERR es heredada por las funciones de shell, las sustituciones de comandos y los comandos ejecutados en un entorno de subshell. La trampa ERR normalmente no se hereda en tales casos.

Sin embargo, debo interpretarlo incorrectamente, porque lo siguiente no funciona:

#!/usr/bin/env bash
# -*- bash -*-

set -e -o pipefail -o errtrace -o functrace

function boom {
  echo "err status: $?"
  exit $?
}
trap boom ERR


echo $( made up name )
echo "  ! should not be reached ! "

Ya sé de asignación simple my_var=$(made_up_name), saldrá del script con set -e(es decir, errexit).

¿Se -E/-o errtracesupone que funciona como el código anterior? O, lo más probable, ¿lo he leído mal?

dgo.a
fuente
2
Esta es una buena pregunta. Reemplazar echo $( made up name )con $( made up name )produce el comportamiento deseado. Sin embargo, no tengo una explicación.
iruvar
No sé acerca de -E de bash, pero sí sé que -e solo efectúa una salida de shell si el error resulta del último comando en una tubería. Por lo tanto, su var=$( pipe )y $( pipe )ejemplos representarían puntos finales de tubería, mientras pipe > echoque no lo haría. Mi página hombre dice: "1. El fracaso de cualquier comando individual en una tubería de varios comandos no causará la cáscara para salir Sólo el fracaso de la tubería en sí serán considerados.."
mikeserv
Sin embargo, puede hacer que falle: echo $ ($ {madeupname?}). Pero eso está listo -e. Nuevamente, -E está fuera de mi propia experiencia.
mikeserv
@mikeserv @ 1_CR El manual de bash @ echo indica que echosiempre devuelve 0. Esto debe tenerse en cuenta en el análisis ...

Respuestas:

5

Nota: zshse quejará de "patrones incorrectos" si no lo configura para aceptar "comentarios en línea" para la mayoría de los ejemplos aquí y no los ejecuta a través de un shell proxy como lo he hecho sh <<-\CMD.

De acuerdo, como dije en los comentarios anteriores, no sé específicamente sobre bashset -E , pero sé que los shells compatibles con POSIX proporcionan un medio simple de probar un valor si lo desea:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
        echo "echo still works" 
    }
    _test && echo "_test doesnt fail"
    # END
    CMD
sh: line 1: empty: error string
+ echo

+ echo 'echo still works'
echo still works
+ echo '_test doesnt fail'
_test doesnt fail

Arriba verá que, aunque solía parameter expansionprobar, ${empty?} _test()todavía returnes un paso, como se evidencia en el último. echoEsto ocurre porque el valor fallido mata la $( command substitution )subshell que lo contiene, pero su shell principal, _testen este momento, continúa transportándose en camiones. Y echono le importa, está muy feliz de servir solo y no\newline; echo es una prueba.

Pero considere esto:

    sh -evx <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    _test ||\
            echo "this doesnt even print"
    # END
    CMD
_test+ sh: line 1: empty: function doesnt run

Debido a que introduje la _test()'sentrada con un parámetro previamente evaluado en el INIT here-documentahora, la _test()función ni siquiera intenta ejecutarse en absoluto. Además, el shcaparazón aparentemente abandona el fantasma por completo y echo "this doesnt even print" ni siquiera imprime.

Probablemente eso no sea lo que quieres.

Esto sucede porque el ${var?}estilo de expansión de parámetros está diseñado para salirshell en caso de que falte un parámetro, funciona así :

${parameter:?[word]}

Indique Error si Nullo Unset.si el parámetro no está establecido o es nulo, el expansion of word(o un mensaje que indica que no está establecido si se omite la palabra) será written to standard errory el shell exits with a non-zero exit status. De lo contrario, el valor de parameter shall be substituted. Un shell interactivo no necesita salir.

No copiaré / pegaré todo el documento, pero si desea una falla para un set but nullvalor, use el formulario:

${var :? error message }

Con lo de :colonarriba. Si desea que un nullvalor tenga éxito, simplemente omita los dos puntos. También puede negarlo y fallar solo para los valores establecidos, como lo mostraré en un momento.

Otra carrera de _test():

    sh <<-\CMD
    _test() { echo $( ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?function doesnt run}
    INIT
    echo "this runs" |\
        ( _test ; echo "this doesnt" ) ||\
            echo "now it prints"
    # END
    CMD
this runs
sh: line 1: empty: function doesnt run
now it prints

Esto funciona con todo tipo de pruebas rápidas, pero arriba verá que se _test()ejecuta desde la mitad de los pipelineerrores y, de hecho, su command listsubshell que falla falla por completo, ya que ninguno de los comandos dentro de la función se ejecuta ni la siguiente echoejecución, aunque también se muestra que puede probarse fácilmente porque echo "now it prints" ahora imprime.

El diablo está en los detalles, supongo. En el caso anterior, el shell que sale no es el script _main | logic | pipelinesino ( subshell in which we ${test?} ) ||que se requiere un pequeño sandboxing.

Y puede que no sea obvio, pero si solo desea pasar por el caso opuesto, o solo los set=valores, también es bastante simple:

    sh <<-\CMD
    N= #N is NULL
    _test=$N #_test is also NULL and
    v="something you would rather do without"    
    ( #this subshell dies
        echo "v is ${v+set}: and its value is ${v:+not NULL}"
        echo "So this ${_test:-"\$_test:="} will equal ${_test:="$v"}"
        ${_test:+${N:?so you test for it with a little nesting}}
        echo "sure wish we could do some other things"
    )
    ( #this subshell does some other things 
        unset v #to ensure it is definitely unset
        echo "But here v is ${v-unset}: ${v:+you certainly wont see this}"
        echo "So this ${_test:-"\$_test:="} will equal NULL ${_test:="$v"}"
        ${_test:+${N:?is never substituted}}
        echo "so now we can do some other things" 
    )
    #and even though we set _test and unset v in the subshell
    echo "_test is still ${_test:-"NULL"} and ${v:+"v is still $v"}"
    # END
    CMD
v is set: and its value is not NULL
So this $_test:= will equal something you would rather do without
sh: line 7: N: so you test for it with a little nesting
But here v is unset:
So this $_test:= will equal NULL
so now we can do some other things
_test is still NULL and v is still something you would rather do without

El ejemplo anterior aprovecha las 4 formas de sustitución de parámetros POSIX y sus diversas :colon nullo not nullpruebas. Hay más información en el enlace de arriba, y aquí está de nuevo .

Y supongo que también deberíamos mostrar el _testfuncionamiento de nuestra función, ¿verdad? Simplemente declaramos empty=somethingcomo parámetro a nuestra función (o en cualquier momento de antemano):

    sh <<-\CMD
    _test() { echo $( echo ${empty:?error string} ) &&\
            echo "echo still works" ; } 2<<-INIT
            ${empty?tested as a pass before function runs}
    INIT
    echo "this runs" >&2 |\
        ( empty=not_empty _test ; echo "yay! I print now!" ) ||\
            echo "suspiciously quiet"
    # END
    CMD
this runs
not_empty
echo still works
yay! I print now!

Cabe señalar que esta evaluación es independiente: no requiere ninguna prueba adicional para fallar. Un par de ejemplos más:

    sh <<-\CMD
    empty= 
    ${empty?null, no colon, no failure}
    unset empty
    echo "${empty?this is stderr} this is not"
    # END
    CMD
sh: line 3: empty: this is stderr

    sh <<-\CMD
    _input_fn() { set -- "$@" #redundant
            echo ${*?WHERES MY DATA?}
            #echo is not necessary though
            shift #sure hope we have more than $1 parameter
            : ${*?WHERES MY DATA?} #: do nothing, gracefully
    }
    _input_fn heres some stuff
    _input_fn one #here
    # shell dies - third try doesnt run
    _input_fn you there?
    # END
    CMD
heres some stuff
one
sh: line :5 *: WHERES MY DATA?

Y finalmente volvemos a la pregunta original: ¿cómo manejar los errores en una $(command substitution)subshell? La verdad es que hay dos formas, pero ninguna es directa. El núcleo del problema es el proceso de evaluación del shell: las expansiones del shell (incluido $(command substitution)) ocurren antes en el proceso de evaluación del shell que la ejecución actual del comando del shell, que es cuando sus errores pueden ser atrapados y atrapados.

El problema que experimenta el operador operativo es que cuando el shell actual evalúa los errores, el $(command substitution)subshell ya se ha sustituido, no quedan errores.

¿Cuáles son las dos formas? O lo hace explícitamente dentro del $(command substitution)subshell con pruebas como lo haría sin él, o absorbe sus resultados en una variable de shell actual y prueba su valor.

Método 1:

    echo "$(madeup && echo \: || echo '${fail:?die}')" |\
          . /dev/stdin

sh: command not found: madeup
/dev/stdin:1: fail: die

    echo $?

126

Método 2:

    var="$(madeup)" ; echo "${var:?die} still not stderr"

sh: command not found: madeup
sh: var: die

    echo $?

1

Esto fallará independientemente del número de variables declaradas por línea:

   v1="$(madeup)" v2="$(ls)" ; echo "${v1:?}" "${v2:?}"

sh: command not found: madeup
sh: v1: parameter not set

Y nuestro valor de retorno permanece constante:

    echo $?
1

AHORA LA TRAMPA:

    trap 'printf %s\\n trap resurrects shell!' ERR
    v1="$(madeup)" v2="$(printf %s\\n shown after trap)"
    echo "${v1:?#1 - still stderr}" "${v2:?invisible}"

sh: command not found: madeup
sh: v1: #1 - still stderr
trap
resurrects
shell!
shown
after
trap

    echo $?
0
revs mikeserv
fuente
Por ejemplo, prácticamente su especificación exacta: echo $ {v = $ (madeupname)}; echo "$ {v:? trap1st}! no se alcanza!"
mikeserv
1
Para resumir: estas expansiones de parámetros son una excelente manera de controlar si se establecen variables, etc. Con respecto al estado de salida del eco, noté que echo "abc" "${v1:?}"no parece ejecutarse (abc nunca se imprime). Y el shell devuelve 1. Esto es cierto con o sin un comando par ( "${v1:?}"directamente en el cli). Pero para el guión de OP, todo lo que se requería para desencadenar la trampa es colocar su asignación variable que contiene una sustitución por un comando inexistente solo en una sola línea. De lo contrario, el comportamiento del eco es devolver 0 siempre, a menos que se interrumpa como con las pruebas que explicó.
Justo v=$( madeup ). No veo qué es inseguro con eso. Es solo una tarea, la persona escribió mal el comando, por ejemplo v="$(lss)". Se equivoca. Sí, puede verificar con el estado de error del último comando $? - porque es el comando en la línea (una asignación sin nombre de comando) y nada más - no es un argumento para repetir. Además, aquí está atrapado por la función como! = 0, por lo que recibe comentarios dos veces. De lo contrario, seguramente, como explica, hay una mejor manera de hacer esto de manera ordenada dentro de un marco, pero OP tenía 1 sola línea: un eco más su sustitución fallida. Se preguntaba sobre el eco.
Sí, en la pregunta se menciona una asignación simple y se da por sentado, y aunque no pude ofrecer consejos específicos sobre cómo usar -E, intenté mostrar resultados similares al problema demostrado que fueron más que posibles sin recurrir a bashismos En cualquier caso, son específicamente los problemas que usted menciona, como la asignación individual por línea, lo que hace que tales soluciones sean difíciles de manejar en una tubería, que también demostré cómo manejar. Aunque es verdad lo que dices, no hay nada inseguro en simplemente asignarlo.
mikeserv
1
Buen punto: quizás se requiera más esfuerzo. Lo pensaré.
mikeserv
1

Si se establece, cualquier trampa en ERR es heredada por las funciones de shell, las sustituciones de comandos y los comandos ejecutados en un entorno de subshell

En su script es una ejecución de comando ( echo $( made up name )). En bash los comandos están delimitados con cualquiera ; o con nueva linea . En el comando

echo $( made up name )

$( made up name )se considera como parte del comando. Incluso si esta parte falla y regresa con error, todo el comando se ejecuta con éxito ya echoque no lo sé. Cuando el comando regresa con 0, no se activa ninguna trampa.

Necesitas ponerlo en dos comandos, asignación y eco

var=$(made_up_name)
echo $var
totti
fuente
echo $varno falla: $varse expande para vaciarse antes de echoecharle un vistazo.
mikeserv
0

Esto se debe a un error en bash. Durante la sustitución de comandos, ingrese

echo $( made up name )

madese ejecuta (o no se puede encontrar) en un subshell, pero el subshell está "optimizado" de tal manera que no utiliza algunas trampas del shell principal. Esto se solucionó en la versión 4.4.5 :

Bajo ciertas circunstancias, un comando simple está optimizado para eliminar una bifurcación, lo que resulta en una trampa EXIT que no se ejecuta.

Con bash 4.4.5 o superior, debería ver el siguiente resultado:

error.sh: line 13: made: command not found
err status: 127
  ! should not be reached !

El controlador de trampa se ha llamado como se esperaba, luego se cierra la subshell. ( set -esolo hace que la subshell salga, no el padre, de modo que el mensaje "no debería ser alcanzado" debería, de hecho, ser alcanzado).

Una solución para las versiones anteriores es forzar la creación de una subshell completa y no optimizada:

echo $( ( made up name ) )

Los espacios adicionales son necesarios para distinguir de la expansión aritmética.

JigglyNaga
fuente