【问题标题】:bash functions: enclosing the body in braces vs. parenthesesbash 函数:将主体括在大括号与括号中
【发布时间】:2015-03-04 07:31:01
【问题描述】:

通常,bash 函数定义使用花括号来包围主体:

foo()
{
    ...
}

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

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

foo()
(
    ...
)

这样做的效果是函数体在子shell中执行,这样做的好处是函数有自己的变量范围,这允许我在没有本地的情况下定义它们。由于拥有一个函数局部范围似乎比所有变量都是全局的更有意义并且更安全,所以我立即问自己:

  • 为什么默认使用大括号而不是括号来包围函数体?

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

  • 使用括号代替大括号是否还有其他主要缺点 (*)(这或许可以解释为什么大括号似乎更受欢迎)?

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

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

  • 什么时候应该用花括号括住函数体,什么时候最好用括号?

编辑:答案要点

感谢您的回答,我现在对此有点清楚了。所以我从答案中得出的结论是:

  • 坚持使用传统的花括号,如果只是为了不混淆脚本的潜在其他用户/开发者(如果整个正文都用括号括起来,甚至可以使用花括号)。

    李>
  • 花括号唯一真正的缺点是可以更改父作用域中的任何变量,尽管在某些情况下这可能是一个优点。这可以通过将变量声明为local 来轻松规避。

  • 另一方面,使用括号可能会产生一些严重的不良影响,例如弄乱退出、导致终止脚本出现问题以及隔离变量范围。

【问题讨论】:

    标签: bash function parentheses curly-braces


    【解决方案1】:

    为什么默认使用大括号而不是括号来括住函数体?

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

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

    使用括号代替大括号是否还有其他主要缺点 (*)(这也许可以解释为什么大括号似乎是首选)?

    是的。有很多事情你不能从子 shell 中做,包括:

    • 更改全局变量。变量更改不会传播到父 shell。
    • 退出脚本。 exit 语句将仅退出子 shell。

    启动子外壳也会严重影响性能。每次调用该函数时,您都会启动一个新进程。

    如果您的脚本被杀死,您也可能会出现奇怪的行为。父壳和子壳接收到的信号将会改变。这是一种微妙的效果,但如果您有 trap 处理程序或 kill 您的脚本,这些部分将无法按您想要的方式工作。

    什么时候应该用花括号括起来函数体,什么时候最好用括号?

    我建议您始终使用花括号。如果您想要一个显式的子外壳,请在花括号内添加一组括号。只使用括号是非常不寻常的语法,会让许多阅读您的脚本的人感到困惑。

    foo() {
       (
           subshell commands;
       )
    }
    

    【讨论】:

    • 虽然我首先考虑了@kojiro 的问题,但我会接受你的回答,因为你明确给出了所有三个问题的答案,而且如果脚本的奇怪行为,你提出了两个非常好的观点被杀死(不会想到这一点)并且使用没有任何大括号的括号将是太不寻常的合成器。
    • 但我仍然看到开箱即用地更改全局变量的能力,但必须明确声明局部变量,这比加号更危险。我非常希望变量默认是本地的,但能够将它们声明为全局,以便仍然可以访问它们。但我会养成总是用local 声明它们的习惯来解决这个问题。
    • @VaticanViolator 我同意,如果它们默认是本地的会更好。这只是你学会接受和处理的事情之一。例如,我用小写命名局部变量,用大写命名全局变量。如果我忘记了 local 声明,它会明确哪个是哪个,并减少意外踩到全局变量的风险。
    • 是的,不久前我也直觉地养成了用大写命名全局变量的习惯。好吧,我想局部/全局变量的困难可以在相当小的脚本中进行管理,如果它变得更大,它应该被分解成碎片或用另一种语言编写。
    • 1) “技术上允许其他三种形式的复合命令”:不是三种,还有更多,例如if then fi,见:pubs.opengroup.org/onlinepubs/9699919799/utilities/… 2)“每次都新进程”这可能是真的,但它是一个很好隐藏的实现细节:-) 它甚至设置了相同的$$:@987654322 @
    【解决方案2】:

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

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

    这在函数体中也可以工作,但即使你用花括号定义函数,仍然可以从子shell中使用它。

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

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

    myfun() {
        local x=123
    }
    

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

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

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

    【讨论】:

      【解决方案3】:

      这真的很重要。由于 bash 函数不返回值,并且它们使用的变量来自全局范围(也就是说,它们可以从其范围“外部”访问变量),处理函数输出的常用方法是将值存储在一个变量,然后调用它。

      当你用() 定义一个函数时,你是对的:它会创建子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
      

      【讨论】:

      • 从技术上讲,函数可以通过return 返回一个退出代码值,该值将通过链接或$? 传递回父访问
      【解决方案4】:

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

      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 :)

      【讨论】:

      • 与大括号或列表无关。试试a=22; a=46|cat; echo $aa=22|cat; { echo $a; a=46; echo $a; }|cat; echo $a。它是产生子外壳的管道。
      猜你喜欢
      • 2013-01-17
      • 2011-07-05
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2019-11-13
      • 2021-01-11
      • 1970-01-01
      相关资源
      最近更新 更多