Fractionner la chaîne en un tableau dans Bash


641

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?


22
C'est le hit n ° 1 de Google, mais la réponse est controversée, car la question porte malheureusement sur la délimitation sur , (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/…
antak

Si vous voulez munir une chaîne et ne vous souciez pas de l'avoir comme tableau, cutc'est une commande bash utile à garder à l'esprit également. Le séparateur est définissable en.wikibooks.org/wiki/Cut Vous pouvez également extraire des données d'une structure d'enregistrement à largeur fixe. en.wikipedia.org/wiki/Cut_(Unix) computerhope.com/unix/ucut.htm
JGFMK

Réponses:


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

On notera que les caractères $IFSsont traités individuellement en tant que séparateurs de sorte que dans ce cas , les champs peuvent être séparés par soit une virgule ou un espace plutôt que la séquence des deux personnages. Fait intéressant cependant, les champs vides ne sont pas créés lorsque un espace virgule 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 ajouter un élément, puis les indices 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 devez donc pas utiliser la longueur pour obtenir le dernier élément. Voici comment vous pouvez 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}"

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


15
Utilisez-le IFS=', ', vous n'aurez pas à supprimer les espaces séparément. Test:IFS=', ' read -a array <<< "Paris, France, Europe"; echo "${array[@]}"
l0b0

4
@ l0b0: Merci. Je ne sais pas à quoi je pensais. Au fait, j'aime utiliser declare -p arraypour la sortie de test.
pause jusqu'à nouvel ordre.

1
Cela ne semble pas respecter les citations. Par exemple, France, Europe, "Congo, The Democratic Republic of the"cela se divisera après le Congo.
Yisrael Dov,

2
@YisraelDov: Bash n'a aucun moyen de gérer le CSV par lui-même. Il ne peut pas faire la différence entre des virgules à l'intérieur des guillemets et celles à l'extérieur. Vous devrez utiliser un outil qui comprend CSV tel qu'une bibliothèque dans un langage de niveau supérieur, par exemple le module csv en Python.
pause jusqu'à nouvel ordre.

5
str="Paris, France, Europe, Los Angeles"; IFS=', ' read -r -a array <<< "$str"se divisera en array=([0]="Paris" [1]="France" [2]="Europe" [3]="Los" [4]="Angeles")note. Donc, cela ne fonctionne qu'avec des champs sans espaces car il IFS=', 's'agit d'un ensemble de caractères individuels - pas d'un délimiteur de chaîne.
dawg

333

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 $IFSvariable 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 readse 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 $IFSest 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 $IFSavec 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 $stringvariable de se contenir de lignocellulosiques, puis readsera arrêtez le traitement une fois qu'il rencontre le premier LF. Le programme readinté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' readinstruction, 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 readinté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 readbuiltin 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 readintégrée n'est pas appropriée et nous devons l'éviter.

3: Un problème potentiel non évident avec cette solution est que readle 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 readfractionnement de mots général utilise également la $IFSvariable 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 $IFScaractè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 extglobest 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 -fau 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 ( trou sed), car l'expansion des paramètres est purement une opération interne au shell. (De plus, pour les solutions tret 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 echocommande 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 $IFSet 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 -fpuis 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 $IFSpour 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 -fet 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 -fet 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 $IFSsur 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' evalappel 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 evalde 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 $IFSne changer que temporairement, ce qui nous permet d'éviter tout le gambit de sauvegarde et de restauration comme celui qui est fait avec la $OIFSvariable 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' $IFSaffectation 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 : builtinpour rendre l' $IFSaffectation temporaire? Cela ne fonctionne pas car cela rendrait également l' $arrayaffectation 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 evalexé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' $arrayaffectation à l'intérieur de l' evalargument pour qu'il prenne effet dans l'environnement shell, tandis que l' $IFSaffectation de préfixe qui est préfixé à la evalcommande ne survivra pas à la evalcommande. 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 $IFSvariable 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 $IFSvariable 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 readcode 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 $IFSvariable 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, $IFSne 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 compatxxparamè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 readc'é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 readde 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 $IFSsa 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 readest de passer zéro argument NAME . Dans ce cas, readstockera 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 readdont 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' -doption, 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 readretourne 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' -doption, 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 readrenvoyé un échec et (2) $REPLYest vide, ce qui signifie qu'il readn'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 readarraybuiltin, 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 -On'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' readanalyse de champ, readarraysupprime 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' -doption:

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 readsolution 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 readarrayconservé 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 callbackoption 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")

8
Il peut également être utile de noter (bien qu'il soit compréhensible que vous n'ayez pas eu de place pour le faire) que l' -doption d' readarrayapparaître en premier dans Bash 4.4.
fbicknel

2
Excellente réponse (+1). Si vous changez votre awk en awk '{ gsub(/,[ ]+|$/,"\0"); print }'et éliminez cette concaténation de la finale, ", " vous n'avez pas à passer par la gymnastique pour éliminer le record final. Donc: readarray -td '' a < <(awk '{ gsub(/,[ ]+/,"\0"); print; }' <<<"$string")sur Bash qui prend en charge readarray. Notez votre méthode est Bash 4.4+ Je pense à cause de la -ddansreadarray
Dawg

3
@datUser C'est malheureux. Votre version de bash doit être trop ancienne pour readarray. Dans ce cas, vous pouvez utiliser la deuxième meilleure solution basée sur read. Je fais référence à ceci: a=(); while read -rd,; do a+=("$REPLY"); done <<<"$string,";(avec la awksubstitution si vous avez besoin d'un support de délimiteur multicaractère). Faites-moi savoir si vous rencontrez des problèmes; Je suis presque sûr que cette solution devrait fonctionner sur des versions assez anciennes de bash, de retour à la version 2-quelque chose, sortie il y a deux décennies.
bgoldst

1
Wow, quelle brillante réponse! Hé hé, ma réponse: abandonné le script bash et mis le feu au python!
artfulrobot

1
@datUser bash sur OSX est toujours bloqué à 3.2 (publié vers 2007); J'ai utilisé le bash trouvé dans Homebrew pour obtenir les versions bash 4.X sur OS X
JDS

222

Voici un moyen sans définir 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 est d'utiliser le remplacement de chaîne:

${string//substring/replacement}

pour remplacer toutes les correspondances de $ substring par un espace blanc, puis en utilisant 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 judicieux de suspendre la globalisation de ce script.


1
J'ai utilisé cette approche ... jusqu'à ce que je tombe sur une longue chaîne à diviser. 100% CPU pendant plus d'une minute (puis je l'ai tué). C'est dommage car cette méthode permet de diviser par une chaîne, pas un caractère dans IFS.
Werner Lehmann

100% de temps processeur pendant une minute me semble comme s'il devait y avoir quelque chose de mal quelque part. Quelle était la longueur de cette chaîne, est-elle de taille Mo ou Go? Je pense que normalement, si vous avez juste besoin d'une petite fente de chaîne, vous voulez rester dans Bash, mais si c'est un énorme fichier, j'exécuterais quelque chose comme Perl pour le faire.

12
AVERTISSEMENT: vient de rencontrer un problème avec cette approche. Si vous avez un élément nommé *, vous obtiendrez également tous les éléments de votre cwd. ainsi string = "1: 2: 3: 4: *" donnera des résultats inattendus et peut-être dangereux selon votre implémentation. N'a pas obtenu la même erreur avec (IFS = ',' read -a array <<< "$ string") et celui-ci semble sûr à utiliser.
Dieter Gribnitz

4
la citation ${string//:/ }empêche l'expansion du shell
Andrew White

1
J'ai dû utiliser ce qui suit sur OSX: array=(${string//:/ })
Mark Thomson

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

Imprime trois


8
En fait, je préfère cette approche. Facile.
shrimpwagon

4
J'ai copié et collé cela et cela n'a pas fonctionné avec l'écho, mais a fonctionné quand je l'ai utilisé dans une boucle for.
Ben

2
Cela ne fonctionne pas comme indiqué. @ Jmoney38 ou shrimpwagon si vous pouvez coller ceci dans un terminal et obtenir la sortie souhaitée, veuillez coller le résultat ici.
abalter

2
@abalter Fonctionne pour moi avec a=($(echo $t | tr ',' "\n")). Même résultat avec a=($(echo $t | tr ',' ' ')).
le

@procrastinator Je viens de l'essayer VERSION="16.04.2 LTS (Xenial Xerus)"dans un bashshell, et le dernier echoimprime juste une ligne vierge. Quelle version de Linux et quel shell utilisez-vous? Malheureusement, impossible d'afficher la session de terminal dans un commentaire.
abalter

29

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 cette façon:

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

2
+1 Cela a complètement fonctionné pour moi. J'avais besoin de mettre plusieurs chaînes, divisées par une nouvelle ligne, dans un tableau, et je read -a arr <<< "$strings"n'ai pas travaillé avec IFS=$'\n'.
Stefan van den Akker


Cela ne répond pas tout à fait à la question d'origine.
Mike

29

La réponse acceptée fonctionne pour les valeurs sur une seule 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 tableau de lecture bash beaucoup plus simple :

readarray -t lines <<<"$string"

L'impression de toutes les lignes est très facile en profitant d'une fonction printf:

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

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

2
Bien que toutes les solutions ne fonctionnent pas pour toutes les situations, votre mention de readarray ... a remplacé mes deux dernières heures par 5 minutes ... vous avez obtenu mon vote
Angry 84

7

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]}

Impressions 1


1
il imprime 1 2 3 4 dans mon cas
minigeek

6

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

Si vous attribuez IFS=", "alors la chaîne se brisera sur ","OU OU sur " "toute combinaison d'entre eux qui n'est pas une représentation précise du délimiteur à deux caractères de ", ".

Vous pouvez utiliser awkou sedpour 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 expression régulière 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 le second formulaire, il n'y a pas de sous-shell et ce sera intrinsèquement plus rapide.


Edit by bgoldst: Voici quelques repères comparant ma readarraysolution à la solution regex de dawg, et j'ai également inclus la readsolution 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 Publier):

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

Solution très cool! Je n'ai jamais pensé à utiliser une boucle sur une correspondance regex, une utilisation astucieuse de $BASH_REMATCH. Cela fonctionne, et évite en effet de créer des sous-coquilles. +1 de moi. Cependant, à titre de critique, l'expression régulière elle-même est un peu non idéale, dans la mesure où il semble que vous ayez été obligé de dupliquer une partie du jeton de délimitation (en particulier la virgule) afin de contourner le manque de support pour les multiplicateurs non gourmands. (également des contournements) dans ERE (saveur regex "étendue" intégrée dans bash). Cela le rend un peu moins générique et robuste.
bgoldst

Deuxièmement, j'ai fait des analyses comparatives, et bien que les performances soient meilleures que les autres solutions pour les petites chaînes, elles s'aggravent exponentiellement en raison de la reconstruction répétée des chaînes, devenant catastrophique pour les très grandes chaînes. Voir ma modification à votre réponse.
bgoldst

@bgoldst: Quelle référence cool! Pour défendre l'expression régulière, pour des dizaines ou des centaines de milliers de champs (ce que le regex fractionne), il y aurait probablement une forme d'enregistrement (comme des \nlignes de texte délimitées) comprenant ces champs, de sorte que le ralentissement catastrophique ne se produirait probablement pas. Si vous avez une chaîne avec 100 000 champs - peut-être que Bash n'est pas idéal ;-) Merci pour le benchmark. J'ai appris une chose ou deux.
dawg

4

Solution de délimiteur multi-caractères Pure Bash.

Comme d'autres l'ont souligné dans ce fil, la question du PO a donné un exemple de chaîne délimitée par des virgules à analyser dans un tableau, mais n'a pas indiqué s'il était uniquement intéressé par les délimiteurs par des virgules, les délimiteurs à caractère unique ou à plusieurs caractères délimiteurs.

Étant donné que Google a tendance à classer cette réponse en haut ou près du haut des résultats de recherche, je voulais fournir aux lecteurs une réponse forte à la question des délimiteurs à plusieurs caractères, car cela est également mentionné dans au moins une réponse.

Si vous êtes à la recherche d'une solution à un problème de délimiteur multi-caractères, je suggère de revoir le post de Mallikarjun M , en particulier la réponse de gniourf_gniourf qui fournit cette élégante solution BASH pure en utilisant l'expansion des paramètres:

#!/bin/bash
str="LearnABCtoABCSplitABCaABCString"
delimiter=ABC
s=$str$delimiter
array=();
while [[ $s ]]; do
    array+=( "${s%%"$delimiter"*}" );
    s=${s#*"$delimiter"};
done;
declare -p array

Lien vers le commentaire cité / l'article référencé

Lien vers la question citée: comment diviser une chaîne sur un délimiteur à plusieurs caractères en bash?


1
Voir mon commentaire pour une approche similaire mais améliorée.
xebeche

3

Cela fonctionne pour moi sur OSX:

string="1 2 3 4 5"
declare -a array=($string)

Si votre chaîne a un délimiteur différent, remplacez d'abord ceux par de l'espace:

string="1,2,3,4,5"
delimiter=","
declare -a array=($(echo $string | tr "$delimiter" " "))

Facile :-)


Fonctionne à la fois pour Bash et Zsh, ce qui est un plus!
Elijah W. Gagne

2

Une autre façon de le faire sans modifier IFS:

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

Plutôt que de changer IFS pour correspondre à notre délimiteur souhaité, nous pouvons remplacer toutes les occurrences de notre délimiteur souhaité ", "par le contenu de $IFSvia "${string//, /$IFS}".

Peut-être que ce sera lent pour les très grandes chaînes?

Ceci est basé sur la réponse de Dennis Williamson.


2

Je suis tombé sur ce post en cherchant à analyser une entrée comme: word1, word2, ...

rien de ce qui précède ne m'a aidé. résolu en utilisant awk. Si cela aide quelqu'un:

STRING="value1,value2,value3"
array=`echo $STRING | awk -F ',' '{ s = $1; for (i = 2; i <= NF; i++) s = s "\n"$i; print s; }'`
for word in ${array}
do
        echo "This is the word $word"
done

1

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 il fonctionne sans dans une nouvelle instance bash


1

Nous pouvons utiliser la commande tr pour diviser la chaîne en objet tableau. Il fonctionne à la fois MacOS et Linux

  #!/usr/bin/env bash
  currentVersion="1.0.0.140"
  arrayData=($(echo $currentVersion | tr "." "\n"))
  len=${#arrayData[@]}
  for (( i=0; i<=$((len-1)); i++ )); do 
       echo "index $i - value ${arrayData[$i]}"
  done

Une autre option utilise la commande IFS

IFS='.' read -ra arrayData <<< "$currentVersion"
#It is the same as tr
arrayData=($(echo $currentVersion | tr "." "\n"))

#Print the split string
for i in "${arrayData[@]}"
do
    echo $i
done

0

Utilisez ceci:

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

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

3
Mauvais: sujet au fractionnement des mots et à l'expansion des noms de chemin. Veuillez ne pas faire revivre les anciennes questions avec de bonnes réponses pour donner de mauvaises réponses.
gniourf_gniourf

2
Cela peut être une mauvaise réponse, mais c'est toujours une réponse valide. Flaggers / relecteurs: Pour les réponses incorrectes comme celle-ci, downvote, ne supprimez pas!
Scott Weldon

2
@gniourf_gniourf Pourriez-vous s'il vous plaît expliquer pourquoi c'est une mauvaise réponse? Je ne comprends vraiment pas quand ça échoue.
George Sovetov

3
@GeorgeSovetov: Comme je l'ai dit, il est sujet à la division des mots et à l'expansion des noms de chemin. Plus généralement, le fractionnement d' une chaîne de caractères dans un tableau comme array=( $string )un (malheureusement très fréquent) anti -modèle : la séparation de mots se produit: string='Prague, Czech Republic, Europe'; L'expansion du nom de chemin se produit: string='foo[abcd],bar[efgh]'échouera si vous avez un fichier nommé, par exemple, foodou barfdans votre répertoire. La seule utilisation valide d'une telle construction est quand stringest un glob.
gniourf_gniourf

0

MISE À JOUR: Ne faites pas cela, en raison de problèmes avec eval.

Avec un peu moins de cérémonie:

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

par exemple

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

4
eval est mal! ne fais pas ça.
caesarsol

1
Pfft. Non. Si vous écrivez des scripts suffisamment gros pour que cela soit important, vous vous trompez. Dans le code d'application, eval est mauvais. Dans les scripts shell, c'est courant, nécessaire et sans conséquence.
user1009908

2
mettez un $dans votre variable et vous verrez ... J'écris de nombreux scripts et je n'ai jamais eu à en utiliser un seuleval
caesarsol

2
Vous avez raison, cela n'est utilisable que lorsque l'entrée est connue pour être propre. Pas une solution robuste.
user1009908

La seule fois où j'ai eu à utiliser eval, c'était pour une application qui générerait elle-même son propre code / modules ... ET cela n'a jamais eu aucune forme d'entrée utilisateur ...
Angry 84

0

Voici mon hack!

Le fractionnement de chaînes par chaînes est une chose assez ennuyeuse à faire en utilisant bash. Ce qui se passe, c'est que nous avons des approches limitées qui ne fonctionnent que dans quelques cas (divisés par ";", "/", "." Et ainsi de suite) ou nous avons une variété d'effets secondaires dans les sorties.

L'approche ci-dessous a nécessité un certain nombre de manœuvres, mais je crois qu'elle fonctionnera pour la plupart de nos besoins!

#!/bin/bash

# --------------------------------------
# SPLIT FUNCTION
# ----------------

F_SPLIT_R=()
f_split() {
    : 'It does a "split" into a given string and returns an array.

    Args:
        TARGET_P (str): Target string 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 string 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 "string" 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 <DB_NAME> -c \"\dt\"
\"

\"
\list or \l: list all databases
\dt: list all tables in the current database
\"

[Ref.: /dba/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 strings are the same!"
    echo " < -----------------------------------------"
fi

0

Pour les éléments multilignés, pourquoi pas quelque chose comme

$ array=($(echo -e $'a a\nb b' | tr ' ' '§')) && array=("${array[@]//§/ }") && echo "${array[@]/%/ INTERELEMENT}"

a a INTERELEMENT b b INTERELEMENT

-1

Une autre façon serait:

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

Vos éléments sont maintenant stockés dans le tableau "arr". Pour parcourir les éléments:

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

1
Je couvre cette idée dans ma réponse ; voir Mauvaise réponse # 5 (vous pourriez être particulièrement intéressé par ma discussion sur l' evalastuce). Votre solution laisse $IFSla valeur de la virgule après le fait.
bgoldst

-1

Puisqu'il existe de nombreuses façons de résoudre ce problème, commençons par définir ce que nous voulons voir dans notre solution.

  1. Bash fournit une fonction intégrée readarrayà cet effet. Utilisons-le.
  2. Évitez les trucs moches et inutiles tels que changer IFS, boucler, utiliser evalou ajouter un élément supplémentaire puis le supprimer.
  3. Trouvez une approche simple et lisible qui peut facilement être adaptée à des problèmes similaires.

La readarraycommande est plus facile à utiliser avec des retours à la ligne comme délimiteur. Avec d'autres délimiteurs, il peut ajouter un élément supplémentaire au tableau. L'approche la plus propre consiste à adapter d'abord notre entrée dans un formulaire qui fonctionne bien readarrayavant de le transmettre.

L'entrée dans cet exemple n'a pas de délimiteur multicaractère. Si nous appliquons un peu de bon sens, il est préférable de comprendre une entrée séparée par des virgules pour laquelle chaque élément peut avoir besoin d'être coupé. Ma solution est de diviser l'entrée par virgule en plusieurs lignes, de découper chaque élément et de le transmettre à readarray.

string='  Paris,France  ,   All of Europe  '
readarray -t foo < <(tr ',' '\n' <<< "$string" |sed 's/^ *//' |sed 's/ *$//')
declare -p foo

# declare -a foo='([0]="Paris" [1]="France" [2]="All of Europe")'

-2

Une autre approche peut être:

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

Après cet «arr» se trouve un tableau avec quatre chaînes. Cela ne nécessite pas de traiter IFS ou de lire ou tout autre élément spécial, donc beaucoup plus simple et direct.


Même motif (malheureusement commun) que les autres réponses: sous réserve de la division des mots et de l'expansion du nom de fichier.
gniourf_gniourf
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.