为什么 POSIX shell 脚本管道中执行的最后一个函数不保留变量值?

为什么 POSIX shell 脚本管道中执行的最后一个函数不保留变量值?

假设以下脚本:

#!/bin/sh

func1() {
  eval $1'=$(cat)'
  eval echo "Value$2 inside function : \$$1"
}

func1 x 1 <<'HEREDOC'
Hello World
HEREDOC

echo "Value1 outside function: $x"

x=""

echo "Hello World" | func1 x 2

echo "Value2 outside function: $x"

在 bash 4.3.43-4.fc25 上,输出为:

Value1 inside function : Hello World
Value1 outside function: Hello World
Value2 inside function : Hello World
Value2 outside function: 

使用 bashismshopt -s lastpipe使最后一行也显示“Hello World”,但直觉上我不明白为什么这不会自动发生。这是预期的吗?

POSIX 标准似乎并没有真正讨论这个主题:http://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09_02

答案1

从历史上看,伯恩壳科恩壳表现不同。由于管道的两侧在不同的进程中并行执行,因此不可能在脚本的其余部分中保留两侧的变量分配。给定unset a; a=b | a=c,变量a最终可能未设置,或等于b,或等于c;它不能同时等于bc

在 Bourne shell 中,管道运算符的每一侧都在子 shell 中运行(即在单独的 shell 进程中),因此不会保留变量赋值。在 Korn shell 中,管道的右侧在原始 shell 进程中执行。

正如在许多现有行为不同的情况下一样,POSIX 允许这两种行为。如本节所述“Shell执行环境”

多命令管道中的每个命令都位于子shell环境中;然而,作为扩展,管道中的任何或所有命令都可以在当前环境中执行。所有其他命令将在当前 shell 环境中执行。

一些系统已经在当前环境中实现了管道的最后阶段,以便诸如以下命令:

command | read foo

foo在当前环境中设置变量。此扩展是允许的,但不是必需的;因此,shell 程序员应该考虑管道位于子 shell 环境中,但不依赖于它。

实际上,除最后一个命令之外的其他命令始终在子 shell 中运行,除非您仔细查看创建的进程数,否则行为相同的某些优化除外。也就是说,没有 shell 有unset a; a=b | true; echo $aprint b

大多数其他类似 Bourne 的 shell,例如 ash、dash、bash、pdksh 和 mksh,其行为与 Bourne shell 类似。 Zsh 的行为类似于 Korn shell。最新版本的 bash 可以使用 切换到 ksh 行为shopt -s lastpipe

答案2

是的,POSIX 确实提到了这个话题。从这里

子 shell 环境应创建为 shell 环境的副本,但未被忽略的信号陷阱应设置为默认操作。对子 shell 环境所做的更改不应影响 shell 环境。命令替换、用括号分组的命令以及异步列表应在子 shell 环境中执行。此外,多命令管道中的每个命令都位于子shell环境中;然而,作为扩展,管道中的任何或所有命令都可以在当前环境中执行。所有其他命令应在当前 shell 环境中执行。

它取决于 shell 选择其默认行为。在:

echo 1 | read x
echo "$x"

onlyzshkshoutput 1,其他现代的类似 Bourne 的 shell 输出空字符串。

答案3

POSIX 说

多命令管道的每个命令都位于子 shell 环境中

然后

对子 shell 环境所做的更改不应影响 shell 环境

如果您考虑实现管道的最直接方法,这是有道理的 - 调用pipe(),分叉一个子 shell,并交换一侧的标准输出/输入,然后在子 shell 中执行命令。这也是Bash 的定义行为

你不能依靠不过,在这种情况发生时,因为

然而,作为扩展,管道中的任何或所有命令都可以在当前环境中执行

这就是lastpipe该案例的作用。在其他一些 shell 中,这是某些情况下的默认行为。

相关内容