Por que o cut falha com o bash e não com o zsh?

10

Eu crio um arquivo com campos delimitados por tabulação.

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 testei.

$ ./zsh.sh input
bar
bar

Isso funciona bem. No entanto, quando altero a primeira linha para chamar bash, ela falha.

$ ./bash.sh input
foo bar baz
foo bar baz

Por que isso falha bashe funciona zsh?

Solução de problemas adicionais

  • Usar caminhos diretos no shebang em vez de envproduzir o mesmo comportamento.
  • Tubulação com em echovez de usar a cadeia aqui <<<$linetambém produz o mesmo comportamento. ie echo $line | cut -f 2.
  • Usando em awkvez de cut funciona para as duas conchas. ie <<<$line awk '{print $2}'.
Sparhawk
fonte
4
By the way, você pode fazer o seu arquivo de teste mais simplesmente fazendo um destes: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...', ou printf 'foo\tbar\tbaz\n...\n'ou variações destes. Isso evita que você precise quebrar individualmente cada guia ou nova linha.
Pausado até novo aviso.

Respostas:

13

O que acontece é que bashsubstitui as guias por espaços. Você pode evitar esse problema dizendo em "$line"vez disso ou cortando explicitamente os espaços.

Michael Vehrs
fonte
1
Existe alguma razão pela qual o Bash vê um \te o substitui por um espaço?
user1717828
@ user1717828 sim, é chamado de operador spit + glob . É o que acontece quando você usa uma variável não citada no bash e em conchas similares.
terdon
1
@terdon, em <<< $line, bashdivide, mas não em glob. Não há razão para isso se dividir aqui, pois <<<espera uma única palavra. Ele se divide e se une nesse caso, o que faz pouco sentido e é contra todas as outras implementações de shells suportadas <<<antes ou depois bash. OMI é um bug.
Stéphane Chazelas
@ StéphaneChazelas justo o suficiente, o problema é com a parte dividida de qualquer maneira.
terdon
2
@ StéphaneChazelas Nenhuma divisão (nem glob) acontece no bash 4.4
17

Isso porque <<< $line, em , basha divisão de palavras (embora não exagere) é ativada, $linepois não é citada lá e, em seguida, junta as palavras resultantes ao caractere de espaço (e coloca isso em um arquivo temporário seguido por um caractere de nova linha e torna isso o stdin de cut).

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

tabpassa a ter o valor padrão de $IFS:

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

A solução com bashé citar a variável.

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

Observe que é o único shell que faz isso. zsh(de onde <<<vem, inspirado na porta Unix de rc) ksh93, mkshe yashque também suporta <<<não o fazem.

Quando se trata de matrizes, mksh, yashe zshjuntar-se no primeiro caractere de $IFS, bashe ksh93no 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/ yashe mksh(versão R52 pelo menos) quando $IFSestá 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 as conchas quando você usa "${a[*]}"(exceto que mkshainda possui um erro quando $IFSestá vazio).

Em echo $line | ..., esse é o operador split + glob usual em todas as conchas do tipo Bourne, mas zsh(e os problemas usuais associados echo).

Stéphane Chazelas
fonte
1
Excelente resposta! Obrigado (+1). Eu aceitarei o interlocutor com o menor número de perguntas, já que eles responderam à pergunta perfeitamente bem o suficiente para revelar minha estupidez.
Sparhawk
10

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 melhor com isso. Agora, cutusa \tcomo o delimitador de campo por padrão. Portanto, como seu bashscript está consumindo as guias (por causa do operador split + glob), cutapenas vê um campo e age de acordo. O que você realmente está executando é:

$ echo "foo bar baz" | cut -f 2
foo bar baz

Portanto, para que seu script funcione conforme o esperado nos dois shells, cite sua variável:

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

Então, ambos produzem a mesma saída:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar
terdon
fonte
Excelente resposta! Obrigado (+1). Eu aceitarei o interlocutor com o menor número de perguntas, já que eles responderam à pergunta perfeitamente bem o suficiente para revelar minha estupidez.
Sparhawk
^ voto por ser a única resposta (ainda) para realmente incluir o corrigiubash.sh
lauir
1

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$

Há 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 das conchas:

/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 <<<quando não está entre aspas.
No entanto, isso foi corrigido na versão 4.4 do bash.
Isso significa que o valor de $IFSafeta 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[@]}", é necessário um espaço para separar os diferentes argumentos, mas algumas shells optam por 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 se unir, 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 quanto 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$

Yash e zsh falham em manter os argumentos separados. Isso é um bug?


fonte
Sobre zsh/ yashe "${l[@]}"em um contexto que não é de lista, é por design que "${l[@]}"é especial apenas em contextos de lista. Em contextos que não são de lista, não há separação possível, você precisa juntar os elementos de alguma forma. Unir-se ao primeiro caractere de $ IFS é mais consistente do que unir-se a um caractere de espaço IMO. dashfaz isso também ( dash -c 'IFS=; a=$@; echo "$a"' x a b). O POSIX, no entanto, pretende mudar esse IIRC. Veja esta discussão (longa)
Stéphane Chazelas
Respondendo a mim mesmo, não, dando uma segunda olhada, o POSIX deixará o comportamento para var=$@não especificado.
Stéphane Chazelas