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