为什么用 bash 而不是 zsh 剪切失败?

Spa*_*awk 10 bash zsh quoting whitespace here-string

我创建了一个带有制表符分隔字段的文件。

echo foo$'\t'bar$'\t'baz$'\n'foo$'\t'bar$'\t'baz > input
Run Code Online (Sandbox Code Playgroud)

我有以下脚本命名 zsh.sh

#!/usr/bin/env zsh
while read line; do
    <<<$line cut -f 2
done < "$1"
Run Code Online (Sandbox Code Playgroud)

我测试一下。

$ ./zsh.sh input
bar
bar
Run Code Online (Sandbox Code Playgroud)

这工作正常。但是,当我将第一行更改为调用时bash,它失败了。

$ ./bash.sh input
foo bar baz
foo bar baz
Run Code Online (Sandbox Code Playgroud)

为什么这会失败并bash与 一起使用zsh

其他故障排除

  • 在 shebang 中使用直接路径而不是env产生相同的行为。
  • 管道使用echo而不是使用 here-string<<<$line也会产生相同的行为。即echo $line | cut -f 2
  • 使用awk而不是cut 适用于两个外壳。即<<<$line awk '{print $2}'

Sté*_*las 17

这是因为在<<< $linebash之前的版本4.4没有分词,上(虽然不是通配符)$line时没有援引那里,然后加入空格字符所产生的词(也放到了一个临时文件后跟一个换行符,并作出这样的标准输入的cut)。

$ a=a,b,,c bash-4.3 -c 'IFS=","; sed -n l <<< $a'
a b  c$
Run Code Online (Sandbox Code Playgroud)

tab恰好在默认值中$IFS

$ a=$'a\tb'  bash-4.3 -c 'sed -n l <<< $a'
a b$
Run Code Online (Sandbox Code Playgroud)

解决方案bash是引用变量。

$ a=$'a\tb' bash -c 'sed -n l <<< "$a"'
a\tb$
Run Code Online (Sandbox Code Playgroud)

请注意,它是唯一执行此操作的 shell。zsh<<<来自哪里,受 Byron Rakitzis 对 的实现启发rc),ksh93mksh并且yash也支持<<<不这样做。

当谈到阵列,mkshyashzsh加入上的第一个字符$IFSbashksh93空间。

$ mksh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ yash -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ ksh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1:2$
$ bash -c 'a=(1 2); IFS=:; sed -n l <<< "${a[@]}"'
1 2$
Run Code Online (Sandbox Code Playgroud)

当为空时zsh/yashmksh(至少版本 R52)之间存在差异$IFS

$ mksh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
12$
Run Code Online (Sandbox Code Playgroud)

当您使用时,跨外壳的行为更加一致"${a[*]}"(除了mksh$IFS为空时仍然存在错误)。

在 中echo $line | ...,这是所有类似 Bourne 的 shell 中常用的 split+glob 运算符,但是zsh(以及与 相关的常见问题echo)。


Mic*_*hrs 13

发生的事情是bash用空格替换制表符。你可以通过说"$line"来避免这个问题,或者明确地减少空格。

  • @StéphaneChazelas 在 bash 4.4 上没有发生分裂(也没有 glob) (2认同)

ter*_*don 10

问题是你没有引用$line. 要进行调查,请更改两个脚本,以便它们简单地打印$line

#!/usr/bin/env bash
while read line; do
    echo $line
done < "$1"
Run Code Online (Sandbox Code Playgroud)

#!/usr/bin/env zsh
while read line; do
    echo $line
done < "$1"
Run Code Online (Sandbox Code Playgroud)

现在,比较它们的输出:

$ bash.sh input 
foo bar baz
foo bar baz
$ zsh.sh input 
foo    bar    baz
foo    bar    baz
Run Code Online (Sandbox Code Playgroud)

正如您所看到的,因为您没有引用$line,所以 bash 无法正确解释选项卡。Zsh 似乎处理得更好。现在,cut使用\t的默认字段分隔符。因此,由于您的bash脚本正在吃制表符(由于 split+glob 运算符),因此cut只能看到一个字段并相应地采取行动。你真正运行的是:

$ echo "foo bar baz" | cut -f 2
foo bar baz
Run Code Online (Sandbox Code Playgroud)

因此,为了让您的脚本在两个 shell 中都能按预期工作,请引用您的变量:

while read line; do
    <<<"$line" cut -f 2
done < "$1"
Run Code Online (Sandbox Code Playgroud)

然后,两者都产生相同的输出:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar
Run Code Online (Sandbox Code Playgroud)