为什么 bash 的 cut 会失败,而 zsh 则不会?

为什么 bash 的 cut 会失败,而 zsh 则不会?

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

echo foo$'\t'bar$'\t'baz$'\n'foo$'\t'bar$'\t'baz > input

我有以下名为的脚本zsh.sh

#!/usr/bin/env zsh
while read line; do
    <<<$line cut -f 2
done < "$1"

我测试一下。

$ ./zsh.sh input
bar
bar

这很好用。但是,当我更改第一行来调用时bash,它失败了。

$ ./bash.sh input
foo bar baz
foo bar baz

为什么这会失败bash并与 一起工作zsh

其他故障排除

  • 在 shebang 中使用直接路径而不是env产生相同的行为。
  • 使用管道echo而不是使用此处字符串<<<$line也会产生相同的行为。 IE echo $line | cut -f 2
  • 使用awk代替cut 作品对于两个外壳。 IE <<<$line awk '{print $2}'

答案1

这是因为在4.4 之前的版本中<<< $line,当没有引用时会进行分词(尽管不是通配符),然后用空格字符连接生成的单词(并将其放入临时文件中,后跟换行符,并将其设为标准输入的)。bash$linecut

$ a=a,b,,c bash-4.3 -c 'IFS=","; sed -n l <<< $a'
a b  c$

tab恰好是默认值$IFS

$ a=$'a\tb'  bash-4.3 -c 'sed -n l <<< $a'
a b$

解决方案bash是引用变量。

$ a=$'a\tb' bash -c 'sed -n l <<< "$a"'
a\tb$

请注意,它是唯一执行此操作的 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$

当为空时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$

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

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

答案2

发生的情况是bash用空格替换制表符。您可以通过说"$line"相反或明确地删除空格来避免此问题。

答案3

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

#!/usr/bin/env bash
while read line; do
    echo $line
done < "$1"

#!/usr/bin/env zsh
while read line; do
    echo $line
done < "$1"

现在,比较他们的输出:

$ bash.sh input 
foo bar baz
foo bar baz
$ zsh.sh input 
foo    bar    baz
foo    bar    baz

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

$ echo "foo bar baz" | cut -f 2
foo bar baz

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

while read line; do
    <<<"$line" cut -f 2
done < "$1"

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

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar

答案4

正如已经回答的那样,使用变量的更便携的方法是引用它:

$ printf '%s\t%s\t%s\n' foo bar baz
foo    bar    baz
$ l="$(printf '%s\t%s\t%s\n' foo bar baz)"
$ <<<$l     sed -n l
foo bar baz$

$ <<<"$l"   sed -n l
foo\tbar\tbaz$

bash 中的实现存在差异,如下所示:

l="$(printf '%s\t%s\t%s\n' foo bar baz)"; <<<$l  sed -n l

这是大多数 shell 的结果:

/bin/sh         : foo bar baz$
/bin/b43sh      : foo bar baz$
/bin/bash       : foo bar baz$
/bin/b44sh      : foo\tbar\tbaz$
/bin/y2sh       : foo\tbar\tbaz$
/bin/ksh        : foo\tbar\tbaz$
/bin/ksh93      : foo\tbar\tbaz$
/bin/lksh       : foo\tbar\tbaz$
/bin/mksh       : foo\tbar\tbaz$
/bin/mksh-static: foo\tbar\tbaz$
/usr/bin/ksh    : foo\tbar\tbaz$
/bin/zsh        : foo\tbar\tbaz$
/bin/zsh4       : foo\tbar\tbaz$

只有 bash 在不加引号时才将变量拆分在右侧<<<
然而,这个问题在 bash 版本 4.4 上已得到纠正,
这意味着 的值$IFS会影响 的结果<<<


与行:

l=(1 2 3); IFS=:; sed -n l <<<"${l[*]}"

所有 shell 都使用 IFS 的第一个字符来连接值。

/bin/y2sh       : 1:2:3$
/bin/sh         : 1:2:3$
/bin/b43sh      : 1:2:3$
/bin/b44sh      : 1:2:3$
/bin/bash       : 1:2:3$
/bin/ksh        : 1:2:3$
/bin/ksh93      : 1:2:3$
/bin/lksh       : 1:2:3$
/bin/mksh       : 1:2:3$
/bin/zsh        : 1:2:3$
/bin/zsh4       : 1:2:3$

对于"${l[@]}",需要一个空格来分隔不同的参数,但某些 shell 选择使用 IFS 中的值(正确吗?)。

/bin/y2sh       : 1:2:3$
/bin/sh         : 1 2 3$
/bin/b43sh      : 1 2 3$
/bin/b44sh      : 1 2 3$
/bin/bash       : 1 2 3$
/bin/ksh        : 1 2 3$
/bin/ksh93      : 1 2 3$
/bin/lksh       : 1:2:3$
/bin/mksh       : 1:2:3$
/bin/zsh        : 1:2:3$
/bin/zsh4       : 1:2:3$

对于空 IFS,这些值应该被连接起来,就像这一行一样:

a=(1 2 3); IFS=''; sed -n l <<<"${a[*]}"

/bin/y2sh       : 123$
/bin/sh         : 123$
/bin/b43sh      : 123$
/bin/b44sh      : 123$
/bin/bash       : 123$
/bin/ksh        : 123$
/bin/ksh93      : 123$
/bin/lksh       : 1 2 3$
/bin/mksh       : 1 2 3$
/bin/zsh        : 123$
/bin/zsh4       : 123$

但 lksh 和 mksh 都未能做到这一点。

如果我们更改为参数列表:

l=(1 2 3); IFS=''; sed -n l <<<"${l[@]}"

/bin/y2sh       : 123$
/bin/sh         : 1 2 3$
/bin/b43sh      : 1 2 3$
/bin/b44sh      : 1 2 3$
/bin/bash       : 1 2 3$
/bin/ksh        : 1 2 3$
/bin/ksh93      : 1 2 3$
/bin/lksh       : 1 2 3$
/bin/mksh       : 1 2 3$
/bin/zsh        : 123$
/bin/zsh4       : 123$

yash 和 zsh 都无法将参数分开。这是一个错误吗?

相关内容