了解“副作用”,或一个命令中的多个命令?

mid*_*ite 5 shell shell-script posix

这个问题重点关注 POSIX 兼容的 shell 脚本。

通常通过以下方式递增变量:

i=3
: $(( i += 1 ))
echo "return code = $?"        # return code = 0
echo "i = $i"                  # i = 4
Run Code Online (Sandbox Code Playgroud)

该命令是否$(( i += 1 ))称为“副作用” :

(我在某处读到:等于true。我尝试用 or 替换:truefalse有效。如果用 替换false,则返回码为 1,正如预期的那样。)

为什么成功增加了值$i,但赋值却不起作用,出现“副作用”?

a='four'
: a='five'
echo "return code = $?"        # return code = 0
echo "a = $a"                  # a = four
Run Code Online (Sandbox Code Playgroud)

有时,我看到脚本在一个命令中运行多个命令。在大多数情况下,人们使用此结构来设置IFS中的变量IFS='' read -r REPLY。这是相同的结构,称为“副作用”吗?然而,所有副作用都a=6在当前 shell 中产生实际影响。作业在这里进行。

a=6 b=7 c=8
echo "a = $a"                  # a = 6
echo "b = $b"                  # b = 7
echo "c = $c"                  # c = 8
Run Code Online (Sandbox Code Playgroud)

这种结构称为“副作用”吗?我在哪里可以找到它的记录?或者我可以在哪里了解它?我在下面的 POSIX 文档中找不到这个结构。

https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V3_chap02.html#tag_18_09

ter*_*don 15

:称为空命令。您可以在以下位置找到其文档man bash

:[论据]

没有效果; 该命令除了扩展参数和执行任何指定的重定向之外不执行任何操作。返回状态为零。

或者,如果运行 bash shell,则使用help :

$ help :
:: :
    Null command.
    
    No effect; the command does nothing.
    
    Exit Status:
    Always succeeds.
Run Code Online (Sandbox Code Playgroud)

它位于 POSIX 规范

姓名

冒号 - null 实用程序概要:[参数...]

描述

该实用程序只能扩展命令参数。当需要命令时使用它,如 if 命令的 then 条件,但该命令不执行任何操作。

现在,您所说的副作用是上面第一句话中描述的结果:“该命令除了扩展参数之外不执行任何操作”。这:是一个命令,因此后面的任何内容都是一个参数。这$(( ))称为“算术扩展”,记录如下man bash

算术扩展

算术展开允许计算算术表达式并替换结果。

算术展开式的格式为:

$((expression))
Run Code Online (Sandbox Code Playgroud)

该表达式被视为位于双引号内,但括号内的双引号不会被特殊处理。表达式中的所有标记都会经历参数和变量扩展、命令替换和引号删除。结果被视为要计算的算术表达式。算术展开式可以嵌套。

[。。.]

so 的$(( i += 1 ))意思是“加i一并返回结果”。然而,这需要评估、扩展这就是为什么你看到它与 一起使用:。由于:将扩展其参数,: $(( i += 1 ))因此平均值i将增加 1。如果您尝试$(( i += 1 ))单独运行,则首先会扩展为4(在您的示例中),然后 shell 会尝试4作为命令执行并返回错误:

$ $(( i += 1 ))
bash: 4: command not found
Run Code Online (Sandbox Code Playgroud)

但是,如果你运行: $(( i += 1 )),shell 会做两件事:首先,它将应用算术扩展并获取i的值4,然后它将执行: 4这是一个空命令,因此不会返回错误,因为:在它们之后忽略其参数已扩展:

$ : 4
$ 
Run Code Online (Sandbox Code Playgroud)

综上所述,我真的不知道为什么你会想要这样做,而不是更简单且 POSIX:

$ i=3
$ i=$(( i + 1 ))
$ echo "$i"
4
Run Code Online (Sandbox Code Playgroud)

或者,在 中bash,但不在 POSIX shell 中:

$ i=3
$ (( i += 1 ))
$ echo "$i"
4
Run Code Online (Sandbox Code Playgroud)

这里的下一点是,这variable=value command是一种不同的野兽,并且以不同的方式对待。POSIX 规范的相关部分是2.9.1 Simple Commands,其中最相关的部分是:

“简单命令”是一系列可选的变量分配和重定向,可以任意顺序,可选地后跟单词和重定向,并由控制运算符终止。

当需要执行给定的简单命令时[. 。.],以下扩展、赋值和重定向均应从命令文本的开头到结尾执行:

  1. 根据 Shell 语法规则识别为变量赋值或重定向的单词将被保存以便在步骤 3 和 4 中进行处理。

  2. 不是变量赋值或重定向的单词应该被扩展。如果扩展后仍有任何字段,则第一个字段应被视为命令名称,其余字段是命令的参数。

  3. 重定向应按照重定向中的描述执行。

  4. 每个变量赋值都应在赋值之前进行扩展,以进行波形符扩展、参数扩展、命令替换、算术扩展和引号删除。

和:

变量赋值应按如下方式执行:

[。。.]

  • 如果命令名称不是特殊的内置实用程序或函数,则应为命令的执行环境导出变量分配[...]。

[。。.]

  • 如果命令名称是特殊的内置实用程序,则变量赋值将影响当前的执行环境。除非 set -a 选项打开(参见 set),否则它是未指定的:

  • 变量在执行特殊内置实用程序期间是否获得导出属性

  • 在特殊内置实用程序完成后,由于变量分配而获得的导出属性是否仍然存在

因此,当 shell 读取命令以执行它时,它会首先解析它以查找任何看起来像变量赋值 ( foo=bar) 的内容,然后分配相关值,当该命令是“正常”命令时,赋值将只影响该命令的运行环境,这就是为什么它有效:。

$ foo=bar sh -c 'echo "foo is $foo"'
foo is bar
$ echo "foo is $foo"
foo is 
Run Code Online (Sandbox Code Playgroud)

变量分配起作用并存在于命令的执行环境中sh -c,但它不存在于启动该命令的父 shell 中。这只是 shell 处理命令的一部分,并且是一种仅为单个命令设置变量而不影响 shell 的方法。

如果命令是特殊的内置命令(例如 ),则未指定分配是否在命令之后继续存在:。这意味着有一些 shell在完成后var=foo :使变量$var可用command,但这不是 POSIX 强制的,并且不同的 shell 的行为方式不同:

$ cat foo.sh 
foo=bar :
echo "foo is $foo"
$ awk -F'/' '/^\//{print $NF}' /etc/shells | sort -u | 
  while read shell; do 
    echo "==== $shell ===="; 
    "$shell" foo.sh; 
done
==== bash ====
foo is 
==== dash ====
foo is bar
==== fish ====
foo is 
==== ksh ====
foo is bar
==== mksh ====
foo is bar
==== rbash ====
foo is 
==== sh ====
foo is bar
==== yash ====
foo is bar
==== zsh ====
foo is 
Run Code Online (Sandbox Code Playgroud)

无论如何,所有这一切正是您看到while IFS= read ...和构造这样的原因:您不想弄乱您的$IFS变量,因此您更改它只是为了运行您需要它具有不同值的特定命令。

最后,说一下这个方法有效的原因:

a=6 b=7 c=8
Run Code Online (Sandbox Code Playgroud)

是因为那里没有命令,只是变量赋值,所以这些变量确实是在当前shell中赋值的。

  • *“未指定分配是否在命令结束后继续存在。”*。除了功能之外,确实如此。请参阅 OP 中规范的链接。请注意,该命令是否是“特殊内置命令”会有所不同,并且碰巧“:”是特殊内置命令,而“true”则不是。另请参阅[在 bash 脚本中更惯用:\`|| true\` 或 \`|| :\`?](//unix.stackexchange.com/q/78408) (2认同)
  • @midnite 我这里有一个错误。我原来的测试脚本(上面的“foo.sh”)有“true”。当 ilkkachu 指出我遇到的另一个错误时,我将其更改为 `:`,但像个白痴一样,在进行更改后实际上并没有保存文件,所以我认为所有 shell 的行为方式也与 `:` 相同。事实证明他们没有,正如 Stéphane 在他的回答中解释的那样,POSIX shell 和其他一些显然确实保留了价值。请参阅上面“dash”、“ksh”、“mksh”、“yash”和“sh”的输出。对于那个很抱歉。 (2认同)