Question Diviser une chaîne en un tableau dans Bash


Dans un script Bash, je voudrais diviser une ligne en morceaux et les stocker dans un tableau.

La ligne:

Paris, France, Europe

Je voudrais les avoir dans un tableau comme celui-ci:

array[0] = Paris
array[1] = France
array[2] = Europe

Je voudrais utiliser du code simple, la vitesse de la commande n'a pas d'importance. Comment puis-je le faire?


426
2018-05-14 15:15


origine


Réponses:


IFS=', ' read -r -a array <<< "$string"

Notez que les caractères dans $IFS sont traités individuellement comme séparateurs de sorte que dans ce cas les champs peuvent être séparés par non plus une virgule ou un espace plutôt que la séquence des deux caractères. Il est intéressant de noter que les champs vides ne sont pas créés lorsque l’espace des virgules apparaît dans l’entrée car l’espace est traité spécialement.

Pour accéder à un élément individuel:

echo "${array[0]}"

Pour itérer sur les éléments:

for element in "${array[@]}"
do
    echo "$element"
done

Pour obtenir à la fois l'index et la valeur:

for index in "${!array[@]}"
do
    echo "$index ${array[index]}"
done

Le dernier exemple est utile car les tableaux Bash sont rares. En d'autres termes, vous pouvez supprimer un élément ou ajouter un élément, puis les index ne sont pas contigus.

unset "array[1]"
array[42]=Earth

Pour obtenir le nombre d'éléments dans un tableau:

echo "${#array[@]}"

Comme mentionné ci-dessus, les tableaux peuvent être clairsemés, donc vous ne devriez pas utiliser la longueur pour obtenir le dernier élément. Voici comment vous pouvez dans Bash 4.2 et plus tard:

echo "${array[-1]}"

dans n'importe quelle version de Bash (de quelque part après 2.05b):

echo "${array[@]: -1:1}"

Les décalages négatifs plus importants sélectionnent plus loin de la fin du tableau. Notez l'espace avant le signe moins dans l'ancien formulaire. C'est requis.


777
2018-05-14 15:16



Voici un moyen sans paramétrer IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

L'idée utilise le remplacement de chaîne:

${string//substring/replacement}

pour remplacer toutes les correspondances de $ substring par des espaces, puis en utilisant la chaîne substituée pour initialiser un tableau:

(element1 element2 ... elementN)

Note: cette réponse utilise le opérateur split + glob. Ainsi, pour empêcher l'expansion de certains caractères (tels que *) c'est une bonne idée de mettre en pause la globbing pour ce script.


189
2018-03-14 02:20



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: Ceci est un abus de $IFS. La valeur du $IFS la variable est ne pas pris comme un une seule longueur variable séparateur de chaîne, plutôt il est pris comme ensemble de un caractère séparateurs de chaînes, où chaque champ read se sépare de la ligne d'entrée peut être terminée par tout caractère dans l'ensemble (virgule ou espace, dans cet exemple).

En fait, pour les vrais automobilistes, le plein sens de $IFS est légèrement plus impliqué. Du manuel bash:

Le shell traite chaque personnage de IFS en tant que délimiteur, et divise les résultats des autres extensions en mots utilisant ces caractères comme terminateurs de champ. Si IFS est non défini, ou sa valeur est exactement <espace> <tab> <nouvelle ligne>, la valeur par défaut, puis les séquences de <espace>, <onglet>, et <newline> au début et à la fin des résultats des expansions précédentes sont ignorés, et toute séquence de IFS les caractères qui ne sont ni au début ni à la fin servent à délimiter les mots. Si IFSa une valeur autre que la valeur par défaut, puis des séquences des caractères d'espacement <espace>, <onglet>, et <newline> 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 IFS Caractère d'espacement). N'importe quel personnage IFS ce n'est pas IFS espace blanc, avec tout adjacent IFS caractères d'espacement, délimite un champ. Une séquence de IFS Les caractères d'espaces sont également traités comme des délimiteurs. Si la valeur de IFS est nul, aucun fractionnement de mot ne se produit.

Fondamentalement, pour les valeurs non nulles par défaut de $IFS, les champs peuvent être séparés avec soit (1) une séquence d'un ou plusieurs caractères provenant de l'ensemble des "caractères d'espacement IFS" (c'est-à-dire <espace>, <onglet>, et <newline> (sens de "nouvelle ligne" saut de ligne (LF)) sont présents partout dans $IFS), ou (2) tout caractère "IFS whitespace" non présent dans $IFS avec tous les caractères "espaces blancs IFS" 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 certains 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 à un seul caractère (comme une virgule seule, c’est-à-dire sans espace ou autre bagage), si la valeur du $string la variable arrive à contenir des LFs, alors read arrêtera le traitement une fois qu'il rencontrera la première LF. le read Builtin ne traite qu'une ligne par invocation. Cela est vrai même si vous utilisez ou redirigez des entrées seulement au read déclaration, comme nous le faisons dans cet exemple avec le ici-chaîne mécanisme, et donc entrée non traitée est garantie d'être perdu. Le code qui alimente le read builtin n'a aucune connaissance du flux de données dans sa structure de commande contenant.

Vous pourriez argumenter que cela ne devrait pas causer de problème, mais il s'agit quand même d'un risque subtil qui devrait être évité si possible. Il est causé par le fait que le read builtin fait deux niveaux de partage d'entrée: d'abord en lignes, puis en champs. Puisque le PO ne veut qu'un seul niveau de division, cet usage du read builtin n'est pas approprié, et nous devrions l'éviter.

3: Un problème potentiel non évident avec cette solution est que read laisse toujours tomber le champ de fin s'il est vide, bien qu'il conserve les champs vides sinon. 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 le PO ne se soucierait pas de cela, mais cela reste une limite à connaître. Cela 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 fictif à la chaîne d'entrée juste avant de l'envoyer à 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"))

(Note: j'ai ajouté les parenthèses manquantes autour de la substitution de commande que le répondant semble avoir omise.)

Idée similaire

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)

Ces solutions tirent parti de la division de mots dans une attribution de tableau pour diviser la chaîne en champs. Curieusement, tout comme read, le fractionnement général des mots utilise également le $IFS variable spéciale, bien que dans ce cas il soit implicite qu’il soit défini sur sa valeur par défaut <espace> <tab> <nouvelle ligne>, et donc toute séquence d'un ou plusieurs caractères IFS (qui sont tous des caractères blancs maintenant) est considérée comme un délimiteur de champ.

Cela résout le problème de deux niveaux de division commis par read, puisque le fractionnement des mots constitue à lui seul un niveau de division. Mais comme avant, le problème ici est que les champs individuels dans la chaîne d'entrée peuvent déjà contenir $IFS caractères, et donc ils seraient incorrectement divisés pendant l'opération de division de mots. Cela arrive à ne pas être le cas pour l'un des exemples de chaînes d'entrée fournis par ces répondeurs (comment pratique ...), mais bien sûr cela ne change pas le fait que toute base de code qui utilisait cet idiome courrait alors le risque de exploser si cette hypothèse a été jamais violé à un moment donné sur la ligne. Encore une fois, considérez mon contre-exemple de 'Los Angeles, United States, North America' (ou 'Los Angeles:United States:North America').

En outre, le fractionnement des mots est normalement suivi par extension de nom de fichier (alias extension de chemin alias globbing), qui, si cela est fait, pourrait corrompre les mots contenant les caractères *, ?, ou [ suivi par ] (et si extglob est défini, les fragments entre parenthèses précédés par ?, *, +, @, ou !) en les comparant aux objets du système de fichiers et en développant les mots ("globs") en conséquence. Le premier de ces trois répondeurs a intelligemment réduit ce problème en exécutant set -f au préalable pour désactiver la globbing. Techniquement, cela fonctionne (même si vous devriez probablement ajouter set +f ensuite pour réactiver la globalisation pour le code suivant qui peut en dépendre), mais il n'est pas souhaitable d'avoir à manipuler les paramètres de shell globaux afin de pirater une opération d'analyse de base de type 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 peut ne pas être un problème, selon l'application.

Remarque: Si vous utilisez cette solution, il est préférable d'utiliser le ${string//:/ } "substitution de motifs" forme de expansion de paramètre, plutôt que d'avoir à invoquer une substitution de commande (qui forke le shell), de lancer un pipeline et d'exécuter un exécutable externe (tr ou sed), puisque l’extension du paramètre est purement une opération interne au shell. (Aussi, pour le tr et sed solutions, la variable d’entrée doit être entre guillemets dans la substitution de commande; sinon, la division des mots prendrait effet dans le echo commande et potentiellement mess avec les valeurs de champ. Également $(...) forme de substitution de commande est préférable à l'ancien `...` forme car elle simplifie l'imbrication des substitutions de commande 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 supposé que les champs sont délimités par deux caractères, l’un étant représenté par défaut. $IFSet l'autre non. Il a résolu ce cas plutôt spécifique en supprimant le caractère non-IFS-représenté en utilisant une expansion de substitution de modèle, puis en utilisant la division de mots pour diviser les champs sur le caractère de délimiteur IFS-représenté survivant.

Ce n'est pas une solution très générique. En outre, on peut affirmer que la virgule est vraiment le caractère de délimitation "principal" ici, et que le dépouiller et ensuite dépendre du caractère d'espace pour le découpage de champ est tout simplement faux. Encore une fois, considérez mon contre-exemple: 'Los Angeles, United States, North America'.

Aussi, encore une fois, l’extension du nom de fichier pourrait corrompre les mots étendus, mais cela peut être évité en désactivant temporairement le regroupement pour l’affectation avec set -f et alors set +f.

De plus, tous les champs vides seront perdus, ce qui peut ou non poser 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 à # 2 et # 3 dans la mesure où il utilise le fractionnement des mots pour faire le travail, seulement maintenant le code définit explicitement $IFS pour ne contenir que le délimiteur de champ à caractère unique 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 champs multi-caractères tels que le délimiteur d’espaces des virgules des 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 scindés involontairement 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, selon les besoins.

L’un des problèmes est que l’extension du nom de fichier corrompra les mots concernés, comme décrit plus haut, même si, une fois de plus, cela peut être résolu en encapsulant la déclaration critique dans set -f et set +f.

Un autre problème potentiel est que, puisque LF est qualifié de "caractère blanc IFS" tel que défini précédemment, tous les champs vides seront perdus, comme dans # 2 et # 3. Bien entendu, cela ne poserait pas de problème si le délimiteur se trouvait être un caractère d'espace blanc "IFS" et, en fonction de l'application, cela n'a pas d'importance, mais cela vicie la généralité de la solution.

Donc, pour résumer, en supposant que vous ayez un délimiteur à un caractère, et que ce soit un caractère d'espace blanc IFS, ou que vous ne vous souciez pas des champs vides, et que vous enveloppez l'énoncé critique dans set -f et set +f, alors cette solution fonctionne, mais pas autrement.

(De plus, pour l'information, assigner un LF à une variable dans bash peut être fait plus facilement avec le $'...' syntaxe, par ex. 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 effectivement un croisement entre #1 (en ce qu'il définit $IFS à l'espace virgule) et # 2-4 (en ce sens qu'il utilise le fractionnement 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.

Aussi, en ce qui concerne la deuxième variante, il peut sembler que eval call 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 évident à utiliser eval de cette façon. Normalement, lorsque vous exécutez une commande simple qui consiste en une affectation de variable seulement, ce qui signifie sans un mot de commande réel qui le suit, l'affectation prend effet dans l'environnement shell:

IFS=', '; ## changes $IFS in the shell environment

Ceci est vrai même si la commande simple implique plusieurs assignations variables; à nouveau, tant qu'il n'y a pas de mot de commande, toutes les affectations de variables affectent l'environnement shell:

IFS=', ' array=($countries); ## changes both $IFS and $array in the shell environment

Mais si l'attribution de la variable est attachée à un nom de commande (j'aime appeler cela une "attribution de préfixe"), alors ne pas affecte l'environnement du shell et n'affecte que l'environnement de la commande exécutée, qu'il s'agisse d'une commande interne 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 de la 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'assignation de variable pour changer $IFS seulement temporairement, ce qui nous permet d’éviter tout le jeu de sauvegarde et de restauration comme celui qui se fait avec le $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 elle-même une simple affectation de variable, et par conséquent, cela ne nécessiterait pas un mot de commande pour $IFS affectation temporaire. Vous pourriez penser à vous-même, eh bien pourquoi ne pas simplement ajouter un mot de commande no-op à la déclaration comme le : builtin pour faire le $IFS affectation temporaire? Cela ne fonctionne pas car cela ferait alors la $array affectation temporaire aussi bien:

IFS=', ' array=($countries) :; ## fails; new $array value never escapes the : command

Donc, nous sommes effectivement dans une impasse, un peu un catch-22. Mais quand eval exécute son code, il l'exécute dans l'environnement shell, comme s'il s'agissait d'un code source statique et normal, et nous pouvons donc exécuter le $array affectation à l'intérieur du eval argument pour le faire prendre effet dans l'environnement shell, tandis que le $IFS affectation de préfixe qui est préfixé à la eval commande ne survivra pas à la eval commander. 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 intelligente, et accomplit exactement ce qui est requis (au moins en ce qui concerne l'effet d'affectation) d'une manière plutôt évidente. Je ne suis pas contre ce tour en général, malgré l'implication de eval; faites juste attention à la simple citation de la chaîne d’argument pour vous protéger contre les menaces de sécurité.

Mais encore une fois, en raison de l'agglomération de problèmes "pire que 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 verbatim de la chaîne d'entrée collée dans un littéral de tableau. Je suppose que c'est une façon de le faire.

On dirait que le répondeur peut avoir supposé que le $IFS La variable affecte tout l'analyse bash dans tous les contextes, ce qui n'est pas vrai. A partir du manuel bash:

IFSLe séparateur de champs interne utilisé pour le fractionnement des mots après expansion et pour séparer les lignes en mots avec lis commande intégrée. La valeur par défaut est <espace> <tab> <nouvelle ligne>.

Alors le $IFS la variable spéciale n'est en réalité utilisée que dans deux contextes: (1) le fractionnement de mots effectué après l'expansion (sens ne pas lors de l'analyse du code source bash) et (2) pour diviser les lignes d'entrée en mots par le read intégré

Laissez-moi essayer de clarifier cela. Je pense qu'il serait bon de faire une distinction entre analyse et exécution. Bash doit d'abord analyser le code source, qui est évidemment un analyse événement, et plus tard il exécute le code, qui est quand l'expansion entre en jeu. L'expansion est vraiment un exécution un événement. En outre, je conteste la description de la $IFS variable que je viens de citer ci-dessus; plutôt que de dire que la division de mots est effectuée après l'expansion, Je dirais que la division des mots est effectuée pendant expansion, ou, peut-être même plus précisément, le fractionnement des mots est partie de le processus d'expansion. L'expression "division des mots" se réfère uniquement à cette étape d'expansion; il ne devrait jamais être utilisé pour se référer à l'analyse syntaxique du code source de bash, bien que malheureusement les docs semblent jeter beaucoup autour des mots "split" et "words". Voici un extrait pertinent de la linux.die.net version du manuel de bash:

L'expansion est effectuée sur la ligne de commande après qu'elle a été divisée en mots. Il y a sept sortes d'expansion effectuées: expansion de l'accolade, expansion de tilde, expansion des paramètres et des variables, substitution de commande, expansion arithmétique, fractionnement des mots, et extension de chemin.

L'ordre des expansions est: expansion des accolades; l'expansion du tilde, l'expansion des paramètres et des variables, l'expansion arithmétique et la substitution de commande (effectuée de gauche à droite); fractionnement des mots; et expansion du chemin d'accès.

Vous pourriez argumenter Version GNU du manuel fait légèrement mieux, puisqu'il 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 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 l'essentiel, le processus d'analyse de bash ne peut pas être modifié par des actions au niveau de l'utilisateur telles que les affectations de variables (en fait, il existe quelques exceptions mineures à cette règle, par exemple, voir les différentes compatxxles paramètres du shell, qui peut changer 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 étendus selon le processus général «d'extension» décrit dans les extraits de documentation ci-dessus, où le fractionnement du texte développé (en expansion?) En aval mots est simplement une étape de ce processus. Le fractionnement des mots ne touche que le texte qui a été craché lors d'une étape d'expansion précédente. cela n'affecte pas le texte littéral qui a été analysé directement par le flux 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 sommes de retour à l'aide read. N'ai-je pas dit plus tôt que read est inapproprié car il effectue deux niveaux de fractionnement, alors que nous n'en avons besoin que d'un seul? L'astuce ici est que vous pouvez appeler read de telle sorte qu’il ne fasse qu’un seul niveau de division, en particulier en divisant un seul champ par invocation, ce qui nécessite le coût de l’appeler plusieurs fois en 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 PRÉNOM argument à read, il ignore automatiquement les espaces de début et de fin dans chaque champ séparé de la chaîne d'entrée. Cela se produit si $IFS est défini sur sa valeur par défaut ou non, comme décrit précédemment dans ce message. Maintenant, l'OP peut ne pas s'en préoccuper 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: un usage peu évident de read est de passer à zéro PRÉNOM arguments. Dans ce cas, read va stocker la ligne d'entrée entière qu'il obtient du flux d'entrée dans une variable nommée $REPLY, et, en prime, il le fait ne pas supprimer les espaces de début et de fin de la valeur. Ceci est une utilisation très robuste de read que j'ai fréquemment exploité dans ma carrière de 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 n’aborde pas réellement le cas d’un séparateur de champ personnalisé, tel que l’espace des virgules des OP. Comme précédemment, les séparateurs de plusieurs caractères ne sont pas pris en charge, ce qui constitue une limitation regrettable de cette solution. Nous pourrions essayer de diviser au moins sur la virgule en spécifiant le séparateur à la -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, les espaces environnants non comptabilisés ont été tirés dans les valeurs du champ, ce qui nécessitait une correction ultérieure par des opérations de rognage (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 renvoie un code retour défaillant s'il rencontre la fin du fichier (dans ce cas, nous pouvons l'appeler fin de chaîne) sans rencontrer de terminateur de champ final sur le dernier champ. Cela provoque la rupture anticipée de la boucle while et nous perdons le champ final.

Techniquement, cette même erreur a également affecté les exemples précédents; la différence est que le séparateur de champ a été pris pour être LF, qui est la valeur par défaut lorsque vous ne spécifiez pas le -d option, et le <<< ("here-string") le mécanisme ajoute automatiquement un LF à la chaîne juste avant de le transmettre en entrée à la commande. Par conséquent, dans ces cas, nous sorte de accidentellement résolu le problème d'un champ final abandonné en ajoutant involontairement un terminateur factice supplémentaire à l'entrée. Appelons cette solution la solution "dummy-terminator". Nous pouvons appliquer la solution dummy-terminator manuellement pour n'importe quel délimiteur personnalisé en la concaténant contre la chaîne d'entrée nous-mêmes lors de l'instanciation 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 est de ne casser la boucle while que si les deux (1) read échec retourné et (2) $REPLY est vide, ce qui signifie read n'était pas capable de lire les caractères avant de frapper la fin du fichier. Démonstration

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 la LF secrète qui est automatiquement ajoutée à la chaîne ici par le <<< opérateur de redirection. Il pourrait bien sûr être séparé séparément par une opération de découpage explicite telle que décrite plus haut, mais de toute évidence, la méthode manuelle du terminateur factice résout le problème directement, nous pourrions donc nous contenter de cela. La solution manuelle de terminaison factice est en fait très pratique car elle résout en une fois ces deux problèmes (le problème du champ final et le problème ajouté).

Donc, dans l'ensemble, c'est une solution assez puissante. C'est seulement la faiblesse restante est un manque de soutien pour les délimiteurs multicharacters, que je vais aborder plus tard.


Mauvaise réponse # 8

string='first line
        second line
        third line'

readarray -t lines <<<"$string"

(Ceci est en fait à partir du même poste 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 dans une variable de tableau en un coup; ne pas jouer avec des boucles, des conditions, des substitutions, ou n'importe quoi d'autre. Et il ne supprime pas subrepticement les espaces de la chaîne d'entrée. Et si -O n'est pas donné) il efface commodément le tableau cible avant de l'assigner. Mais ce n'est toujours pas parfait, d'où ma critique de "mauvaise réponse".

Tout d'abord, juste pour obtenir ce hors de la route, notez que, tout comme le comportement de read lors de l'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 être le cas pour certains cas d'utilisation. Je reviendrai dans un instant.

Deuxièmement, comme précédemment, il ne supporte pas les délimiteurs multicharacters. Je vais donner une solution pour cela dans un instant aussi.

Troisièmement, la solution telle qu'é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 développer sur ce moment aussi bien.

Pour les raisons susmentionnées, je considère toujours que c'est une "mauvaise réponse" à la question du PO. Ci-dessous, je vais donner ce que je considère comme la bonne réponse.


Bonne réponse

Voici une tentative naïve de faire # 8 travailler en spécifiant simplement le -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 obtenu par l’approche double conditionnelle du bouclage read solution discutée dans #7. nous pouvons presque résolvez ceci avec l'astuce manuelle de terminaison factice:

readarray -td, a <<<"$string,"; declare -p a;
## declare -a a=([0]="Paris" [1]=" France" [2]=" Europe" [3]=$'\n')

Le problème ici est que readarray préservé le champ de fuite, depuis le <<< l'opérateur de redirection a ajouté le LF à la chaîne d'entrée, et donc le champ arrière était ne pas vide (sinon il aurait été abandonné). 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 subsistent, qui sont en fait liés, sont (1) l'espace externe qui doit être réduit, et (2) le manque de support pour les délimiteurs multicharacters.

L'espace blanc pourrait bien sûr être coupé par la suite (par exemple, voir Comment couper des espaces à partir d'une variable Bash?). Mais si nous pouvons pirater un délimiteur multicaractère, cela résoudrait les deux problèmes en un seul coup.

Malheureusement, il n'y a pas direct façon 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 multicharacters avec un délimiteur à un seul caractère qui sera garanti pour ne pas entrer en collision avec le contenu de la chaîne d'entrée. Le seul personnage qui a cette garantie est le NUL octet. En effet, dans bash (mais pas dans zsh, incidemment), 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")

Là, enfin! Cette solution ne divisera pas les champs au milieu, ne sera pas découpée prématurément, ne laissera pas tomber les champs vides, ne se corrompra pas lors des extensions de fichiers, ne supprimera pas automatiquement les espaces de début et de fin, ne laissera pas de LF ne nécessite pas de boucles et ne se contente pas d'un délimiteur à un seul caractère.


Solution de coupe

Enfin, je voulais démontrer ma propre solution de coupe assez complexe en utilisant l'obscur -C callback option de readarray. Malheureusement, je n'ai plus de place face à la limite draconienne de 30 000 caractères de Stack Overflow, je ne serai donc 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")

132
2017-07-19 21:20



t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

Imprime trois


33
2017-07-14 11:54



Parfois, il m'est arrivé que la méthode décrite dans la réponse acceptée ne fonctionne pas, surtout si le séparateur est un retour chariot.
Dans ces cas, j'ai résolu de la façon suivante:

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"

for line in "${lines[@]}"
    do
        echo "--> $line"
done

29
2017-11-02 13:44



La réponse acceptée fonctionne pour les valeurs d'une ligne.
 Si la variable a plusieurs lignes:

string='first line
        second line
        third line'

Nous avons besoin d'une commande très différente pour obtenir toutes les lignes:

while read -r line; do lines+=("$line"); done <<<"$string"

Ou le bash beaucoup plus simple readarray:

readarray -t lines <<<"$string"

L'impression de toutes les lignes est très facile en tirant parti d'une fonctionnalité printf:

printf ">[%s]\n" "${lines[@]}"

>[first line]
>[        second line]
>[        third line]

23
2017-07-24 21:24



Ceci est similaire à l'approche de Jmoney38, mais en utilisant sed:

string="1,2,3,4"
array=(`echo $string | sed 's/,/\n/g'`)
echo ${array[0]}

Imprime 1


4
2018-06-03 15:24



La clé pour diviser votre chaîne en un tableau est le délimiteur multi-caractères de ", ". Toute solution utilisant IFS pour les délimiteurs multi-caractères est intrinsèquement faux puisque IFS est un ensemble de ces caractères, pas une chaîne.

Si vous affectez IFS=", " alors la chaîne se cassera sur "," OU " " ou toute combinaison de ceux-ci qui n'est pas une représentation précise du délimiteur à deux caractères de ", ".

Vous pouvez utiliser awk ou sed diviser la chaîne, avec substitution de processus:

#!/bin/bash

str="Paris, France, Europe"
array=()
while read -r -d $'\0' each; do   # use a NUL terminated field separator 
    array+=("$each")
done < <(printf "%s" "$str" | awk '{ gsub(/,[ ]+|$/,"\0"); print }')
declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output

Il est plus efficace d'utiliser une regex directement dans Bash:

#!/bin/bash

str="Paris, France, Europe"

array=()
while [[ $str =~ ([^,]+)(,[ ]+|$) ]]; do
    array+=("${BASH_REMATCH[1]}")   # capture the field
    i=${#BASH_REMATCH}              # length of field + delimiter
    str=${str:i}                    # advance the string by that length
done                                # the loop deletes $str, so make a copy if needed

declare -p array
# declare -a array=([0]="Paris" [1]="France" [2]="Europe") output...

Avec la deuxième forme, il n'y a pas de sous-shell et il sera intrinsèquement plus rapide.


Modifier par bgoldst: Voici quelques benchmarks comparant mes readarray solution à la solution regex de dawg, et j'ai également inclus le read solution pour le diable (note: j'ai légèrement modifié la solution regex pour une plus grande harmonie avec ma solution) (voir aussi mes commentaires ci-dessous le post):

## competitors
function c_readarray { readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); unset 'a[-1]'; };
function c_read { a=(); local REPLY=''; while read -r -d ''; do a+=("$REPLY"); done < <(awk '{ gsub(/, /,"\0"); print; };' <<<"$1, "); };
function c_regex { a=(); local s="$1, "; while [[ $s =~ ([^,]+),\  ]]; do a+=("${BASH_REMATCH[1]}"); s=${s:${#BASH_REMATCH}}; done; };

## helper functions
function rep {
    local -i i=-1;
    for ((i = 0; i<$1; ++i)); do
        printf %s "$2";
    done;
}; ## end rep()

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    elif [[ $n -eq 1 ]]; then
        echo 'first field';
    elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

function testAll_splitIntoArray {
    local -i n=$1; ## number of fields in input string
    local s='';
    echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) =====";
    s="$(makeStringToSplit "$n")";
    testAll c_readarray c_read c_regex : "$s";
}; ## end testAll_splitIntoArray()

## results
testAll_splitIntoArray 1;
## ===== 1 field =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.000s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 10;
## ===== 10 fields =====
## c_readarray   real  0m0.067s   user 0m0.000s   sys  0m0.000s
## c_read        real  0m0.064s   user 0m0.000s   sys  0m0.000s
## c_regex       real  0m0.001s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 100;
## ===== 100 fields =====
## c_readarray   real  0m0.069s   user 0m0.000s   sys  0m0.062s
## c_read        real  0m0.065s   user 0m0.000s   sys  0m0.046s
## c_regex       real  0m0.005s   user 0m0.000s   sys  0m0.000s
##
testAll_splitIntoArray 1000;
## ===== 1000 fields =====
## c_readarray   real  0m0.084s   user 0m0.031s   sys  0m0.077s
## c_read        real  0m0.092s   user 0m0.031s   sys  0m0.046s
## c_regex       real  0m0.125s   user 0m0.125s   sys  0m0.000s
##
testAll_splitIntoArray 10000;
## ===== 10000 fields =====
## c_readarray   real  0m0.209s   user 0m0.093s   sys  0m0.108s
## c_read        real  0m0.333s   user 0m0.234s   sys  0m0.109s
## c_regex       real  0m9.095s   user 0m9.078s   sys  0m0.000s
##
testAll_splitIntoArray 100000;
## ===== 100000 fields =====
## c_readarray   real  0m1.460s   user 0m0.326s   sys  0m1.124s
## c_read        real  0m2.780s   user 0m1.686s   sys  0m1.092s
## c_regex       real  17m38.208s   user 15m16.359s   sys  2m19.375s
##

2
2017-11-26 19:59



Essaye ça

IFS=', '; array=(Paris, France, Europe)
for item in ${array[@]}; do echo $item; done

C'est simple. Si vous le souhaitez, vous pouvez également ajouter une déclaration (et également supprimer les virgules):

IFS=' ';declare -a array=(Paris France Europe)

L'IFS est ajouté pour annuler ce qui précède mais cela fonctionne sans cela dans une nouvelle instance de bash


1
2018-03-04 06:02



Utilisez ceci:

countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe

0
2017-12-19 15:27