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 用户可以给我一些一般性的指导:
- 什么时候应该用大括号括起函数体,什么时候改用圆括号?
编辑:答案摘要
感谢您的回答,我现在对这个问题比较清楚了。所以我从答案中得到的是:
坚持使用传统的大括号,如果只是为了不混淆脚本的潜在其他 users/developers(如果整个主体都包含在括号中,甚至使用大括号)。
花括号唯一真正的缺点是可以更改父作用域中的任何变量,尽管在某些情况下这可能是一个优点。这可以很容易地通过将变量声明为 local
.
来规避
另一方面,使用括号可能会产生一些严重的不良影响,例如弄乱退出、导致终止脚本和隔离变量范围的问题。
这真的很重要。由于 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 :)
通常,bash 函数定义时使用花括号将函数体括起来:
foo()
{
...
}
今天在处理大量使用函数的 shell 脚本时,我遇到了一些问题,这些变量在被调用函数中的名称与调用函数中的名称相同,即这些变量是相同的。然后我发现可以通过将函数内的局部变量定义为局部变量来防止这种情况发生:local var=xyz
.
然后,在某个时候,我发现了一个线程 (Defining bash function body using parenthesis instead of braces),其中解释说使用括号定义函数同样有效:
foo()
(
...
)
这样做的效果是函数体是在一个subshell中执行的,好处是函数有自己的变量作用域,这样我就可以不用local来定义它们了。由于具有函数局部作用域似乎比所有变量都是全局的更有意义并且更安全,我立即问自己:
- 为什么默认使用大括号而不是圆括号括起函数体?
然而,我很快也发现了在子shell中执行函数的一个主要缺点,特别是从函数内部退出脚本不再起作用,而是迫使我使用 return 整个调用树的状态(在嵌套函数的情况下)。这让我想到了这个后续问题:
- 使用圆括号代替大括号是否还有其他主要缺点 (*)(这可能解释了为什么大括号似乎更受青睐)?
(*) 我知道(从我随时间偶然发现的与异常相关的讨论中)有人会争辩说,明确使用错误状态比能够退出要好得多任何地方,但我更喜欢后者
显然这两种风格各有优缺点。所以我希望你们中一些更有经验的 bash 用户可以给我一些一般性的指导:
- 什么时候应该用大括号括起函数体,什么时候改用圆括号?
编辑:答案摘要
感谢您的回答,我现在对这个问题比较清楚了。所以我从答案中得到的是:
坚持使用传统的大括号,如果只是为了不混淆脚本的潜在其他 users/developers(如果整个主体都包含在括号中,甚至使用大括号)。
花括号唯一真正的缺点是可以更改父作用域中的任何变量,尽管在某些情况下这可能是一个优点。这可以很容易地通过将变量声明为
local
. 来规避
另一方面,使用括号可能会产生一些严重的不良影响,例如弄乱退出、导致终止脚本和隔离变量范围的问题。
这真的很重要。由于 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 :)