Pourquoi cut échoue avec bash et non zsh?

10

Je crée un fichier avec des champs délimités par des tabulations.

echo foo$'\t'bar$'\t'baz$'\n'foo$'\t'bar$'\t'baz > input

J'ai le script suivant nommé zsh.sh

#!/usr/bin/env zsh
while read line; do
    <<<$line cut -f 2
done < "$1"

Je le teste.

$ ./zsh.sh input
bar
bar

Cela fonctionne bien. Cependant, lorsque je modifie la première ligne à appeler à la bashplace, cela échoue.

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

Pourquoi cela échoue bashet fonctionne avec zsh?

Dépannage supplémentaire

  • L'utilisation de chemins directs dans le shebang au lieu de envproduit le même comportement.
  • Tuyauter avec echoau lieu d'utiliser la chaîne here-string <<<$lineproduit également le même comportement. ie echo $line | cut -f 2.
  • Utilisation awkau lieu de cut travaux pour les deux coques. ie <<<$line awk '{print $2}'.
Sparhawk
la source
4
Soit dit en passant, vous pouvez rendre votre fichier de test plus simplement en effectuant l' une de ces: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...'ou printf 'foo\tbar\tbaz\n...\n'ou des variations de ceux - ci. Cela vous évite d'avoir à envelopper individuellement chaque onglet ou nouvelle ligne.
pause jusqu'à nouvel ordre.

Réponses:

13

Ce qui se passe c'est que bashles tabulations sont remplacées par des espaces. Vous pouvez éviter ce problème en disant à la "$line"place ou en coupant explicitement les espaces.

Michael Vehrs
la source
1
Y a-t-il une raison pour laquelle Bash voit un \tet le remplace par un espace?
user1717828
@ user1717828 oui, cela s'appelle l' opérateur spit + glob . C'est ce qui se passe lorsque vous utilisez une variable non citée dans bash et des shells similaires.
terdon
1
@terdon, in <<< $line, se bashdivise mais pas glob. Il n'y a aucune raison qu'il se divise ici, comme le <<<prévoit un seul mot. Il se divise puis rejoint dans ce cas, ce qui n'a pas de sens et est contre toutes les autres implémentations de shells qui ont pris en charge <<<avant ou après bash. OMI c'est un bug.
Stéphane Chazelas
@ StéphaneChazelas juste, le problème est quand même avec la partie scindée.
terdon
2
@ StéphaneChazelas Aucune division (ni glob) ne se produit sur bash 4.4
17

En effet, dans <<< $line, le bashfractionnement des mots est activé (mais pas en globes) $linecar il n'y est pas cité, puis joint les mots résultants avec le caractère espace (et le place dans un fichier temporaire suivi d'un caractère de nouvelle ligne et en fait le stdin de cut).

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

tabse trouve être dans la valeur par défaut de $IFS:

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

La solution avec bashest de citer la variable.

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

Notez que c'est le seul shell qui fait ça. zsh(d'où <<<vient, inspiré par le port Unix de rc) ksh93, mkshet yashqui supporte aussi <<<ne le faites pas.

En ce qui concerne les tableaux, mksh, yashet zshrejoindre le premier caractère $IFS, bashet ksh93sur l' espace.

$ 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$

Il y a une différence entre zsh/ yashet mksh(version R52 au moins) quand $IFSest vide:

$ mksh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
1 2$
$ zsh -c 'a=(1 2); IFS=; sed -n l <<< "${a[@]}"'
12$

Le comportement est plus cohérent entre les shells lorsque vous utilisez "${a[*]}"(sauf qu'il mkshy a toujours un bug quand $IFSest vide).

Dans echo $line | ..., c'est l'opérateur split + glob habituel dans tous les shells de type Bourne mais zsh(et les problèmes habituels associés à echo).

Stéphane Chazelas
la source
1
Excellente réponse! Merci (+1). J'accepterai cependant le plus bas intervenant, car ils ont parfaitement répondu à la question pour révéler ma stupidité.
Sparhawk
10

Le problème est que vous ne citez pas $line. Pour enquêter, modifiez les deux scripts afin qu'ils s'impriment simplement $line:

#!/usr/bin/env bash
while read line; do
    echo $line
done < "$1"

et

#!/usr/bin/env zsh
while read line; do
    echo $line
done < "$1"

Maintenant, comparez leur sortie:

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

Comme vous pouvez le voir, parce que vous ne citez pas $line, les onglets ne sont pas interprétés correctement par bash. Zsh semble mieux gérer cela. Maintenant, cututilise \tcomme délimiteur de champ par défaut. Par conséquent, puisque votre bashscript mange les onglets (en raison de l'opérateur split + glob), cutne voit qu'un seul champ et agit en conséquence. Ce que vous courez vraiment, c'est:

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

Donc, pour que votre script fonctionne comme prévu dans les deux shells, citez votre variable:

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

Ensuite, les deux produisent la même sortie:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar
terdon
la source
Excellente réponse! Merci (+1). J'accepterai cependant le plus bas intervenant, car ils ont parfaitement répondu à la question pour révéler ma stupidité.
Sparhawk
^ votez pour être la seule réponse (pour l'instant) à inclure réellement le corrigébash.sh
lauir
1

Comme il a déjà été répondu, une façon plus portable d'utiliser une variable est de la citer:

$ 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$

Il y a une différence d'implémentation en bash, avec la ligne:

l="$(printf '%s\t%s\t%s\n' foo bar baz)"; <<<$l  sed -n l

C'est le résultat de la plupart des obus:

/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$

Seul bash divise la variable à droite de <<<lorsqu'elle n'est pas citée.
Cependant, cela a été corrigé dans la version 4.4 de bash.
Cela signifie que la valeur de $IFSaffecte le résultat de <<<.


Avec la ligne:

l=(1 2 3); IFS=:; sed -n l <<<"${l[*]}"

Tous les shells utilisent le premier caractère d'IFS pour joindre des valeurs.

/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$

Avec "${l[@]}", un espace est nécessaire pour séparer les différents arguments, mais certains shells choisissent d'utiliser la valeur d'IFS (est-ce correct?).

/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$

Avec un IFS nul, les valeurs doivent devenir jointes, comme avec cette ligne:

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$

Mais lksh et mksh n'y parviennent pas.

Si nous passons à une liste d'arguments:

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 et zsh ne parviennent pas à séparer les arguments. Est-ce un bug?


la source
À propos de zsh/ yashet "${l[@]}"dans un contexte non-liste, c'est par conception où il "${l[@]}"n'est spécial que dans des contextes de liste. Dans les contextes non listés, il n'y a pas de séparation possible, vous devez en quelque sorte joindre les éléments. La jonction avec le premier caractère de $ IFS est plus cohérente que la jonction avec un caractère espace IMO. dashle fait aussi ( dash -c 'IFS=; a=$@; echo "$a"' x a b). Cependant, POSIX a l'intention de changer l'IIRC. Voir cette (longue) discussion
Stéphane Chazelas
Répondant à moi-même, non, ayant un deuxième regard, POSIX laissera le comportement pour var=$@non spécifié.
Stéphane Chazelas