La réponse simple est: réduire tous les délimiteurs à un (le premier).
Cela nécessite une boucle (qui s'exécute moins de log(N)
fois):
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
Il ne reste plus qu'à diviser correctement la chaîne sur un délimiteur et à l'imprimer:
readarray -td "$f" arr < <(printf '%s%s' "$v"'' "$f")
printf '<%s>' "${arr[@]}" ; echo
Pas besoin set -f
ni de changer IFS.
Testé avec des espaces, des nouvelles lignes et des caractères globaux. Tout le travail. Assez lent (comme devrait l'être une boucle shell).
Mais uniquement pour bash (bash 4.4+ en raison de l'option -d
de readarray).
sh
Une version shell ne peut pas utiliser un tableau, le seul tableau disponible sont les paramètres positionnels.
L'utilisation tr -s
n'est qu'une ligne (IFS ne change pas dans le script):
set -f; IFS=$f command eval set -- '$(echo "$var" | tr -s "$d" "[$f*]" )""'
Et imprimez-le:
printf '<%s>' "$@" ; echo
Toujours lent, mais pas beaucoup plus.
La commande command
n'est pas valide dans Bourne.
Dans zsh, command
appelle uniquement les commandes externes et fait échouer eval si command
est utilisé.
Dans ksh, même avec command
, la valeur d'IFS est modifiée dans la portée globale.
Et command
fait échouer le fractionnement dans les shells liés à mksh (mksh, lksh, posh) La suppression de la commande command
fait exécuter le code sur plus de shells. Mais: la suppression command
fera que IFS conservera sa valeur dans la plupart des shells (eval est une fonction spéciale) sauf en bash (sans mode posix) et zsh en mode par défaut (pas d'émulation). Ce concept ne peut pas fonctionner avec ou sans zsh par défaut command
.
IFS à plusieurs caractères
Oui, IFS peut être multi-caractères, mais chaque caractère génère un argument:
set -f; IFS="$d" command eval set -- '$(echo "$var" )""'
printf '<%s>' "$@" ; echo
Sortira:
<><a bc><><d ef><><><><><><><><>< ><><><><><><><><><
><><><><><><ghi><><><><><>
Avec bash, vous pouvez omettre le command
mot s'il n'est pas dans l'émulation sh / POSIX. La commande échouera dans ksh93 (IFS conserve la valeur modifiée). Dans zsh, la commande command
oblige zsh à rechercher eval
une commande externe (qu'elle ne trouve pas) et échoue.
Ce qui se passe, c'est que les seuls caractères IFS qui sont automatiquement réduits à un délimiteur sont les espaces blancs IFS.
Un espace dans IFS réduira tous les espaces consécutifs en un seul. Un onglet réduira tous les onglets. Un espace et un onglet réduiront les séries d'espaces et / ou d'onglets dans un délimiteur. Répétez l'idée avec la nouvelle ligne.
Pour réduire plusieurs délimiteurs, une jonglerie est nécessaire.
En supposant que l'ASCII 3 (0x03) n'est pas utilisé dans l'entrée 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.
La plupart des commentaires sur ksh, zsh et bash (about command
et IFS) s'appliquent toujours ici.
Une valeur de $'\0'
serait moins probable dans la saisie de texte, mais les variables bash ne peuvent pas contenir de NUL ( 0x00
).
Il n'y a pas de commandes internes dans sh pour effectuer les mêmes opérations de chaîne, donc tr est la seule solution pour les scripts sh.