Dok*_*r J 76 bash shell-script function
在工作中,我经常编写 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
Run Code Online (Sandbox Code Playgroud)
除了“可读性”之外,还有什么理由这样做吗,我认为可以通过更多的评论和一些行间距同样很好地建立起来?
它是否使脚本运行更有效(我实际上希望相反,如果有的话),或者它是否可以更容易地修改超出上述可读性潜力的代码?或者它真的只是一种风格偏好?
请注意,虽然脚本没有很好地展示它,但我们实际脚本中函数的“运行顺序”往往是非常线性的——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
Run Code Online (Sandbox Code Playgroud)
小智 71
可读性是一回事。但是模块化不仅仅是这个。(半模块化可能更适合功能。)
在函数中,您可以将一些变量保留在本地,这会增加可靠性,减少事情变得一团糟的机会。
功能的另一个优点是可重用性。一旦一个函数被编码,它就可以在脚本中多次应用。您还可以将其移植到另一个脚本。
现在您的代码可以是线性的,但在未来,你可以进入的领域多线程,或者多处理在Bash的世界。一旦你学会了在函数中做事,你就会为进入平行阶段做好充分的准备。
还有一点要补充。正如 Etsitpab Nioliv 在下面的评论中所注意到的,很容易将函数重定向为一个连贯的实体。但是还有一个功能重定向的方面。即,可以沿函数定义设置重定向。例如。:
f () { echo something; } > log
Run Code Online (Sandbox Code Playgroud)
现在函数调用不需要显式重定向。
$ f
Run Code Online (Sandbox Code Playgroud)
这可能会避免多次重复,这再次提高了可靠性并有助于使事情井然有序。
也可以看看
Ser*_*nyy 58
在阅读了Kfir Lavi 的博客文章“防御性 Bash 编程”后,我开始使用这种相同的 bash 编程风格。他给出了很多很好的理由,但我个人认为这些是最重要的:
过程变得具有描述性:弄清楚代码的特定部分应该做什么要容易得多。您看到的不是代码墙,而是“哦,该find_log_errors函数读取该日志文件中的错误”。将它与在冗长的脚本中间找到大量使用 awk/grep/sed 的行进行比较,天知道什么类型的正则表达式 - 除非有注释,否则你不知道它在那里做什么。
您可以通过包含在set -xand 中来调试函数set +x。一旦您知道其余代码工作正常,您就可以使用此技巧专注于仅调试该特定功能。当然,您可以附上脚本的一部分,但是如果它是一个很长的部分呢?做这样的事情更容易:
set -x
parse_process_list
set +x
Run Code Online (Sandbox Code Playgroud)使用cat <<- EOF . . . EOF. 我已经多次使用它来使我的代码更加专业。另外,parse_args()带getopts功能还是挺方便的。同样,这有助于提高可读性,而不是将所有内容都作为巨大的文本墙塞进脚本中。重用这些也很方便。
显然,对于了解 C 或 Java 或 Vala,但 bash 经验有限的人来说,这更具可读性。就效率而言,您可以做的事情并不多 - bash 本身并不是最有效的语言,在速度和效率方面,人们更喜欢 perl 和 python。但是,您可以使用nice一个函数:
nice -10 resource_hungry_function
Run Code Online (Sandbox Code Playgroud)
与在每一行代码上调用 nice 相比,这减少了大量的输入,并且当您只想以较低优先级运行脚本的一部分时可以方便地使用。
在我看来,在后台运行函数也有助于当您希望在后台运行一大堆语句时。
我使用过这种风格的一些例子:
Joh*_*024 40
在我的评论中,我提到了函数的三个优点:
它们更容易测试和验证正确性。
函数可以在未来的脚本中轻松重用(来源)
你的老板喜欢他们。
而且,永远不要低估数字 3 的重要性。
我想再解决一个问题:
...所以能够任意交换运行顺序不是我们通常会做的事情。例如,您不会突然想把 放在
declare_variables之后walk_into_bar,那会破坏事情。
为了获得将代码分解成函数的好处,应该尝试使函数尽可能独立。如果walk_into_bar需要一个未在其他地方使用的变量,则该变量应在walk_into_bar. 将代码分离成函数并最小化它们之间的依赖关系的过程应该使代码更清晰、更简单。
理想情况下,函数应该易于单独测试。如果由于交互作用,它们不容易测试,那么这表明它们可能会从重构中受益。
chi*_*cks 17
虽然我完全同意可重用性、可读性和巧妙地亲吻老板,但bash 中的函数还有另一个优点:可变作用域。正如自民党所示:
#!/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.
Run Code Online (Sandbox Code Playgroud)
我在现实世界的 shell 脚本中并不经常看到这种情况,但对于更复杂的脚本来说,这似乎是一个好主意。减少内聚有助于避免在代码的另一部分中破坏预期变量的错误。
可重用性通常意味着创建一个公共函数库并将source该库嵌入到您的所有脚本中。这不会帮助它们运行得更快,但它会帮助您更快地编写它们。
cou*_*ode 16
您将代码分解为函数的原因与对 C/C++、python、perl、ruby 或任何编程语言代码执行此操作的原因相同。更深层次的原因是抽象 - 您将较低级别的任务封装到较高级别的原语(函数)中,这样您就无需担心事情是如何完成的。同时,代码变得更具可读性(和可维护性),程序逻辑也变得更加清晰。
但是,查看您的代码,我发现有一个函数来声明变量很奇怪;这真是让我眉毛一挑。
hvd*_*hvd 11
与其他答案中已经给出的原因完全不同的原因:有时使用这种技术的一个原因,其中顶层的唯一非函数定义语句是调用main, 是为了确保脚本不会意外地做任何令人讨厌的事情如果脚本被截断。如果脚本从进程 A 传输到进程 B(外壳程序),并且进程 A 在完成整个脚本的编写之前因任何原因终止,则该脚本可能会被截断。如果进程 A 从远程资源获取脚本,这种情况尤其可能发生。虽然出于安全原因,这不是一个好主意,但它已经完成,并且已经修改了一些脚本以预测问题。
关于编程的一些相关真理:
注释开始是因为无法在代码中清楚地表达您的想法的权宜之计*,并且随着更改变得更糟(或干脆是错误的)。因此,如果可能的话,将概念、结构、推理、语义、流程、错误处理以及任何其他与将代码理解为代码相关的内容表达出来。
也就是说,Bash 函数有一些在大多数语言中都没有发现的问题:
local关键字会导致污染全局命名空间。local foo="$(bar)"的结果失去的退出代码bar。"$@"在不同上下文中的含义。* 如果这冒犯了我,我很抱歉,但是在使用评论多年并在没有它们的情况下开发**多年之后,很明显哪个更好。
** 使用许可、API 文档等的注释仍然是必要的。
一个过程需要一个序列。大多数任务是连续的。弄乱顺序是没有意义的。
但是关于编程的最重要的事情——包括脚本——是测试。测试,测试,再测试。您目前需要哪些测试脚本来验证脚本的正确性?
你的老板正试图引导你从脚本小子变成程序员。这是一个很好的方向。追求你的人会喜欢你。
但。永远记住你的面向过程的根源。如果按照它们通常执行的顺序对函数进行排序是有意义的,那么至少在第一次通过时这样做。
稍后,您将看到您的一些函数正在处理输入、其他输出、其他处理、其他建模数据和其他操作数据,因此将类似的方法分组可能是明智的,甚至可能将它们移到单独的文件中.
稍后,您可能会意识到您现在已经编写了在许多脚本中使用的小辅助函数库。
正如我将演示的那样,注释和间距无法达到函数所能达到的可读性。没有函数,您就不能只见树木不见森林 - 大问题隐藏在许多细节线中。换句话说,人们不能同时关注细节和大局。这在简短的脚本中可能并不明显;只要它保持简短,它就可能足够可读。然而,软件变得更大,而不是更小,当然它是贵公司整个软件系统的一部分,它肯定要大得多,可能有数百万行。
考虑一下我是否给了你这样的指示:
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)
Run Code Online (Sandbox Code Playgroud)
当你完成一半,甚至 5% 时,你会忘记前几个步骤是什么。您不可能发现大多数问题,因为只见树木不见森林。与函数比较:
stand_up();
walk_to(break_room);
pour(coffee);
walk_to(office);
Run Code Online (Sandbox Code Playgroud)
这当然更容易理解,无论您可能在逐行顺序版本中添加多少注释。这也使得它远远更有可能你会发现,你忘了做咖啡,并可能忘了sit_down()结尾。当你的大脑在思考 grep 和 awk 正则表达式的细节时,你不能考虑大局——“如果没有咖啡怎么办”?
功能主要是让您看到全局,并注意到您忘记煮咖啡(或者有人可能更喜欢茶)。有时,在不同的心态下,您担心详细的实现。
当然,其他答案中还讨论了其他好处。其他答案中没有明确说明的另一个好处是函数提供了一种保证,这对于预防和修复错误很重要。如果您发现正确函数 walk_to() 中的某个变量 $foo 是错误的,您知道您只需查看该函数的其他 6 行即可找到可能受该问题影响的所有内容,以及所有可能受到该问题影响的内容导致它是错误的。如果没有(适当的)函数,整个系统中的任何事物都可能是 $foo 不正确的原因,并且任何事物都可能受到 $foo 的影响。因此,如果不重新检查程序的每一行,就无法安全地修复 $foo。如果 $foo 是函数的局部变量,
还有其他很好的答案可以阐明模块化编写脚本的技术原因,该脚本可能很长,在工作环境中开发,开发供一群人使用,而不仅仅是供您自己使用。
我想专注于一个期望:在“时间就是金钱”的工作环境中。因此,代码的缺陷和性能与可读性、可测试性、可维护性、可重构性、可重用性一起评估......
在“模块”中编写代码不仅会减少编码员本身所需的阅读时间,还会减少测试人员或老板使用的时间。此外请注意,老板的时间通常比编码员的时间多,而且你的老板会评估你的工作质量。
此外,在独立的“模块”中编写代码(甚至是 bash 脚本)将允许您与团队的其他组件“并行”工作,从而缩短整体制作时间并最多使用单曲的专业知识来审查或重写部分对其他人没有副作用,可以“按原样”回收您刚刚编写的代码对于另一个程序/脚本,创建库(或片段库),减少整体大小和相关的错误可能性,调试和测试每个单独的部分......当然它会在你的程序的逻辑部分进行组织/script 并增强其可读性。所有可以节省时间和金钱的东西。缺点是您必须遵守标准并评论您的功能(您必须在工作环境中完成)。
坚持一个标准会在开始时减慢你的工作,但它会加快所有其他人(和你的)之后的工作。事实上,当合作涉及的人数增加时,这成为不可避免的需求。因此,例如,即使我认为必须全局定义全局变量而不是在函数中定义全局变量,我也可以理解一个标准,该标准将它们初始化为一个declare_variables()名为 always的函数,该函数位于该函数 的第一行中main()...
最后但并非最不重要的一点是,不要低估现代源代码编辑器显示或隐藏选择性分离例程(代码折叠)的可能性。这将保持代码紧凑并集中用户再次节省时间。
在上面你可以看到它是如何展开的,只有walk_into_bar()功能。即使其他的每个都有 1000 行长,您仍然可以控制单个页面中的所有代码。请注意,它甚至在您声明/初始化变量的部分也被折叠。
另一个经常被忽视的原因是 bash 的语法解析:
set -eu
echo "this shouldn't run"
{
echo "this shouldn't run either"
Run Code Online (Sandbox Code Playgroud)
这个脚本显然包含一个语法错误,bash 根本不应该运行它,对吗?错误的。
~ $ bash t1.sh
this shouldn't run
t1.sh: line 7: syntax error: unexpected end of file
Run Code Online (Sandbox Code Playgroud)
如果我们将代码包装在函数中,则不会发生这种情况:
set -eu
main() {
echo "this shouldn't run"
{
echo "this shouldn't run either"
}
main
Run Code Online (Sandbox Code Playgroud)
~ $ bash t1.sh
t1.sh: line 10: syntax error: unexpected end of file
Run Code Online (Sandbox Code Playgroud)