Ответ прост: сверните все разделители в один (первый).
Это требует цикла (который выполняется меньше, чем log(N)
раз):
var=':a bc::d ef:#$%_+$$% ^%&*(*&*^
$#,.::ghi::*::' # a long test string.
d=':@!#$%^&*()_+,.' # delimiter set
f=${d:0:1} # first delimiter
v=${var//["$d"]/"$f"}; # convert all delimiters to
: # the first of the delimiter set.
tmp=$v # temporal variable (v).
while
tmp=${tmp//["$f"]["$f"]/"$f"}; # collapse each two delimiters to one
[[ "$tmp" != "$v" ]]; # If there was a change
do
v=$tmp; # actualize the value of the string.
done
Осталось только правильно разделить строку на один разделитель и вывести ее:
readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
printf '<%s>' "${arr[@]}" ; echo
Нет необходимости set -f
ни менять IFS.
Протестировано с пробелами, символами новой строки и глобусными символами. Все работают. Довольно медленный (как и следовало ожидать от цикла оболочки).
Но только для bash (bash 4.4+ из-за опции -d
readarray).
ш
Версия оболочки не может использовать массив, единственный доступный массив - это позиционные параметры.
Использование tr -s
- это всего одна строка (IFS не изменяется в скрипте):
set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'
И распечатать это:
printf '<%s>' "$@" ; echo
Все еще медленно, но не намного.
Команда command
недопустима в Борне.
В zsh command
вызывает только внешние команды и приводит к сбою eval, если command
используется.
В ksh, даже при том command
, что значение IFS изменяется в глобальной области видимости.
И command
делает разделение неудачным в оболочках, связанных с mksh (mksh, lksh, posh). Удаление команды command
заставляет код работать на большем количестве оболочек. Но: удаление command
заставит IFS сохранять свое значение в большинстве оболочек (eval - это специальная встроенная функция), за исключением bash (без режима posix) и zsh в режиме по умолчанию (без эмуляции). Эту концепцию нельзя заставить работать по умолчанию в zsh, с или без command
.
Многосимвольный IFS
Да, IFS может быть многосимвольным, но каждый символ будет генерировать один аргумент:
set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
printf '<%s>' "$@" ; echo
Будет выводить:
<><a bc><><d ef><><><><><><><><>< ><><><><><><><><><
><><><><><><ghi><><><><><>
С bash вы можете опустить command
слово, если не в эмуляции sh / POSIX. Команда завершится ошибкой в ksh93 (IFS сохраняет измененное значение). В zsh команда command
заставляет zsh пытаться найти eval
внешнюю команду (которую она не находит) и завершается неудачно.
То, что происходит, - то, что единственные символы IFS, которые автоматически свернуты в один разделитель, являются пробелом IFS.
Один пробел в IFS свернет все последовательные пробелы в один. Одна вкладка свернет все вкладки. Один пробел и одна вкладка сворачивают серии пробелов и / или табуляций в один разделитель. Повторите идею с новой строкой.
Чтобы свернуть несколько разделителей, требуется некоторое жонглирование.
Предполагая, что ASCII 3 (0x03) не используется во входных данных var
:
var=${var// /$'\3'} # protect spaces
var=${var//["$d"]/ } # convert all delimiters to spaces
set -f; # avoid expanding globs.
IFS=" " command eval set -- '""$var""' # split on spaces.
set -- "${@//$'\3'/ }" # convert spaces back.
Большинство комментариев о ksh, zsh и bash (about command
и IFS) все еще применимы здесь.
Значение $'\0'
будет менее вероятным при вводе текста, но переменные bash не могут содержать NUL ( 0x00
).
В sh нет внутренних команд для выполнения одинаковых строковых операций, поэтому tr - единственное решение для sh-скриптов.