Shell читать * иногда * полоски трейлинг-разделителя

Чтобы разобрать поля с разделителями двоеточия, я могу использовать read с пользовательским IFS:

$ echo 'foo.c:41:switch (color) {' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 41 | switch (color) {

Если последнее поле содержит двоеточие, никаких проблем, двоеточия сохраняются.

$ echo 'foo.c:42:case RED: //alert' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED: //alert

Задерживающий разделитель также сохраняется...

$ echo 'foo.c:42:case RED: //alert:' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED: //alert:

... Если это не единственный дополнительный разделитель. Затем он лишился. Чего ждать?

$ echo 'foo.c:42:case RED:' | { IFS=: read file line text && echo "$file | $line | $text"; }
foo.c | 42 | case RED

Bash, ksh93 и dash все это делают, поэтому я предполагаю, что это стандартное поведение POSIX.

  1. Почему это происходит?
  2. Какая лучшая альтернатива?

Я хочу проанализировать строки выше на три переменные, и я не хочу калечить какой-либо текст в третьем поле. Я думал, что read - это путь, но теперь я пересматриваю.

Ответ 1

Да, это стандартное поведение (см. Спецификацию read и разбиение поля). Несколько оболочек (ash -based, включая dash, pdksh -based, zsh, yash по крайней мере) использовали не для этого, но кроме zsh (если не в режиме POSIX), busybox sh, большинство из них были обновлены для Соответствие POSIX.

То же самое для:

$ var='a:b:c:' IFS=:
$ set -- $var; echo "$#"
3

(см. то, как спецификация POSIX для read фактически отменяет механизм разделения полей, где a:b:c: разделяется на 3 поля, и поэтому с IFS=: read -r abc, столько полей, сколько переменных).

Обоснованием является то, что в ksh (на котором основана спецификация POSIX) $IFS (первоначально в оболочке Bourne, внутренний разделитель полей) стал полевым разделителем, я думаю, что любой список элементов (не содержащий разделителя) может быть представлен.

Когда $IFS является разделителем, нельзя представить список одного пустого элемента ("" разбивается на список из 0 элементов, ":" в список из двух пустых элементов¹). Когда это разделитель, вы можете выразить список нулевого элемента с помощью "" или один пустой элемент с ":" или двумя пустыми элементами с "::".

Это немного неудачно, поскольку одним из наиболее распространенных способов использования $IFS является разделение $PATH. A $PATH like /bin: /usr/bin: предназначен для разделения на "/bin", "/usr/bin", "", а не только "/bin" и "/usr/bin".

Теперь, с оболочками POSIX (но не все оболочки совместимы в этом отношении), для разделения слов при расширении параметра, с которым можно работать:

IFS=:; set -o noglob
for dir in $PATH""; do
  something with "${dir:-.}"
done

Этот трейлинг "" гарантирует, что если $PATH заканчивается в трейлинг : добавляется дополнительный пустой элемент. А также, что пустой $PATH рассматривается как один пустой элемент, как и должно быть.

Однако этот подход не может быть использован для read.

За исключением переключения на zsh, нет никакой простой работы, кроме вставки дополнительной : и удалите ее потом, как:

echo a:b:c: | sed 's/:/::/2' | { IFS=: read -r x y z; z=${z#:}; echo "$z"; }

Или (менее портативный):

echo a:b:c: | paste -d: - /dev/null | { IFS=: read -r x y z; z=${z%:}; echo "$z"; }

Я также добавил -r который вы обычно хотите использовать при read.

Скорее всего, здесь вы захотите использовать надлежащую утилиту для обработки текста, такую как sed/awk/perl вместо того, чтобы писать свернутый и, вероятно, неэффективный код вокруг read который не был разработан для этого.


¹ Хотя в оболочке Bourne все еще было разделено на нулевые элементы, поскольку не было различий между IFS-пробелами и символами IFS-non-whitespace, что также было добавлено ksh

Ответ 2

Одна "особенность" read заключается в том, что он будет разделять разделители разделителей разделов пробега и конца на переменные, которые он заполняет, - это объясняется более подробно в связанном ответе. Это позволяет новичкам read то, что они ожидают, например, read first rest <<< ' foo bar ' (обратите внимание на дополнительные пробелы).

Вынос? Трудно выполнять точную обработку текста с использованием инструментов Bash и shell. Если вам нужен полный контроль, вероятно, лучше использовать "более строгий" язык, например, Python, где split() будет делать то, что вы хотите, но где вам, возможно, придется углубиться в обработку строк, чтобы явно удалить разделители строк или обработать кодировку,