为什么要在函数中编写整个 bash 脚本?

为什么要在函数中编写整个 bash 脚本?

在工作中,我经常编写bash脚本。我的主管建议将整个脚本分解为函数,类似于以下示例:

#!/bin/bash

# Configure variables
declare_variables() {
    noun=geese
    count=three
}

# Announce something
i_am_foo() {
    echo "I am foo"
    sleep 0.5
    echo "hear me roar!"
}

# Tell a joke
walk_into_bar() {
    echo "So these ${count} ${noun} walk into a bar..."
}

# Emulate a pendulum clock for a bit
do_baz() {
    for i in {1..6}; do
        expr $i % 2 >/dev/null && echo "tick" || echo "tock"
        sleep 1
    done
}

# Establish run order
main() {
    declare_variables
    i_am_foo
    walk_into_bar
    do_baz
}

main

除了“可读性”之外,还有什么理由这样做,我认为通过更多的注释和一些行距也可以同样很好地建立“可读性”?

它是否使脚本运行更有效(我实际上期望相反,如果有的话),或者它是否使修改代码超出上述可读性潜力变得更容易?或者这真的只是一种风格偏好?

请注意,虽然脚本没有很好地演示它,但我们实际脚本中函数的“运行顺序”往往是非常线性的——walk_into_bar取决于i_am_foo已完成的内容,并do_baz作用于设置的内容walk_into_bar——所以能够任意交换运行顺序并不是我们通常会做的事情。例如,你不会突然想把 放在declare_variables后面walk_into_bar,那会破坏事情。

我如何编写上述脚本的一个例子是:

#!/bin/bash

# Configure variables
noun=geese
count=three

# Announce something
echo "I am foo"
sleep 0.5
echo "hear me roar!"

# Tell a joke
echo "So these ${count} ${noun} walk into a bar..."

# Emulate a pendulum clock for a bit
for i in {1..6}; do
    expr $i % 2 >/dev/null && echo "tick" || echo "tock"
    sleep 1
done

答案1

可读性是一回事。但还有更多模块化不仅仅是这样。(半模块化对于函数来说可能更正确。)

在函数中,您可以将一些变量保留为局部变量,这会增加可靠性,减少事情变得混乱的机会。

函数的另一个优点是可重用性。一旦函数被编码,它就可以在脚本中多次应用。您还可以将其移植到另一个脚本。

你现在的代码可能是线性的,但将来你可能会进入线性的领域多线程, 或者多重处理在 Bash 世界中。一旦你学会了在函数中做事,你就已经做好了进入并行的准备。

还有一点要补充。正如 Etsitpab Nioliv 在下面的评论中注意到的那样,很容易从函数重定向为一个连贯的实体。但是函数重定向还有一个方面。即,可以沿着函数定义设置重定向。例如。:

f () { echo something; } > log

现在函数调用不需要显式重定向。

$ f

这可能会避免许多重复,从而再次提高可靠性并有助于保持事情井然有序。

也可以看看

答案2

阅读后我开始使用同样风格的 bash 编程Kfir Lavi 的博客文章“防御性 Bash 编程”。他给出了很多很好的理由,但我个人认为这些是最重要的:

  • 过程变得具有描述性:更容易弄清楚代码的特定部分应该做什么。您看到的不是代码墙,而是“哦,该find_log_errors函数读取该日志文件中的错误”。与在冗长的脚本中间找到大量使用上帝知道什么类型的正则表达式的 awk/grep/sed 行进行比较 - 你不知道它在那里做什么,除非有注释。

  • 您可以通过将set -x和括起来来调试函数set +x。一旦您知道代码的其余部分工作正常,您就可以使用此技巧来专注于仅调试该特定功能。当然,您可以附上部分脚本,但如果它很长怎么办?这样做更容易:

       set -x
       parse_process_list
       set +x
    
  • 打印用法与cat <<- EOF . . . EOF.我已经使用它很多次了,使我的代码更加专业。另外,parse_args()getopts功能也相当方便。同样,这有助于提高可读性,而不是将所有内容都像巨大的文本墙一样塞进脚本中。重复使用这些也很方便。

显然,对于了解 C、Java 或 Vala 但 bash 经验有限的人来说,这更具可读性。就效率而言,你能做的并不多——bash 本身并不是最高效的语言,而且在速度和效率方面人们更喜欢 Perl 和 Python。但是,您可以使用nice一个函数:

nice -10 resource_hungry_function

与在每一行代码上调用nice相比,这减少了大量的输入,并且当您只想以较低优先级运行脚本的一部分时可以方便地使用。

在我看来,当您想要在后台运行一大堆语句时,在后台运行函数也很有帮助。

我使用这种风格的一些例子:

答案3

在我的评论中,我提到了函数的三个优点:

  1. 它们更容易测试和验证正确性。

  2. 函数可以在未来的脚本中轻松重用(来源)

  3. 你的老板喜欢他们。

并且,永远不要低估第三点的重要性。

我还想解决一个问题:

...所以能够任意交换运行顺序并不是我们通常会做的事情。例如,你不会突然想把 放在declare_variables后面walk_into_bar,那会破坏事情。

为了获得将代码分解为函数的好处,应该尝试使函数尽可能独立。如果walk_into_bar需要一个未在其他地方使用的变量,则应在 中定义该变量并将其设置为本地变量walk_into_bar。将代码分成函数并最小化它们的相互依赖性的过程应该使代码更加清晰和简单。

理想情况下,函数应该易于单独测试。如果由于交互的原因,它们不容易测试,那么这表明它们可能会从重构中受益。

答案4

将代码分解为函数的原因与对 C/C++、python、perl、ruby 或任何编程语言代码进行分解的原因相同。更深层次的原因是抽象——您将较低级别的任务封装到较高级别的原语(函数)中,这样您就不需要担心事情是如何完成的。同时,代码变得更具可读性(和可维护性),程序逻辑也变得更加清晰。

然而,看看你的代码,我发现有一个函数来声明变量很奇怪;这真让我皱起了眉头。

相关内容