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


59

在工作中,我经常编写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

30
我喜欢你的老板。在我的脚本中,我还放在main()顶部并main "$@"在底部添加以进行调用。这使您在打开高级脚本逻辑时首先看到它。
约翰·库格曼

22
我不同意这样的观念,即“可以通过添加一些注释和一些行距来同样很好地建立可读性”。除了可能是小说以外,我不想处理一本书,该书没有每一章和每一节的目录和描述性名称。在编程语言中,这就是函数可以提供的可读性,而注释则不能。
Rhymoid

6
请注意,应声明在函数中声明的local变量-这提供了变量作用域,这在任何非平凡的脚本中都非常重要。
蜘蛛鲍里斯(Boris)

8
我不同意你的老板。如果必须将脚本分解为功能,则可能一开始就不应该编写Shell脚本。改写一个程序。
el.pescado

6
函数用于在脚本内或在多个脚本中重复的过程。它们还允许采用统一的方法。例如,使用函数写入syslog。只要每个人都使用相同的功能,您的syslog条目就会更加一致。像您的示例这样的单次使用功能不必要地使脚本复杂化。在某些情况下,它们会带来问题(可变范围)。
Xalorous

Answers:


39

在阅读Kfir Lavi的博客文章“防御性Bash编程”之后,我开始使用这种风格的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
我不确定您是否应该认真考虑该文章的任何建议。当然,它有一些好主意,但显然不是一个习惯于编写shell脚本的人。没有一个单一的任何实例变量引用(!),并建议使用大写变量名这往往是一个非常糟糕的主意,因为它们可以与现有的ENV瓦尔冲突。在这个答案中的观点是有道理的,但是链接的文章似乎是由曾经习惯于其他语言并试图将其风格推到bash上的人写的。
terdon

1
@terdon,我回到文章重新阅读。作者唯一提到大写变量命名的地方是“不可变全局变量”。如果您将全局变量视为必须在函数环境中的变量,则使其成为资本是有意义的。顺带一提,bash的手册没有说明可变大小写的约定。即使在这里 接受的答案也说“通常”,唯一的“标准”是由Google提出的,它并不代表整个IT行业。
Sergiy Kolodyazhnyy

@terdon在另一个注释上,我确实100%同意在文章中应该提到变量引用,并且在博客的评论中也已指出。而且,无论他们是否习惯另一种语言,我都不会判断使用这种编码风格的人。整个问题和答案清楚地表明了它的优势,这里的人习惯于另一种语言的程度可能与此无关。
Sergiy Kolodyazhnyy

1
@terdon好,这篇文章是作为“源”材料的一部分发布的。我本可以将所有内容发布为我自己的观点,但是我只能相信我从本文中学到的一些知识,而且这些都是经过一段时间的研究后得出的。作者的linkedin页面显示,他们总体上具有Linux和IT方面的良好经验,所以我想这篇文章并没有真正表明这一点,但是我相信您在Linux和Shell脚本方面的经验,因此您可能是对的。 。
Sergiy Kolodyazhnyy

1
这是一个很好的答案,但我也想补充一点,Bash中的可变范围很时髦。因此,我更喜欢在函数内部声明变量local,并通过main()函数调用所有函数。这使事情变得更易于管理,并且可以避免潜在的混乱情况。
侯斯尼

65

可读性是一回事。但是模块化不仅限于此。(对于功能,半模块化可能更正确。)

在函数中,您可以将一些变量保留在局部变量,从而提高可靠性,减少混乱的可能性。

功能的另一个优点是可重用性。对函数进行编码后,就可以在脚本中多次应用它。您也可以将其移植到另一个脚本。

现在您的代码可以是线性的,但在未来,你可以进入的领域多线程,或者多处理在Bash的世界。一旦学习了函数功能,您将为步入并行世界做好准备。

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

f () { echo something; } > log

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

$ f

这样可以避免很多重复,从而再次提高了可靠性并有助于保持秩序。

也可以看看


55
很好的答案,尽管将其分解为功能会更好。
皮埃尔·阿劳德

1
也许要添加的功能是,您可以将该脚本导入到另一个脚本中(通过使用source. scriptname.sh,并使用这些功能,就像它们在新脚本中一样。)
SnakeDoc

另一个答案已经涵盖了这一点。
Tomasz

1
我很感激。但是我宁愿让其他人也很重要。
Tomasz

7
今天我遇到一个案例,我不得不将脚本的某些输出重定向到文件(通过电子邮件发送),而不是回显。我只需要执行myFunction >> myFile即可重定向所需函数的输出。很方便 可能是相关的。
Etsitpab Nioliv '16

39

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

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

  2. 在将来的脚本中可以轻松地重用(获取)功能

  3. 你的老板喜欢他们。

而且,永远不要低估数字3的重要性。

我想再解决一个问题:

...因此能够随意交换运行顺序不是我们通常要做的。例如,您不会突然想放在declare_variables之后walk_into_bar,这会破坏事情。

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

理想情况下,功能应该易于单独测试。如果由于交互而不容易测试它们,则表明它们可能会从重构中受益。


我认为,建模和执行这些依赖有时是明智的,而不是进行重构以避免它们(因为如果它们足够多,并且它们足够毛茸茸,那只会导致情况不再模块化)功能)。一个非常复杂的用例曾经启发了框架来做到这一点
查尔斯·达菲

4
应该将功能划分为什么,但是该示例将其过分了。我认为真正困扰我的唯一一个就是变量声明函数。全局变量,尤其是静态变量,应在专用于此目的的注释部分中全局定义。动态变量应位于使用和修改它们的函数的本地。
Xalorous

@Xalorous我已经看到了 在过程中初始化全局变量的实践 ,这是在开发从外部文件读取其值的过程之前的中间步骤和快速步骤。我同意,将定义和初始化,但很少需要屈服于3号优势;-)
Hastur

13

出于将C / C ++,python,perl,ruby或其他任何编程语言代码所用的相同原因,将代码分成函数。更深层的原因是抽象-将较低级别的任务封装到较高级别的基元(函数)中,这样您就不必费心去做事情了。同时,代码变得更具可读性(和可维护性),并且程序逻辑变得更加清晰。

但是,查看您的代码,我发现有一个函数来声明变量是很奇怪的。这真的使我眉头一亮。


被低估的答案恕我直言。那么,您是否建议在main函数/方法中声明变量?
David Tabernero M.

12

虽然我完全同意可重用性可读性以及与老板进行巧妙的接吻,但是中函数的另一个优点是:可变范围。如LDP所示

#!/bin/bash
# ex62.sh: Global and local variables inside a function.

func ()
{
  local loc_var=23       # Declared as local variable.
  echo                   # Uses the 'local' builtin.
  echo "\"loc_var\" in function = $loc_var"
  global_var=999         # Not declared as local.
                         # Therefore, defaults to global. 
  echo "\"global_var\" in function = $global_var"
}  

func

# Now, to see if local variable "loc_var" exists outside the function.

echo
echo "\"loc_var\" outside function = $loc_var"
                                      # $loc_var outside function = 
                                      # No, $loc_var not visible globally.
echo "\"global_var\" outside function = $global_var"
                                      # $global_var outside function = 999
                                      # $global_var is visible globally.
echo                      

exit 0
#  In contrast to C, a Bash variable declared inside a function
#+ is local ONLY if declared as such.

我在现实世界的shell脚本中很少看到这种情况,但是对于更复杂的脚本来说,这似乎是个好主意。减少内聚力有助于避免在破坏代码另一部分中预期的变量的错误。

可重用性通常意味着创建一个通用的函数库并将source该库纳入所有脚本中。这不会帮助他们更快地运行,但是会帮助您更快地编写它们。


很少有人明确地使用local,但是我认为大多数编写分成功能的脚本的人仍然遵循设计原则。Usign local使得引入错误更加困难。
Voo

local使变量可用于函数及其子函数,因此拥有一个可以从函数A传递而下的变量,但对于函数B却不可用,函数B可能想要具有相同名称但用途不同的变量,这确实很棒。所以这是很好的界定范围,并作为VOO说-更少的错误
谢尔盖Kolodyazhnyy

10

与其他答案中已经给出的完全不同的原因:有时使用此技术的一个原因main是,确保脚本不会意外地做任何令人讨厌的操作,其中顶级的唯一非功能定义语句是对的调用。如果脚本被截断。如果脚本从进程A通过管道传递到进程B(外壳程序),并且进程A在完成编写整个脚本之前出于任何原因终止,则脚本可能会被截断。如果进程A从远程资源中获取脚本,则很可能会发生这种情况。虽然出于安全原因这不是一个好主意,但这是可以完成的工作,并且已修改了一些脚本来预料到该问题。


5
有趣!但是我发现令人烦恼的是,每个程序都必须照顾这些事情。另一方面,正是这种main()模式在Python中很常见,其中一个if __name__ == '__main__': main()在文件末尾使用。
Martin Ueding '16

1
python惯用语的优点是import无需运行即可让其他脚本成为当前脚本main。我想可以在bash脚本中放置类似的防护措施。
杰克·科布

@杰克·科布是的。现在,我在所有新的bash脚本中都这样做。我有一个脚本,其中包含所有新脚本使用的功能的核心基础结构。该脚本可以被获取或执行。如果来源,则不执行其主要功能。通过BASH_SOURCE包含执行脚本的名称来检测源与执行。如果与核心脚本相同,则正在执行脚本。否则,它就是来源。
DocSalvager '16

7

一个过程需要一个序列。大多数任务是顺序的。弄乱订单毫无意义。

但是关于编程的超级大事-包括脚本-是测试。测试,测试,测试。您目前需要什么测试脚本来验证脚本的正确性?

您的老板试图引导您从脚本小子变成程序员。这是一个很好的方向。跟随你的人会喜欢你的。

但。永远记住您面向过程的根源。如果有意义的是按通常执行它们的顺序对功能进行排序,则至少应作为第一步。

稍后,您将看到一些功能正在处理输入,其他输出,其他处理,其他建模数据以及其他对数据进行操作的功能,因此将相似的方法进行分组可能很聪明,甚至可能将它们移到单独的文件中。

稍后,您可能会意识到,您现在已经编写了在许多脚本中使用的小助手函数库。


6

正如我将演示的那样,注释和间距不能达到函数可以读取的范围。没有功能,您就看不到树木茂密的森林-大问题隐藏在许多细节之中。换句话说,人们不能同时专注于细节和全局。在简短的脚本中这可能并不明显。只要保持简短,它可能就足够可读。软件变大了,但是变小了,当然它是公司整个软件系统的一部分,该系统肯定很大,可能有数百万行。

考虑一下我是否给您这样的指示:

Place your hands on your desk.
Tense your arm muscles.
Extend your knee and hip joints.
Relax your arms.
Move your arms backwards.
Move your left leg backwards.
Move your right leg backwards.
(continue for 10,000 more lines)

到您完成一半甚至达到5%的时间时,您已经忘记了前几步。您不可能发现大多数问题,因为您看不到森林覆盖树木。与功能比较:

stand_up();
walk_to(break_room);
pour(coffee);
walk_to(office);

不管您在逐行顺序版本中输入多少注释,这当然是可以理解的。这也使得它远远更有可能你会发现,你忘了咖啡,并可能忘了sit_down()结尾。当您想到grep和awk正则表达式的细节时,您不会想到大局-“如果不煮咖啡怎么办”?

功能主要是让您看到全景,并注意到您忘了煮咖啡(或者有人可能更喜欢喝茶)。在另一时间,您将以不同的心态担心详细的实现。

当然,在其他答案中还讨论了其他好处。在其他答案中未明确说明的另一个好处是,函数提供了对防止和修复错误很重要的保证。如果您发现适当的函数walk_to()中的某个变量$ foo错误,那么您只需要查看该函数的其他6行,即可查找可能受该问题影响的所有内容以及可能导致该问题的所有内容。导致它是错误的。没有(适当的)功能,整个系统中的任何事物都可能是$ foo错误的原因,并且任何事物都可能受到$ foo的影响。因此,如果不重新检查程序的每一行,就无法安全地修复$ foo。如果$ foo对于函数而言是本地的,


1
这不是bash语法。真可惜 我认为没有办法将输入传递给类似的函数。(即pour();< coffee)。它看起来更像是c++php(我认为)。
声音

2
@ tjt263不带括号,它是bash语法:倒咖啡。有了括号,几乎所有其他语言都可以使用。:)
雷·莫里斯

5

有关编程的一些相关事实:

  • 即使您的老板坚持事实并非如此,您的程序也会改变
  • 仅代码和输入会影响程序的行为。
  • 命名很困难。

注释起初是一个权宜之计,因为它不能用代码*清晰地表达您的想法,并且因更改而变得更糟(或完全错误)。因此,尽可能表达概念,结构,推理,语义,流程,错误处理以及与将代码理解为代码有关的任何其他内容

也就是说,Bash函数具有大多数语言中未发现的一些问题:

  • 在Bash中,命名空间非常糟糕。例如,忘记使用local关键字会导致污染全局名称空间。
  • 使用会local foo="$(bar)"导致丢失退出代码bar
  • 没有命名参数,因此您必须记住"$@"在不同上下文中的含义。

*很抱歉,如果这样冒犯了您,但是在使用了几年的评论并在没有评论的情况下开发了**以后,很明显哪个更好。

**仍然需要使用注释进行许可,API文档等。


我通过宣布他们在函数的开头空...设置几乎所有局部变量local foo=""然后使用命令执行作用于结果来设定他们... foo="$(bar)" || { echo "bar() failed"; return 1; }。当无法设置所需的值时,这会使我们快速退出功能。大括号是必要的,以确保return 1仅在失败时执行。
DocSalvager

5

时间就是金钱

还有其他一些好的答案,可以从技术上阐明如何模块化地编写脚本(可能很长),该脚本是在工作环境中开发的,旨在供一群人使用,而不仅仅是您自己使用。

我想重点关注一个期望:在工作环境中,“时间就是金钱”。因此,将评估是否存在错误以及代码的性能以及可读性,可测试性,可维护性,可重构可重用性 ...

“模块”中编写代码不仅会减少编码器本身所需的读取时间,甚至会减少测试人员或老板使用的时间。此外请注意,老板的时间通常要比编码员的时间多,老板会评估您的工作质量。

此外,在独立的“模块”中编写代码(甚至是bash脚本)将使您能够与团队中的其他组件“并行”工作,从而缩短了总体生产时间,并充其量使用了该专家的专业知识,以使用对其他人没有副作用,可以按原样回收您刚刚编写的代码对于另一个程序/脚本,创建库(或代码段库),以减小整体大小和相关的错误可能性,对每个部分进行调试和测试...当然,它将在程序的逻辑部分进行组织/ script并增强其可读性。所有可以节省时间和金钱的事物。缺点是您必须遵循标准并注释功能(尽管如此,您仍必须在工作环境中进行操作)。

遵守标准会在一开始就减慢您的工作,但是此后会加快所有其他人(以及您自己)的工作。的确,当合作的参与人数增加时,这将成为不可避免的需求。因此,例如,即使我认为必须全局定义全局变量而不是在函数中定义全局变量,我也可以理解一种标准,该标准可以在一个declare_variables()始终位于第一行的名为all的函数中初始化它们 main()

最后但并非最不重要的一点是,不要低估现代源代码编辑器中显示或隐藏有选择地分离的例程(代码折叠)的可能性。这将保持紧凑的代码,并集中用户再次节省时间。

在此处输入图片说明

在上方,您可以看到仅如何展开walk_into_bar()功能。即使其他每行都有1000行,您仍然可以控制单个页面中的所有代码。请注意,即使您声明/初始化变量的部分也被折叠了。


1

除了其他答案中给出的原因外:

  1. 心理:以代码行衡量生产力的程序员将有动力编写不必要的冗长代码。管理层越关注代码行,程序员就越有动机以不必要的复杂性扩展代码。这是不希望的,因为增加的复杂性可能导致维护成本增加以及错误修复所需的精力增加。

正如不赞成投票的人所说,答案不是那么糟糕。注意:agc说,这也是一种可能,是的。他没有说那是唯一的可能性,也没有指责任何人,只说了事实。尽管我认为今天几乎没有听说过直接的“代码行”->“ $$”样式的合同工作,但间接表示这很普遍,是的,领导者/上司计算的代码量很大。
user259412 '16

0

经常被忽略的另一个原因是bash的语法解析:

set -eu

echo "this shouldn't run"
{
echo "this shouldn't run either"

这个脚本显然包含语法错误,bash根本不应该运行它,对吧?错误。

~ $ bash t1.sh
this shouldn't run
t1.sh: line 7: syntax error: unexpected end of file

如果我们将代码包装在一个函数中,则不会发生:

set -eu

main() {
  echo "this shouldn't run"
  {
  echo "this shouldn't run either"
}

main
~ $ bash t1.sh
t1.sh: line 10: syntax error: unexpected end of file
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.