Почему вырезать не удается с помощью bash, а не zsh?

10

Я создаю файл с разделенными табуляцией полями.

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

У меня есть следующий скрипт с именем zsh.sh

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

Я проверяю это.

$ ./zsh.sh input
bar
bar

Это отлично работает. Однако, когда я изменяю первую строку, чтобы вызвать bashвместо этого, это терпит неудачу.

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

Почему это терпит неудачу bashи работает с zsh?

Дополнительное устранение неполадок

  • Использование прямых путей в шебанге вместо того envже самого поведения.
  • Трубопровод с echoиспользованием здесь-строки <<<$lineтакже приводит к тому же самому поведению. то есть echo $line | cut -f 2.
  • Использование awkвместо cut работает для обеих оболочек. то есть <<<$line awk '{print $2}'.
Sparhawk
источник
4
Кстати, вы можете сделать свой файл тест более просто, выполнив одно из них: echo -e 'foo\tbar\tbaz\n...', echo $'foo\tbar\tbaz\n...'или , printf 'foo\tbar\tbaz\n...\n'или вариации этих. Это избавляет вас от необходимости индивидуально переносить каждую вкладку или новую строку.
Приостановлено до дальнейшего уведомления.

Ответы:

13

Что происходит, это bashзаменяет вкладки пробелами. Вы можете избежать этой проблемы, говоря "$line"вместо этого, или явно сокращая пробелы.

Майкл Верс
источник
1
Есть ли какая-то причина, по которой Bash видит \tи заменяет ее пробелом?
user1717828 10.06.16
@ user1717828 да, это называется оператор spit + glob . Это то, что происходит, когда вы используете переменную без кавычек в bash и аналогичных оболочках.
Тердон
1
@terdon, in <<< $line, делит, bashно не глобус. Нет никаких причин, по которым это могло бы быть разделено здесь, как <<<ожидается, одно слово. В этом случае он разделяется и затем присоединяется, что не имеет большого смысла и противоречит всем другим реализациям оболочек, которые поддерживаются <<<до или после bash. ИМО это ошибка.
Стефан Шазелас
@ StéphaneChazelas достаточно справедливо, проблема с разделенной частью в любом случае.
Тердон
2
@ StéphaneChazelas На bash 4.4 не происходит разделения (и глобуса)
17

Это связано с тем <<< $line, bashчто разделение слов выполняется (хотя и не глобально), так $lineкак оно там не заключено в кавычки, а затем соединяет результирующие слова с пробелом (и помещает их во временный файл, после которого следует символ новой строки и делает его стандартным cut).

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

tabслучается в значении по умолчанию $IFS:

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

Решение с помощью bashкавычки переменной.

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

Обратите внимание, что это единственная оболочка, которая делает это. zsh(откуда <<<взято, вдохновлено портом Unix rc) ksh93, mkshи yashкоторые также поддерживают <<<, не делают этого.

Когда речь идет о массивах mksh, yashи zshприсоединиться на первый символ $IFS, bashи ksh93на пространстве.

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

Существует разница между zsh/ yashи mksh(по крайней мере, версия R52), когда $IFSпусто:

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

Поведение является более согласованным для всех оболочек, когда вы используете "${a[*]}"(за исключением того, что mkshошибка $IFSостается пустой).

Во- echo $line | ...первых, это обычный оператор split + glob во всех Bourne-подобных оболочках, но zsh(и обычные проблемы, связанные с echo).

Стефан Шазелас
источник
1
Отличный ответ! Спасибо (+1). Тем не менее, я приму спрашивающего с самым низким ответом, так как они ответили на вопрос достаточно хорошо, чтобы раскрыть мою глупость.
Sparhawk
10

Проблема в том, что вы не цитируете $line. Чтобы исследовать, измените два сценария, чтобы они просто печатали $line:

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

и

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

Теперь сравните их вывод:

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

Как вы можете видеть, поскольку вы не цитируете $line, вкладки неправильно интерпретируются bash. Zsh, кажется, справляется с этим лучше. Теперь по умолчанию cutиспользуется \tв качестве разделителя полей. Следовательно, поскольку ваш bashсценарий потребляет вкладки (из-за оператора split + glob), он cutвидит только одно поле и действует соответственно. То, что вы действительно запускаете, это:

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

Итак, чтобы ваш скрипт работал как положено в обеих оболочках, заключите вашу переменную в кавычки:

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

Затем оба выдают одинаковый результат:

$ bash.sh input 
bar
bar
$ zsh.sh input 
bar
bar
Тердон
источник
Отличный ответ! Спасибо (+1). Тем не менее, я приму спрашивающего с самым низким ответом, так как они ответили на вопрос достаточно хорошо, чтобы раскрыть мою глупость.
Sparhawk
^ голосовать за единственный ответ (пока), чтобы фактически включить исправленныйbash.sh
lauir
1

Как уже было сказано, более переносимый способ использования переменной - заключить ее в кавычки:

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

В bash есть разница в реализации:

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

Это результат большинства оболочек:

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

Только bash разделяет переменную справа от <<<кавычек.
Однако это было исправлено в bash версии 4.4.
Это означает, что значение $IFSвлияет на результат <<<.


С линией:

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

Все оболочки используют первый символ IFS для объединения значений.

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

При этом "${l[@]}"необходим пробел для разделения различных аргументов, но некоторые оболочки предпочитают использовать значение из IFS (это правильно?).

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

При нулевом IFS значения должны быть объединены, как в этой строке:

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$

Но и lksh, и mksh не могут этого сделать.

Если мы перейдем к списку аргументов:

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, и zsh не могут разделить аргументы. Это ошибка?


источник
О zsh/ yashи "${l[@]}"в контексте, не относящемся к списку, это дизайн, который "${l[@]}"является особенным только в контексте списка. В контекстах, не относящихся к списку, разделение невозможно, вам нужно как-то объединить элементы. Присоединение к первому символу $ IFS более согласованно, чем соединение с пробелом IMO. dashделает это также ( dash -c 'IFS=; a=$@; echo "$a"' x a b). Тем не менее, POSIX намеревается изменить этот IIRC. Смотрите это (длинное) обсуждение
Стефан Шазелас
Отвечая себе, нет, имея второй взгляд, POSIX оставит поведение для var=$@неопределенного.
Стефан Шазелас