En un script Bash, ¿cómo puedo salir del script completo si ocurre una determinada condición?

717

Estoy escribiendo un script en Bash para probar un código. Sin embargo, parece una tontería ejecutar las pruebas si la compilación del código falla en primer lugar, en cuyo caso simplemente abortaré las pruebas.

¿Hay alguna manera de hacer esto sin envolver todo el script dentro de un ciclo while y usar descansos? Algo así como un dun dun dun goto?

Samoz
fuente

Respuestas:

810

Prueba esta afirmación:

exit 1

Reemplace 1con los códigos de error apropiados. Consulte también Códigos de salida con significados especiales .

Michael Foukarakis
fuente
44
@CMCDragonkai, generalmente cualquier código que no sea cero funcionará. Si no necesita nada especial, puede usarlo 1constantemente. Si el script está destinado a ser ejecutado por otro script, es posible que desee definir su propio conjunto de código de estado con un significado particular. Por ejemplo, 1== pruebas fallidas, 2== la compilación falló. Si el script es parte de otra cosa, es posible que deba ajustar los códigos para que coincidan con las prácticas utilizadas allí. Por ejemplo, cuando parte del conjunto de pruebas ejecutado por automake, el código 77se usa para marcar una prueba omitida.
Michał Górny
21
no, esto también cierra la ventana, no solo sale del guión
Toni Leigh
77
@ToniLeigh bash no tiene el concepto de una "ventana", probablemente esté confundido con respecto a lo que hace su configuración particular, por ejemplo, un emulador de terminal.
Michael Foukarakis
44
@ToniLeigh Si está cerrando la "ventana", probablemente esté poniendo el exit #comando dentro de una función, no un script. (En cuyo caso, use return #en su lugar.)
Jamie
1
@Sevenearths 0 significa que tuvo éxito, por lo tanto, exit 0salga del script y devuelve 0 (indicando a otros scripts que podrían usar el resultado de este script que tuvo éxito)
VictorGalisson
689

Use set -e

#!/bin/bash

set -e

/bin/command-that-fails
/bin/command-that-fails2

El script finalizará después de la primera línea que falla (devuelve un código de salida distinto de cero). En este caso, command-that-fail2 no se ejecutará.

Si tuviera que verificar el estado de retorno de cada comando, su script se vería así:

#!/bin/bash

# I'm assuming you're using make

cd /project-dir
make
if [[ $? -ne 0 ]] ; then
    exit 1
fi

cd /project-dir2
make
if [[ $? -ne 0 ]] ; then
    exit 1
fi

Con set -e se vería así:

#!/bin/bash

set -e

cd /project-dir
make

cd /project-dir2
make

Cualquier comando que falle hará que el script completo falle y devolverá un estado de salida que puede verificar con $? . Si su secuencia de comandos es muy larga o está creando muchas cosas, se volverá bastante feo si agrega comprobaciones de estado de devolución en todas partes.

Shizzmo
fuente
10
Con set -eUsted todavía puede hacer alguna salida de comandos con errores sin detener la secuencia de comandos: command 2>&1 || echo $?.
Adobe
66
set -eabortará el script si una tubería o estructura de comando devuelve un valor distinto de cero. Por ejemplo foo || bar, fallará solo si ambos fooy bardevuelven un valor distinto de cero. Por lo general, un script de bash bien escrito funcionará si agrega set -eal principio y la adición funciona como un control de sanidad automatizado: cancele el script si algo sale mal.
Mikko Rantalainen
66
Si está canalizando comandos juntos, también puede fallar si alguno de ellos falla configurando la set -o pipefailopción.
Jake Biesinger
18
En realidad, el código idiomático sin set -esería justo make || exit $?.
tripleee
44
También tienes set -u. Echar un vistazo a la oficial modo estricto fiesta : set -euo pipefail.
Pablo A
236

Un tipo de SysOps una vez me enseñó la técnica de la Garra de tres dedos:

yell() { echo "$0: $*" >&2; }
die() { yell "$*"; exit 111; }
try() { "$@" || die "cannot $*"; }

Estas funciones son * NIX OS y shell con buen sabor. Póngalos al comienzo de su script (bash o de otro modo), try()su declaración y código.

Explicación

(basado en el comentario de ovejas voladoras ).

  • yell: imprime el nombre del script y todos los argumentos en stderr:
    • $0 es el camino hacia el guión;
    • $* Son todos argumentos.
    • >&2significa >redireccionar stdout a & pipe2 . La tubería1 sería en stdoutsí misma.
  • diehace lo mismo que yell, pero sale con un estado de salida que no es 0 , lo que significa "falla".
  • tryusa el ||(booleano OR), que solo evalúa el lado derecho si el izquierdo falló.
    • $@Es todo argumentos de nuevo, pero diferente .
c.gutierrez
fuente
3
Soy bastante nuevo en las secuencias de comandos de Unix. ¿Puede explicar cómo se ejecutan las funciones anteriores? Estoy viendo una nueva sintaxis con la que no estoy familiarizado. Gracias.
kaizenCoder
15
grito : $0es el camino hacia el guión. $*Son todos argumentos. >&2significa " >redirigir stdout a la &tubería 2". la tubería 1 sería stdout en sí misma. entonces grito antepone todos los argumentos con el nombre del script e imprime en stderr. morir hace lo mismo que gritar , pero sale con un estado de salida que no es 0, lo que significa "falla". try usa el booleano o ||, que solo evalúa el lado derecho si el izquierdo no falló. $@Es todo argumentos de nuevo, pero diferente . Espero que eso lo explique todo
ovejas voladoras
1
Modifiqué esto para die() { yell "$1"; exit $2; }que pueda pasar un mensaje y salir del código con die "divide by zero" 115.
Mark Lakata
3
Puedo ver cómo usaría yelly die. Sin embargo, tryno tanto. ¿Puedes dar un ejemplo de cómo lo usas?
kshenoy
2
Hmm, pero ¿cómo los usas en un guión? No entiendo a qué te refieres con "prueba () tu declaración y código".
TheJavaGuy-Ivan Milosavljević
33

Si va a invocar la secuencia de comandos source, puede usar return <x>dónde <x>estará el estado de salida de la secuencia de comandos (use un valor distinto de cero para error o falso). Pero si invoca una secuencia de comandos ejecutable (es decir, directamente con su nombre de archivo), la declaración de devolución dará como resultado una queja (mensaje de error "return: solo puede" regresar "de una función o secuencia de comandos fuente).

Si exit <x>se usa en su lugar, cuando se invoca el script source, dará como resultado la salida del shell que inició el script, pero un script ejecutable simplemente terminará, como se esperaba.

Para manejar cualquier caso en el mismo script, puede usar

return <x> 2> /dev/null || exit <x>

Esto manejará cualquier invocación que sea adecuada. Eso supone que utilizará esta declaración en el nivel superior del script. Aconsejaría que no salga directamente del script desde una función.

Nota: <x>se supone que es solo un número.

kavadias
fuente
No funciona para mí en un script con retorno / salida dentro de una función, es decir, es posible salir dentro de la función sin el shell, pero sin hacer que la persona que llama cuide la verificación correcta del código de retorno de la función ?
enero
@jan Lo que hace (o no hace) la persona que llama con los valores de retorno es completamente ortogonal (es decir, independiente de) cómo regresa de la función (... sin salir del shell, sin importar la invocación). Es sobre todo depende del código de llamadas, que no forma parte de este Q & A. Incluso podría adaptar el valor de retorno de la función a las necesidades de la persona que llama, pero esta respuesta no limita lo que este valor de retorno puede ser ...
kavadias
11

A menudo incluyo una función llamada run () para manejar errores. Cada llamada que quiero hacer se pasa a esta función para que todo el script salga cuando se produce un error. La ventaja de esto sobre la solución set -e es que el script no sale silenciosamente cuando falla una línea, y puede decirle cuál es el problema. En el siguiente ejemplo, la tercera línea no se ejecuta porque la secuencia de comandos sale en la llamada a falso.

function run() {
  cmd_output=$(eval $1)
  return_value=$?
  if [ $return_value != 0 ]; then
    echo "Command $1 failed"
    exit -1
  else
    echo "output: $cmd_output"
    echo "Command succeeded."
  fi
  return $return_value
}
run "date"
run "false"
run "date"
Joseph Sheedy
fuente
1
Hombre, por alguna razón, me gusta mucho esta respuesta. Reconozco que es un poco más complicado, pero parece muy útil. Y dado que no soy un experto en bash, me lleva a creer que mi lógica es defectuosa, y hay algo mal con esta metodología, de lo contrario, siento que otros lo habrían alabado más. Entonces, ¿cuál es el problema con esta función? ¿Hay algo que debería estar buscando aquí?
No recuerdo mi razón para usar eval, la función funciona bien con cmd_output = $ ($ 1)
Joseph Sheedy
Acabo de implementar esto como parte de un complejo proceso de implementación y funcionó fantástico. Gracias y aquí hay un comentario y un voto positivo.
fuzzygroup
¡Un trabajo realmente increíble! Esta es la solución más simple y limpia que funciona bien. Para mí, agregué esto antes de un comando en un bucle FOR ya que los bucles FOR no recogerán el set -e option. A continuación, el comando, ya que es con argumentos, que utiliza comillas simples para evitar problemas de bash, así: runTry 'mysqldump $DB_PASS --user="$DB_USER" --host="$BV_DB_HOST" --triggers --routines --events --single-transaction --verbose $DB_SCHEMA $tables -r $BACKUP_DIR/$tables$BACKUP_FILE_NAME'. Tenga en cuenta que he cambiado el nombre de la función para ejecutar Try.
Tony-Caffe
1
evales potencialmente peligroso si acepta entradas arbitrarias, pero de lo contrario, esto se ve bastante bien.
dragon788
5

En lugar de ifconstruir, puede aprovechar la evaluación de cortocircuito :

#!/usr/bin/env bash

echo $[1+1]
echo $[2/0]              # division by 0 but execution of script proceeds
echo $[3+1]
(echo $[4/0]) || exit $? # script halted with code 1 returned from `echo`
echo $[5+1]

Tenga en cuenta el par de paréntesis que es necesario debido a la prioridad del operador de alternancia. $?es una variable especial establecida para salir del código del comando llamado más recientemente.

skalee
fuente
1
si lo hago command -that --fails || exit $?funciona sin paréntesis, ¿de qué se trata echo $[4/0]que nos hace necesitarlos?
Anentropic
2
@Anentropic @skalee Los paréntesis no tienen nada que ver con la precedencia, sino con el manejo de excepciones. Una división entre cero provocará una salida inmediata del shell con el código 1. Sin los paréntesis (es decir, un simple echo $[4/0] || exit $?) bash nunca ejecutará el echo, y mucho menos obedecerá ||.
bobbogo
1

Tengo la misma pregunta pero no puedo hacerla porque sería un duplicado.

La respuesta aceptada, usando exit, no funciona cuando el script es un poco más complicado. Si usa un proceso en segundo plano para verificar la condición, la salida solo sale de ese proceso, ya que se ejecuta en un sub-shell. Para matar el script, debes matarlo explícitamente (al menos esa es la única forma en que lo sé).

Aquí hay un pequeño script sobre cómo hacerlo:

#!/bin/bash

boom() {
    while true; do sleep 1.2; echo boom; done
}

f() {
    echo Hello
    N=0
    while
        ((N++ <10))
    do
        sleep 1
        echo $N
        #        ((N > 5)) && exit 4 # does not work
        ((N > 5)) && { kill -9 $$; exit 5; } # works 
    done
}

boom &
f &

while true; do sleep 0.5; echo beep; done

Esta es una mejor respuesta pero aún incompleta, realmente no sé cómo deshacerme de la parte del boom .

Gyro Gearloose
fuente