括号`()`在shell函数定义中的作用是什么?

一个函数被定义为:
do_something () {
    do it
}

我能理解它的名字'do_something'和用花括号来封装动作代码,但是我不明白这里的目的是什么,因为Bash脚本中没有命名参数。将其定义为可能更好和直接。
do_something {
  do it
}

不与其当前语法冲突,甚至更进一步声明没有命名参数。这里的()有什么用途?

1相关链接:https://unix.stackexchange.com/questions/73750/difference-between-function-foo-and-foo - Carlos Campderrós
1在Bourne shell和大多数类似Bourne的shell中(bash是最明显的例外),函数定义为do_something () any-command,而在所有POSIX shell(包括bash)中,函数定义为do_something () any-compound-command。这里的any-command/any-compound-command不一定是{ command-group; }这种复合命令,它可以是任何其他类型的复合命令,包括(subshell)for/while/if语句或者在类似Korn的shell(包括bash)中的[[...]]((...)) - Stéphane Chazelas
3个回答

没有(),语法真的会变得模棱两可。
必须有一些明确的语法来定义一个函数,而且在不大幅改变其他shell语法的情况下,它不能是这样的:
do_something {
    # one or more commands go here
}

你说这个“与其当前的语法不冲突”,但实际上是有冲突的!注意,当你尝试运行那段代码的第一行时,并没有出现任何语法错误。虽然会报错,但并非语法错误。而第二行的}则是一个语法错误,但第一行却不是。相反,do_something { 是在尝试运行一个名为do_something的命令,并将{作为该命令的参数传递进去。
$ do_something {
do_something: command not found

如果已经有一个名为do_something的命令,那么你正在运行它。如果已经有一个名为do_something的函数,那么你正在调用它。一般来说,语法应该是明确无歧义的,但特别重要的是,重新定义一个函数时不要意外地调用它。定义一个函数和调用一个函数的写法不应该相同。
关于shell如何处理{(
正如type {所示,{是一个shell关键字。这使得它类似于[[。如果在本应是命令的情况下使用{,它具有特殊的语义。具体而言,它执行命令分组。然而,在其他情况下,它可以未经转义地用作表示字面意义上的{字符。这包括将其作为命令的第二个或后续单词传递的情况。
当然,Bash也可以设计成与当前处理方式不同的方式来处理{。然而,这样的话,它的语法就不再与POSIX shell兼容,Bash也不会真正成为一个Bourne风格的shell,并且无法运行许多shell脚本。
相比之下,( 是一个shell元字符。如果它出现在命令中并且没有被引用(使用' '" "\),它总是被特殊处理的。因此,在语法上没有歧义。
do_something() {
    # one or more commands go here
}

那只能有一个意思。如果Bash没有函数,那么它将是一个语法错误,原因与echo foo(bar)一样。
如果你真的不喜欢()的表示方法,那么你可以使用关键字function并省略它,就像sudodus提到的那样。请注意,在大多数其他Bourne风格的shell中,这不是定义函数的语法的一部分--在某些shell中,虽然支持这种方式定义函数,但其语义是不同的--因此使用这种语法的脚本将不具备可移植性。(之所以这种语法能够明确无歧义,是因为function本身是Bash中的一个关键字,表示其后的内容是函数定义的开始。)
最后,请注意,虽然大多数函数定义实际上使用{,但任何复合命令都是允许的。如果你有一个希望始终在子shell中运行的函数体,你可以使用( )而不是{ }

2在更高的层面上,这涉及到人类的期望和可读性。在大多数语言中,特别是在C、Fortran等在bash脚本语言开发时常见的语言中,函数/子程序总是有一个参数列表,可能为空,而且用括号括起来。改变这种范式会使语言更难理解。 - jamesqf
我遇到了另一个问题,涉及以下函数:function gsq { git reset --hard HEAD~5; git merge --squash HEAD@{1}; git commit } 如何使它能够正确处理括号内的内容?我应该发表一个问题吗? - Timo
1@Timo 你应该发一个问题,但是我的猜测是问题有:*(a)* HEAD@ { 1 } 应该是没有花括号周围的空格,而是 HEAD@{1}(与其是否在函数中无关),和 (b) 如果最后的 } 旨在结束函数定义之前没有换行符(我无法确定,因为注释通常以不同于所需格式的方式出现),那么它必须在 ; 前面,否则它将被视为它前面的命令的参数(git commit }do_something { 存在相同的问题),而复合命令实际上从未完成。 - Eliah Kagan
@Eliah,这听起来很有道理,我会试试看。我经常阅读你的帖子,最后一段也可能是一个选择。但是我不想要一个子shell - Timo

()标记是告诉shell解释器你正在声明一个函数。
$ do_something () { echo 'do it'; } ; do_something
do it

在bash中,一个替代的写法是使用function。
function do_something {
 echo 'do it'
}

或者作为一行代码,你可以进行测试
$ bash -c "function do_something { echo 'do it'; } ; do_something"
do it

2function 关键字是一个在 POSIX 之前的 ksh-ism,bash 为了向后兼容而支持它;然而,bash 对其的支持不佳(默认情况下不将变量作为函数局部变量,就像旧版 ksh 在以那种形式声明的函数中所做的那样)。因此,对于新代码来说并不理想。请参阅 http://wiki.bash-hackers.org/scripting/obsolete - Charles Duffy
@CharlesDuffy,谢谢你的解释 :-) - sudodus
1@CharlesDuffy 你是说ksh和bash接受一些语法,使变量在ksh中为局部变量,在bash中为全局变量吗?这听起来不太对。根据我的理解,部分基于该帖子检查(在ksh93中),ksh使变量在更少的情况下成为局部变量,而不是更多。在bash中,函数中没有-gtypeset总是声明一个局部变量。使用function关键字,bash和ksh的变量作用域相同,不是吗? - Eliah Kagan
@EliahKagan我认为Charles一直在提到的内容已经在这里的Gille's答案中得到证明。 - Sergiy Kolodyazhnyy
@CharlesDuffy 很遗憾,那个链接已经过期了,而且该域名现在正在出售中 :( - SimonC

你的one-true-brace-style主义非常明显!

考虑其他完全合适的大括号样式:

foo
{
  ...
}
foo
  {
    ...
  }
foo
  while ...; do ...; done # Look, ma! No braces!
foo
( ... ) # Look, ma! No braces and a subshell to boot!

如何让shell区分这些是函数定义而不仅仅是一个命令“foo”后面跟着一系列命令?在所有这些情况下,额外的区分因素,比如“()”或者关键字“function”,是必要的。

转念一想,实际上这并不符合OTBS的规定,因为OTBS允许在新行上使用大括号的唯一情况是函数定义。 - muru
4我认为可以提出一个论点,即你正确地将其描述为OTBS,因为与C和C++中的函数不同,在Bourne风格的shell脚本中,函数定义可以直接嵌套在另一个函数定义的主体中,因此可以说它不值得被区分。(或者也许这是Java编程中流行的风格,其中方法的开括号与OTBS不同,而是在同一行上。)无论哪种方式,你对于语法如何强制约束大括号的放置以使其在没有()或类似物的情况下正常工作提出了一个很好的观点。 - Eliah Kagan