灵感来自最近的这个问题:
bash$ a=(1 2 3)
bash$ echo $a
1
但
zsh% a=(1 2 3)
zsh% echo $a
1 2 3
zsh% printf '%s\n' $a
1
2
3
(最后一部分演示了数组扩展为单独的参数,相当于"${a[@]}"
和 not "${a[*]}"
)
bash 行为(与 ksh 匹配)极其违反直觉。 “仅第一个元素”如何是“扩展此数组变量”的合理反应?
在 zsh 存在分歧的其他领域,这是因为 ksh 和 bash 更接近原始的 Bourne shell。但 Bourne 没有用户定义的数组变量。
为什么 bash 做出这个奇怪的决定?如果是复制 ksh,为什么 ksh 会做出这个奇怪的决定呢?
一长串评论后继续:
这不应该是批评或赞扬zsh的问题。 zsh 只是一个易于理解的示例可能已经采取了不同的做法。
解释设计决策的可能性之一是向后兼容性。向后兼容并不是一种观点。这是客观事实。
如果您可以显示一个脚本(a完整脚本,不是脱离上下文的摘录),在 Bourne shell 中以已知的方式运行(即不只是因为语法错误而轰炸),并且在假设的“具有完整 $array 扩展的 Korn shell”中表现不同,那么你就赢了!这是一个向后兼容性问题。
还没有给出这样的脚本。这不是一个:
a=(1 2 3)
printf '%s\n' $a
因为这是 Bourne shell 中的语法错误。为曾经的语法错误赋予新的含义是创建新功能同时保持向后兼容性的一种方法。
据我所知,a=(...)
最初是语法错误的事实在(尝试)使用数组的脚本和不使用数组的脚本之间创建了清晰的分离。在第一类中,不能将向后兼容性作为任何原因,因为这些脚本无论如何都不会在旧 shell 中运行。在第二类中,无论您的数组变量扩展规则如何,向后兼容性都成立,因为没有要扩展的数组!
这不是一个证明因为我部分依赖直觉来决定没有办法将数组走私到脚本中=(
,因此不存在会表现出不兼容行为的脚本。声称不存在的好处是你只需要举出一个反例就可以结束它。
评论中提出的事情a=$@
看起来确实有助于解释。如果它创建一个数组变量a
,则此脚本:
a=$@
printf '%s\n' $a
应该显示差异。但在我的测试中,这种情况并没有发生。所有 shell(heirloom sh、modern ksh、bash 和 zsh)似乎都以相同的方式处理第一行。a
不是一个数组,它只是一个带有空格的字符串。 (zsh 在第二行出现分歧,因为它不对 的值进行分词$a
,但这与数组变量无关)
答案1
我无法给出答案,但建议一些可能的解释。
确实,除了 ksh 及其克隆(pdksh 及其进一步的衍生物和 bash)之外,所有其他具有数组 ( csh
, tcsh
, rc
, es
, akanga
, fish
, zsh
, yash
) 的 shell 都已扩展到$array
数组的所有成员。
但是在 和yash
(zsh
在sh
仿真时)中,该列表中的两个类似 Bourne 的 shell,该扩展仍然受到 split+glob 的影响(即使zsh
不在sh
仿真中,仍然是空删除),因此您仍然需要使用笨拙的"${array[@]}"
语法(或者"${(@)array}"
很难更容易输入"$array[@]"
的zsh
or )来保留列表(csh
并且tcsh
有类似的问题)。 split+glob 和空删除是 Bourne 遗产(本身在某种程度上是由 Thompson shell 遗产造成的,更像$1
是宏扩展)。
rc
和fish
是后来的 shell 的两个例子,它们没有 Bourne 包袱,并且采用了更干净的方法。他们承认 shell 是一个命令行解释器,他们处理的主要内容是列表(命令的参数列表),因此列表/数组是主要数据类型(只有一种类型,它是列表rc
)并且摆脱了 Bourne shell 的 split+glob-upon-expansion 错误/错误功能(现在不再需要,因为主要类型是数组)。
尽管如此,这并不能解释为什么 David Korn 选择不扩展$array
到所有元素而是扩展到索引 0 的元素。
现在,除了csh
/之外tcsh
,所有这些 shell 都比 80 年代初开发的新得多ksh
,距 Bourne shell 和 Unix V7 发布仅几年。 Unix V7 也引入了环境。这在当时是一件很新奇的事情。环境整洁且有用,但环境变量不能包含数组,除非您使用某种形式的编码。
这只是推测,但我怀疑 David Korn 选择这种方法的一个原因是为了不修改与环境的界面。
在 ksh88 中,像 rc 一样,所有变量都是数组(虽然很稀疏;有点像键仅限于正整数的关联数组,与其他 shell 或编程语言相比,这是另一个奇怪的地方,你可以看出它还没有被完全考虑清楚,因为例如,不可能检索密钥列表)。在新设计中,var=value
成为 的缩写var[0]=value
。您仍然可以导出所有变量,但export var
将数组索引 0 的元素导出到环境中。
rc
确实将其所有变量放入环境中,fish
支持导出数组,但要为具有多个元素的数组做到这一点(至少对于来自 plan9 的 rc 到 Unix 的移植),它们必须诉诸某种形式的编码这只有他们自己明白。
csh
、tcsh
、zsh
不支持导出数组(尽管现在这听起来可能不是一个很大的限制)。您可以在 中导出数组yash
,但它们会导出为环境变量,该环境变量是与 相连接的数组元素:
(因此(a "" "" b)
和(a : b)
导出为相同的值),并且在导入时不会转换回数组。
另一个可能的理由可能是与 Bourne 的$@
/保持一致$*
(但是为什么数组索引从 0 而不是 1 开始(与当时的其他 shell/语言相比,这是另一个奇怪的地方)?)。ksh
不是自由软件,它是商业企业,要求之一是 Bourne 兼容性。ksh
确实删除了完成的字段分割每一个列表上下文中的非引用单词(因为这在 Bourne shell 中显然没有用),但必须保留它以进行扩展(因为脚本确实使用了诸如var="file1 file2"; cmd $var
Bourne shell 没有数组之类的东西,但是"$@"
)。将其保留在具有数组的 shell 中没有什么意义,但如果 Ksh 仍然能够解释消费者群的脚本,Korn 几乎没有其他选择。如果$scalar
受到 split+glob 的影响,$array
则必须是为了一致性,因此"${array[@]}"
作为一种概括是"$@"
有一定意义的。zsh
没有类似的约束,因此可以在扩展时在添加数组的同时自由地删除 split+glob (但要为破坏 Bourne 向后兼容付出代价)。
@Arrow 提供的另一种解释可能是他不想重载现有运算符以使它们对不同类型的变量表现不同(例如${#var}
,${#array}
尽管 Bourne shell 没有那个或${var-value}
, ${var#pattern}
),这可以导致用户感到困惑(因为zsh
某些运算符如何使用数组与标量并不总是显而易见)。
一些相关阅读:
- https://www.usenix.org/legacy/publications/library/proceedings/vhll/full_papers/korn.ksh.aDavid Korn 解释了其发展
ksh
(数组首先作为 Bourne shell 上的补丁添加到表格录入系统)。 - https://news.slashdot.org/story/01/02/06/2030205/david-korn-tells-all
至于a=$@
您编辑中的情况,这实际上是 ksh 破坏了与 Bourne shell 兼容性的一种情况。
在 Bourne shell 中,$@
并且$*
包含位置参数与空格字符的串联。仅$@
当引用时才特殊,因为它扩展为与插入的空格相同"$*"
但未引用(在新版本中空列表的特殊情况已像在 Solaris 上一样进行处理)。您会注意到,如果从 中删除空格$IFS
,则"$@"
扩展为列表上下文中仅一个参数(0 表示列表中的空列表)固定的上面提到的版本)。当不加引号时,$*
和 的$@
行为与任何其他变量一样(按 的字符分割$IFS
,不一定在原始位置参数上分割)。例如,在 Bourne shell 中:
'set' 'a:b' 'c'
IFS=:
printf '<%s>\n' $@
printf '[%s]\n' "$@"
会输出:
<a>
<b c>
[a:b c]
Ksh88 对此进行了更改,以便将$@
和$*
与 的第一个字符连接起来$IFS
。"$@"
在列表上下文中分隔位置参数,除非为$IFS
空。
当$IFS
为空时,$*
按空格连接,除非$*
引用时不使用分隔符连接。
例子:
$ set a b
$ IFS=:
$ a=$@ b=$* c="$@" d="$*"
$ printf '<%s>\n' "$a" "$b" "$c" "$d" $@ $* "$@" "$*"
<a:b>
<a:b>
<a:b>
<a:b>
<a>
<b>
<a>
<b>
<a>
<b>
<a:b>
$ IFS=
$ a=$@ b=$* c="$@" d="$*"
$ printf '<%s>\n' "$a" "$b" "$c" "$d" $@ $* "$@" "$*"
<a b>
<a b>
<a b>
<ab>
<a b>
<a b>
<a b>
<ab>
您会看到不同的类似 Bourne/Korn shell 的许多变化,包括 ksh93 与 ksh88。情况也有一些变化,例如:
set --
cmd ''"$@"
cmd $empty"$@"
或者当$IFS
包含多字节字符或不形成有效字符的字节时。
yash
然而,在 中,"$array"
行为类似于"${array[@]}"
whilezsh
的"$array"
行为类似于"${array[*]}"
。不那么丑陋/尴尬,但也许更令人惊讶。
答案2
从您对此答案的评论中,您期望答案会说并接受 zsh 的范例“更好”,因此是所有 shell 应该工作的方式。 “更好”只是一种意见。意见无需讨论。
$a
也许您期望的是为什么也可以用于数组的 详细信息。这就是 zsh 正在尝试做的事情,即使它在这个方向上做了很多工作,但
在某些情况下仍然会失败。它(还)不复制数组。$ a=(1 2 3) $ b=$a $ printf '<%s>' $a ' ' $b; echo <1><2><3>< ><1 2 3>
- 但这个问题确实与此相去甚远。这是语言的问题。
在我们的语言中,我们为每个想法使用一个名称。每个新想法都与以前的想法截然不同必须有自己的名字。在我们的语言中,我们很自然地接受新词代表新想法。想想“互联网”这个名字,一个新想法的新名字。使用旧名称代表新概念总是会导致混乱和误解。这就是我们人类的构造,当我们仔细思考时,这听起来很合理。
- 在 shell(以及任何编程语言)中,我们对每个特定的想法使用一些特定的语法。
从一开始,shell 中的变量就使用名称(假设a
),其值是符号 展开(shell 过程)的结果$a
。变量能够包含字符串或数字(自动转换)。
- 引入变量的新内容(值数组)必须使用新的语法。
@a
这正是 Perl 使用 using来表示的做法一个列表,同时$a
保持an scalar
。
那是:上面两个分别称为SCALAR和LIST上下文。(在 Perl 中)。
这就是为什么我们要a=(1 2)
分配一个数组(还有其他方法,但这是最常见的)。在以前的 shell 中无效的语法。
在 sh 中(在本例中为破折号):
$ a=(12)
sh: 2: Syntax error: "(" unexpected
并且变量的扩展是从新语法$a
或等效${a}
于新语法(在 sh 中无效)`${a[@]} 扩展而来:
$ a=(aa bb cc)
$ printf '%s\n' "${a[@]}"
aa
bb
cc
在更简单的 shell 中(本例中为 ash):
$ a=Strin-Of-Text
$ printf '%s\n' "$a"
ash: syntax error: bad substitution
不幸的是,我们选择了如此复杂的数组编写方式。
如果我要提出一种新语法,我可能会遵循 Perl 的指导,并使用类似于“@a”的值列表(或者可能是#a
or %a
)。这应该是一些讨论以达成共识的问题。
- 但这不是所做的(可悲的是),选择的是:
${a[@]}
。
简而言之:为了向后兼容,预计简单变量的扩展$a
只会导致一值,不是值列表,而且是分隔值。
由于 POSIX 中没有定义数组,因此引用 POSIX 中的定义可能无效参数扩展说:
参数值(如果有)应被替换。
而且,事实上,大多数 shell 只打印一个标量$a
bash(4.4) : <1><====><1><2><3>
lksh : <1><====><1><2><3>
mksh : <1><====><1><2><3>
ksh93 : <1><====><1><2><3>
astsh : <1><====><1><2><3>
zsh/ksh : <1><====><1><2><3>
zsh : <1><2><3><====><1><2><3>
因此,对于编写 shell 脚本来说,zsh [a]是个奇怪的选择。
使用此脚本进行测试:
a=(1 2 3)
printf '<%s>' $a '====';
printf '<%s>' "${a[@]}" ;
echo
[a]也雅什。未测试 csh 兼容 shell。使用 csh 的脚本是一个已知问题。