¿Por qué (salida 1) no sale del script?

48

Tengo un script que no sale cuando lo quiero.

Un script de ejemplo con el mismo error es:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

echo '2'

Supongo que vería la salida:

:~$ ./test.sh
1
:~$

Pero en realidad veo:

:~$ ./test.sh
1
2
:~$

¿El ()comando encadenamiento de alguna manera crea un alcance? ¿De qué exitsale, si no del guión?

Minix
fuente
55
Esto requiere una respuesta de una sola palabra: subshell
Joshua
Relacionado: unix.stackexchange.com/q/247187/135943
Comodín el

Respuestas:

87

()ejecuta comandos en el subshell, por lo exitque saldrá del subshell y volverá al shell principal. Use llaves {}si desea ejecutar comandos en el shell actual.

Del manual bash:

(lista) la lista se ejecuta en un entorno de subshell. Las asignaciones variables y los comandos incorporados que afectan el entorno del shell no permanecen vigentes una vez que se completa el comando. El estado de retorno es el estado de salida de la lista.

{lista; } la lista simplemente se ejecuta en el entorno de shell actual. La lista debe terminarse con una nueva línea o punto y coma. Esto se conoce como un comando de grupo. El estado de retorno es el estado de salida de la lista. Tenga en cuenta que, a diferencia de los metacaracteres (y), {y} son palabras reservadas y deben aparecer donde se permite reconocer una palabra reservada. Como no causan un salto de palabra, deben estar separados de la lista por espacios en blanco u otro metacarácter de shell.

Vale la pena mencionar que la sintaxis del shell es bastante consistente y el subshell participa también en las otras ()construcciones, como la sustitución de comandos (también con la `..`sintaxis de estilo antiguo ) o la sustitución de procesos, por lo que tampoco se saldrá del shell actual:

echo $(exit)
cat <(exit)

Si bien puede ser obvio que las subcapas están involucradas cuando los comandos se colocan explícitamente dentro (), el hecho menos visible es que también se generan en estas otras estructuras:

  • comando comenzó en segundo plano

    exit &

    no sale del shell actual porque (después man bash)

    Si el operador de control termina un comando &, el shell ejecuta el comando en segundo plano en un subshell. El shell no espera a que termine el comando y el estado de retorno es 0.

  • la tubería

    exit | echo foo

    todavía sale solo de la subshell.

    Sin embargo, los diferentes depósitos se comportan de manera diferente a este respecto. Por ejemplo, bashcoloca todos los componentes de la tubería en subcapas separadas (a menos que use la lastpipeopción en invocaciones donde el control de trabajo no está habilitado), pero AT&T kshy zshejecuta la última parte dentro del shell actual (POSIX permite ambos comportamientos). Así

    exit | exit | exit

    hace prácticamente nada en bash, pero sale del zsh debido a la última exit .

  • coproc exitTambién se ejecuta exiten una subshell.

jimmij
fuente
55
Ah Ahora para encontrar todos los lugares, donde mi predecesor usó las llaves equivocadas. Gracias por la perspicacia.
Minix
10
Tome nota cuidadosamente del espacio en la página de manual: {y }no son sintaxis, son palabras reservadas y deben estar rodeadas de espacios, y la lista debe terminar con un terminador de comando (punto y coma, nueva línea, ampersand)
glenn jackman
Por interés, ¿esto tiende a ser otro proceso o simplemente un entorno separado en una pila interna? Utilizo () mucho para aislar chdirs, y debo haber tenido suerte con mi uso de $$, etc. si es el primero.
Dan Sheppard
55
@DanSheppard Es otro proceso, pero (echo $$)imprime el ID del shell principal porque $$se expande incluso antes de crear el subshell. De hecho, la identificación del proceso de subshell de impresión podría ser difícil, consulte stackoverflow.com/questions/9119885/…
jimmij
@jimmij, ¿cómo puede ser que $$se expanda antes de crear la subshell y, sin embargo, $BASHPIDmuestre el valor correcto para una subshell?
Comodín el
13

Ejecutar el exiten una subshell es una trampa:

#!/bin/bash
function calc { echo 42; exit 1; }
echo $(calc)

El script imprime 42, sale de la subshell con el código de retorno 1y continúa con el script. Incluso reemplazar la llamada por echo $(CALC) || exit 1no ayuda porque el código de retorno de echoes 0 independientemente del código de retorno de calc. Y calcse ejecuta antes de echo.

Aún más desconcertante es frustrar el efecto exitenvolviéndolo en un localbuiltin como en el siguiente script. Me topé con el problema cuando escribí una función para verificar un valor de entrada. Ejemplo:

Quiero crear un archivo llamado "año mes día.log", es decir, 20141211.logpara hoy. La fecha es ingresada por un usuario que puede no proporcionar un valor razonable. Por lo tanto, en mi función verifico el fnamevalor de retorno de datepara verificar la validez de la entrada del usuario:

#!/bin/bash

doit ()
    {
    local FNAME=$(fname "$1") || exit 1
    touch "${FNAME}"
    }

fname ()
    {
    date +"%Y%m%d.log" -d"$1" 2>/dev/null
    if [ "$?" != 0 ] ; then
        echo "fname reports \"Illegal Date\"" >&2
        exit 1
    fi
    }

doit "$1"

Se ve bien. Deje que se nombre el guión s.sh. Si el usuario llama al script con ./s.sh "Thu Dec 11 20:45:49 CET 2014", 20141211.logse crea el archivo . Sin embargo, si el usuario escribe ./s.sh "Thu hec 11 20:45:49 CET 2014", el script genera:

fname reports "Illegal Date"
touch: cannot touch ‘’: No such file or directory

La línea fname…dice que los datos de entrada incorrectos se han detectado en la subshell. Pero el exit 1final de la local …línea nunca se activa porque la localdirectiva siempre regresa 0. Esto se debe a que localse ejecuta después $(fname) y, por lo tanto, sobrescribe su código de retorno. Y debido a eso, el script continúa e invoca touchcon un parámetro vacío. Este ejemplo es simple, pero el comportamiento de bash puede ser bastante confuso en una aplicación real. Lo sé, los programadores reales no usan locales.

Para que quede claro: sin el local, el script se aborta como se esperaba cuando se ingresa una fecha no válida.

La solución es dividir la línea como

local FNAME
FNAME=$(fname "$1") || exit 1

El extraño comportamiento se ajusta a la documentación localdentro de la página de manual de bash: "El estado de retorno es 0 a menos que se use local fuera de una función, se proporcione un nombre no válido o el nombre sea una variable de solo lectura".

Aunque no es un error, siento que el comportamiento de bash es contradictorio. Soy consciente de la secuencia de ejecución local, sin embargo, no debería enmascarar una tarea rota.

Mi respuesta inicial contenía algunas imprecisiones. Después de una discusión reveladora y profunda con mikeserv (gracias por eso) fui a arreglarlos.

Hermannk
fuente
@mikeserv: agregué un ejemplo para mostrar la relevancia.
hermannk
@mikeserv: Sí, tienes razón. Incluso terser. Pero la trampa todavía está ahí.
hermannk
@mikeserv: Lo siento, mi ejemplo estaba roto. Había olvidado la prueba en doit().
hermannk
Continuemos esta discusión en el chat .
hermannk
2

La solución real:

#!/bin/bash

function bla() {
    return 1
}

bla || { echo '1'; exit 1; }

echo '2'

La agrupación de errores solo se ejecutará si bladevuelve un estado de error, y exitno está en una subshell, por lo que todo el script se detiene.

Walf
fuente
1

Los corchetes comienzan una subshell y la salida solo sale de esa subshell.

Puede leer el código de salida con $?y agregar esto en su secuencia de comandos para salir de la secuencia de comandos si se salió de la subshell:

#!/bin/bash

function bla() {
    return 1
}

bla || ( echo '1' ; exit 1 )

exitcode=$?
if [ $exitcode != 0 ]; then exit $exitcode; fi

echo '2'
rubo77
fuente