Diviser la 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 un code simple, la vitesse de la commande importe peu. Comment puis-je le faire?

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

Notez que les caractères de $IFS sont traités individuellement comme des séparateurs, de sorte que, dans ce cas, les champs peuvent être séparés par une virgule ou un espace plutôt que par 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 entre 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 parcourir 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 append 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 rares, vous ne devriez donc pas utiliser la longueur pour obtenir le dernier élément. Voici ce que vous pouvez faire dans Bash 4.2 et versions ultérieures:

 echo "${array[-1]}" 

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

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

Les plus grands décalages négatifs sont plus éloignés de la fin du tableau. Notez l'espace avant le signe moins dans l'ancienne forme. C'est requirejs.

Voici un moyen sans définir IFS:

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

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

 ${ssortingng//subssortingng/replacement} 

remplacer toutes les correspondances de $ sous-chaîne par des espaces blancs, puis utiliser la chaîne substituée pour initialiser un tableau:

 (element1 element2 ... elementN) 

Remarque: cette réponse utilise l’ opérateur split + glob . Ainsi, pour empêcher l’expansion de certains caractères (tels que * ), il est conseillé de suspendre la mise en globalisation pour ce script.

Toutes les réponses à cette question sont fausses d’une manière ou d’une autre.


Mauvaise réponse n ° 1

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

1: Ceci est une mauvaise utilisation de $IFS . La valeur de la variable $IFS n'est pas considérée comme un seul séparateur de chaîne de longueur variable , mais plutôt comme un ensemble de séparateurs de chaînes à un caractère , où chaque champ read à partir de la ligne d'entrée peut être terminé par n'importe quel caractère. dans l'ensemble (virgule ou espace, dans cet exemple).

En fait, pour les vrais sticklers, la signification de $IFS est légèrement plus importante. A partir du manuel bash :

Le shell traite chaque caractère de 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 si sa valeur est exactement , la valeur par défaut, alors les séquences de , et 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 n'est pas au début ou à la fin sert à délimiter des mots. Si IFS a une valeur autre que la valeur par défaut, alors les séquences des caractères d' espacement , et sont ignorées au début et à la fin du mot, tant que le caractère d'espacement est dans la valeur de IFS (un caractère d'espacement 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 par défaut de $IFS , les champs peuvent être séparés avec (1) une séquence d'un ou plusieurs caractères qui proviennent tous de l'ensemble des "caractères d' espacement IFS" (c'est-à-dire celui de , , et ("newline", signifiant saut de ligne (LF) ) sont présents n'importe où dans $IFS ), ou (2) tout caractère "IFS blanc" présent dans $IFS avec " Les caractères 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, si sa chaîne de saisie était 'Los Angeles, United States, North America' ?

 IFS=', ' read -ra a <<<'Los Angeles, United States, North America'; declare -pa; ## declare -aa=([0]="Los" [1]="Angeles" [2]="United" [3]="States" [4]="North" [5]="America") 

2: Même si vous utilisiez 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 de la variable $ssortingng contient des LF, alors read arrête le traitement une fois qu'il rencontre le premier LF. La read intégrée ne traite qu'une ligne par invocation. Cela est vrai même si vous ne faites que canaliser ou redirect les entrées uniquement vers l'instruction read , comme nous le faisons dans cet exemple avec le mécanisme here-ssortingng , et que les entrées non traitées sont donc perdues. Le code qui alimente la read intégrée n'a aucune connaissance du stream de données dans sa structure de commande contenant.

Vous pourriez faire valoir que cela est peu susceptible de causer un problème, mais néanmoins, il s'agit d'un danger subtil qui devrait être évité si possible. Cela est dû au fait que le readin intégré effectue en fait deux niveaux de fractionnement des entrées: d'abord en lignes, puis en champs. Puisque l'OP ne veut qu'un seul niveau de division, cette utilisation de la read intégrée n'est pas appropriée 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:

 ssortingng=', , a, , b, c, , , '; IFS=', ' read -ra a <<<"$string"; declare -pa; ## declare -aa=([0]="" [1]="" [2]="a" [3]="" [4]="b" [5]="c" [6]="" [7]="") 

Peut-être que le PO ne se soucierait pas de cela, mais c'est toujours une limitation à 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 arrière factice à la chaîne d'entrée juste avant de le read , comme je le démontrerai plus tard.


Mauvaise réponse # 2

 ssortingng="1:2:3:4:5" set -f # avoid globbing (expansion of *). array=(${ssortingng//:/ }) 

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 omise.)

Idée similaire:

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

Ces solutions exploitent le fractionnement des mots dans une affectation de tableau pour diviser la chaîne en champs. Curieusement, tout comme le fait de read , le fractionnement général des mots utilise également la variable spéciale $IFS , bien que dans ce cas il soit implicite que sa valeur par défaut soit , et donc toute séquence de un. ou plusieurs caractères IFS (qui sont tous des caractères d'espacement) sont considérés comme un délimiteur de champ.

Cela résout le problème des deux niveaux de division engagés par la read , puisque le fractionnement des mots en soi ne constitue qu'un seul niveau de division. Mais comme précédemment, le problème réside dans le fait que les champs individuels de la chaîne d'entrée peuvent déjà contenir des caractères $IFS et qu'ils seraient donc fractionnés de manière incorrecte lors de l'opération de fractionnement des mots. Ce n'est pas le cas pour les exemples de chaînes d'entrée fournis par ces répondeurs (quelle commodité ...), mais cela ne change rien au fait que toute base de code utilisant cet idiome risquerait alors de exploser si cette hypothèse a été violée à un moment donné dans 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, le fractionnement des mots est normalement suivi de l’ extension du nom de fichier ( alias extension de nom de fichier, alias globbing), ce qui, s’il est fait, pourrait corrompre les mots contenant les caractères * ? ou [ suivi de ] (et, si extglob est défini, des fragments entre parenthèses précédés de ? * , + , @ ou ! ) en les comparant aux objects 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ésolu ce problème en exécutant au préalable set -f pour désactiver la globalisation. Techniquement, cela fonctionne (bien que vous devriez probablement append set +f après pour le réactiver pour le code suivant qui peut en dépendre), mais il n'est pas souhaitable d'avoir à jouer avec les parameters globaux du shell pour pirater une parsing de base de la chaîne en 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 envisagez d’utiliser cette solution, il est préférable d’utiliser la forme d’ extension de paramètre ${ssortingng//:/ } "substitution de modèle", plutôt que d’avoir à invoquer une substitution de commande ), lancer un pipeline et exécuter un exécutable externe ( tr ou sed ), car l'extension des parameters est purement interne au shell. (De plus, pour les solutions tr et sed , la variable d'entrée doit être entre guillemets dans la substitution de commande; sinon, le fractionnement des mots prendrait effet dans la commande echo et risquerait de perturber les valeurs des champs. $(...) la forme de substitution de commande est préférable à l'ancienne forme `...` car elle simplifie l'imbrication des substitutions de commandes et permet une meilleure coloration syntaxique 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 dans $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 extension de substitution de motif, puis en utilisant le fractionnement de mots pour séparer les champs du caractère de délimiteur représenté par IFS.

Ce n'est pas une solution très générique. De plus, on peut affirmer que la virgule est réellement le caractère "primaire" du délimiteur, et que sa suppression, puis sa dépendance au caractère d'espace pour la division des champs, est tout simplement fausse. 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 la mise à jour pour l’assignation avec set -f , puis en 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

 ssortingng='first line second line third line' oldIFS="$IFS" IFS=' ' IFS=${IFS:0:1} # this is useful to format your code with tabs lines=( $ssortingng ) IFS="$oldIFS" 

Ceci est similaire à # 2 et # 3 dans la mesure où il utilise le fractionnement des mots pour faire le travail, mais maintenant seulement 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 à plusieurs caractères tels que le délimiteur d’espaces comma 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 division, selon les besoins.

Un problème est que l’extension du nom de fichier corrompra les mots concernés comme décrit précédemment, bien qu’une fois de plus 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 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 . Cela ne serait bien sûr pas un problème si le délimiteur se trouve être un "caractère blanc IFS", et selon l'application, cela peut ne pas avoir d'importance, mais cela ne corrompt pas 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 blanc IFS" ou que vous ne vous souciez pas des champs vides, et que vous enrouliez l'instruction critique dans set -f et set +f , alors cette solution fonctionne, mais sinon pas.

(Aussi, pour information, assigner un LF à une variable dans bash peut être fait plus facilement avec la syntaxe $'...' , par exemple IFS=$'\n'; )


Mauvaise réponse # 5

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

Idée similaire:

 IFS=', ' eval 'array=($ssortingng)' 

Cette solution est en fait un croisement entre # 1 (en définissant $IFS sur un espace de virgule) et # 2-4 (en ce sens qu'elle 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 des mondes.

En outre, en ce qui concerne la seconde variante, il peut sembler que l'appel eval 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 un avantage très évident à utiliser eval de cette façon. Normalement, lorsque vous exécutez une commande simple qui consiste uniquement en une affectation de variable, c'est-à-dire sans un mot de commande réel, l'assignation 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=($counsortinges); ## changes both $IFS and $array in the shell environment 

Mais si l'atsortingbution de la variable est attachée à un nom de commande (j'aime appeler cela une "atsortingbution 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 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 fonction d’affectation de variables pour modifier temporairement $IFS , ce qui nous permet d’éviter tout le jeu de sauvegarde et de restauration comme celui qui est fait avec la variable $OIFS dans la première variante. Mais le défi auquel nous sums 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 n'impliquerait pas un mot de commande pour rendre l'affectation $IFS temporaire. Vous pourriez penser à vous-même, bien pourquoi ne pas simplement append un mot de commande no-op à l'instruction comme : builtin pour rendre l'affectation $IFS temporaire? Cela ne fonctionne pas car cela rendrait aussi l'assignation $array temporaire aussi:

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

Donc, nous sums effectivement dans une impasse, un peu un catch-22. Mais lorsque 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 l'affectation $array dans l'argument eval pour qu'il prenne effet dans l'environnement shell. l'affectation du préfixe $IFS préfixé à la commande eval ne survit pas à la commande eval . C'est exactement le truc qui est utilisé dans la deuxième variante de cette solution:

 IFS=', ' eval 'array=($ssortingng)'; ## $IFS does not outlive the eval command, but $array does 

Donc, comme vous pouvez le voir, c'est en fait un truc astucieux, et accomplit exactement ce qui est requirejs (du moins en ce qui concerne l'affectation de l'affectation) d'une manière plutôt évidente. Je ne suis en fait pas contre cette astuce en général, malgré l'implication d' 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", cela rest 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 le faire.

Il semble que le répondant ait supposé que la variable $IFS affecte tous les parsings de bash dans tous les contextes, ce qui n'est pas vrai. A partir du manuel bash:

IFS Le séparateur de champs interne utilisé pour le fractionnement des mots après expansion et pour séparer les lignes en mots avec la commande read intégrée. La valeur par défaut est .

La variable spéciale $IFS n'est donc utilisée que dans deux contextes: (1) le fractionnement des mots effectué après l'expansion (c'est-à-dire non lors de l'parsing du code source bash) et (2) le fractionnement des lignes d'entrée en mots.

Laissez-moi essayer de rendre cela plus clair. Je pense qu'il serait bon de faire une distinction entre parsing et exécution . Bash doit d'abord parsingr le code source, qui est évidemment un événement d' parsing syntaxique , puis, plus tard, il exécute le code, c'est-à-dire quand l'extension entre en jeu. L'expansion est vraiment un événement d' exécution . En outre, je conteste la description de la variable $IFS que je viens de citer ci-dessus; Plutôt que de dire que le fractionnement des mots est effectué après l'expansion , je dirais que le fractionnement des mots est effectué pendant l' expansion ou, peut-être plus précisément encore, que le fractionnement des mots fait partie du processus d'expansion. L'expression "fractionnement de mots" se réfère uniquement à cette étape d'expansion; il ne devrait jamais être utilisé pour faire référence à l'parsing du code source bash, bien que, malheureusement, les docs semblent beaucoup entourer 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 existe sept types d'expansion: l' expansion des accolades, l'expansion des tilde, l'expansion des parameters et des variables , la substitution de commandes , l'expansion arithmétique , le fractionnement des mots et l' extension des chemins .

L'ordre des expansions est: expansion des accolades; expansion de tilde, expansion de parameters et de variables, expansion arithmétique et substitution de commandes (effectuée de gauche à droite); fractionnement des mots; et expansion du chemin d'access.

Vous pourriez soutenir que la version GNU du manuel est légèrement meilleure, 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 que $IFS ne change pas la façon dont bash parsing le code source. L'parsing 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 parameters, les substitutions arithmétiques et les substitutions de commandes. Pour la plupart, le processus d'parsing bash ne peut pas être modifié par des actions de niveau utilisateur telles que les affectations de variables (en fait, il existe quelques exceptions mineures à cette règle; par exemple, voir les différents compatxx shell compatxx à la volée). Les «mots» / «jetons» en amont qui résultent de ce processus d'parsing 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é recraché lors d'une étape précédente; cela n'affecte pas le texte littéral qui a été analysé directement par le stream source.


Mauvaise réponse # 7

 ssortingng='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 revenons à l'utilisation de read . N'ai-je pas dit plus tôt que la read est inappropriée parce qu'elle effectue deux niveaux de division, alors que nous n'en avons besoin que d'une seule? L'astuce ici est que vous pouvez appeler la read de telle manière qu'elle ne fasse qu'un seul niveau de fractionnement, en particulier en ne séparant qu'un seul champ par invocation, ce qui nécessite le coût de devoir l'appeler plusieurs fois dans une boucle. C'est un peu un tour de passe-passe, mais ça marche.

Mais il y a des problèmes. Tout d'abord: lorsque vous fournissez au moins un argument NAME à 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 cet article. 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'parsing. Mais tous ceux qui veulent parsingr une chaîne dans des champs ne le voudront pas. Il existe toutefois une solution: une utilisation peu évidente de read consiste à transmettre des arguments NAME nuls. Dans ce cas, read stockera l'intégralité de la ligne d'entrée qu'il reçoit du stream d'entrée dans une variable nommée $REPLY et, en prime, il ne supprime pas les espaces de début et de fin de la valeur. C'est un usage très robuste de la read que j'ai fréquemment exploité dans ma carrière de programmation shell. Voici une démonstration de la différence de comportement:

 ssortingng=$' ab \ncd \nef '; ## input ssortingng a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -pa; ## declare -aa=([0]="ab" [1]="cd" [2]="ef") ## read trimmed surrounding whitespace a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -pa; ## declare -aa=([0]=" ab " [1]=" cd " [2]=" ef ") ## 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 de l’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 la virgule en spécifiant le séparateur à l'option -d , mais regardez ce qui se passe:

 ssortingng='Paris, France, Europe'; a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string"; declare -pa; ## declare -aa=([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 on peut 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 affectait également les exemples précédents; la différence est que le séparateur de champs a été pris pour LF, qui est la valeur par défaut lorsque vous ne spécifiez pas l'option -d , et que le mécanisme <<< ("here-ssortingng") ajoute automatiquement un LF à la chaîne juste avant de l’alimenter en entrée de la commande. Par conséquent, dans ces cas, nous avons en quelque sorte 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 manuellement la solution factice-terminateur à tout délimiteur personnalisé en la concaténant avec la chaîne d'entrée elle-même lors de l'instanciation dans la chaîne ici:

 a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,"; declare -pa; declare -aa=([0]="Paris" [1]=" France" [2]=" Europe") 

Là, le problème est résolu. Une autre solution consiste à ne casser que la boucle while (1) en cas d'échec de la read (2) et que (2) $REPLY est vide, ce qui signifie que read n'a pu lire aucun caractère avant la fin du fichier. Démo:

 a=(); while read -rd,|| [[ -n "$REPLY" ]]; do a+=("$REPLY"); done <<<"$string"; declare -pa; ## declare -aa=([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 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. Il ne rest que la faiblesse est un manque de support pour les délimiteurs multicaractères, dont je parlerai plus tard.


Mauvaise réponse # 8

 ssortingng='first line second line third line' readarray -t lines <<<"$string" 

(Il s'agit en fait du même article que le numéro 7 ; le répondeur a fourni deux solutions dans le même article.)

Le readarray readarray, qui est synonyme de mapfile , est idéal. C'est une commande intégrée qui parsing un stream d'octets en une variable de tableau en une seule fois. pas de problèmes avec les boucles, les conditionnels, les substitutions ou toute autre chose. Et il ne dépouille pas subrepticement les espaces de la chaîne d'entrée. Et (si -O n'est pas donné), il efface le tableau cible avant de lui assigner. Mais ce n'est toujours pas parfait, d'où ma critique comme une "mauvaise réponse".

Tout d'abord, pour éviter cela, notez que, tout comme le comportement de read lors de l'parsing de readarray , 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 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 à caractères multiples. Je vais donner une solution à cela dans un instant.

Troisièmement, la solution telle qu'écrite n'parsing pas la chaîne d'entrée de l'OP, et en fait, elle ne peut pas être utilisée telle quelle pour l'parsingr. Je vais développer cela aussi brièvement.

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 le # 8 en spécifiant simplement l'option -d :

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

Nous voyons que le résultat est identique au résultat obtenu par l’approche à deux conditions de la solution de read boucle décrite au n ° 7 . Nous pouvons presque résoudre ce problème avec l'astuce manuelle:

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

Le problème ici est que readarray préservé le champ de fin, puisque l'opérateur de redirection <<< ajouté le LF à la chaîne d'entrée et que le champ de fin n'était donc 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 -pa; ## declare -aa=([0]="Paris" [1]=" France" [2]=" Europe") 

Les deux seuls problèmes qui restnt, et qui sont en réalité liés, sont (1) les espaces blancs à réduire et (2) le manque de prise en charge des délimiteurs à caractères multiples.

Les espaces peuvent bien sûr être tronqués par la suite (par exemple, voir Comment dé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 de moyen direct pour 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 seul caractère qui sera garanti pour ne pas entrer en collision avec le contenu de la chaîne d'entrée. Le seul caractère qui possède cette garantie est l' octet NUL . 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 procéder en utilisant awk :

 readarray -td '' a < <(awk '{ gsub(/, /,"\0"); print; }' <<<"$string, "); unset 'a[-1]'; declare -pa; ## declare -aa=([0]="Paris" [1]="France" [2]="Europe") 

Là enfin! Cette solution ne divisera pas les champs par erreur au milieu, ne sera pas interrompue prématurément, ne laissera pas tomber les champs vides, ne se corrompra pas sur les extensions de noms, ne supprimera pas automatiquement les espaces avant, ne nécessite pas de boucles et ne se limite pas à un délimiteur à un seul caractère.


Solution d'ébarbage

Enfin, je voulais démontrer ma propre solution de découpage assez complexe en utilisant l’option de -C callback de readarray . Malheureusement, je suis à court de place face à la limite draconienne de 30 000 caractères de Stack Overflow, donc je ne pourrai pas l'expliquer. Je vais laisser cela comme un exercice pour le lecteur.

 function mfcb { local val="$4"; "$1"; eval "$2[$3]=\$val;"; }; function val_lsortingm { if [[ "$val" =~ ^[[:space:]]+ ]]; then val="${val:${#BASH_REMATCH[0]}}"; fi; }; function val_rsortingm { if [[ "$val" =~ [[:space:]]+$ ]]; then val="${val:0:${#val}-${#BASH_REMATCH[0]}}"; fi; }; function val_sortingm { val_lsortingm; val_rsortingm; }; readarray -c1 -C 'mfcb val_sortingm a' -td, <<<"$string,"; unset 'a[-1]'; declare -pa; ## declare -aa=([0]="Paris" [1]="France" [2]="Europe") 
 t="one,two,three" a=($(echo "$t" | tr ',' '\n')) echo "${a[2]}" 

Prints three

Sometimes it happened to me that the method described in the accepted answer didn’t work, especially if the separator is a carriage return.
In those cases I solved in this way:

 ssortingng='first line second line third line' oldIFS="$IFS" IFS=' ' IFS=${IFS:0:1} # this is useful to format your code with tabs lines=( $ssortingng ) IFS="$oldIFS" for line in "${lines[@]}" do echo "--> $line" done 

The accepted answer works for values in one line.
If the variable has several lines:

 ssortingng='first line second line third line' 

We need a very different command to get all lines:

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

Or the much simpler bash readarray :

 readarray -t lines <<<"$string" 

Printing all lines is very easy taking advantage of a printf feature:

 printf ">[%s]\n" "${lines[@]}" >[first line] >[ second line] >[ third line] 

This is similar to the approach by Jmoney38, but using sed:

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

Prints 1

The key to splitting your ssortingng into an array is the multi character delimiter of ", " . Any solution using IFS for multi character delimiters is inherently wrong since IFS is a set of those characters, not a ssortingng.

If you assign IFS=", " then the ssortingng will break on EITHER "," OR " " or any combination of them which is not an accurate representation of the two character delimiter of ", " .

You can use awk or sed to split the ssortingng, with process substitution:

 #!/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 

It is more efficient to use a regex you directly in 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 ssortingng 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... 

With the second form, there is no sub shell and it will be inherently faster.


Edit by bgoldst: Here are some benchmarks comparing my readarray solution to dawg's regex solution, and I also included the read solution for the heck of it (note: I slightly modified the regex solution for greater harmony with my solution) (also see my comments below the 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 -ii=-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 makeSsortingngToSplit { local -in=$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 makeSsortingngToSplit() function testAll_splitIntoArray { local -in=$1; ## number of fields in input ssortingng local s=''; echo "===== $n field$(if [[ $n -ne 1 ]]; then echo 's'; fi;) ====="; s="$(makeSsortingngToSplit "$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 ## 

Essaye ça

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

C’est simple. If you want, you can also add a declare (and also remove the commas):

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

The IFS is added to undo the above but it works without it in a fresh bash instance

Utilisez ceci:

 counsortinges='Paris, France, Europe' OIFS="$IFS" IFS=', ' array=($counsortinges) IFS="$OIFS" #${array[1]} == Paris #${array[2]} == France #${array[3]} == Europe 

Here’s my hack!

Splitting ssortingngs by ssortingngs is a pretty boring thing to do using bash. What happens is that we have limited approaches that only work in a few cases (split by “;”, “/”, “.” and so on) or we have a variety of side effects in the outputs.

The approach below has required a number of maneuvers, but I believe it will work for most of our needs!

 #!/bin/bash # -------------------------------------- # SPLIT FUNCTION # ---------------- F_SPLIT_R=() f_split() { : 'It does a "split" into a given ssortingng and returns an array. Args: TARGET_P (str): Target ssortingng to "split". DELIMITER_P (Optional[str]): Delimiter used to "split". If not informed the split will be done by spaces. Returns: F_SPLIT_R (array): Array with the provided ssortingng separated by the informed delimiter. ' F_SPLIT_R=() TARGET_P=$1 DELIMITER_P=$2 if [ -z "$DELIMITER_P" ] ; then DELIMITER_P=" " fi REMOVE_N=1 if [ "$DELIMITER_P" == "\n" ] ; then REMOVE_N=0 fi # NOTE: This was the only parameter that has been a problem so far! # By Questor # [Ref.: https://unix.stackexchange.com/a/390732/61742] if [ "$DELIMITER_P" == "./" ] ; then DELIMITER_P="[.]/" fi if [ ${REMOVE_N} -eq 1 ] ; then # NOTE: Due to bash limitations we have some problems getting the # output of a split by awk inside an array and so we need to use # "line break" (\n) to succeed. Seen this, we remove the line breaks # momentarily afterwards we reintegrate them. The problem is that if # there is a line break in the "ssortingng" informed, this line break will # be lost, that is, it is erroneously removed in the output! # By Questor TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}") fi # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the # amount of "\n" that there was originally in the string (one more # occurrence at the end of the string)! We can not explain the reason for # this side effect. The line below corrects this problem! By Questor TARGET_P=${TARGET_P%????????????????????????????????} SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}") while IFS= read -r LINE_NOW ; do if [ ${REMOVE_N} -eq 1 ] ; then # NOTE: We use "'" to prevent blank lines with no other characters # in the sequence being erroneously removed! We do not know the # reason for this side effect! By Questor LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'") # NOTE: We use the commands below to revert the intervention made # immediately above! By Questor LN_NOW_WITH_N=${LN_NOW_WITH_N%?} LN_NOW_WITH_N=${LN_NOW_WITH_N#?} F_SPLIT_R+=("$LN_NOW_WITH_N") else F_SPLIT_R+=("$LINE_NOW") fi done <<< "$SPLIT_NOW" } # -------------------------------------- # HOW TO USE # ---------------- STRING_TO_SPLIT=" * How do I list all databases and tables using psql? \" sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\" sudo -u postgres /usr/pgsql-9.4/bin/psql  -c \"\dt\" \" \" \list or \l: list all databases \dt: list all tables in the current database \" [Ref.: https://dba.stackexchange.com/questions/1285/how-do-i-list-all-databases-and-tables-using-psql] " f_split "$STRING_TO_SPLIT" "bin/psql -c" # -------------------------------------- # OUTPUT AND TEST # ---------------- ARR_LENGTH=${#F_SPLIT_R[*]} for (( i=0; i<=$(( $ARR_LENGTH -1 )); i++ )) ; do echo " > -----------------------------------------" echo "${F_SPLIT_R[$i]}" echo " < -----------------------------------------" done if [ "$STRING_TO_SPLIT" == "${F_SPLIT_R[0]}bin/psql -c${F_SPLIT_R[1]}" ] ; then echo " > -----------------------------------------" echo "The ssortingngs are the same!" echo " < -----------------------------------------" fi 

Another way to do it without modifying IFS:

 read -r -a myarray <<< "${string//, /$IFS}" 

Rather than changing IFS to match our desired delimiter, we can replace all occurrences of our desired delimiter ", " with contents of $IFS via "${ssortingng//, /$IFS}" .

Maybe this will be slow for very large ssortingngs though?

This is based on Dennis Williamson's answer.

Another approach can be:

 str="a, b, c, d" # assuming there is a space after ',' as in Q arr=(${str//,/}) # delete all occurrences of ',' 

After this ‘arr’ is an array with four ssortingngs. This doesn’t require dealing IFS or read or any other special stuff hence much simpler and direct.

UPDATE: Don’t do this, due to problems with eval.

With slightly less ceremony:

 IFS=', ' eval 'array=($ssortingng)' 

par exemple

 ssortingng="foo, bar,baz" IFS=', ' eval 'array=($ssortingng)' echo ${array[1]} # -> bar 

Another way would be:

 ssortingng="Paris, France, Europe" IFS=', ' arr=(${ssortingng}) 

Now your elements are stored in “arr” array. To iterate through the elements:

 for i in ${arr[@]}; do echo $i; done