Toutes les réponses à cette question sont fausses d'une manière ou d'une autre.
Mauvaise réponse # 1
IFS=', ' read -r -a array <<< "$string"
1: Il s'agit d'une mauvaise utilisation de $IFS
. La valeur de la $IFS
variable n'est pas considérée comme un seul séparateur de chaînes de longueur variable , mais plutôt comme un ensemble de séparateurs de chaînes à un caractère , où chaque champ qui read
se sépare de la ligne d'entrée peut être terminé par n'importe quel caractère de l'ensemble (virgule ou espace, dans cet exemple).
En fait, pour les vrais tenanciers, la pleine signification de $IFS
est un peu plus impliquée. Du manuel bash :
Le shell traite chaque caractère d' IFS comme un délimiteur et divise les résultats des autres extensions en mots utilisant ces caractères comme terminateurs de champ. Si IFS n'est pas défini ou que sa valeur est exactement <espace><tab> <newline> , la valeur par défaut, puis des séquences de <space> , <tab> et <newline> au début et à la fin des résultats des extensions précédentes sont ignorés et toute séquence de caractères IFS qui ne se trouve pas au début ou à la fin sert à délimiter les mots. Si IFS a une valeur autre que la valeur par défaut, les séquences des caractères d' espacement <espace> , <tab> et <sont ignorés au début et à la fin du mot, tant que le caractère d'espace est dans la valeur de IFS (un caractère d'espace IFS ). Tout caractère dans IFS qui n'est pas un espace IFS , ainsi que tout caractère d'espace IFS adjacent , délimite un champ. Une séquence de caractères blancs IFS est également traitée comme un délimiteur. Si la valeur de IFS est nulle, aucun fractionnement de mot ne se produit.
Fondamentalement, pour les valeurs non nulles non par défaut de $IFS
, les champs peuvent être séparés avec (1) une séquence d'un ou plusieurs caractères qui font tous partie de l'ensemble des "espaces blancs IFS" (c'est-à-dire, selon <espace> , <tab> et <newline> ("newline" signifiant un saut de ligne (LF) ) sont présents n'importe où dans $IFS
), ou (2) tout autre "caractère d'espacement IFS" qui est présent $IFS
avec les "caractères d'espacement IFS" qui l'entourent dans la ligne d'entrée.
Pour l'OP, il est possible que le deuxième mode de séparation que j'ai décrit dans le paragraphe précédent soit exactement ce qu'il veut pour sa chaîne d'entrée, mais nous pouvons être assez confiants que le premier mode de séparation que j'ai décrit n'est pas correct du tout. Par exemple, que faire si sa chaîne d'entrée était 'Los Angeles, United States, North America'
?
IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -p a;
## declare -a a=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America")
2: Même si vous deviez utiliser cette solution avec un séparateur à caractère unique (comme une virgule par lui - même, qui est, sans espace suivant ou autres bagages), si la valeur de la $string
variable de se contenir de lignocellulosiques, puis read
sera arrêtez le traitement une fois qu'il rencontre le premier LF. Le programme read
intégré ne traite qu'une seule ligne par appel. Cela est vrai même si vous canalisez ou redirigez l'entrée uniquement vers l' read
instruction, comme nous le faisons dans cet exemple avec le mécanisme here-string , et donc l'entrée non traitée est garantie d'être perdue. Le code qui alimente la fonction read
intégrée n'a aucune connaissance du flux de données au sein de sa structure de commande contenant.
Vous pourriez faire valoir qu'il est peu probable que cela cause un problème, mais c'est quand même un danger subtil qui devrait être évité si possible. Cela est dû au fait que le read
builtin fait en fait deux niveaux de division d'entrée: d'abord en lignes, puis en champs. Étant donné que l'OP ne souhaite qu'un seul niveau de fractionnement, cette utilisation de la fonction read
intégrée n'est pas appropriée et nous devons l'éviter.
3: Un problème potentiel non évident avec cette solution est que read
le champ de fin est toujours supprimé s'il est vide, bien qu'il conserve les champs vides dans le cas contraire. Voici une démo:
string=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -p a;
## declare -a a=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="")
Peut-être que l'OP ne s'en soucierait pas, mais c'est toujours une limitation à connaître. Il réduit la robustesse et la généralité de la solution.
Ce problème peut être résolu en ajoutant un délimiteur de fin factice à la chaîne d'entrée juste avant de l'alimenter read
, comme je le démontrerai plus tard.
Mauvaise réponse # 2
string="1:2:3:4:5"
set -f # avoid globbing (expansion of *).
array=(${string//:/ })
Idée similaire:
t="one,two,three"
a=($(echo $t | tr ',' "\n"))
(Remarque: j'ai ajouté les parenthèses manquantes autour de la substitution de commande que le répondeur semble avoir omis.)
Idée similaire:
string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
Ces solutions exploitent la division des mots dans une affectation de tableau pour diviser la chaîne en champs. Curieusement, tout comme le read
fractionnement de mots général utilise également la $IFS
variable spéciale, bien que dans ce cas, cela implique qu'il est défini sur sa valeur par défaut <espace><tab> <newline> , et donc toute séquence d'un ou plusieurs IFS Les caractères (qui sont désormais tous des espaces) sont considérés comme un délimiteur de champ.
Cela résout le problème de deux niveaux de division commis par read
, car la division de mot en elle-même ne constitue qu'un seul niveau de division. Mais tout comme auparavant, le problème ici est que les champs individuels de la chaîne d'entrée peuvent déjà contenir des $IFS
caractères, et donc ils seraient incorrectement divisés pendant l'opération de fractionnement de mots. Il se trouve que cela n'est le cas pour aucun des exemples de chaînes d'entrée fournies par ces répondeurs (comme c'est pratique ...), mais bien sûr, cela ne change pas le fait que toute base de code qui utilise cet idiome courrait alors le risque de exploser si cette hypothèse était jamais violée à un moment donné sur la ligne. Encore une fois, considérons mon contre-exemple de 'Los Angeles, United States, North America'
(ou 'Los Angeles:United States:North America'
).
En outre, la séparation de mots est normalement suivi par extension de nom de fichier ( aka développement des chemins aka globbing), qui, si elle est faite, seraient des mots potentiellement corrompus contenant les caractères *
, ?
ou [
suivie de ]
(et, le cas extglob
est défini, fragments parenthesized précédé par ?
, *
, +
, @
, ou !
) en les comparant aux objets du système de fichiers et en développant les mots ("globes") en conséquence. Le premier de ces trois répondeurs a habilement résolu ce problème en exécutant set -f
au préalable pour désactiver la globalisation. Techniquement, cela fonctionne (même si vous devriez probablement ajouterset +f
après pour réactiver la globalisation pour le code suivant qui peut en dépendre), mais il n'est pas souhaitable d'avoir à jouer avec les paramètres globaux du shell afin de pirater une opération d'analyse de base chaîne à tableau dans le code local.
Un autre problème avec cette réponse est que tous les champs vides seront perdus. Cela peut ou non être un problème, selon l'application.
Remarque: Si vous allez utiliser cette solution, il vaut mieux utiliser la ${string//:/ }
forme "substitution de modèle" de l' expansion des paramètres , plutôt que de vous donner la peine d'invoquer une substitution de commande (qui bifurque le shell), de démarrer un pipeline, et exécuter un exécutable externe ( tr
ou sed
), car l'expansion des paramètres est purement une opération interne au shell. (De plus, pour les solutions tr
et sed
, la variable d'entrée doit être placée entre guillemets à l'intérieur de la substitution de commande; sinon, le fractionnement de mots prendrait effet dans la echo
commande et risquerait de perturber les valeurs de champ. De plus, la $(...)
forme de substitution de commande est préférable à l'ancienne`...`
forme car il simplifie l'imbrication des substitutions de commandes et permet une meilleure mise en évidence de la syntaxe par les éditeurs de texte.)
Mauvaise réponse # 3
str="a, b, c, d" # assuming there is a space after ',' as in Q
arr=(${str//,/}) # delete all occurrences of ','
Cette réponse est presque la même que # 2 . La différence est que le répondeur a fait l'hypothèse que les champs sont délimités par deux caractères, l'un étant représenté par défaut $IFS
et l'autre non. Il a résolu ce cas assez spécifique en supprimant le caractère non représenté par IFS en utilisant une expansion de substitution de modèle, puis en utilisant la séparation de mots pour diviser les champs sur le caractère de délimiteur représenté par IFS survivant.
Ce n'est pas une solution très générique. En outre, on peut faire valoir que la virgule est vraiment le caractère de délimiteur "principal" ici, et que le supprimer, puis dépendre du caractère d'espace pour le fractionnement de champ, est tout simplement faux. Encore une fois, pensez à mes contre - : 'Los Angeles, United States, North America'
.
De plus, encore une fois, l'expansion du nom de fichier pourrait corrompre les mots développés, mais cela peut être évité en désactivant temporairement la globalisation pour l'affectation avec set -f
puis set +f
.
De plus, encore une fois, tous les champs vides seront perdus, ce qui peut ou non être un problème selon l'application.
Mauvaise réponse # 4
string='first line
second line
third line'
oldIFS="$IFS"
IFS='
'
IFS=${IFS:0:1} # this is useful to format your code with tabs
lines=( $string )
IFS="$oldIFS"
Ceci est similaire aux # 2 et # 3 dans la mesure où il utilise la séparation des mots pour faire le travail, seulement maintenant le code définit explicitement $IFS
pour ne contenir que le délimiteur de champ à un caractère présent dans la chaîne d'entrée. Il convient de répéter que cela ne peut pas fonctionner pour les délimiteurs de champ à caractères multiples tels que le délimiteur d'espace virgule de l'OP. Mais pour un délimiteur à un seul caractère comme le LF utilisé dans cet exemple, il est presque parfait. Les champs ne peuvent pas être involontairement divisés au milieu, comme nous l'avons vu avec les mauvaises réponses précédentes, et il n'y a qu'un seul niveau de fractionnement, comme requis.
Un problème est que l'expansion du nom de fichier corrompra les mots affectés comme décrit précédemment, bien qu'une fois encore cela puisse être résolu en encapsulant l'instruction critique dans set -f
et set +f
.
Un autre problème potentiel est que, puisque LF se qualifie comme un "caractère d'espace blanc IFS" tel que défini précédemment, tous les champs vides seront perdus, tout comme dans # 2 et # 3 . Cela ne serait bien sûr pas un problème si le délimiteur se trouve être un non-"caractère d'espacement IFS", et selon l'application, cela peut ne pas avoir d'importance de toute façon, mais cela vicie la généralité de la solution.
Donc, pour résumer, en supposant que vous avez un délimiteur à un caractère, et que ce soit un non-"caractère blanc IFS" ou vous ne vous souciez pas des champs vides, et vous encapsulez l'instruction critique dans set -f
et set +f
, puis cette solution fonctionne , mais sinon non.
(De plus, à titre d'information, l'attribution d'un LF à une variable dans bash peut être effectuée plus facilement avec la $'...'
syntaxe, par exemple IFS=$'\n';
.)
Mauvaise réponse # 5
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
Idée similaire:
IFS=', ' eval 'array=($string)'
Cette solution est en fait un croisement entre # 1 (en ce sens qu'elle se définit $IFS
sur virgule) et # 2-4 (en ce qu'elle utilise la séparation de mots pour diviser la chaîne en champs). Pour cette raison, il souffre de la plupart des problèmes qui affligent toutes les mauvaises réponses ci-dessus, un peu comme le pire de tous les mondes.
En outre, en ce qui concerne la deuxième variante, il peut sembler que l' eval
appel est complètement inutile, car son argument est un littéral de chaîne entre guillemets simples et est donc statiquement connu. Mais il y a en fait un avantage très non évident à utiliser eval
de cette façon. Normalement, lorsque vous exécutez une commande simple qui consiste en une affectation de variable uniquement , c'est-à-dire sans un mot de commande réel qui la suit, l'affectation prend effet dans l'environnement shell:
IFS=', '; ## changes $IFS in the shell environment
Cela est vrai même si la commande simple implique plusieurs affectations de variables; encore une fois, tant qu'il n'y a pas de mot de commande, toutes les affectations de variables affectent l'environnement du shell:
IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment
Mais, si l'affectation de variable est attachée à un nom de commande (j'aime appeler cela une "affectation de préfixe"), cela n'affecte pas l'environnement shell, et affecte uniquement l'environnement de la commande exécutée, qu'il s'agisse d'une commande intégrée ou non ou externe:
IFS=', ' :; ## : is a builtin command, the $IFS assignment does not outlive it
IFS=', ' env; ## env is an external command, the $IFS assignment does not outlive it
Citation pertinente du manuel bash :
Si aucun nom de commande ne résulte, les affectations de variables affectent l'environnement shell actuel. Sinon, les variables sont ajoutées à l'environnement de la commande exécutée et n'affectent pas l'environnement shell actuel.
Il est possible d'exploiter cette fonctionnalité d'affectation de variable pour $IFS
ne changer que temporairement, ce qui nous permet d'éviter tout le gambit de sauvegarde et de restauration comme celui qui est fait avec la $OIFS
variable dans la première variante. Mais le défi auquel nous sommes confrontés ici est que la commande que nous devons exécuter est en soi une simple affectation de variable, et donc elle n'impliquerait pas un mot de commande pour rendre l' $IFS
affectation temporaire. Vous pourriez penser à vous-même, eh bien pourquoi ne pas simplement ajouter un mot de commande sans opération à la déclaration comme le : builtin
pour rendre l' $IFS
affectation temporaire? Cela ne fonctionne pas car cela rendrait également l' $array
affectation temporaire:
IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command
Donc, nous sommes effectivement dans une impasse, un peu un catch-22. Mais, lorsqu'il eval
exécute son code, il l'exécute dans l'environnement shell, comme s'il s'agissait d'un code source statique normal, et nous pouvons donc exécuter l' $array
affectation à l'intérieur de l' eval
argument pour qu'il prenne effet dans l'environnement shell, tandis que l' $IFS
affectation de préfixe qui est préfixé à la eval
commande ne survivra pas à la eval
commande. C'est exactement l'astuce qui est utilisée dans la deuxième variante de cette solution:
IFS=', ' eval 'array=($string)'; ## $IFS does not outlive the eval command, but $array does
Donc, comme vous pouvez le voir, c'est en fait une astuce assez intelligente, et accomplit exactement ce qui est requis (au moins en ce qui concerne la mise en œuvre des affectations) d'une manière plutôt non évidente. Je ne suis en fait pas contre cette astuce en général, malgré l'implication de eval
; faites juste attention à ne citer que la chaîne d'arguments pour vous prémunir contre les menaces de sécurité.
Mais encore une fois, en raison de l'agglomération de problèmes "les pires de tous les mondes", il s'agit toujours d'une mauvaise réponse à l'exigence du PO.
Mauvaise réponse # 6
IFS=', '; array=(Paris, France, Europe)
IFS=' ';declare -a array=(Paris France Europe)
Euh, quoi? L'OP a une variable de chaîne qui doit être analysée dans un tableau. Cette «réponse» commence par le contenu textuel de la chaîne d'entrée collée dans un littéral de tableau. Je suppose que c'est une façon de procéder.
Il semble que le répondeur ait pu supposer que la $IFS
variable affecte toutes les analyses bash dans tous les contextes, ce qui n'est pas vrai. Du manuel bash:
IFS Le séparateur de champ interne qui est utilisé pour le fractionnement de mots après expansion et pour diviser des lignes en mots avec la commande intégrée read . La valeur par défaut est <space><tab> <newline> .
Ainsi, la $IFS
variable spéciale n'est en fait utilisée que dans deux contextes: (1) le fractionnement de mots qui est effectué après l'expansion (ce qui ne signifie pas lors de l'analyse du code source bash) et (2) pour le fractionnement des lignes d'entrée en mots par le read
code intégré.
Permettez-moi de clarifier les choses. Je pense qu'il pourrait être bon de faire une distinction entre l' analyse et l' exécution . Bash doit d'abord analyser le code source, qui est évidemment un événement d' analyse , puis il exécute le code, c'est-à-dire lorsque l'expansion apparaît dans l'image. L'expansion est vraiment un événement d' exécution . De plus, je conteste la description de la $IFS
variable que je viens de citer ci-dessus; plutôt que de dire que le fractionnement de mots est effectué après l'expansion , je dirais que le fractionnement de mots est effectué pendant l' expansion, ou, peut-être encore plus précisément, le fractionnement de mots fait partie dele processus d'expansion. L'expression "séparation des mots" se réfère uniquement à cette étape d'expansion; il ne devrait jamais être utilisé pour faire référence à l'analyse du code source de bash, bien que malheureusement les documents semblent beaucoup contourner les mots "split" et "words". Voici un extrait pertinent de la version linux.die.net du manuel bash:
L'expansion est effectuée sur la ligne de commande après avoir été divisée en mots. Il y a sept types d'expansion EFFECTUES: expansion des accolades , tilde extension , paramètres et variables , la substitution de commande , l' expansion arithmétique , le découpage des mots , et l' expansion du chemin .
L'ordre des expansions est le suivant: expansion de l'accolade; expansion de tilde, expansion de paramètres et de variables, expansion arithmétique et substitution de commandes (effectuées de gauche à droite); division de mots; et expansion du nom de chemin.
Vous pourriez dire que la version GNU du manuel fait un peu mieux, car elle opte pour le mot "jetons" au lieu de "mots" dans la première phrase de la section Expansion:
L'expansion est effectuée sur la ligne de commande après avoir été divisée en jetons.
Le point important est, $IFS
ne change pas la façon dont bash analyse le code source. L'analyse du code source bash est en fait un processus très complexe qui implique la reconnaissance des divers éléments de la grammaire du shell, tels que les séquences de commandes, les listes de commandes, les pipelines, les extensions de paramètres, les substitutions arithmétiques et les substitutions de commandes. Pour la plupart, le processus d'analyse bash ne peut pas être modifié par des actions au niveau de l'utilisateur comme les affectations de variables (en fait, il y a quelques exceptions mineures à cette règle; par exemple, voir les différents compatxx
paramètres du shell, ce qui peut modifier certains aspects du comportement d'analyse à la volée). Les "mots" / "jetons" en amont qui résultent de ce processus d'analyse complexe sont ensuite développés selon le processus général d '"expansion" tel que décomposé dans les extraits de documentation ci-dessus, où la division des mots du texte développé (en expansion?) En aval les mots ne sont qu'une étape de ce processus. Le fractionnement de mots ne touche que le texte qui a été recraché lors d'une étape d'expansion précédente; cela n'affecte pas le texte littéral qui a été analysé directement à partir du flux source source.
Mauvaise réponse # 7
string='first line
second line
third line'
while read -r line; do lines+=("$line"); done <<<"$string"
C'est l'une des meilleures solutions. Notez que nous recommençons à utiliser read
. N'avais-je pas dit plus tôt que read
c'était inapproprié parce qu'il effectuait deux niveaux de fractionnement, alors que nous n'en avions besoin que d'un? L'astuce ici est que vous pouvez appeler read
de manière à ce qu'il ne fasse effectivement qu'un seul niveau de fractionnement, en particulier en séparant un seul champ par appel, ce qui nécessite le coût de devoir l'appeler à plusieurs reprises dans une boucle. C'est un peu un tour de passe-passe, mais ça marche.
Mais il y a des problèmes. Premièrement: lorsque vous fournissez au moins un argument NAME à read
, il ignore automatiquement les espaces blancs de début et de fin dans chaque champ qui est séparé de la chaîne d'entrée. Cela se produit, que $IFS
sa valeur par défaut soit définie ou non, comme décrit plus haut dans cet article. Maintenant, l'OP peut ne pas se soucier de cela pour son cas d'utilisation spécifique, et en fait, cela peut être une caractéristique souhaitable du comportement d'analyse. Mais tous ceux qui veulent analyser une chaîne dans des champs ne le voudront pas. Il existe cependant une solution: une utilisation quelque peu non évidente de read
est de passer zéro argument NAME . Dans ce cas, read
stockera la ligne d'entrée entière qu'il obtient à partir du flux d'entrée dans une variable nommée $REPLY
, et, en prime, il ne passupprimer les espaces de début et de fin de la valeur. C'est une utilisation très robuste read
dont j'ai fréquemment exploité au cours de ma carrière en programmation shell. Voici une démonstration de la différence de comportement:
string=$' a b \n c d \n e f '; ## input string
a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a b" [1]="c d" [2]="e f") ## read trimmed surrounding whitespace
a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]=" a b " [1]=" c d " [2]=" e f ") ## no trimming
Le deuxième problème avec cette solution est qu'elle ne traite pas réellement le cas d'un séparateur de champ personnalisé, tel que l'espace virgule de l'OP. Comme précédemment, les séparateurs multicaractères ne sont pas pris en charge, ce qui est une malheureuse limitation de cette solution. Nous pourrions essayer au moins de diviser la virgule en spécifiant le séparateur de l' -d
option, mais regardez ce qui se passe:
string='Paris, France, Europe';
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France")
Comme on pouvait s'y attendre, l'espace blanc environnant non comptabilisé a été tiré dans les valeurs de champ, et donc cela devrait être corrigé par la suite par des opérations de découpage (cela pourrait également être fait directement dans la boucle while). Mais il y a une autre erreur évidente: l'Europe manque! Qu'est-ce qui lui est arrivé? La réponse est que read
retourne un code retour défaillant s'il atteint la fin du fichier (dans ce cas, nous pouvons l'appeler fin de chaîne) sans rencontrer de terminateur de champ final sur le champ final. Cela provoque une rupture prématurée de la boucle while et nous perdons le champ final.
Techniquement, cette même erreur a également frappé les exemples précédents; la différence est que le séparateur de champ a été pris pour être LF, ce qui est la valeur par défaut lorsque vous ne spécifiez pas l' -d
option, et le <<<
mécanisme ("ici-chaîne") ajoute automatiquement un LF à la chaîne juste avant de l'alimenter en tant que entrée à la commande. Par conséquent, dans ces cas, nous avons en quelque sorte résolu accidentellement le problème d'un champ final abandonné en ajoutant involontairement un terminateur factice supplémentaire à l'entrée. Appelons cette solution la solution "terminateur factice". Nous pouvons appliquer la solution de terminaison factice manuellement pour tout délimiteur personnalisé en la concaténant nous-mêmes par rapport à la chaîne d'entrée lorsque nous l'instancions dans la chaîne ici:
a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -p a;
declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Là, problème résolu. Une autre solution consiste à ne rompre la boucle while que si (1) a read
renvoyé un échec et (2) $REPLY
est vide, ce qui signifie qu'il read
n'a pas été en mesure de lire les caractères avant d'appuyer sur la fin du fichier. Démo:
a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Cette approche révèle également le LF secret qui est automatiquement ajouté à la chaîne ici par l' <<<
opérateur de redirection. Il pourrait bien sûr être supprimé séparément via une opération de découpage explicite comme décrit il y a un instant, mais évidemment l'approche manuelle de terminaison factice le résout directement, nous pourrions donc simplement y aller. La solution manuelle de terminaison factice est en fait assez pratique en ce qu'elle résout ces deux problèmes (le problème du champ final abandonné et le problème LF ajouté) en une seule fois.
Donc, dans l'ensemble, c'est une solution assez puissante. La seule faiblesse qui subsiste est le manque de prise en charge des délimiteurs multicaractères, que j'aborderai plus tard.
Mauvaise réponse # 8
string='first line
second line
third line'
readarray -t lines <<<"$string"
(Il s'agit en fait du même message que # 7 ; le répondeur a fourni deux solutions dans le même message.)
Le readarray
builtin, qui est synonyme de mapfile
, est idéal. C'est une commande intégrée qui analyse un bytestream en une variable de tableau en une seule fois; pas de problème avec les boucles, les conditions, les substitutions ou autre chose. Et il ne supprime subrepticement aucun espace de la chaîne d'entrée. Et (s'il -O
n'est pas indiqué), il efface commodément le tableau cible avant de lui être affecté. Mais ce n'est pas encore parfait, d'où ma critique de cela comme une "mauvaise réponse".
Tout d'abord, pour éviter cela, notez que, tout comme le comportement de l' read
analyse de champ, readarray
supprime le champ de fin s'il est vide. Encore une fois, ce n'est probablement pas une préoccupation pour le PO, mais cela pourrait l'être pour certains cas d'utilisation. J'y reviendrai dans un instant.
Deuxièmement, comme précédemment, il ne prend pas en charge les délimiteurs multicaractères. Je vais également vous donner une solution dans un instant.
Troisièmement, la solution telle qu'elle est écrite n'analyse pas la chaîne d'entrée de l'OP et, en fait, elle ne peut pas être utilisée telle quelle pour l'analyser. Je vais m'étendre là-dessus aussi momentanément.
Pour les raisons ci-dessus, je considère toujours qu'il s'agit d'une "mauvaise réponse" à la question du PO. Ci-dessous, je donnerai ce que je considère être la bonne réponse.
Bonne réponse
Voici une tentative naïve de faire fonctionner # 8 en spécifiant simplement l' -d
option:
string='Paris, France, Europe';
readarray -td, a <<<"$string"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=$' Europe\n')
Nous voyons que le résultat est identique au résultat que nous avons obtenu de l'approche double conditionnelle de la read
solution de bouclage discutée dans # 7 . Nous pouvons presque résoudre ce problème avec l'astuce de terminaison factice manuelle:
readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')
Le problème ici est qu'il a readarray
conservé le champ de fin, car l' <<<
opérateur de redirection a ajouté le LF à la chaîne d'entrée, et donc le champ de fin n'était pas vide (sinon il aurait été supprimé). Nous pouvons nous en occuper en supprimant explicitement l'élément final du tableau après coup:
readarray -td, a <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe")
Les deux seuls problèmes qui restent, qui sont en fait liés, sont (1) l'espace blanc étranger qui doit être coupé, et (2) le manque de prise en charge des délimiteurs multicaractères.
L'espace blanc pourrait bien sûr être coupé par la suite (par exemple, voir Comment découper un espace blanc à partir d'une variable Bash? ). Mais si nous pouvons pirater un délimiteur multicaractère, cela résoudrait les deux problèmes en une seule fois.
Malheureusement, il n'existe aucun moyen direct de faire fonctionner un délimiteur multicaractère. La meilleure solution à laquelle j'ai pensé est de prétraiter la chaîne d'entrée pour remplacer le délimiteur multicaractère par un délimiteur à un caractère qui sera garanti de ne pas entrer en collision avec le contenu de la chaîne d'entrée. Le seul caractère qui a cette garantie est l' octet NUL . En effet, dans bash (mais pas dans zsh, d'ailleurs), les variables ne peuvent pas contenir l'octet NUL. Cette étape de prétraitement peut être effectuée en ligne dans une substitution de processus. Voici comment le faire en utilisant awk :
readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]';
declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
Voilà enfin! Cette solution ne divisera pas par erreur les champs au milieu, ne se coupera pas prématurément, ne supprimera pas les champs vides, ne se corrompra pas sur les extensions de nom de fichier, ne supprimera pas automatiquement les espaces blancs de début et de fin, ne laissera pas de LF clandestin à la fin, ne nécessite pas de boucles et ne se contente pas d'un délimiteur à un caractère.
Solution de coupe
Enfin, je voulais démontrer ma propre solution de découpage assez complexe en utilisant l' -C callback
option obscure de readarray
. Malheureusement, je n'ai plus de place contre la limite de publication draconienne de 30 000 caractères de Stack Overflow, donc je ne serai pas en mesure de l'expliquer. Je vais laisser cela comme un exercice pour le lecteur.
function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; };
function val_ltrim { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; };
function val_rtrim { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; };
function val_trim { val_ltrim; val_rtrim; };
readarray -c1 -C 'mfcb val_trim a' -td, <<<"$string,"; unset 'a[-1]'; declare -p a;
## declare -a a=([0]="Paris" [1]="France" [2]="Europe")
,
(espace virgule) et non sur un seul caractère tel que la virgule. Si vous n'êtes intéressé que par ce dernier, les réponses sont plus faciles à suivre: stackoverflow.com/questions/918886/…