¿Cómo puedo hacer que bash salga en caso de falla de backtick de una manera similar a la falla de tubería?

55

Por lo tanto, me gusta endurecer mis scripts de bash siempre que pueda (y cuando no pueda delegar a un lenguaje como Python / Ruby) para garantizar que los errores no se descubran.

En ese sentido, tengo un estricto.sh, que contiene cosas como:

set -e
set -u
set -o pipefail

Y la fuente en otros scripts. Sin embargo, mientras pipefail se recuperaría:

false | echo it kept going | true

No recogerá:

echo The output is '`false; echo something else`' 

La salida sería

El resultado es ''

False devuelve el estado distinto de cero y no stdout. En una tubería, habría fallado, pero aquí no se detecta el error. Cuando este es realmente un cálculo almacenado en una variable para más adelante, y el valor se establece en blanco, esto puede causar problemas posteriores.

Entonces, ¿hay alguna manera de obtener bash para tratar un código de retorno diferente de cero dentro de un backtick como razón suficiente para salir?

Danny Staple
fuente

Respuestas:

51

El lenguaje exacto utilizado en la especificación Single UNIX para describir el significado deset -e es:

Cuando esta opción está activada, si un comando simple falla por alguno de los motivos enumerados en Consecuencias de los errores del shell o devuelve un valor de estado de salida> 0, y no es [un comando condicional o negado], el shell se cerrará inmediatamente.

Existe una ambigüedad en cuanto a lo que sucede cuando dicho comando se produce en una subshell . Desde un punto de vista práctico, todo lo que el subshell puede hacer es salir y devolver un estado distinto de cero al shell principal. El hecho de que el shell primario salga a su vez depende de si este estado distinto de cero se traduce en un simple comando que falla en el shell primario.

Uno de estos casos problemáticos es el que encontró: un estado de retorno distinto de cero de una sustitución de comando . Como este estado se ignora, no hace que el shell principal salga. Como ya descubrió , una forma de tener en cuenta el estado de salida es utilizar la sustitución de comando en una asignación simple : entonces el estado de salida de la asignación es el estado de salida de la última sustitución de comando en la (s) asignación (es) .

Tenga en cuenta que esto funcionará según lo previsto solo si hay una única sustitución de comando, ya que solo se tiene en cuenta el estado de la última sustitución. Por ejemplo, el siguiente comando es exitoso (de acuerdo con el estándar y en cada implementación que he visto):

a=$(false)$(echo foo)

Otro caso a tener en cuenta es subniveles explícitas : (somecommand). Según la interpretación anterior, el subshell puede devolver un estado distinto de cero, pero dado que este no es un comando simple en el shell principal, el shell principal debe continuar. De hecho, todos los proyectiles que conozco hacen que los padres regresen en este punto. Si bien esto es útil en muchos casos, como (cd /some/dir && somecommand)cuando los paréntesis se usan para mantener una operación como un cambio de directorio actual local, viola la especificación si set -ese desactiva en la subshell, o si la subshell devuelve un estado distinto de cero de una manera que no lo terminaría, como usar !un comando verdadero. Por ejemplo, todas las cenizas, bash, pdksh, ksh93 y zsh salen sin mostrarse fooen los siguientes ejemplos:

set -e; (set +e; false); echo "This should be displayed"
set -e; (! true); echo "This should be displayed"

¡Sin embargo, ningún comando simple ha fallado mientras set -eestaba en vigor!

Un tercer caso problemático son los elementos en una tubería no trivial . En la práctica, todos los shells ignoran las fallas de los elementos de la tubería que no sean la última, y ​​exhiben uno de los dos comportamientos con respecto al último elemento de la tubería:

  • ATT ksh y zsh, que ejecutan el último elemento de la canalización en el shell principal, funcionan como de costumbre: si un comando simple falla en el último elemento de la canalización, el shell ejecuta ese comando, que resulta ser el shell principal, salidas
  • Otros shells aproximan el comportamiento al salir si el último elemento de la tubería devuelve un estado distinto de cero.

Al igual que antes, apagar set -eo usar una negación en el último elemento de la tubería hace que devuelva un estado distinto de cero de una manera que no debería terminar el shell; los shells que no sean ATT ksh y zsh saldrán.

La pipefailopción de Bash hace que una tubería salga inmediatamente debajo set -esi alguno de sus elementos devuelve un estado distinto de cero.

Tenga en cuenta que, como complicación adicional, bash se apaga set -een subcapas a menos que esté en modo POSIX ( set -o posixo tenga POSIXLY_CORRECTen el entorno cuando se inicia bash).

Todo esto muestra que la especificación POSIX desafortunadamente hace un mal trabajo al especificar la -eopción. Afortunadamente, las conchas existentes son en su mayoría consistentes en su comportamiento.

Gilles 'SO- deja de ser malvado'
fuente
Gracias por esto. Una experiencia que tuve fue que algunos errores atrapados en una versión posterior de bash, fueron ignorados en versiones anteriores con set -e. Mi intención aquí es endurecer los scripts en la medida en que cualquier condición de error / retorno de error no manejado hará que se salga un script. Esto está en un sistema heredado que se sabe que produce archivos de salida de basura y un código de salida feliz "0" después de media hora de caos con el entorno incorrecto, a menos que esté viendo la salida como un halcón (y no todos los errores están en stderr, algunas están en stdout y algunas partes son / dev / null'd), simplemente no lo sabes.
Danny Staple
"Por ejemplo, todas las cenizas, bash, pdksh, ksh93 y zsh salen sin mostrar foo en los siguientes ejemplos": la ceniza BusyBox no se comporta de esa manera, muestra la salida según las especificaciones. (Probado con BusyBox 1.19.4.) Tampoco sale con set -e; (cd /nonexisting).
dudoso
27

(Respondiendo la mía porque he encontrado una solución) Una solución es siempre asignar esto a una variable intermedia. De esta forma se establece el código de retorno ( $?).

Entonces

ABC=`exit 1`
echo $?

Sin embargo, saldrá 1(o en su lugar saldrá si set -eestá presente):

echo `exit 1`
echo $?

Saldrá 0después de una línea en blanco. El código de retorno del eco (u otro comando que se ejecutó con la salida de retroceso) reemplazará el código de retorno 0.

Todavía estoy abierto a soluciones que no requieren la variable intermedia, pero esto me ayuda en parte.

Danny Staple
fuente
23

Como el OP señaló en su propia respuesta, asignar la salida del subcomando a una variable resuelve el problema; el $?que queda indemne.

Sin embargo, un caso extremo aún puede confundirlo con falsos negativos (es decir, el comando falla pero el error no aparece), localdeclaración de variable:

local myvar=$(subcommand)será siempre volver 0!

bash(1) señala esto:

   local [option] [name[=value] ...]
          ... The return status is 0 unless local is used outside a function,
          an invalid name is supplied, or name is a readonly variable.

Aquí hay un caso de prueba simple:

#!/bin/bash

function test1() {
  data1=$(false) # undeclared variable
  echo 'data1=$(false):' "$?"
  local data2=$(false) # declaring and assigning in one go
  echo 'local data2=$(false):' "$?"
  local data3
  data3=$(false) # assigning a declared variable
  echo 'local data3; data3=$(false):' "$?"
}

test1

La salida:

data1=$(false): 1
local data2=$(false): 0
local data3; data3=$(false): 1
mogsie
fuente
gracias, la inconsistencia entre VAR=...y local VAR=...realmente me desconcertó!
Jonny
13

Como otros han dicho, localsiempre devolverá 0. La solución es declarar la variable primero:

function testcase()
{
    local MYRESULT

    MYRESULT=$(false)
    if (( $? != 0 )); then
        echo "False returned false!"
        return 1
    fi

    return 0
}

Salida:

$ testcase
False returned false!
$ 
Será
fuente
4

Para salir en una falla de sustitución de comando, puede establecer explícitamente -een un subshell, como este:

set -e
x=$(set -e; false; true)
echo "this will never be shown"
errr
fuente
1

Punto interesante!

Nunca me he tropezado con eso, porque no soy amigo de set -e(en cambio, prefiero hacerlo trap ... ERR), pero ya lo he probado: trap ... ERRtampoco detecta errores dentro $(...)(o los anticuados backticks).

Creo que el problema es (como tan a menudo) que aquí se llama un subshell y -eexplícitamente significa el shell actual .

La otra solución que se me ocurrió en este momento sería usar read:

 ls -l ghost_under_bed | read name

Este lanzamiento ERRy con -eel caparazón se terminará. Único problema: esto funciona solo para comandos con una línea de salida (o canaliza a través de algo que une líneas).

ktf
fuente
1
No estoy seguro sobre el truco de lectura, intentar que conduzca a que la variable no esté vinculada. Creo que esto puede deberse a que el otro lado de la tubería es efectivamente una subshell, el nombre no estará disponible.
Danny Staple el