¿Por qué falla el corte con bash y no con zsh?

10

Creo un archivo con campos delimitados por tabuladores.

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

Tengo el siguiente script llamado zsh.sh

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

Lo pruebo

$ ./zsh.sh input
bar
bar

Esto funciona bien Sin embargo, cuando cambio la primera línea para invocar bash, falla.

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

¿Por qué falla bashy funciona con esto zsh?

Solución de problemas adicional

  • Usar rutas directas en el shebang en lugar de envproducir el mismo comportamiento.
  • Tubería en echolugar de usar la cadena aquí <<<$linetambién produce el mismo comportamiento. es decir echo $line | cut -f 2.
  • Utilizando en awklugar de cut obras para ambos proyectiles. es decir <<<$line awk '{print $2}'.
Gavilán
fuente
44
Por cierto, usted puede hacer su archivo de prueba más simple mediante una de las siguientes: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...'o printf 'foo\tbar\tbaz\n...\n'o variaciones de éstos. Le ahorra tener que ajustar individualmente cada pestaña o nueva línea.
Pausado hasta nuevo aviso.

Respuestas:

13

Lo que sucede es que bashreemplaza las pestañas con espacios. Puede evitar este problema diciendo en su "$line"lugar, o cortando explícitamente espacios.

Michael Vehrs
fuente
1
¿Hay alguna razón por la que Bash ve un \ty lo reemplaza con un espacio?
user1717828
@ user1717828 sí, se llama el operador spit + glob . Es lo que sucede cuando usa una variable sin comillas en bash y shells similares.
terdon
1
@terdon, en <<< $line, bashse divide pero no es global. No hay razón para que se divida aquí, ya que <<<espera una sola palabra. Se divide y luego se une en ese caso, lo que tiene poco sentido y está en contra de todas las otras implementaciones de shells que han sido compatibles <<<antes o después bash. OMI es un error.
Stéphane Chazelas
@ StéphaneChazelas bastante justo, el problema es con la parte dividida de todos modos.
terdon
2
@ StéphaneChazelas No hay división (ni glob) en bash 4.4
17

Esto se debe a que <<< $line, en , la bashdivisión de palabras (aunque no engloba) $lineya que no se cita allí y luego une las palabras resultantes con el carácter de espacio (y lo coloca en un archivo temporal seguido de un carácter de nueva línea y lo convierte en el stdin de cut).

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

tabpasa a estar en el valor predeterminado de $IFS:

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

La solución con bashes citar la variable.

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

Tenga en cuenta que es el único shell que hace eso. zsh(de dónde <<<viene, inspirado en el puerto Unix de rc) ksh93, mkshy yashque también es compatible <<<, no lo hagas.

Cuando se trata de matrices, mksh, yashy zshunirse en el primer carácter de $IFS, bashy ksh93en el espacio.

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

Hay una diferencia entre zsh/ yashy mksh(versión R52 al menos) cuando $IFSestá vacío:

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

El comportamiento es más consistente en todos los shells cuando se usa "${a[*]}"(excepto que mkshtodavía tiene un error cuando $IFSestá vacío).

En echo $line | ..., ese es el operador habitual de split + glob en todos los shells similares a Bourne pero zsh(y los problemas habituales asociados con echo).

Stéphane Chazelas
fuente
1
Excelente respuesta! Gracias (+1) Sin embargo, aceptaré al interrogador más bajo, ya que respondieron la pregunta lo suficientemente bien como para revelar mi estupidez.
Sparhawk
10

El problema es que no estás citando $line. Para investigar, cambie los dos scripts para que simplemente impriman $line:

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

y

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

Ahora, compare su salida:

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

Como puede ver, porque no está citando $line, bash no interpreta las pestañas correctamente. Zsh parece lidiar con eso mejor. Ahora, se cutusa \tcomo delimitador de campo por defecto. Por lo tanto, dado que su bashscript está comiendo las pestañas (debido al operador split + glob), cutsolo ve un campo y actúa en consecuencia. Lo que realmente está ejecutando es:

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

Entonces, para que su script funcione como se espera en ambos shells, cite su variable:

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

Entonces, ambos producen la misma salida:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar
terdon
fuente
Excelente respuesta! Gracias (+1) Sin embargo, aceptaré al interrogador más bajo, ya que respondieron la pregunta lo suficientemente bien como para revelar mi estupidez.
Sparhawk
^ vote por ser la única respuesta (hasta ahora) que realmente incluya la correcciónbash.sh
lauir
1

Como ya se ha respondido, una forma más portátil de usar una variable es citarla:

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

Hay una diferencia de implementación en bash, con la línea:

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

Este es el resultado de la mayoría de los proyectiles:

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

Solo bash divide la variable a la derecha de <<<cuando no está entre comillas.
Sin embargo, eso se ha corregido en la versión 4.4 de bash.
Eso significa que el valor de $IFSafecta el resultado de <<<.


Con la linea:

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

Todos los shells usan el primer carácter de 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$

Con "${l[@]}", se necesita un espacio para separar los diferentes argumentos, pero algunos shells eligen usar el valor de IFS (¿Es correcto?).

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

Con un IFS nulo, los valores deben unirse, como con esta línea:

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$

Pero tanto lksh como mksh no lo hacen.

Si cambiamos a una 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 yash como zsh no pueden mantener los argumentos separados. ¿Eso es un error?


fuente
Acerca de zsh/ yashy "${l[@]}"en contextos que no son listas, eso es por diseño, donde "${l[@]}"solo es especial en contextos de listas. En contextos que no son listas, no hay separación posible, debe unir los elementos de alguna manera. Unirse con el primer personaje de $ IFS es más consistente que unirse con un personaje espacial IMO. dashlo hace también ( dash -c 'IFS=; a=$@; echo "$a"' x a b). Sin embargo, POSIX tiene la intención de cambiar ese IIRC. Ver esta (larga) discusión
Stéphane Chazelas
Respondiéndome a mí mismo, no, teniendo una segunda mirada, POSIX dejará el comportamiento sin var=$@especificar.
Stéphane Chazelas