如何实现从 bash 脚本中的代码块提前退出(类似于 perl 的“last”指令)?

如何实现从 bash 脚本中的代码块提前退出(类似于 perl 的“last”指令)?

我真正喜欢的 Perl 功能之一是它对循环控制关键字的概括任何大括号分隔的词汇块1

例如,可以使用 Perl 的last指令退出任何此类块,如以下玩具脚本所示:

#!/usr/bin/env perl

use strict;

my $value = $ARGV[0] || 0;
my $overall_status = 'failed';
{
  $value > 0 and print "$value > 0\n" or last;
  $value > 1 and print "$value > 1\n" or last;
  $value > 2 and print "$value > 2\n" or last;
  $value > 3 and print "$value > 3\n" or last;
  $overall_status = 'ok';
}

die 'EARLY EXIT' unless $overall_status eq 'ok';

print "ok\n";

在此示例中,大括号分隔块中的前四个语句旨在代表一系列强制性条件: 的失败任何它们中的任何一个都应该导致打印错误消息(其内容与失败的条件无关)并终止脚本的执行。

我想强调的是对块内所有可能的故障的共同响应(在这种情况下,此响应是通用错误消息,不区分终止后的所有可能的故障点)是这个问题的一个重要的、不可协商的方面。事实上,我认为正是问题的一个方面使得能够从代码块的任何位置退出代码块是可取的。

重要的:为了使上面的例子易于理解,条件被人为地简单了。然而在实践中,每个条件的评估可能会占用几行代码! 当您制定答案时请记住这一点。 该示例的要点是可以从块中的任何位置退出代码块。


我正在寻找一种在 bash 脚本中实现类似效果的好方法。

我想不出比这更好的东西了:

#!/bin/bash

overall_status=failed
value=$1
while true; do
    (( value > 0 )) && echo "$value > 0" || break;
    (( value > 1 )) && echo "$value > 1" || break;
    (( value > 2 )) && echo "$value > 2" || break;
    (( value > 3 )) && echo "$value > 3" || break;
    overall_status=ok
    break
done

if [[ "$overall_status" != ok ]]; then
    echo 'EARLY EXIT' >&2
    exit 1
fi

...这充其量是令人困惑的,因为它使用了一个while不打算循环的循环结构()。

在 zsh 脚本中,我至少可以while用一次性匿名函数2替换看起来奇怪的循环:

() {
    (( value > 0 )) && echo "$value > 0" || return 1;
    (( value > 1 )) && echo "$value > 1" || return 1;
    (( value > 2 )) && echo "$value > 2" || return 1;
    (( value > 3 )) && echo "$value > 3" || return 1;
    overall_status=ok
}

我仍然有兴趣学习其他方法来实现 zsh 脚本中代码块的提前退出。


1这种快速的、从内存中进行的描述可能有些过于简单化了...
2事实上,通过这种方法,人们还可以overall_status完全省去变量,并进行测试$?

编辑:我在 Perl 示例之后添加了“重要:...”说明。

编辑2:强调对块内所有故障的共同响应是问题的一个重要方面。

答案1

回避标题中的问题,并专注于错误退出的特定情况,我只是将整个错误消息和退出放在一个函数中,然后直接调用它而不是breaking。这也避免了主代码路径中的额外块和缩进级别。

#!/bin/bash
die() {
    echo 'error: mandatory conditions not met!' >&2
    exit 1
}
value=$1
(( value > 0 )) && echo "$value > 0" || die;
(( value > 1 )) && echo "$value > 1" || die;
(( value > 2 )) && echo "$value > 2" || die;
(( value > 3 )) && echo "$value > 3" || die;

显然,如果您想在带有条件的块之后继续执行,这是行不通的。在这种情况下,您需要一个空循环。仅包含一项的循环for在这里可能是合乎逻辑的,因为它在一次迭代后自然结束(但您需要命名循环变量)。

for __ in once; do
    echo this prints only once
    (( value > 0 )) && echo "$value > 0" || break;
    ...
done
echo continuing from here

一般来说,我还建议为每个错误情况提供不同的错误消息,以便用户知道哪个要求失败。这可以通过将die消息作为参数来完成。在某些情况下,也可以执行所有检查,无论其中一项是否失败,然后才退出。这将使用户同时了解所有问题。

#!/bin/bash
errexit=
error() {
    echo "error: $1" >&2
    errexit=1   # global
}
value=$1
(( value > 0 )) && echo "$value > 0" || error 'value <= 0';
(( value > 1 )) && echo "$value > 1" || error 'value <= 1';
(( value > 2 )) && echo "$value > 2" || error 'value <= 2';
(( value > 3 )) && echo "$value > 3" || error 'value <= 3';
[ "$errexit" ] && exit 1

答案2

我不知道有什么方法可以在 Bash 中完成您想要的操作(除了您已经确定的选项之外)。

如果我要尝试做你所追求的事情,我可能会这样做:

#!/bin/bash

overall_status=failed
value=$1

if   (( value > 0 )); then echo "value > 0"
elif (( value > 1 )); then echo "value > 1"
elif (( value > 2 )); then echo "value > 2"
elif (( value > 3 )); then echo "value > 3"
else overall_status=ok
fi

if [[ "$overall_status" != ok ]]; then
    echo 'EARLY EXIT' >&2
    exit 1
fi

答案3

提前退出命令组的简单方法是并列出使用which 在第一次失败时中断,这是or语句&&的简短替代。将示例中的虚拟 while 循环替换为:ifcase

(( value > 0 )) && echo "$value > 0" &&
(( value > 1 )) && echo "$value > 1" &&
(( value > 2 )) && echo "$value > 2" &&
(( value > 3 )) && echo "$value > 3" &&
overall_status=ok

或者类似于你的 zsh 匿名函数,为什么你没有一个命名的 bash 函数。

#!/bin/bash

arg=$1

function f() {
    value=$1
    (( value > 0 )) && echo "$value > 0" || return 1
    (( value > 1 )) && echo "$value > 1" || return 1
    (( value > 2 )) && echo "$value > 2" || return 1
    (( value > 3 )) && echo "$value > 3" || return 1
    return 0
}

if f arg; then
    exit 0
else
    echo 'EARLY EXIT' >&2
    exit 1
fi

答案4

你也用同样的方法来做。

在 Perl 中,{...}块是只运行一次的循环。所以你在 shell 中做同样的事情——一个只运行一次的循环。并使用break而不是last

for _ in "$_"; do
    foo || break
    bar || break
    baz || break
    {
        quux
        moo
    } || break     
done

$value > 0 and print "$value > 0\n" or last;

foo && bar || quux由于与 shell 中相同的原因,这很糟糕。如果print失败会发生什么?与 shell 不同,perl 有三元?:运算符。用它:

$value > 0 ? print "$value > 0\n" : last;

if(...){...}else{...}或者,如果您发现它太简洁,请使用显式的。

而且,这#!/usr/bin/env perl是自找麻烦。为什么您必须针对用户路径中可能拥有的所有可能的 perl 版本来测试您的脚本,而不仅仅是/usr/bin/perl您可以从发行版中确定的版本?

相关内容