O que acontece é que bash
substitui as guias por espaços. Você pode evitar esse problema dizendo "$line"
ou explicitamente cortando espaços.
Eu criei um arquivo com campos delimitados por tabulações.
echo foo$'\t'bar$'\t'baz$'\n'foo$'\t'bar$'\t'baz > input
Eu tenho o seguinte script chamado zsh.sh
#!/usr/bin/env zsh
while read line; do
<<<$line cut -f 2
done < "$1"
Eu testo isso.
$ ./zsh.sh input
bar
bar
Isso funciona bem. No entanto, quando eu mudo a primeira linha para invocar bash
, ele falha.
$ ./bash.sh input
foo bar baz
foo bar baz
Por que isso falha com bash
e funciona com zsh
?
env
, produz o mesmo comportamento. echo
em vez de usar o aqui string <<<$line
também produz o mesmo comportamento. ou seja, echo $line | cut -f 2
. awk
em vez de cut
funciona para ambos os shells. ou seja, <<<$line awk '{print $2}'
. Isso porque, em <<< $line
, bash
faz a divisão de palavras (embora não globbing) em $line
, pois não é mencionado e, em seguida, une as palavras resultantes ao caractere de espaço (e coloca isso em um arquivo temporário seguido por um caractere de nova linha e faz com que o stdin de cut
).
$ a=a,b,,c bash -c 'IFS=","; sed -n l <<< $a'
a b c$
tab
está no valor padrão de $IFS
:
$ a=$'a\tb' bash -c 'sed -n l <<< $a'
a b$
A solução com bash
é para citar a variável.
$ a=$'a\tb' bash -c 'sed -n l <<< "$a"'
a\tb$
Note que é o único shell que faz isso. zsh
(onde <<<
vem, inspirado pela porta Unix de rc
), ksh93
, mksh
e yash
que também suportam <<<
não o façam.
Quando se trata de matrizes, mksh
, yash
e zsh
se associam ao primeiro caractere de $IFS
, bash
e ksh93
no espaço.
$ 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$
Há uma diferença entre zsh
/ yash
e mksh
(versão R52 pelo menos) quando $IFS
está vazio:
$ mksh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
12$
O comportamento é mais consistente entre os shells quando você usa "${a[*]}"
(exceto que mksh
ainda tem um bug quando $IFS
está vazio).
Em echo $line | ...
, esse é o operador usual de divisão + glob em todos os shells parecidos com Bourne, mas zsh
(e os problemas comuns associados a echo
).
O problema é que você não está citando $line
. Para investigar, altere os dois scripts para que eles simplesmente imprimam $line
:
#!/usr/bin/env bash
while read line; do
echo $line
done < "$1"
e
#!/usr/bin/env zsh
while read line; do
echo $line
done < "$1"
Agora, compare sua saída:
$ bash.sh input
foo bar baz
foo bar baz
$ zsh.sh input
foo bar baz
foo bar baz
Como você pode ver, porque você não está citando $line
, as guias não são interpretadas corretamente pelo bash. Zsh parece lidar com isso melhor. Agora, cut
usa \t
como o delimitador de campo por padrão. Portanto, como o script bash
está comendo as guias (por causa do operador split + glob), cut
vê apenas um campo e age de acordo. O que você está realmente executando é:
$ echo "foo bar baz" | cut -f 2
foo bar baz
Então, para que seu script funcione como esperado em ambos os shells, cite sua variável:
while read line; do
<<<"$line" cut -f 2
done < "$1"
Em seguida, ambos produzem a mesma saída:
$ bash.sh input
bar
bar
$ zsh.sh input
bar
bar
Como já foi respondido, uma maneira mais portátil de usar uma variável é citá-la:
$ 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$
Existe uma diferença de implementação no bash, com a linha:
l="$(printf '%s\t%s\t%s\n' foo bar baz)"; <<<$l sed -n l
Este é o resultado da maioria dos shells:
/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$
Somente o bash divide a variável à direita de <<<
quando não estiver em uma lista.
No entanto, isso foi corrigido na versão bash 4.4.
Isso significa que o valor de $IFS
afeta o resultado de <<<
.
Com a linha:
l=(1 2 3); IFS=:; sed -n l <<<"${l[*]}"
Todos os shells usam o primeiro caractere do IFS para unir valores.
/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$
Com "${l[@]}"
, um espaço é necessário para separar os diferentes argumentos, mas alguns shells escolhem usar o valor do IFS (Isso está correto?).
/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$
Com um IFS nulo, os valores devem ser unidos, como nesta linha:
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$
Mas tanto o lksh como o mksh não conseguem fazê-lo.
Se mudarmos para uma lista de argumentos:
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$
Tanto o yash quanto o zsh não mantêm os argumentos separados. Isso é um bug?
Tags bash zsh whitespace quoting here-string