【问题标题】:Bourne shell function return variable always emptyBourne shell 函数返回变量始终为空
【发布时间】:2014-08-21 21:01:50
【问题描述】:

下面的 Bourne shell 脚本,给定一个路径,应该测试路径的每个组件是否存在;然后设置一个仅包含实际存在的组件的变量。

#! /bin/sh
set -x             # for debugging

test_path() {
  path=""
  echo $1 | tr ':' '\012' | while read component
  do
    if [ -d "$component" ]
    then
      if [ -z "$path" ]
      then path="$component"
      else path="$path:$component"
      fi
    fi
  done
  echo "$path"    # this prints nothing
}

paths=/usr/share/man:\
/usr/X11R6/man:\
/usr/local/man

MANPATH=`test_path $paths`
echo $MANPATH

运行时,它总是不打印任何内容。使用set -x 的跟踪是:

+ paths=/usr/share/man:/usr/X11R6/man:/usr/local/man
++ test_path /usr/share/man:/usr/X11R6/man:/usr/local/man
++ path=
++ echo /usr/share/man:/usr/X11R6/man:/usr/local/man
++ tr : '\012'
++ read component
++ '[' -d /usr/share/man ']'
++ '[' -z '' ']'
++ path=/usr/share/man
++ read component
++ '[' -d /usr/X11R6/man ']'
++ read component
++ '[' -d /usr/local/man ']'
++ '[' -z /usr/share/man ']'
++ path=/usr/share/man:/usr/local/man
++ read component
++ echo ''
+ MANPATH=
+ echo

为什么最后的echo $path 是空的? while 循环中的 $path 变量在每次迭代中增量设置就好了。

【问题讨论】:

  • /bin/sh 通常只表示符合 POSIX 的 shell,不一定(甚至通常)不是真正的 Bourne shell。

标签: function shell return-value sh


【解决方案1】:

管道运行子shell 中涉及的所有命令,包括整个while ... 循环。因此,该循环中对变量的所有更改都仅限于子 shell,对父 shell 脚本不可见。

解决此问题的一种方法是将while ... 循环和echo 放入一个完全在子shell 中执行的列表中,这样修改后的变量$pathecho 可见:

test_path()
{
  echo "$1" | tr ':' '\n' | {
  while read component
    do
      if [ -d "$component" ]
      then
        if [ -z "$path" ]
        then
          path="$component"
        else
          path="$path:$component"
        fi
      fi
    done
    echo "$path"
  }
}

但是,我建议使用这样的东西:

test_path()
{
    echo "$1" | tr ':' '\n' |
    while read dir
    do
        [ -d "$dir" ] && printf "%s:" "$dir"
    done |
    sed 's/:$/\n/'
}

...但这是一个品味问题。

编辑:正如其他人所说,您观察到的行为取决于外壳。 POSIX standard 将流水线命令描述为在子 shell 中运行,但这不是必需的:

另外,多命令管道的每个命令都处于子shell环境中;但是,作为扩展,管道中的任何或所有命令都可以在当前环境中执行。

Bash 在子 shell 中运行它们,但有些 shell 在主脚本的上下文中运行最后一个命令,而管道中只有前面的命令在子 shell 中运行。

【讨论】:

  • 有什么方法可以得到我想要的结果吗?
【解决方案2】:

这应该可以在理解函数的 Bourne shell 中工作(并且也可以在 Bash 和其他 shell 中工作):

test_path() {
  echo $1 | tr ':' '\012' |
  {
  path=""
  while read component
  do
    if [ -d "$component" ]
    then
      if [ -z "$path" ]
      then path="$component"
      else path="$path:$component"
      fi
    fi
  done
  echo "$path"    # this prints nothing
  }
}

内部的大括号将命令组合成一个单元,因此path 仅设置在子shell 中,但会从同一个子shell 中回显。

【讨论】:

    【解决方案3】:

    为什么最后的 echo $path 是空的?

    直到最近,Bash 才会为管道的所有组件提供自己的进程,与运行管道的 shell 进程分开。 独立的进程 == 独立的地址空间,并且没有变量共享。

    在 ksh93 和最近的 Bash 中(可能需要 shopt 设置),shell 将在调用 shell 中运行管道的 last 组件,因此循环内更改的任何变量都会在循环退出。

    实现您想要的另一种方法是使用括号确保echo $path 与循环处于同一进程中:

    #! /bin/sh
    set -x             # for debugging
    
    test_path() {
      path=""
      echo $1 | tr ':' '\012' | ( while read component
      do
        [ -d "$component" ] || continue
    
        path="${path:+$path:}$component"
      done
      echo "$path"
      )
    }
    

    注意:我简化了内部if。没有else,因此可以用快捷方式替换测试。此外,可以使用S{var:+ ...} 参数替换技巧将两个路径分配合并为一个。

    【讨论】:

      【解决方案4】:

      您的脚本在 Solaris 11 和大多数商业 Unix(如 AIX 和 HP-UX)下运行良好,没有任何变化,因为在这些操作系统下,/bin/sh 的底层实现由ksh 提供。如果/bin/sh 得到zsh 的支持,情况也是如此。

      它可能对您不起作用,因为您的 /bin/sh 是由 bashdashmkshbusybox sh 之一实现的,它们都在子 shell 中处理管道的每个组件,而 @987654330 @ 和 zsh 都将管道的最后一个元素保留在当前 shell 中,从而节省了不必要的分叉。

      当 bash 提供 sh 时,可以通过在管道之前的某处添加此行来“修复”您的脚本以使其工作:

      shopt -s lastpipe
      

      或者更好,如果你不想保持便携性:

      command -v shopt > /dev/null && shopt -s lastpipe
      

      这将使脚本在 kshzsh 上正常工作,但对于 dashmksh 或原始 Bourne shell 仍然不起作用。

      请注意,POSIX 标准允许 bashksh 行为。

      【讨论】:

      • 这很有趣,我不知道 bash 中的 lastpipe 选项。感谢您的指点。这可能允许在 bash 脚本中进行一些简化。
      猜你喜欢
      • 2020-01-02
      • 2019-09-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-02-19
      相关资源
      最近更新 更多