Есть ли в bash оператор «goto»?


207

Есть ли в bash оператор "goto"? Я знаю, что это считается плохой практикой, но мне нужно специально "Перейти".


4
Нет, gotoв Bash нет (по крайней мере, это говорит command not foundдля меня). Зачем? Скорее всего, есть лучший способ сделать это.
Никлас Б.

153
У него могут быть свои причины. Я нашел этот вопрос, потому что хочу, чтобы gotoоператор пропустил много кода для отладки большого скрипта, не дожидаясь часа, пока не будут выполнены различные несвязанные задачи. Я бы, конечно, не использовал a gotoв рабочем коде, но для отладки моего кода это сделало бы мою жизнь бесконечно проще, и было бы легче заметить, когда это удастся удалить.
Карл Николл

30
@delnan Но отсутствие goto может усложнить некоторые вещи. Там действительно есть варианты использования.
glglgl

72
Мне надоел этот миф о гото! С Goto все в порядке! Все, что ты пишешь, в конце концов становится гото В ассемблере есть только goto. Хорошей причиной для использования goto в более высоких языках программирования является, например, чистое и удобочитаемое выпрыгивание из вложенных циклов.
Александр Чутро

25
«Избегай goto» - великое правило. Как и любое правило, его следует изучать в три этапа. Первое: следуйте правилу, пока оно не станет второй натурой. Во-вторых, научиться понимать причины правила. В-третьих, выучите хорошие исключения из правила, основанные на всестороннем понимании того, как следовать правилу и причин этого правила. Старайтесь не пропускать шаги, описанные выше, что было бы похоже на использование «goto». ;-)
Джефф Лирман

Ответы:


75

Нет, нет; см §3.2.4 «Составные команды» в Bash Reference Manual для получения информации о структурах управления , которые делают существование. В частности, обратите внимание на упоминание breakи continue, которые не так гибки, как goto, но более гибки в Bash, чем в некоторых языках, и могут помочь вам достичь того, чего вы хотите. (Что бы вы ни хотели ...)


10
Не могли бы вы рассказать о «более гибком в Bash, чем в некоторых языках»?
user239558

21
@ user239558: Некоторые языки позволяют вам входить breakили выходить continueиз самого внутреннего цикла, в то время как Bash позволяет вам указать, сколько уровней цикла должно пройти. (И даже в тех языках, которые позволяют вам выполнять произвольные циклы breakили выходить continueиз них, большинство требует, чтобы они были выражены статически, например, break foo;выходили из цикла с меткой, fooтогда как в Bash это выражалось динамически, например, break "$foo"выходили из $fooциклов. )
ruakh

Я бы порекомендовал определять функции с необходимыми функциями и вызывать их из других частей кода.
blmayer

@ruakh На самом деле, многие языки позволяют break LABEL_NAME;, а не отвратительно Баш break INTEGER;.
Sapphire_Brick

1
@Sapphire_Brick: Да, я упоминал об этом в комментарии, на который вы отвечаете.
Руах

125

Если вы используете его, чтобы пропустить часть большого скрипта для отладки (см. Комментарий Карла Николла), то если false может быть хорошим вариантом (не уверен, всегда ли доступен false, для меня он находится в / bin / false) :

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

if false; then

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

fi

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

Трудность возникает, когда приходит время вырвать ваш код отладки. Конструкция «if false» довольно проста и запоминаема, но как найти подходящий файл? Если ваш редактор позволяет блокировать отступ, вы можете сделать отступ пропущенному блоку (тогда вы захотите вернуть его, когда закончите). Или комментарий к файлу, но это должно быть что-то, что вы запомните, что, я подозреваю, будет зависеть от программиста.


6
Да falseвсегда доступно. Но если у вас есть блок кода, который вы не хотите выполнять, просто закомментируйте его. Или удалите его (и посмотрите в вашей системе контроля версий, если вам нужно восстановить его позже).
Кит Томпсон

1
Если этот блок кода слишком длинный, чтобы нудно комментировать одну строку за раз, посмотрите эти приемы. stackoverflow.com/questions/947897/… Тем не менее, они не помогают текстовому редактору сопоставить начало и конец, так что они не слишком улучшены.
Камиль Гудесюн

4
«if false» часто намного лучше, чем комментирование кода, потому что это гарантирует, что вложенный код продолжает оставаться юридическим кодом. Лучшее оправдание для комментирования кода - это когда его действительно нужно удалить, но есть что-то, что нужно запомнить - так что это просто комментарий, а не «код».
Джефф Лирман

1
Я использую комментарий «#if false;». Таким образом, я могу просто найти это и найти начало и конец раздела удаления отладки.
Джон

За этот ответ не следует голосовать, поскольку он никак не отвечает на вопрос ФП.
Виктор Жорас

54

Это действительно может быть полезно для некоторых отладочных или демонстрационных нужд.

Я обнаружил, что решение Боба Коупленда 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

результаты в:

$ ./test.sh
x is 100
$ ./test.sh foo
x is 10
$ ./test.sh mid
This is not printed!
x is 101

36
«Мое стремление сделать bash похожим на ассемблер приближается к завершению». - Вот это да. Просто вау.
Брайан Агнью

5
единственное, что я хотел бы изменить, это сделать так, чтобы метки начинались так, : start:чтобы они не были синтаксическими ошибками.
Алексей Магура

5
Было бы лучше, если бы вы сделали это: cmd = $ (sed -n "/ # $ label: / {: a; n; p; ba};" $ 0 | grep -v ': $') с метками, начинающимися как: # start: => это предотвратит ошибки скрипта
access_granted

2
Хм, действительно это скорее всего моя ошибка. У меня есть редактировать пост. Спасибо.
Hubbitus

2
set -xпомогает понять, что происходит
Джон Лин

30

Вы можете использовать caseв bash для имитации goto:

#!/bin/bash

case bar in
  foo)
    echo foo
    ;&

  bar)
    echo bar
    ;&

  *)
    echo star
    ;;
esac

производит:

bar
star

4
Обратите внимание, что это требует bash v4.0+. Это, однако, не универсальный, gotoа сквозной вариант для caseзаявления.
mklement0

3
я думаю, что это должен быть ответ. Мне действительно нужно перейти для поддержки возобновления выполнения сценария из данной инструкции. это, во всех отношениях, но семантика, goto, и семантика и синтаксические сахара симпатичны, но не строго необходимы. отличное решение, ИМО.
Натан г

1
@nathang, будь то ответ зависит от того, происходит ли ваш случай к сетке с подмножеством общего случая ОП спросил о. К сожалению, вопрос задает об общем случае, делая этот ответ слишком узким, чтобы быть правильным. (Должен ли этот вопрос быть закрыт как слишком широкий по этой причине - другое обсуждение).
Чарльз Даффи,

1
Goto - это больше, чем выбор. Функция goto означает, что вы можете прыгать в места в соответствии с некоторыми условиями, создавая даже петлю, похожую на поток ...
Серхио Абреу,

19

Если вы тестируете / отлаживаете bash-скрипт и просто хотите пропустить форварды после одного или нескольких разделов кода, вот очень простой способ сделать это, который также очень легко найти и удалить позже (в отличие от большинства методов описано выше).

#!/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"

Чтобы вернуть скрипт в нормальное состояние, просто удалите все строки с помощью GOTO.

Мы также можем протестировать это решение, добавив gotoкоманду в качестве псевдонима:

#!/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"

Псевдонимы обычно не работают в скриптах bash, поэтому нам нужна shoptкоманда, чтобы это исправить.

Если вы хотите иметь возможность включать / отключать свои goto, нам нужно немного больше:

#!/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"

Затем вы можете сделать export DEBUG=TRUEперед запуском сценария.

Метки являются комментариями, поэтому не будут вызывать синтаксические ошибки, если отключить наши goto(установив goto« :no-op»), но это означает, что нам нужно заключить их в кавычкиgoto утверждениях.

Всякий раз, когда вы используете какое-либо gotoрешение, вы должны быть осторожны, чтобы код, через который вы переходите, не устанавливал переменные, на которые вы полагаетесь позже, - вам может потребоваться переместить эти определения в начало вашего сценария или чуть выше одного. ваших gotoзаявлений.


Не вдаваясь в хорошие / плохие идеи Goto, решение Лоуренса просто круто. Вы получили мой голос.
Mogens TrasherDK

1
Это больше похоже на переход, if(false)кажется, имеет больше смысла (для меня).
Весперто

2
Цитирование goto «метка» также означает, что документ here не раскрывается / не оценивается, что избавляет вас от некоторых трудных для поиска ошибок (без кавычек это может быть связано с: расширением параметров, подстановкой команд и арифметическим расширением, которые все могут иметь побочные эффекты). Вы также можете немного подправить это alias goto=":<<"и catвообще отказаться от него .
mrsspuratic

да, Весперто, вы можете использовать оператор «если», и я сделал это во многих сценариях, но он становится очень грязным и его намного сложнее контролировать и отслеживать, особенно если вы хотите часто менять точки перехода.
Лоуренс Реншоу

12

Хотя другие уже пояснили, что gotoв bash нет прямого эквивалента (и предоставили наиболее близкие альтернативы, такие как функции, циклы и разрывы), я хотел бы проиллюстрировать, как использование цикла plus breakможет имитировать определенный тип оператора goto.

Ситуация, когда я нахожу это наиболее полезным, - это когда мне нужно вернуться к началу раздела кода, если определенные условия не выполняются. В приведенном ниже примере цикл while будет работать вечно, пока ping не перестанет сбрасывать пакеты на тестовый IP.

#!/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

7

Это решение имело следующие проблемы:

  • Без разбора удаляет все строки кода, заканчивающиеся на :
  • Обрабатывает в label:любом месте строки как метку

Вот исправленная ( shell-checkчистая) версия:


#!/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

6

Есть еще одна возможность достичь желаемых результатов: команда trap. Например, его можно использовать для очистки.


4

Здесь нет goto в bash.

Вот некоторый грязный обходной путь, trapкоторый прыгает только назад :)

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

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

Вывод:

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

Это не должно использоваться таким образом, но только в образовательных целях. Вот почему это работает:

trapиспользует обработку исключений для достижения изменения в потоке кода. В этом случае trapперехватывает все, что заставляет скрипт завершиться. Команда gotoне существует, и, следовательно, выдает ошибку, которая обычно завершает работу сценария. Эта ошибка обнаруживается trap, и 2>/dev/nullскрывает сообщение об ошибке, которое обычно отображается.

Эта реализация goto, очевидно, ненадежна, поскольку любая несуществующая команда (или любая другая ошибка, связанная с этим способом) будет выполнять ту же команду прерывания. В частности, вы не можете выбрать, какой ярлык перейти.


По сути, в реальном сценарии вам не нужны никакие операторы goto, они избыточны, поскольку случайные вызовы в разные места только затрудняют понимание вашего кода.

Если ваш код вызывается много раз, рассмотрите возможность использования цикла и изменения его рабочего процесса для использования continueи break.

Если ваш код повторяет сам, подумайте о написании функции и вызове ее столько раз, сколько вы хотите.

Если ваш код должен перейти в конкретный раздел, основанный на значении переменной, то подумайте об использовании caseоператора.

Если вы можете разделить свой длинный код на более мелкие части, рассмотрите возможность его перемещения в отдельные файлы и вызова их из родительского сценария.


в чем разница между этой формой и нормальной функцией?
yurenchen

2
@yurenchen - Подумайте об trapиспользовании exception handlingдля достижения изменений в потоке кода. В этом случае ловушка перехватывает все, что вызывает сценарий EXIT, который запускается при вызове несуществующей команды goto. Кстати: аргумент goto trapможет быть любым, goto ignoredпотому что именно он gotoвызывает EXIT, и 2>/dev/nullскрывает сообщение об ошибке, сообщающее, что ваш скрипт завершается.
Джесси Чисхолм

2

Это небольшое исправление сценария Джуди Шмидт, созданного Хабббитом.

Помещение в сценарий неэкранированных меток было проблематично на компьютере и приводило к его аварийному завершению. Это было достаточно легко решить, добавив #, чтобы убрать метки. Спасибо Алексею Магуре и access_granted за их предложения.

#!/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

Конечно, я попробовал код! Я проверил в 5 или 6 различных условиях, прежде чем опубликовать все успешно. Как код потерпел неудачу для вас, какое сообщение об ошибке или код?
thebunnyrules

Каким бы ни был мужчина. Я хочу помочь вам, но вы действительно расплывчаты и необщительны (не говоря уже об оскорблении этим вопросом «Вы проверяли это»). Что значит «ты не правильно вставил»? Если вы имеете в виду "/ $ # label #: / {: a; n; p; ba};" часть, я очень хорошо понимаю, что это другое. Я изменил его для нового формата меток (aka # label # vs: label в другом ответе, который в некоторых случаях вызывал сбой скрипта). У вас есть какая-то действительная проблема с моим ответом (например, сбой скрипта или неправильный синтаксис) или вы просто -1 мой ответ, потому что вы подумали: «Я не знаю, как вырезать и вставлять»?
thebunnyrules

1
@thebunnyrules Я столкнулся с той же проблемой, что и вы, но решил по-другому +1, хотя для вашего решения!
Fabby

2

Простой способ поиска с возможностью комментирования блоков кода при отладке.

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

Результат-> GOTO сделано


1

Я нашел способ сделать это с помощью функций.

Скажем, к примеру, у вас есть 3 варианта: A, Bи C. Aи Bвыполните команду, но Cвы получите больше информации и снова перейдете к исходному приглашению. Это можно сделать с помощью функций.

Обратите внимание, что, поскольку линия связи function demoFunctionпросто устанавливает функцию, вам нужно вызватьdemoFunction после этого сценария, чтобы функция фактически выполнялась.

Вы можете легко адаптировать это, написав несколько других функций и вызывая их, если вам нужно « GOTO» другое место в вашем скрипте оболочки.

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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.