A resposta simples é: reduza todos os delimitadores para um (o primeiro).
Isso requer um loop (que é executado menos que o log(N)tempo):
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
Tudo o que resta a fazer é dividir corretamente a sequência em um delimitador e imprimi-la:
readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
printf '<%s>' "${arr[@]}" ; echo
Não há necessidade de set -fnem IFS mudança.
Testado com espaços, novas linhas e caracteres glob. Todo o trabalho. Muito lento (como se espera que um loop de shell seja).
Mas apenas para o bash (bash 4.4+ por causa da opção -dde readarray).
sh
Uma versão shell não pode usar uma matriz, a única matriz disponível são os parâmetros posicionais.
Usar tr -sé apenas uma linha (o IFS não muda no script):
set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'
E imprima:
printf '<%s>' "$@" ; echo
Ainda lento, mas não muito mais.
O comando commandé inválido no Bourne.
No zsh, commandchama apenas comandos externos e faz com que eval falhe se commandfor usado.
Em ksh, mesmo com command, o valor do IFS é alterado no escopo global.
E commandfaz com que a divisão falhe em shells relacionados ao mksh (mksh, lksh, posh) A remoção do comando commandfaz com que o código seja executado em mais shells. Mas: a remoção commandfará com que o IFS mantenha seu valor na maioria dos shells (eval é um built-in especial), exceto no bash (sem modo posix) e zsh no modo padrão (sem emulação). Esse conceito não pode ser criado para funcionar no zsh padrão com ou sem command.
IFS de vários caracteres
Sim, o IFS pode ter vários caracteres, mas cada caractere irá gerar um argumento:
set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
printf '<%s>' "$@" ; echo
Saída:
<><a bc><><d ef><><><><><><><><>< ><><><><><><><><><
><><><><><><ghi><><><><><>
Com o bash, você pode omitir a commandpalavra se não estiver na emulação sh / POSIX. O comando falhará no ksh93 (o IFS mantém o valor alterado). No zsh, o comando commandfaz com que o zsh tente encontrar evalcomo um comando externo (que não encontra) e falha.
O que acontece é que os únicos caracteres IFS recolhidos automaticamente para um delimitador são os espaços em branco do IFS.
Um espaço no IFS recolherá todos os espaços consecutivos para um. Uma guia recolherá todas as guias. Um espaço e uma guia recolherão execuções de espaços e / ou guias em um delimitador. Repita a ideia com nova linha.
Para recolher vários delimitadores, é necessário algum malabarismo.
Supondo que ASCII 3 (0x03) não seja usado na entrada 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.
A maioria dos comentários sobre ksh, zsh e bash (about commande IFS) ainda se aplica aqui.
Um valor de $'\0'seria menos provável na entrada de texto, mas as variáveis bash não podem conter NULs ( 0x00).
Não há comandos internos no sh para executar as mesmas operações de cadeia, portanto, tr é a única solução para scripts sh.