在bash中有"goto"语句吗?

298
在Bash中有"goto"语句吗?
我知道这被认为是不良实践,但我确切地需要一个"goto"。

6
不,Bash 中没有 goto 命令(至少对我来说会显示“未找到命令”)。为什么呢?很可能有更好的方法来完成相同的任务。 - Niklas B.
197
他可能有他的理由。我发现这个问题是因为我想要一个goto语句来跳过大量代码,以便在调试一个大型脚本时不必等待一小时进行各种无关的任务。我肯定不会在生产代码中使用goto,但对于调试我的代码来说,它会让我的生活变得轻松得多,而且在删除它时更容易发现它。 - Karl Nicoll
38
但是没有goto语句可能会使一些事情变得更加复杂。确实存在使用场景。 - glglgl
98
我厌倦了这个“goto神话”!使用goto没有什么问题!你编写的所有代码最终都会变成goto。在汇编语言中,只有goto。在高级编程语言中使用goto的一个很好的原因是可以以清晰易读的方式跳出嵌套循环。 - ACz
46
“避免使用goto”是一条很好的规则。像任何规则一样,应该分三个阶段学习。首先要遵循这个规则,直到它变成本能反应。其次,学习理解这个规则的原因。第三,基于全面了解如何遵循这个规则以及规则的原因,学习好的例外情况。避免跳过上述步骤,这就像使用“goto”。;-) - Jeff Learman
显示剩余16条评论
14个回答

175

如果您正在使用它来跳过大型脚本的某些部分进行调试(请参见Karl Nicoll的评论),那么如果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" 结构相当直观和易记,但是如何找到匹配的 fi 呢?如果您的编辑器允许块缩进,可以对跳过的代码块进行缩进(然后在完成后将其放回原位)。或者在 fi 行上添加注释,但它必须是您会记得的内容,我认为这将非常依赖于程序员。


8
是的,false 始终可用。但是如果你有一段代码不想执行,可以将其注释掉或者删除它(如果需要后期恢复,可以查看源代码控制系统)。 - Keith Thompson
4
使用“if false”比注释代码通常更好,因为它确保封闭的代码仍然是合法的代码。注释掉代码的最好理由是当它真正需要被删除,但有些事情需要记住--所以它只是一个注释,不再是“代码”。 - Jeff Learman
3
我使用注释“#if false;”。这样我只需搜索它就可以找到调试移除部分的开头和结尾。 - Jon
2
这个回答不应该被点赞,因为它没有以任何方式回答问题。 - Viktor Joras
1
不确定是否“false”始终可用,对我来说它在/bin/false中。Bash有自己的false命令,不会触发操作系统/bin/false。因此,由于OP正在引用Bash,即使是那些没有/bin/false的“无发行版”Docker映像,false也始终可用。 - SOFe
显示剩余2条评论

103
不,没有;有关已存在的控制结构的信息,请参见《Bash参考手册》中的§3.2.4“复合命令”。特别是要注意提到的breakcontinue,它们虽然不像goto那样灵活,但在Bash中比某些语言更灵活,并且可能有助于实现您想要的内容。(无论您想要什么……)

17
你能否对“在Bash中比一些语言更灵活”这句话进行详细解释? - user239558
28
有些编程语言只允许你从最内层循环中使用 breakcontinue 命令跳出,而 Bash 则允许你指定要跳过多少层循环。即使是那些允许你从任意循环结构中使用 breakcontinue 命令的编程语言,大多数也要求在代码中静态地标明要退出的循环结构 -- 例如,break foo; 会使代码跳出名为 foo 的循环结构 -- 而在 Bash 中则是动态地表示 -- 例如,break "$foo" 将会跳出 $foo 所表示的循环结构。 - ruakh
1
我建议定义具有所需功能的函数,并从代码的其他部分调用它们。 - blmayer
@ruakh 实际上,许多编程语言允许使用 break LABEL_NAME;,而不是 Bash 中令人讨厌的 break INTEGER; - Sapphire_Brick
2
@Sapphire_Brick:是的,我在你回复的评论中提到了这一点。 - ruakh
1
@ruakh 哦,我的错。 - Sapphire_Brick

82

对于一些调试或演示的需要,它确实可能是有用的。

我发现 Bob Copeland 的解决方案 http://bobcopeland.com/blog/2012/10/goto-in-bash/ 很优雅:

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

62
我的努力让Bash看起来像汇编语言的探索即将完成。- 哇,简直太棒了。 - Brian Agnew
5
唯一需要更改的是将标签以 : start: 开头,这样它们就不会成为语法错误。 - Alexej Magura
6
如果你这样做会更好:cmd=$(sed -n "/#$label:/{:a;n;p;ba};" $0 | grep -v ':$')标签应该以以下方式开始:#start: => 这将防止脚本错误 - access_granted
2
嗯,很可能是我的错误。我已经编辑了帖子。谢谢。 - Hubbitus
3
set -x 帮助理解正在发生的事情。 - John Lin
显示剩余12条评论

36

如果你正在测试/调试一个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语句上面。


3
不涉及goto的好坏,劳伦斯的解决方案非常棒。你得到了我的支持。 - Mogens TrasherDK
1
这更像是一个跳过,对我来说if(false)似乎更有意义。 - vesperto
2
引用goto "label"也意味着here-doc不会被扩展/评估,这可以避免一些难以发现的错误(如果未加引号,则可能受到参数扩展、命令替换和算术扩展的影响,这些都可能产生副作用)。您还可以使用alias goto=":<<"进行微调,并完全省略cat - mr.spuratic
1
是的,vesperto,你可以使用“if”语句,在许多脚本中我都这样做了,但它会变得非常混乱,而且更难控制和跟踪,特别是如果你经常想要更改跳转点。 - Laurence Renshaw
1
这非常有趣。您可以通过解释为什么这样做起作用,以及cat <<命令如何起作用来改进答案。 - ysap
@ysap,这不是 cat <<,而是Bash的 heredoc[n]<<[-]word\nhere-document\ndelimiter,其内容为 stdin,由 cat 打印到 /dev/null - Gerold Broser

35

您可以在bash中使用case命令来模拟goto:

#!/bin/bash

case bar in
  foo)
    echo foo
    ;&

  bar)
    echo bar
    ;&

  *)
    echo star
    ;;
esac

产生:

bar
star

4
请注意,这需要 bash v4.0+。然而,它并不是通用的 goto,而是用于 case 语句的一个向下选择项。 - mklement0
5
我认为这应该是答案。我有真正的需求要跳转到给定指令,以便支持脚本的恢复执行。 从语义上讲,这就是goto,除了语义和语法糖之外,其他方面都一样。语义和语法糖很可爱,但并非必需。在我看来,这是一个很好的解决方案。 - nathan g
1
@nathang,这是否是答案取决于您的情况是否与OP提出的一般情况子集相匹配。不幸的是,该问题询问了一般情况,使得此答案过于狭窄而无法正确。 (关于是否应因此将该问题视为太宽泛而关闭是另一个讨论)。 - Charles Duffy
1
goto不仅仅是选择。goto功能意味着能够根据某些条件跳转到某些位置,甚至创建类似循环的流程... - Sergio Abreu

18
虽然其他人已经澄清bash中没有直接的goto等效语句(并提供了最接近的替代方案,如函数,循环和break),但我想说明如何使用循环加上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

11

这个解决方案 存在以下问题:

  • 不加区分地删除所有以 : 结尾的代码行;
  • 将任何位置上的 label: 当作标签来处理。

下面是已经修复(干净无误的 shell-check 和符合 POSIX 标准的)版本:


#!/bin/sh

# GOTO for bash, based upon https://dev59.com/5mkw5IYBdhLWcg3wzdpE#31269848
goto() {
  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

一旦你学会了goto,你就再也不需要if或for了。 - Ярослав Рахматуллин

6

有一个实现所需结果的能力:命令trap。例如,它可用于清理目的。


6

在Bash中没有goto

这里有一些不太优雅的解决方法,使用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的实现显然不可靠,因为任何不存在的命令(或任何其他错误)都会执行相同的trap命令。特别地,您不能选择要转到哪个标签。
基本上,在实际场景中,您不需要任何goto语句,因为随机调用不同的位置只会使您的代码难以理解。
如果您的代码被多次调用,则考虑使用循环并更改其工作流程以使用continuebreak
如果您的代码重复自己,请考虑编写函数并根据需要调用它。
如果您的代码需要根据变量值跳转到特定部分,请考虑使用case语句。
如果您可以将长代码分成较小的部分,请考虑将其移动到单独的文件中并从父脚本调用它们。

这个表单和普通函数有什么区别? - yurenchen
2
@yurenchen - 把trap看作使用异常处理来实现代码流的改变。在这种情况下,陷阱会捕获任何导致脚本退出的东西,这是通过调用不存在的命令goto来触发的。顺便说一句:参数goto trap可以是任何东西,例如goto ignored,因为是goto导致了退出,并且2>/dev/null隐藏了错误消息,说明您的脚本正在退出。 - Jesse Chisholm
我遇到了以下错误:permission denied: /dev/nul - alper

3
我使用函数找到了一种方法来实现这个功能。
例如,假设你有三个选择:ABCAB 执行一个命令,但是 C 提供更多信息并将您带回原始提示符。这可以使用函数来完成。
请注意,由于包含 function demoFunction 的行只是设置函数,因此需要在该脚本之后调用 demoFunction,以便函数实际运行。
如果需要在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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接