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


10

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

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代替而不使用here-string <<<$line也会产生相同的行为。即echo $line | cut -f 2
  • 使用awk的,而不是cut 作品的两个壳。即<<<$line awk '{print $2}'

4
顺便说一句,你可以通过做这些中的一个使你的测试文件更简单:echo -e 'foo\tbar\tbaz\n...'echo $'foo\tbar\tbaz\n...'printf 'foo\tbar\tbaz\n...\n'或这些变化。它使您不必单独包装每个选项卡或换行符。
暂停,直到另行通知。

Answers:


13

发生的情况是bash用空格替换了选项卡。您可以改成说"$line",也可以通过明确地减少空格来避免此问题。


1
Bash有任何理由看到a \t并将其替换为空格吗?
user1717828

@ user1717828是的,它称为spit + glob运算符。当您在bash和类似的shell中使用未引用的变量时,就会发生这种情况。
terdon

1
@terdon在<<< $linebash不分裂而不是glob的。它没有理由像<<<期望的那样在这里拆分。在这种情况下,它将拆分然后再加入,这没有什么意义,并且与<<<之前或之后支持的所有其他shell实现相反bash。IMO这是一个错误。
斯特凡Chazelas

@StéphaneChazelas足够公平,问题仍然在于拆分部分。
terdon

2
@StéphaneChazelas在bash 4.4上未发生任何分裂(也未出现全局球)

17

这是因为in <<< $linebash会进行单词拆分(尽管不会引起混乱),$line因为它没有被引用,然后将结果单词与空格字符连接在一起(并将其放在一个临时文件中,然后是一个换行符,使它成为stdin cut)。

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

tab碰巧是默认值$IFS

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

解决的办法bash是引用变量。

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

请注意,它是唯一执行此操作的外壳程序。zsh(其<<<来源来自,受Unix端口启发rcksh93mksh并且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/ yash和之间mksh(至少$IFS是R52版)之间存在差异:

$ 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)。


1
很好的答案!谢谢(+1)。不过,我会接受最低限度的提问者,因为他们回答得很好,足以显示出我的愚蠢。
Sparhawk

10

问题是您没有报价$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

因此,要使脚本在两个外壳中都能正常工作,请引用变量:

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

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

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

很好的答案!谢谢(+1)。不过,我会接受最低限度的提问者,因为他们回答得很好,足以显示出我的愚蠢。
Sparhawk

^投票表示(是迄今为止)唯一实际包含更正的答案bash.sh
lauir

1

正如已经回答的那样,使用变量的一种更可移植的方法是将其引用:

$ 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[*]}"

所有外壳程序都使用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[@]}",需要一个空格来分隔不同的参数,但是某些外壳程序选择使用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都无法使参数分开。那是个错误吗?


关于zsh/ yash"${l[@]}"在非列表上下文中,这是设计使然"${l[@]}",仅在列表上下文中是特殊的。在非列表上下文中,不可能分离,您需要以某种方式加入元素。与$ IFS的第一个字符联接比与空格字符IMO联接更一致。dash也做到了(dash -c 'IFS=; a=$@; echo "$a"' x a b)。但是,POSIX打算更改该IIRC。请参见本(长)讨论
斯特凡Chazelas


回答自己,不,再看一遍,POSIX会保留var=$@未指定的行为。
斯特凡Chazelas
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.