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

10

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 ?

Resolução de problemas adicionais

  • O uso de caminhos diretos no shebang, em vez de env , produz o mesmo comportamento.
  • O piping com echo em vez de usar o aqui string <<<$line também produz o mesmo comportamento. ou seja, echo $line | cut -f 2 .
  • Usando awk em vez de cut funciona para ambos os shells. ou seja, <<<$line awk '{print $2}' .
por Sparhawk 10.06.2016 / 13:21

4 respostas

13

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

    
por 10.06.2016 / 13:44
17

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

    
por 10.06.2016 / 13:44
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 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
    
por 10.06.2016 / 13:44
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$

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?

    
por 10.06.2016 / 23:05