Bourne shell函数返回变量总是为空

时间:2014-08-21 21:01:51

标签: function shell return-value sh

给定路径的以下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为空? $path循环中的while变量为每次迭代递增设置就好了。

4 个答案:

答案 0 :(得分:2)

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

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

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/'
}

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

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

  

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

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

答案 1 :(得分: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中回显。

答案 2 :(得分:0)

  

为什么最终的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:+ ...}参数替换技巧将两个路径分配合并为一个。

答案 3 :(得分:0)

您的脚本在Solaris 11下可以正常运行,也可能在AIX和HP-UX等大多数商业Unix上都没有变化,因为在这些操作系统下,/bin/sh的底层实现由ksh提供。如果/bin/sh支持zsh,也会出现这种情况。

它可能不适合您,因为/bin/shbashdashmkshbusybox sh之一实施,子shell中管道的组件,而kshzsh都保留当前shell中管道的最后一个元素,从而节省了不必要的fork。

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

shopt -s lastpipe

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

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

这将使脚本适用于kshzsh,但仍然不适用于dashmksh或原始Bourne shell。

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