Why does cut fail with bash and not zsh?
That's because in <<< $line
, bash
versions prior to 4.4 did word splitting, (though not globbing) on $line
when not quoted there and then joined the resulting words with the space character (and put that in a temporary file followed by a newline character and make that the stdin of cut
).
$ a=a,b,,c bash-4.3 -c 'IFS=","; sed -n l <<< $a'
a b c$
tab
happens to be in the default value of $IFS
:
$ a=$'a\tb' bash-4.3 -c 'sed -n l <<< $a'
a b$
The solution with bash
is to quote the variable.
$ a=$'a\tb' bash -c 'sed -n l <<< "$a"'
a\tb$
Note that it's the only shell that does that. zsh
(where <<<
comes from, inspired by Byron Rakitzis's implementation of rc
), ksh93
, mksh
and yash
which also support <<<
don't do it.
When it comes to arrays, mksh
, yash
and zsh
join on the first character of $IFS
, bash
and ksh93
on space.
$ 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$
There's a difference between zsh
/yash
and mksh
(version R52 at least) when $IFS
is empty:
$ mksh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
12$
The behaviour is more consistent across shells when you use "${a[*]}"
(except that mksh
still has a bug when $IFS
is empty).
In echo $line | ...
, that's the usual split+glob operator in all Bourne-like shells but zsh
(and the usual problems associated with echo
).
What happens is that bash
replaces the tabs with spaces. You can avoid this problem by saying "$line"
instead, or by explicitly cutting on spaces.
The problem is that you're not quoting $line
. To investigate, change the two scripts so they simply print $line
:
#!/usr/bin/env bash
while read line; do
echo $line
done < "$1"
and
#!/usr/bin/env zsh
while read line; do
echo $line
done < "$1"
Now, compare their output:
$ bash.sh input
foo bar baz
foo bar baz
$ zsh.sh input
foo bar baz
foo bar baz
As you can see, because you're not quoting $line
, the tabs aren't interpreted correctly by bash. Zsh seems to deal with that better. Now, cut
uses \t
as the field delimiter by default. Therefore, since your bash
script is eating the tabs (because of the split+glob operator), cut
only sees one field and acts accordingly. What you are really running is:
$ echo "foo bar baz" | cut -f 2
foo bar baz
So, to get your script to work as expected in both shells, quote your variable:
while read line; do
<<<"$line" cut -f 2
done < "$1"
Then, both produce the same output:
$ bash.sh input
bar
bar
$ zsh.sh input
bar
bar