bash 函数:将正文括在大括号与圆括号中

bash functions: enclosing the body in braces vs. parentheses

通常,bash 函数定义时使用花括号将函数体括起来:

foo()
{
    ...
}

今天在处理大量使用函数的 shell 脚本时,我遇到了一些问题,这些变量在被调用函数中的名称与调用函数中的名称相同,即这些变量是相同的。然后我发现可以通过将函数内的局部变量定义为局部变量来防止这种情况发生:local var=xyz.

然后,在某个时候,我发现了一个线程 (Defining bash function body using parenthesis instead of braces),其中解释说使用括号定义函数同样有效:

foo()
(
    ...
)

这样做的效果是函数体是在一个subshell中执行的,好处是函数有自己的变量作用域,这样我就可以不用local来定义它们了。由于具有函数局部作用域似乎比所有变量都是全局的更有意义并且更安全,我立即问自己:

然而,我很快也发现了在子shell中执行函数的一个主要缺点,特别是从函数内部退出脚本不再起作用,而是迫使我使用 return 整个调用树的状态(在嵌套函数的情况下)。这让我想到了这个后续问题:

(*) 我知道(从我随时间偶然发现的与异常相关的讨论中)有人会争辩说,明确使用错误状态比能够退出要好得多任何地方,但我更喜欢后者

显然这两种风格各有优缺点。所以我希望你们中一些更有经验的 bash 用户可以给我一些一般性的指导:

编辑:答案摘要

感谢您的回答,我现在对这个问题比较清楚了。所以我从答案中得到的是:

这真的很重要。由于 bash 函数没有 return 值并且它们使用的变量来自全局范围(也就是说,它们可以从 "outside" 其范围访问变量),通常的处理方式函数的输出是将值存储在变量中,然后调用它。

当您用 () 定义函数时,您是对的:它将创建子 shell。该 sub-shell 将包含与原始值相同的值,但无法修改它们。这样你就失去了改变全局范围变量的资源。

看例子:

$ cat a.sh
#!/bin/bash

func_braces() { #function with curly braces
echo "in $FUNCNAME. the value of v=$v"
v=4
}

func_parentheses() (
echo "in $FUNCNAME. the value of v=$v"
v=8
)


v=1
echo "v=$v. Let's start"
func_braces
echo "Value after func_braces is: v=$v"
func_parentheses
echo "Value after func_parentheses is: v=$v"

让我们执行它:

$ ./a.sh
v=1. Let's start
in func_braces. the value of v=1
Value after func_braces is: v=4
in func_parentheses. the value of v=4
Value after func_parentheses is: v=4   # the value did not change in the main shell

当我想更改目录时,我倾向于使用子 shell,但总是从同一个原始目录,并且懒得使用 pushd/popd 或自己管理目录。

for d in */; do
    ( cd "$d" && dosomething )
done

这在函数体中同样有效,但即使您定义带有花括号的函数,仍然可以在子 shell 中使用它。

doit() {
    cd "" && dosomething
}
for d in */; do
    ( doit "$d" )
done

当然,您仍然可以使用 declare 或 local 在花括号定义的函数内维护变量范围:

myfun() {
    local x=123
}

所以我想说的是,仅当 而不是 作为子 shell 不利于该函数的明显正确行为时,才将您的函数明确定义为子 shell。

琐事:作为旁注,请考虑 bash 实际上 总是 将函数视为花括号复合命令。它只是有时有括号:

$ f() ( echo hi )
$ type f
f is a function
f () 
{ 
    ( echo hi )
}

Why are braces used by default to enclose the function body instead of parentheses?

函数体可以是任何复合命令。这通常是 { list; },但技术上允许使用其他三种形式的复合命令:(list)((expression))[[ expression ]].

C 和 C 家族中的语言,如 C++、Java、C# 和 JavaScript 都使用大括号来分隔函数体。对于熟悉这些语言的程序员来说,大括号是最自然的语法。

Are there other major downsides (*) to using parentheses instead of braces (which might explain why braces seem to be preferred)?

是的。 sub-shell 中有许多您无法做的事情,包括:

  • 更改全局变量。变量更改不会传播到 parent shell.
  • 退出脚本。 exit 语句将仅退出 sub-shell.

启动 sub-shell 也会严重影响性能。每次调用函数时都会启动一个新进程。

如果您的脚本被终止,您也可能会遇到奇怪的行为。 parent 和 child shell 收到的信号将会改变。这是一个微妙的效果,但如果您有 trap 个处理程序或您 kill 您的脚本,这些部分将无法按您想要的方式工作。

When shall I use curly braces to enclose the function body, and when is it advisable to switch to parentheses?

我建议您始终使用大括号。如果您想要显式 sub-shell,则在花括号内添加一组 parentheses。仅使用 parentheses 是一种非常不寻常的语法,会使许多阅读您的脚本的人感到困惑。

foo() {
   (
       subshell commands;
   )
}

注意:有时大括号列表不在同一进程中运行:

a=22; { echo $a; a=46; echo $a; }; echo $a
says 22 46 46

但是

a=22; { echo $a; a=46; echo $a; }|cat; echo $a
says 22 46 22

感谢 fedorqui :)