使用 Bash 的功能来更改控制流
1. 概述
在大多数编程和脚本语言中,我们都有流控制语句,允许我们将执行移动到脚本中的其他地方。通常,这些包括goto语句。
Bash 不支持goto,但提供了一些其他的流控制机制。
在本教程中,我们将了解如何在 Bash 中模拟goto,以及如何使用内置的流控制功能来跳过代码、跳出循环或执行替代操作。
2. 在 Bash 脚本中跳过代码
让我们从模拟goto的跳跃行为开始。
2.1. goto的基本语法
goto语句通常将控制转移到由标签标识的行:
statement1
goto labe11
statement2 #skipped
#... more lines skipped
statement9 #skipped
label1:
statement10 #executed
2.2. 从 Bash 脚本中读取多行
虽然我们不能在 Bash 中本地执行此操作,但运算符 «HERE 将脚本的行重定向到stdin,直到遇到字符串HERE 。这通常被称为“ heredoc ”。
2.3. 说明性示例
让我们通过使用*cat * «label 来模拟goto来尝试一下:
#!/bin/bash
echo "Starting the script"
cat >/dev/null <<LABEL_1
echo "This command is not run"
LABEL_1
echo "And this command is run"
cat >/dev/null <<LABEL_EXIT
echo "This command is not run as the first one"
LABEL_EXIT
echo "The script is done"
接下来,让我们运行它并查看输出:
./cat_goto_example
Starting the script
And this command is run
The script is done
可以看到,cat和LABEL_1之间的echo语句没有被执行。
2.4. cat中的代码会发生什么
让我们看看cat在做什么。所以,让我们省略*/dev/null*重定向:
#!/bin/bash
echo "Starting the script"
cat <<LABEL_1
echo "This command is not run"
LABEL_1
echo "And this command is run"
cat <<LABEL_EXIT
echo "This command is not run as the first one"
LABEL_EXIT
echo "The script is done"
现在,我们可以看到cat输出出现在我们的控制台上:
Starting the script
echo "This command is not run"
And this command is run
echo "This command is not run as the first one"
The script is done
因此,cat和heredoc方法允许我们读取但不执行脚本中的行。我们应该注意到,这种模拟goto的解决方法只能通过脚本向前运行。
3. 有条件地跳过 Bash 脚本中的代码
最常见的是,我们会在脚本中结合某些条件使用goto,可能会跳过我们不想执行的步骤。
3.1. 使用cat «HERE 创建条件goto
我们正在尝试实现类似于此伪代码的逻辑:
if condition
goto label1
statement1 #skipped if condition is met
# ...
label1
statement10 #always executed
使用上述技术,我们可以使用一些额外的技巧来实现这一点:
if [ -n "$GOTO_CONDITION" ] ; then
cat >/dev/null
else
eval "$(cat -)"
fi <<'LABEL'
在这里,我们使用if语句的两个分支来创建一个cat表达式来满足我们的需要。当GOTO_CONDITION为true时执行的部分会像以前一样去掉直到label1的代码行**。
相反,“ else ”部分从*«*运算符提供的行中重构脚本并作为一个整体执行。
详细地说,*“$(cat -)”*命令连接脚本行并将结果作为 bash 变量呈现给eval 。因此,我们避免了单独执行脚本的每一行。
此外,我们应该使用带引号的标签名称的heredoc构造,以避免变量扩展。
3.2. 条件跳转示例
让我们创建一个脚本来使用我们的构造:
#!/bin/bash
n=2
echo "Starting the script with n = $n"
foo="FOO"
if [ $n -eq 2 ] ; then
cat >/dev/null
else
eval "$(cat -)"
fi <<'LABEL_1'
echo "This is the foo variable: $foo"
echo "This command is not run when jumped over with goto"
bar="BAR"
echo "This is the bar variable: $bar"
LABEL_1
echo "After LABEL_1"
echo "This is the foo variable: $foo"
echo "This is the bar variable: $bar"
echo "And this command is run"
echo "The script is done"
我们检查算术表达式以强调goto的条件 可能会在脚本执行期间进行评估。此外,我们使用两个变量foo和bar来研究它们的可见性。
3.3. 测试条件goto
让我们在goto处于活动状态时检查结果- n=2:
Starting the script with n = 2
After LABEL_1
This is the foo variable: FOO
This is the bar variable:
And this command is run
The script is done
这一次,我们跳过了代码。此外,变量foo是定义的,而bar不是。
现在,让我们在不执行goto时启动我们的脚本,其中n = 10:
Starting the script with n = 10
This is the foo variable: FOO
This command is not run when jumped over with goto
This is the bar variable: BAR
After LABEL_1
This is the foo variable: FOO
This is the bar variable: BAR
And this command is run
The script is done
在这里,我们发现所有的脚本都被执行了。此外,这两个变量都已正确定义且可见。 最后,正确使用局部变量foo和bar证明脚本是作为一个整体执行的。
4. 启用或禁用部分代码的 Bash 替代方案
在开发或调试脚本的过程中,我们经常需要跳过代码中的某些行。我们不需要为此使用我们的goto技术,因为有一些常见的替代方法。
4.1. 注释掉
到目前为止,忽略某些代码的最简单的解决方案是将其注释掉。 Bash 使用井号 ‘#’ 忽略从它到行尾的所有字符:
#!/bin/bash
echo "This shows up in terminal"
#echo "but this not"
不幸的是,bash 中没有可用的块注释。
4.2. 在if语句中使用false函数
我们可以在if语句中使用false :
#!/bin/bash
if false
then
echo "Disabled code here"
fi
echo "Not disabled code here"
为了重新启用代码,我们可以将false替换为true。
4.3. 设置 Bash 变量
或者,我们可以使用从命令行参数获取其值的环境变量或 Bash 变量来创建一些代码条件。
假设有一个DEBUG环境变量:
#!/bin/bash
if [ -n "$DEBUG" ]
then
echo "Disabled code here"
fi
echo "Not disabled code here"
-n开关测试变量是否不是空字符串。此外,DEBUG可以设置为任何值。
这个版本的优点是我们不需要修改脚本来改变它的行为。
相反,我们应该只设置和导出变量:
export DEBUG=true
./debug_test
#result:
Disabled code here
Not disabled code here
或在启动脚本之前取消设置:
unset DEBUG
./debug_test
#result:
Not disabled code here
5. 在多级循环中跳来跳去
大多数编程语言都提供break和continue语句,但它们的效果通常只达到直接封闭的循环。
相比之下,Bash 提供了break n和continue n。
5.1. 打破多级循环
break n跳过n级循环。 让我们尝试跳出嵌套循环,在满足条件时有效地停止所有迭代:
#!/bin/bash
for x in {1..10}
do
echo "x = $x"
for y in {1..5}
do
echo " y = $y"
if [ $x -ge 2 ] && [ $y -eq 3 ]
then
echo "Breaking"
break 2
fi
done
done
echo "Now all loops are done"
让我们检查一下结果:
x = 1
y = 1
y = 2
y = 3
y = 4
y = 5
x = 2
y = 1
y = 2
y = 3
Breaking
Now all loops are done
我们应该注意到,一旦满足条件*[ $x -ge 2 ]* && [ $y -eq 3 ],两个循环都会终止,控制权会传递到它们之后的第一个语句。
5.2. 连续多级循环
continue n命令省略循环内的操作,并允许跳过n层嵌套循环。
不仅省略了当前循环的其余部分,而且还省略了所有封闭循环的主体,直至n级:
#!/bin/bash
for x in {1..5}
do
for y in {1..3}
do
if [ $x -gt 2 ] && [ $y -eq 2 ]
then
echo "Continue"
continue 2
fi
echo " y = $y"
done
echo "x = $x"
done
echo "Now all loops are done"
现在,让我们检查一下结果:
y = 1
y = 2
y = 3
x = 1
y = 1
y = 2
y = 3
x = 2
y = 1
Continue
y = 1
Continue
y = 1
Continue
Now all loops are done
一旦循环变量满足条件*[$x -gt 2]* && [$y -eq 2],则不执行continue语句后的任何操作。
此外,它还涉及带有变量x的外部循环。
6. 错误处理
错误处理是我们可以通过流控制解决的另一个问题。
6.1. 模型控制流程
此伪代码说明了我们如何针对错误采取一些特定的操作,或恢复程序:
if ( !functionA() ) goto errorA
if ( !functionB() ) goto errorB
#...
exit(0) # everything is OK
errorA:
concludeA() #some specific A fallback
exit(codeA)
errorB:
concludeB() #some specific B fallback
exit(codeB)
6.2. 案例研究
让我们编写一个脚本,将目录更改为给定的目录,然后将新行写入该目录内的文件。
这两种操作都可能会发出它们自己的错误——例如,当目标文件夹不存在或用户没有写入文件的权限时。
6.3. some_action || handle_error构造
在 Bash 中,语句*分隔符 || * 仅当左侧命令失败时才执行右侧命令。
因此,让我们假设some_action部分是一个容易出错的代码,而handle_error是一个合适的后备函数:
#!/bin/bash
target_folder="test"
target_file="foo"
cd "$target_folder" || { echo "Can not cd to $target_folder" && exit 1; }
echo -n > "$target_file" || { echo "Can not write to file $target_file in $target_folder" && exit 1; }
echo "Successfully wrote new line to file $target_file in $target_folder"
6.4. 测试脚本
假设一开始,“test”文件夹不存在:
./error_example
./error_example: line 6: cd: test: No such file or directory
Can not cd to test
现在,让我们创建文件夹:
mkdir test #create a folder
./error_example Successfully wrote new line to file foo in test
最后,取消用户的写权限:
chmod u-w test/foo #take away the write permission from the user
./error_example
./error_example: line 8: foo: Permission denied
Can not write to file foo in test
让我们发现,我们不仅获得了常规的 Bash 错误消息,而且还获得了预期的通知。
6.5. 脚本亮点
让我们注意脚本error_example的重要特征: