我创建一个带有制表符分隔字段的文件。
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
也会产生相同的行为。 IEecho $line | cut -f 2
。 - 使用
awk
代替cut
作品对于两个外壳。 IE<<<$line awk '{print $2}'
。
答案1
这是因为在4.4 之前的版本中<<< $line
,当没有引用时会进行分词(尽管不是通配符),然后用空格字符连接生成的单词(并将其放入临时文件中,后跟换行符,并将其设为标准输入的)。bash
$line
cut
$ 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
),ksh93
,mksh
并且yash
也支持<<<
不这样做。
当涉及到数组时,mksh
、yash
和zsh
连接在 的第一个字符上$IFS
,bash
并ksh93
在空格上连接。
$ 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
(至少 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 都无法将参数分开。这是一个错误吗?