¿Hay una declaración "goto" en bash?

206

¿Hay una declaración "goto" en bash? Sé que se considera una mala práctica, pero necesito específicamente "ir a".

kofucii
fuente
44
No, no hay gotoen bash (al menos lo dice command not foundpara mí). ¿Por qué? Lo más probable es que haya una mejor manera de hacerlo.
Niklas B.
153
Él puede tener sus razones. Encontré esta pregunta porque quiero que una gotodeclaración omita mucho código para depurar un script grande sin esperar una hora para que se completen varias tareas no relacionadas. Ciertamente no usaría un gotoen el código de producción, pero para depurar mi código, haría mi vida infinitamente más fácil, y sería más fácil detectarlo cuando se tratara de eliminarlo.
Karl Nicoll el
30
@delnan Pero no tener goto puede hacer que algunas cosas sean más complicadas. De hecho, hay casos de uso.
glglgl
72
Estoy harto de este mito goto! ¡No hay nada malo en ir a Goto! Todo lo que escribes eventualmente se convierte en goto. En ensamblador, solo hay goto. Una buena razón para usar goto en lenguajes de programación superiores es, por ejemplo, saltar de bucles anidados de una manera limpia y legible.
Alexander Czutro
25
"Evitar goto" es una gran regla. Como cualquier regla, se debe aprender en tres fases. Primero: sigue la regla hasta que sea una segunda naturaleza. Segundo, aprenda a entender las razones de la regla. Tercero, aprenda buenas excepciones a la regla, basándose en una comprensión integral de cómo seguir la regla y los motivos de la regla. Evite omitir los pasos anteriores, que serían como usar "goto". ;-)
Jeff Learman

Respuestas:

75

No no hay; ver §3.2.4 "Comandos de compuestos" en la Referencia Bash manual para obtener información sobre las estructuras de control que hacen existir. En particular, tenga en cuenta la mención de breaky continue, que no son tan flexibles como goto, pero son más flexibles en Bash que en algunos idiomas, y pueden ayudarlo a lograr lo que desea. (Lo que sea que quieras ...)

ruakh
fuente
10
¿Podría ampliar "más flexible en Bash que en algunos idiomas"?
user239558
21
@ user239558: Algunos idiomas sólo le permiten al breako continuedesde el bucle más interior, mientras que Bash le permite especificar cuántos niveles de bucle para saltar. (E incluso de los idiomas que le permiten breako continuedesde bucles arbitrarios, la mayoría requiere que se expresen estáticamente, por ejemplo, break foo;saldrán del bucle etiquetado foo, mientras que en Bash se expresará dinámicamente, por ejemplo, break "$foo"saldrán de $foobucles. )
ruakh
Recomiendo definir funciones con las características necesarias y llamarlas desde otras partes del código.
blmayer
@ruakh En realidad, muchos idiomas permiten break LABEL_NAME;, en lugar de asqueroso Bash break INTEGER;.
Sapphire_Brick
1
@Sapphire_Brick: Sí, lo mencioné en el comentario al que estás respondiendo.
ruakh
125

Si lo está utilizando para omitir parte de un script grande para la depuración (vea el comentario de Karl Nicoll), entonces si false podría ser una buena opción (no estoy seguro si "false" siempre está disponible, para mí está en / bin / false) :

# ... Code I want to run here ...

if false; then

# ... Code I want to skip here ...

fi

# ... I want to resume here ...

La dificultad viene cuando es hora de extraer su código de depuración. El constructo "si es falso" es bastante sencillo y memorable, pero ¿cómo encuentra la coincidencia? Si su editor le permite bloquear la sangría, puede sangrar el bloque omitido (entonces querrá volver a colocarlo cuando haya terminado). O un comentario en la línea fija, pero tendría que ser algo que recordará, que sospecho que dependerá mucho del programador.

Michael Rusch
fuente
66
falsesiempre está disponible. Pero si tiene un bloque de código que no desea ejecutar, simplemente coméntelo. O elimínelo (y busque en su sistema de control de fuente si necesita recuperarlo más tarde).
Keith Thompson
1
Si el bloque de código es demasiado largo para comentar tediosamente una línea a la vez, vea estos trucos. stackoverflow.com/questions/947897/… Sin embargo, tampoco ayudan a que un editor de texto coincida de principio a fin, por lo que no son una gran mejora.
Camille Goudeseune
44
"si es falso" suele ser mucho mejor que comentar el código, ya que garantiza que el código adjunto siga siendo código legal. La mejor excusa para comentar el código es cuando realmente necesita eliminarse, pero hay algo que debe recordarse, por lo que es solo un comentario y ya no es "código".
Jeff Learman
1
Yo uso el comentario '#if false;'. De esa forma, puedo buscar eso y encontrar el principio y el final de la sección de eliminación de depuración.
Jon
Esta respuesta no debe ser votada ya que no responde a la pregunta del OP de ninguna manera.
Viktor Joras
54

De hecho, puede ser útil para algunas necesidades de depuración o demostración.

Encontré que la solución de Bob Copeland http://bobcopeland.com/blog/2012/10/goto-in-bash/ elegant:

#!/bin/bash
# include this boilerplate
function jumpto
{
    label=$1
    cmd=$(sed -n "/$label:/{:a;n;p;ba};" $0 | grep -v ':$')
    eval "$cmd"
    exit
}

start=${1:-"start"}

jumpto $start

start:
# your script goes here...
x=100
jumpto foo

mid:
x=101
echo "This is not printed!"

foo:
x=${x:-10}
echo x is $x

resultados en:

$ ./test.sh
x is 100
$ ./test.sh foo
x is 10
$ ./test.sh mid
This is not printed!
x is 101
Hubbitus
fuente
36
"Mi búsqueda para hacer que bash parezca lenguaje ensamblador se acerca cada vez más a su finalización". - Guau. Simplemente guau.
Brian Agnew
55
Lo único que cambiaría es hacer que las etiquetas comiencen de manera : start:tal que no sean errores de sintaxis.
Alexej Magura
55
Sería mejor si hicieras eso: cmd = $ (sed -n "/ # $ label: / {: a; n; p; ba};" $ 0 | grep -v ': $') con etiquetas que comienzan como: # inicio: => esto evitaría errores de script
access_granted
2
Hm, de hecho es muy probable que sea mi error. Tengo una publicación de edición. Gracias.
Hubbitus
2
set -xayuda a entender lo que está sucediendo
John Lin
30

Puedes usar caseen bash para simular un goto:

#!/bin/bash

case bar in
  foo)
    echo foo
    ;&

  bar)
    echo bar
    ;&

  *)
    echo star
    ;;
esac

produce:

bar
star
Paul Brannan
fuente
44
Tenga en cuenta que esto requiere bash v4.0+. Sin embargo, no es una gotoopción de propósito general sino una alternativa para la casedeclaración.
mklement0
3
Creo que esta debería ser la respuesta. Tengo una verdadera necesidad de ir a fin de admitir la reanudación de la ejecución de un script, a partir de una instrucción dada. Esto es, en todos los sentidos, pero la semántica, el goto, la semántica y los azúcares sintácticos son lindos, pero no estrictamente necesarios. Gran solución, OMI.
nathan g
1
@nathang, si es la respuesta depende de si su caso coincide con el subconjunto del caso general sobre el que preguntó el OP. Desafortunadamente, la pregunta se refiere al caso general, por lo que esta respuesta es demasiado estrecha para ser correcta. (Si esa pregunta debe cerrarse como demasiado amplia por esa razón es una discusión diferente).
Charles Duffy
1
goto es más que seleccionar. Ir a la función significa poder saltar a lugares según algunas condiciones, creando incluso un ciclo como el flujo ...
Sergio Abreu
19

Si está probando / depurando un script bash, y simplemente quiere saltarse una o más secciones de código, aquí hay una forma muy simple de hacerlo que también es muy fácil de encontrar y eliminar más tarde (a diferencia de la mayoría de los métodos descrito arriba).

#!/bin/bash

echo "Run this"

cat >/dev/null <<GOTO_1

echo "Don't run this"

GOTO_1

echo "Also run this"

cat >/dev/null <<GOTO_2

echo "Don't run this either"

GOTO_2

echo "Yet more code I want to run"

Para que su script vuelva a la normalidad, simplemente elimine las líneas con GOTO.

También podemos embellecer esta solución, agregando un gotocomando como un alias:

#!/bin/bash

shopt -s expand_aliases
alias goto="cat >/dev/null <<"

goto GOTO_1

echo "Don't run this"

GOTO_1

echo "Run this"

goto GOTO_2

echo "Don't run this either"

GOTO_2

echo "All done"

Los alias no suelen funcionar en scripts de bash, por lo que necesitamos el shoptcomando para solucionarlo.

Si desea habilitar / deshabilitar gotolos suyos, necesitamos un poco más:

#!/bin/bash

shopt -s expand_aliases
if [ -n "$DEBUG" ] ; then
  alias goto="cat >/dev/null <<"
else
  alias goto=":"
fi

goto '#GOTO_1'

echo "Don't run this"

#GOTO1

echo "Run this"

goto '#GOTO_2'

echo "Don't run this either"

#GOTO_2

echo "All done"

Entonces puedes hacerlo export DEBUG=TRUEantes de ejecutar el script.

Las etiquetas son comentarios, por lo que no causarán errores de sintaxis si deshabilita nuestro goto'(configurando gotoel' :'no-op), pero esto significa que necesitamos citarlos en nuestras gotodeclaraciones.

Siempre que use cualquier tipo de gotosolución, debe tener cuidado de que el código que está saltando no establezca ninguna variable en la que confíe más adelante; es posible que deba mover esas definiciones a la parte superior de su script, o justo por encima de una de sus gotodeclaraciones

Laurence Renshaw
fuente
Sin entrar en lo bueno / malo de Goto's, la solución de Laurence es simplemente genial. Tienes mi voto.
Mogens TrasherDK
1
Eso es más como un salto, if(false)parece tener más sentido (para mí).
vesperto
2
Citar la "etiqueta" de goto también significa que here-doc no se expande / evalúa, lo que le ahorra algunos errores difíciles de encontrar (sin comillas, estaría sujeto a: expansión de parámetros, sustitución de comandos y expansión aritmética, que pueden tener efectos secundarios). También puede modificar esto un poco alias goto=":<<"y prescindir por catcompleto.
señor púrpura el
sí, vesperto, puede usar una declaración 'if', y lo he hecho en muchos scripts, pero se vuelve muy complicado y es mucho más difícil de controlar y rastrear, especialmente si desea cambiar sus puntos de salto con frecuencia.
Laurence Renshaw
12

Aunque otros ya han aclarado que no hay un gotoequivalente directo en bash (y han proporcionado las alternativas más cercanas, como funciones, bucles y pausa), me gustaría ilustrar cómo usar un bucle plus breakpuede simular un tipo específico de instrucción goto.

La situación en la que encuentro esto más útil es cuando necesito volver al comienzo de una sección de código si no se cumplen ciertas condiciones. En el siguiente ejemplo, el ciclo while se ejecutará para siempre hasta que el ping deje de soltar paquetes a una IP de prueba.

#!/bin/bash

TestIP="8.8.8.8"

# Loop forever (until break is issued)
while true; do

    # Do a simple test for Internet connectivity
    PacketLoss=$(ping "$TestIP" -c 2 | grep -Eo "[0-9]+% packet loss" | grep -Eo "^[0-9]")

    # Exit the loop if ping is no longer dropping packets
    if [ "$PacketLoss" == 0 ]; then
        echo "Connection restored"
        break
    else
        echo "No connectivity"
    fi
done
Seth McCauley
fuente
7

Esta solución tenía los siguientes problemas:

  • Elimina indiscriminadamente todas las líneas de código que terminan en un :
  • Trata en label:cualquier lugar de una línea como etiqueta

Aquí hay una shell-checkversión fija ( limpia):


#!/bin/bash

# GOTO for bash, based upon https://stackoverflow.com/a/31269848/5353461
function goto
{
 local label=$1
 cmd=$(sed -En "/^[[:space:]]*#[[:space:]]*$label:[[:space:]]*#/{:a;n;p;ba};" "$0")
 eval "$cmd"
 exit
}

start=${1:-start}
goto "$start"  # GOTO start: by default

#start:#  Comments can occur after labels
echo start
goto end

  # skip: #  Whitespace is allowed
echo this is usually skipped

# end: #
echo end
Tom Hale
fuente
6

Hay una habilidad más para lograr los resultados deseados: comando trap. Se puede utilizar para fines de limpieza, por ejemplo.

Serge Roussak
fuente
4

No hay gotoen bash.

Aquí hay algunas soluciones sucias trapque usan saltos solo hacia atrás :)

#!/bin/bash -e
trap '
echo I am
sleep 1
echo here now.
' EXIT

echo foo
goto trap 2> /dev/null
echo bar

Salida:

$ ./test.sh 
foo
I am
here now.

Esto no debe usarse de esa manera, sino solo con fines educativos. Aquí es por qué esto funciona:

trapestá utilizando el manejo de excepciones para lograr el cambio en el flujo de código. En este caso, trapestá detectando cualquier cosa que provoque que el script salga. El comando gotono existe, y por lo tanto arroja un error, que normalmente saldría del script. Este error se está detectando trapy 2>/dev/nulloculta el mensaje de error que normalmente se mostraría.

Esta implementación de goto obviamente no es confiable, ya que cualquier comando inexistente (o cualquier otro error, de esa manera), ejecutaría el mismo comando trap. En particular, no puede elegir a qué etiqueta ir.


Básicamente, en un escenario real, no necesita ninguna instrucción goto, son redundantes ya que las llamadas aleatorias a diferentes lugares solo hacen que su código sea difícil de entender.

Si su código se invoca muchas veces, considere usar loop y cambiar su flujo de trabajo para usar continuey break.

Si su código se repite, considere escribir la función y llamarla tantas veces como desee.

Si su código necesita saltar a una sección específica basada en el valor de la variable, considere usar la caseinstrucción.

Si puede separar su código largo en partes más pequeñas, considere moverlo a archivos separados y llamarlos desde el script principal.

kenorb
fuente
¿Cuáles son las diferencias entre esta forma y una función normal?
yurenchen 01 de
2
@yurenchen: piense trapen usar exception handlingpara lograr el cambio en el flujo de código. En este caso, la trampa está atrapando cualquier cosa que provoque que el script se active EXIT, invocando el comando inexistente goto. Por cierto: el argumento goto trappodría ser cualquier cosa, goto ignoredporque es el gotoque causa la SALIDA, y 2>/dev/nulloculta el mensaje de error que dice que su script está saliendo.
Jesse Chisholm
2

Esta es una pequeña corrección del guión Judy Schmidt presentado por Hubbbitus.

Poner etiquetas sin escape en el script fue problemático en la máquina y causó que se bloqueara. Esto fue bastante fácil de resolver agregando # para escapar de las etiquetas. Gracias a Alexej Magura y access_granted por sus sugerencias.

#!/bin/bash
# include this boilerplate
function goto {  
label=$1
cmd=$(sed -n "/$#label#:/{:a;n;p;ba};" $0 | grep -v ':$')
eval "$cmd"
exit
}

start=${1:-"start"}

goto $start

#start#
echo "start"
goto bing

#boom#
echo boom
goto eof

#bang#
echo bang
goto boom

#bing#
echo bing
goto bang

#eof#
echo "the end mother-hugger..."
thebunnyrules
fuente
Esta copia pegar no funciona => todavía hay un jumpto.
Alexis Paques
Qué significa eso? ¿Qué no funciona? ¿Podrías ser más específico?
thebunnyrules
¡Por supuesto que probé el código! Probé en 5 o 6 condiciones diferentes antes de publicar todo exitoso. ¿Cómo le falló el código? ¿Qué mensaje de error o código?
thebunnyrules
Cualquier hombre. Quiero ayudarte, pero estás siendo muy vago y poco comunicativo (sin mencionar insultante con esa pregunta de "¿lo probaste?"). ¿Qué significa "no pegaste correctamente"? Si te refieres a "/ $ # label #: / {: a; n; p; ba};" parte, soy muy consciente de que es diferente. Lo modifiqué para el nuevo formato de etiqueta (también conocido como # etiqueta # vs: etiqueta en otra respuesta que estaba bloqueando el script en algunos casos). ¿Tiene algún problema válido con mi respuesta (como bloqueo del script o sintaxis incorrecta) o simplemente -1 mi respuesta porque pensó "No sé cómo cortar y pegar"?
thebunnyrules
1
@thebunnyrules ¡Me encontré con el mismo problema que tú, pero lo resolví de forma diferente para tu solución!
Fabby
2

Un simple goto de búsqueda para el uso de comentar bloques de código al depurar.

GOTO=false
if ${GOTO}; then
    echo "GOTO failed"
    ...
fi # End of GOTO
echo "GOTO done"

El resultado es-> GOTO hecho

ztalbot
fuente
1

Encontré una manera de hacer esto usando funciones.

Digamos, por ejemplo, usted tiene 3 opciones: A, B, y C. Ay Bejecuta un comando, pero Cte da más información y te lleva al indicador original nuevamente. Esto se puede hacer usando funciones.

Tenga en cuenta que dado que el contacto de la línea solo function demoFunctionestá configurando la función, debe llamar demoFunctiondespués de ese script para que la función realmente se ejecute.

Puede adaptar esto fácilmente escribiendo varias otras funciones y llamándolas si necesita " GOTO" otro lugar en su script de shell.

function demoFunction {
        read -n1 -p "Pick a letter to run a command [A, B, or C for more info] " runCommand

        case $runCommand in
            a|A) printf "\n\tpwd being executed...\n" && pwd;;
            b|B) printf "\n\tls being executed...\n" && ls;;
            c|C) printf "\n\toption A runs pwd, option B runs ls\n" && demoFunction;;
        esac
}

demoFunction
cutrightjm
fuente