Scinde une chaîne en un tableau dans Bash

Dans un script Bash, je voudrais diviser une ligne en morceaux et de 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?

443
demandé sur codeforester 2012-05-14 19:15:58

15 réponses

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

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

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

echo "${array[0]}"

à itérer sur les éléments:

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

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

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

le dernier exemple est utile parce que les tableaux Bash sont épars. En d'autres termes, vous pouvez supprimer un élément ou d'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 clairsemés donc vous ne devriez pas utilisez la longueur pour obtenir le dernier élément. Voici comment vous pouvez à Bash 4.2 et plus tard:

echo "${array[-1]}"

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

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

les plus gros décalages négatifs sélectionnent plus loin de l'extrémité du tableau. Notez l'espace avant le signe moins dans l'ancien formulaire. Il est requis.

808
répondu Dennis Williamson 2017-11-27 15:02:34

voici un moyen sans Fi:

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 de caractères:

${string//substring/replacement}

pour remplacer toutes les correspondances de $ substring par de l'espace blanc et ensuite en utilisant la chaîne substituée pour initialiser un tableau:

(element1 element2 ... elementN)

Note: Cette réponse utilise l'opérateur split+glob . Ainsi, pour empêcher l'expansion de certains caractères (tels que * ) il est un bon idée de pause globbing pour ce script.

193
répondu Jim Ho 2017-04-13 12:36:27

Toutes les réponses à cette question ont tort d'une manière ou d'une autre.


mauvaise réponse #1

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

1: il s'agit d'un abus de $IFS . La valeur de la variable $IFS est et non prise comme une simple longueur variable séparateur de chaîne de caractères, plutôt il est pris comme un jeu de mono-caractère séparateurs de chaîne de caractères, où chaque zone que read se sépare de la ligne d'entrée peut être terminé par n'importe quel caractère dans l'ensemble (virgule ou espace, dans ce exemple).

en fait, pour les vrais autocollants là-bas, la pleine signification de $IFS est un peu plus impliquer. Du manuel de bash :

le shell traite chaque caractère de IFS comme un délimiteur, et divise les résultats des autres extensions en mots en utilisant ces caractères comme terminateurs de champ. Si IFS est désactivé, ou si sa valeur est exactement < space> , les séquences par défaut de , , et au début et à la fin des résultats de la précédente expansions sont ignorés, et n'importe quelle séquence de IFS caractères non au début ou à la fin sert à séparer les mots. Si IFS a une valeur autre que la valeur par défaut, alors les séquences des caractères blancs < space> , < tab> , et sont ignorés au début et à la fin du mot, aussi longtemps que le caractère espace est dans la valeur de IFS (un IFS caractère d'espacement). N'importe quel caractère dans IFS qui n'est pas IFS espaces blancs, avec n'importe quel adjacent IFS espaces blancs, délimite un champ. Une séquence de IFS whitespace les caractères sont également traités comme un délimiteur. Si la valeur de IFS est nulle, il n'y a pas de division de mots.

fondamentalement, pour les valeurs non nulles non par défaut de $IFS , les champs peuvent être séparés par (1) une séquence d'un ou plusieurs caractères qui sont tous de l'ensemble de "IFS whitespace characters" (c'est-à-dire, celui de , , et ("newline" signifiant line feed (LF) ) sont présents n'importe où dans $IFS ), ou (2) n'importe quel non-"IFS whitespace character" qui est présent dans $IFS avec n'importe quel "IFS whitespace characters" l'entourent dans la ligne d'entrée.

pour L'OP, il est possible que le second 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 de saisie, mais nous pouvons être assez sûrs que le premier le mode de séparation que j'ai décrit n'est pas correct du tout. Par exemple, et si sa chaîne de caractères était 'Los Angeles, United States, North America' ?

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

2: même si vous deviez utiliser cette solution avec un séparateur à un caractère (comme une virgule en soi, c'est-à-dire sans espace suivant ou autre bagage), si la valeur de la variable $string se trouve contenir n'importe quel LFs, alors read cessera le traitement une fois qu'il rencontrera le premier LF. Le read ne traite qu'une ligne par invocation. Ceci est vrai même si vous pipez ou redirigez l'entrée seulement vers l'instruction read , comme nous le faisons dans cet exemple avec le mécanisme here-string , et donc l'entrée non traitée est garantie perdue. Le code qui alimente le read n'a aucune connaissance du flux de données dans sa structure de commande contenant.

vous pourriez argumentez que cela est peu susceptible de causer un problème, mais tout de même, c'est un risque subtil qui devrait être évité si possible. Il est causé par le fait que le read fait en fait deux niveaux de division d'entrée: d'abord en lignes, puis en champs. Puisque L'OP ne veut qu'un seul niveau de division, cette utilisation du read n'est pas appropriée, et nous devrions l'éviter.

3: un problème potentiel non évident avec ce la solution est que read laisse toujours tomber le champ de fuite s'il est vide, bien qu'il préserve les champs vides autrement. 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 se soucierait pas de ça, mais c'est toujours une limite à 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 mannequin de fuite délimiteur à la chaîne d'entrée juste avant de nourrir read , comme Je vais le démontrer plus tard.


mauvaise réponse #2

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

idée similaire:

t="one,two,three"
a=($(echo $t | tr ',' "\n"))

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

similaire idée:

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

ces solutions tirent parti de la division de mots dans une assignation de tableau pour diviser la chaîne en champs. Assez curieusement, tout comme read , le mot général splitting utilise également la variable spéciale $IFS , bien que dans ce cas il est implicite qu'il est défini à sa valeur par défaut de < space> < tab> , et donc toute séquence d'un ou plusieurs caractères IFS (qui sont tous des caractères blancs) maintenant, est considéré comme un délimiteur de champ.

cela résout le problème de deux niveaux de scission commis par read , puisque la scission de mots par elle-même ne constitue qu'un niveau de scission. Mais comme auparavant, le problème ici est que les champs individuels dans la chaîne de saisie peuvent déjà contenir les caractères $IFS , et donc ils seraient mal divisés pendant l'opération de division de mots. Il se trouve que ce n'est le cas pour aucune des entrées de l'échantillon. les cordes fournies par ces answerers (comment commode...), mais bien sûr cela ne change rien au fait qu'une base de code qui utilise cet idiome risquerait alors d'exploser si cette hypothèse était violée à un moment ou à un autre de la ligne. Encore une fois, considérez mon contre-exemple de 'Los Angeles, United States, North America' (ou 'Los Angeles:United States:North America' ).

de plus, la division de mots est normalement suivie par extension du nom de fichier ( alias chemin expansion alias globbing), qui, si elle était faite, pourrait corrompre les mots contenant les caractères * , ? , ou [ suivi de ] (et, si extglob est placé, les fragments entre parenthèses précédés de ? , * , + , @ , ou ! ) en les mettant en correspondance avec les objets du système de fichiers et en élargissant les mots ("globs") en conséquence. La première de ces trois réponses a habilement réduisez ce problème en lançant set -f à l'avance pour désactiver globbing. Techniquement, cela fonctionne (bien que vous devriez probablement ajouter set +f par la suite pour rendre disponible le globbing pour le code qui peut en dépendre), mais il n'est pas souhaitable d'avoir à modifier les paramètres globaux de l'interpréteur de commandes afin de hacker une opération de base d'analyse string-to-array en code local.

un autre problème avec cette réponse est que tous les champs vides seront perdus. Cela peut être ou ne pas être problème, selon l'application.

Note: Si vous utilisez cette solution, il est préférable d'utiliser la forme ${string//:/ } "pattern substitution" de parameter expansion , plutôt que d'invoquer une substitution de commande (qui bifurque le shell), de démarrer un pipeline, et d'exécuter un exécutable externe ( tr ou sed ), puisque l'expansion de paramètre est une opération purement interne à l'interpréteur de commandes. (Aussi, pour dans le cas des solutions tr et sed , la variable d'entrée devrait être double-citée dans la substitution de commande; sinon, le dédoublement de mots entrerait en vigueur dans la commande echo et pourrait affecter les valeurs du champ. En outre, la forme de substitution de commande $(...) est préférable à l'ancienne forme `...` car elle simplifie l'imbrication des substitutions de commande et permet une meilleure mise en évidence de la syntaxe par les éditeurs de texte.)


mauvaise réponse #3

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

cette réponse est presque la même que #2 . La différence est que le répondeur a fait l'hypothèse que les champs sont délimités par deux caractères, l'un étant représenté dans le défaut $IFS , et l'autre pas. Il a résolu ce cas assez spécifique en supprimant la le caractère non-IFS-représenté en utilisant une extension de substitution de modèle et en utilisant ensuite le fractionnement de mots pour diviser les champs sur le caractère de délimiteur IFS-représenté survivant.

Ce n'est pas une solution générique. De plus, on peut faire valoir que la virgule est en fait le caractère de délimiteur "primaire" ici, et que le supprimer et ensuite dépendre du caractère d'espace pour séparer le champ est tout simplement erroné. Encore une fois, considérez mon contre-exemple: 'Los Angeles, United States, North America' .

de même, encore une fois, l'extension du nom de fichier pourrait corrompre les mots élargis, mais cela peut être évité en désactivant temporairement globbing pour la tâche avec set -f et ensuite set +f .

aussi, encore une fois, tous les champs vides seront perdus, ce qui peut être ou non 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"

c'est similaire à #2 et #3 en ce qu'il utilise le dédoublement de mot pour obtenir le travail fait, seulement maintenant le code définit explicitement $IFS pour contenir seulement le délimiteur de champ d'un caractère présent dans la chaîne de saisie. Il convient de répéter que cela ne peut pas fonctionner pour les délimiteurs de champ multicharacter tels que le délimiteur d'espace de virgule de L'OP. Mais pour un seul caractère délimiteur comme le LF utilisé dans cet exemple, il est en fait proche d'être parfait. Les champs ne peuvent pas être divisé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, comme requis.

un problème est que l'extension du nom de fichier corrompt les mots touchés comme décrit précédemment, bien qu'une fois de plus cela puisse être résolu en enveloppant l'énoncé critique dans set -f et set +f .

un autre problème potentiel est que, puisque LF se qualifie comme un" IFS whitespace character "tel que défini précédemment, tous les champs vides seront perdus, tout comme dans #2 et #3 . Ce ne serait évidemment pas un problème si le délimiteur se trouve être un caractère non-"IFS whitespace", et en fonction de 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 d'un caractère, et que ce soit un caractère non-"IFS whitespace" ou que vous ne vous souciez pas des champs vides, et que vous enveloppez l'énoncé critique dans set -f et set +f , alors cette solution fonctionne, mais autrement 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

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 qu'elle fixe $IFS à virgule-espace) et #2-4 (en ce qu'elle utilise la division de mot pour diviser la chaîne en champs). De ce fait, il souffre de la plupart des problèmes qui affligent toutes les mauvaises réponses ci-dessus, un peu comme le pire de tous les mondes.

aussi, en ce qui concerne la seconde variante, il peut sembler que l'appel eval est complètement inutile, puisque son argument est une chaîne de caractères littérale, et est donc statiquement connu. Mais il y a en fait un avantage non évident à utiliser eval de cette façon. Normalement, lorsque vous exécutez une commande simple qui se compose d'une variable affectation seulement , c'est-à-dire sans un mot de commande suivant, l'affectation prend effet dans l'environnement shell:

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

ceci est vrai même si la commande simple implique multiples affectations variables; encore une fois, aussi longtemps qu'il n'y a pas de mot de commande, toutes les affectations variables affectent l'environnement shell:

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

mais, si la cession variable est attaché à un nom de commande (j'aime appeler cela une "assignation de préfixe") alors il fait et non affecte l'environnement shell, et au lieu de cela affecte seulement l'environnement de la commande exécutée, indépendamment du fait qu'il s'agisse d'un builtin ou d'un 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 bash manual :

S'il n'en résulte pas de nom de commande, la variable assignations affecte le courant de l'environnement de shell. Sinon, les variables sont ajoutées à l'environnement de la commande exécutée et n'affectent pas l'environnement du shell actuel.

il est possible d'exploiter cette caractéristique d'assignation de variable pour changer $IFS seulement temporairement, ce qui nous permet d'éviter l'ensemble sauver-et-restaurer gambit comme ce qui est fait avec la variable $OIFS dans la première variante. Mais le défi auquel nous sommes confrontés ici est que la commande nous need to run est lui-même une simple affectation variable, et par conséquent, il n'impliquerait pas un mot de commande pour rendre l'affectation $IFS temporaire. Vous pourriez vous dire, pourquoi ne pas simplement ajouter un mot de commande no-op à la déclaration comme le : builtin pour rendre l'affectation $IFS temporaire? Cela ne fonctionne pas, car cela rendrait l'affectation $array également temporaire:

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

donc, nous sommes effectivement dans une impasse, un peu fourre-22. Mais, quand eval exécute son code, il l'exécute dans l'environnement shell, comme si c'était normal, code source statique, et donc nous pouvons exécuter l'assignation $array à l'intérieur de l'argument eval pour qu'elle prenne effet dans l'environnement shell, tandis que l'assignation de préfixe $IFS qui est préfixée à la commande eval ne survivra pas à la commande eval . C'est exactement le truc qui est utilisé 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 tout à fait un tour intelligent, et accomplit exactement ce qui est requis (au moins en ce qui concerne l'exécution de la tâche) d'une manière plutôt non évidente. En fait, je ne suis pas contre cette ruse en général, malgré l'implication de eval ; faites juste attention à citer la chaîne d'arguments pour se prémunir contre les menaces à la sécurité.

mais encore, à cause du " pire de tous "l'agglomération des problèmes, c'est encore une mauvaise réponse à L'exigence de L'OP.


mauvaise réponse #6

IFS=', '; array=(Paris, France, Europe)

IFS=' ';declare -a array=(Paris France Europe)
"15191310920 de messagerie Unifiée"... quoi? L'OP a une variable string qui doit être analysée dans un tableau. Cette "réponse" commence avec le contenu mot à mot de la chaîne de saisie collée dans un tableau littéral. Je suppose que c'est une façon de le faire.

il semble que le répondeur puisse avoir supposé que la variable $IFS affecte tous les Bash parsing dans tous les contextes, ce qui n'est pas vrai. Du manuel de bash:

IFS le séparateur de champ interne qui est utilisé pour séparer les mots après l'expansion et pour séparer les lignes en mots avec la commande lire . La valeur par défaut est .

ainsi, la variable spéciale $IFS n'est en fait utilisée que dans deux contextes: (1) le fractionnement de mots qui est effectué après expansion (ce qui signifie pas lors de l'analyse du code source de bash) et (2) pour séparer les lignes d'entrée en mots par le read builtin.

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

L'extension

est effectuée sur la ligne de commande après avoir été divisée en mots. Il existe sept types d'expansion réalisée: accolade de l'expansion , tilde expansion , paramètre et variable d'extension , la substitution de commande , l'expansion arithmétique , , de couper un mot , et chemin d'accès de l'expansion .

l'ordre des extensions est le suivant: extension de l'accrochage; expansion de l'inclinaison, expansion des paramètres et des variables, expansion arithmétique, et substitution de commandes (faite de gauche à droite); Division de mots; et expansion du chemin.

vous pourriez argumenter que la version GNU du manuel fait un peu mieux, car il opte pour le mot "tokens" au lieu de "words" dans la première phrase de la section Expansion:

L'extension

est réalisée sur la ligne de commande après avoir été scindée en tokens.

le point important est, $IFS ne change pas la façon dont bash interprète le code source. L'analyse du code source de bash est en fait un processus très complexe qui implique la reconnaissance des divers éléments de la grammaire shell, tels que les séquences de commandes, les listes de commandes, les pipelines, les extensions de paramètres, les substitutions arithmétiques et les substitutions de commandes. Pour la plupart, le processus de parsing de bash ne peut pas être modifié par des actions au niveau de l'utilisateur comme les assignations de variables (en fait, il y a quelques exceptions mineures à cette règle; par exemple, voir les différents shell compatxx les paramètres , qui peuvent changer certains aspects du comportement d'analyse à la volée). Les "mots"/"jetons" en amont qui résultent de ce processus complexe d'analyse sont ensuite élargis selon le processus général d '"expansion" tel que décomposé dans les extraits de la documentation ci-dessus, où le fractionnement de mots de l'expanded (expanding?) texte en aval des mots est simplement une étape de ce processus. La division de mot ne touche que le texte qui a été recraché d'une étape d'expansion précédente; il ne affecter littérale du texte qui a été analysée à droite au large de la source bytestream.


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. Remarquez que nous sommes de retour à utiliser read . N'ai-je pas dit plus tôt que read est inapproprié parce qu'il effectue deux niveaux de division, quand nous avons besoin d'un seul? Le l'astuce ici est que vous pouvez appeler read de telle sorte qu'il ne fait effectivement qu'un niveau de division, spécifiquement en séparant un seul champ par invocation, ce qui nécessite le coût d'avoir à l'appeler à plusieurs reprises dans une boucle. C'est un peu un tour de main, mais il fonctionne.

mais il y a des problèmes. Premièrement: lorsque vous fournissez au moins un argument nom à read , il ignore automatiquement l'avance et la fuite les espaces dans chaque champ est séparé de la chaîne d'entrée. Cela se produit si $IFS est défini à sa valeur par défaut ou non, comme décrit plus tôt dans ce post. Maintenant, L'OP peut ne pas se soucier de cela pour son cas d'utilisation spécifique, et en fait, il peut être une caractéristique souhaitable du comportement d'analyse. Mais tous ceux qui veulent couper une corde en champs ne le veulent pas. Il y a une solution, cependant: une utilisation un peu non évidente de read est de passer le zéro nom arguments. Dans ce cas, read stockera la ligne d'entrée entière qu'il obtient du flux d'entrée dans une variable nommée $REPLY , et, en prime, il ne pas bande menant et espace traînant de la valeur. Il s'agit d'un usage très robuste de read que j'ai souvent exploité dans ma carrière de programmation shell. Voici une démonstration de la différence de comportement:

string=$'  a  b  \n  c  d  \n  e  f  '; ## input string

a=(); while read -r line; do a+=("$line"); done <<<"$string"; declare -p a;
## declare -a a=([0]="a  b" [1]="c  d" [2]="e  f") ## read trimmed surrounding whitespace

a=(); while read -r; do a+=("$REPLY"); done <<<"$string"; declare -p a;
## declare -a a=([0]="  a  b  " [1]="  c  d  " [2]="  e  f  ") ## no trimming

la seconde problème avec cette solution est qu'il ne fait pas du cas d'un champ personnalisé séparateur, comme l'OP virgule-espace. Comme auparavant, les séparateurs multicaractères ne sont pas pris en charge, ce qui est une limitation regrettable de cette solution. Nous pourrions au moins essayer de diviser sur la virgule en spécifiant le séparateur à l'option -d , 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 non comptabilisé entourant whitespace a été tiré dans les valeurs de champ, et cela devrait donc être corrigé ultérieurement 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 a disparu! Ce qui s'est passé? La réponse est que read renvoie un code de retour défectueux s'il atteint la fin du fichier (dans ce cas, nous pouvons l'appeler fin de la chaîne) sans rencontrer un terminateur de champ final sur le champ final. Cela provoque la alors la boucle de rompre prématurément et nous perdons la finale champ.

techniquement, cette même erreur a aussi affecté les exemples précédents; la différence est que le séparateur de champ a été pris pour LF, ce qui est la valeur par défaut quand vous ne spécifiez pas l'option -d , et le mécanisme <<< ("here-string") ajoute automatiquement un LF à la chaîne juste avant qu'elle ne l'alimente comme entrée à la commande. Par conséquent, dans ces cas, nous sorte de accidentellement résolu le problème d'un champ final tombé par ajoutant involontairement un terminateur factice supplémentaire à l'entrée. Appelons cette solution la solution" dummy-terminator". Nous pouvons appliquer la solution de terminateur fictif manuellement pour n'importe quel délimiteur personnalisé en le concaténant nous-mêmes contre la chaîne de saisie lors de son instanciation dans la chaîne here-string:

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

ici, Problème résolu. Une autre solution est de ne briser la boucle que si les deux (1) read echec retourné et (2) $REPLY est vide, ce qui signifie read n'était pas capable de lire des caractères avant de frapper fin de 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 here-string par l'opérateur de redirection <<< . Il pourrait bien sûr être enlevé séparément par une opération de réglage explicite comme décrit ci-dessus, mais il est évident que l'approche manuelle du mannequin-terminateur le résout directement, donc nous pourrions simplement aller avec cela. La solution manuelle de l'extrémité du mannequin est en fait très commode en ce qu'elle résout en une seule fois ces deux problèmes (le problème du champ final abandonné et le problème du champ final ajouté).

donc, dans l'ensemble, c'est une solution assez puissante. La seule faiblesse restante est le manque de support pour les délimiteurs multicharacter, que je vais aborder 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 post que #7 ; le répondeur a fourni deux solutions dans le même poste.)

le readarray builtin, qui est un synonyme de mapfile , est idéal. C'est une commande intégrée qui divise un bytestream en une variable de tableau en un seul coup; ne pas jouer avec les boucles, les conditionnels, les substitutions, ou quoi que ce soit d'autre. Et il ne supprime pas subrepticement tout espace blanc de la chaîne de saisie. Et (si -O n'est pas donné) il efface commodément le tableau cible avant de lui assigner. Mais c'est pas encore parfait, d'où ma critique comme une "mauvaise réponse".

tout d'abord, juste pour vous débarrasser de cela, notez que, tout comme le comportement de read lors de l'analyse de champ, readarray laisse tomber le champ de fuite s'il est vide. Encore une fois, ce n'est probablement pas un souci pour le OP, mais il pourrait être pour certains cas d'utilisation. Je vais revenir dans un instant.

Deuxièmement, comme auparavant, il ne supporte pas les délimiteurs multicaracter. Je vais vous donner un correctif pour résoudre cette question 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 comme-est pour l'analyser. Je vais développer sur cette momentanément.

Pour les raisons ci-dessus, je considère toujours ce "mauvaise réponse" à la question de L'OP. Ci-dessous, je vais donner 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'option -d :

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-conditionnel de la boucle read solution décrite dans #7 . Nous pouvons presque résoudre ce problème avec le Manue l dummy-terminator truc:

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

le problème ici est que readarray a préservé le champ de fuite, puisque l'opérateur de redirection <<< a ajouté le LF à la chaîne de saisie, et donc le champ de fuite était et non vide (sinon il aurait été abandonné). Nous pouvons prendre en charge de ceci en désactivant explicitement l'élément final du tableau après le fait:

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

les deux seuls problèmes qui subsistent, et qui sont en fait liés, sont (1) les espaces blancs étrangers qui doivent être coupés, et (2) le manque de support pour les délimiteurs multicaracter.

l'espace blanc peut bien sûr être coupé par la suite (par exemple, voir comment couper l'espace blanc à partir d'une variable Bash? ). Mais si on peut pirater un délimiteur multicaractère, qui résoudrait les deux problèmes en un coup.

malheureusement, il n'y a pas de direct façon de faire fonctionner un délimiteur multicaractère. La meilleure solution à laquelle j'ai pensé est de pré-traiter la chaîne de saisie pour remplacer le délimiteur multicharacter par un délimiteur de caractère unique qui sera garanti de ne pas entrer en collision avec le contenu de la chaîne de saisie. Le seul caractère qui a cette garantie est le NUL byte . C'est parce que, dans bash (mais pas dans zsh, incidemment), les variables ne peuvent pas contenir le octet NUL. Cette étape de prétraitement peut être effectuée en ligne dans une substitution de processus. Voici comment faire en utilisant awk :

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

enfin! Cette solution ne divisera pas par erreur les champs au milieu, ne coupera pas prématurément, ne laissera pas tomber les champs vides, ne se corrompra pas sur filename les expansions, ne seront pas automatiquement strip menant et traînant l'espace blanc, ne laisseront pas un stowaway LF Sur la fin, ne nécessitent pas de boucles, et ne se contentent pas d'un délimiteur de caractère simple.


solution de parage

enfin, j'ai voulu démontrer ma propre solution de coupe assez complexe en utilisant l'obscure -C callback option de readarray . Malheureusement, j'ai couru hors de la chambre contre la limite de 30 000 caractères draconienne de Stack Overflow, donc je ne pourrai pas l'expliquer. Je vais laisser ça comme un exercice pour le lecteur.

function mfcb { local val=""; ""; eval "[]=$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")
149
répondu bgoldst 2017-07-20 15:49:24
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"

imprime trois

46
répondu Jmoney38 2017-11-06 17:51:37

parfois, il m'est arrivé que la méthode décrite dans la réponse acceptée ne fonctionnait pas, surtout si le séparateur est un retour de 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
29
répondu Luca Borrione 2012-11-02 13:44:37

la réponse acceptée fonctionne pour les valeurs dans une ligne.

si la variable a plusieurs lignes:

string='first line
        second line
        third line'

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

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

Ou le beaucoup plus simple bash readarray :

readarray -t lines <<<"$string"

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

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

>[first line]
>[        second line]
>[        third line]
23
répondu 2015-07-24 21:24:27

c'est similaire à L'approche de Jmoney38, mais en utilisant sed:

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

Imprime 1

4
répondu ssanch 2016-06-03 15:24:19

la clé pour séparer votre chaîne en un tableau est le délimiteur de caractères multiples de ", " . Toute solution utilisant IFS pour les délimiteurs de caractères multiples est intrinsèquement erronée puisque IFS est un ensemble de ces caractères, pas une chaîne.

si vous assignez IFS=", " alors la chaîne se cassera sur "," ou " " ou toute combinaison d'eux qui n'est pas une représentation précise des deux délimiteurs de caractère de ", " .

vous pouvez utiliser awk ou sed pour séparer la chaîne, avec substitution de processus:

#!/bin/bash

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

il est plus efficace d'utiliser un regex vous directement en Bash:

#!/bin/bash

str="Paris, France, Europe"

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

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

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


Modifier par bgoldst: voici quelques repères comparant mon readarray solution dawg regex de la solution, et j'ai aussi inclus la read solution pour le fun (note: j'ai légèrement modifié la regex solution pour une plus grande harmonie avec ma solution) (voir aussi mes commentaires sous le post):

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

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

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "" != ':' ]]; do
        func="";
        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=; ## 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 $[-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

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

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

Essayez cette

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

c'est simple. Si vous voulez, vous pouvez aussi ajouter une declaration (et aussi supprimer les virgules):

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

le IFS est ajouté pour annuler ce qui précède mais il fonctionne sans lui dans une nouvelle instance de bash

1
répondu Geoff Lee 2016-03-04 06:02:07

utilisez ceci:

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

#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe
0
répondu Eduardo Cuomo 2016-12-19 19:00:33

Voilà mon hack!

séparer les cordes par les cordes est une chose assez ennuyeuse à faire en utilisant bash. Ce qui se passe, c'est que nous avons limité les approches qui ne fonctionnent que dans quelques cas (répartition par ";", "/", "."et ainsi de suite) ou si nous avons une variété d'effets secondaires dans les sorties.

l'approche ci-dessous a exigé un certain nombre de manœuvres, mais je crois qu'il 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=
    DELIMITER_P=
    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 "151900920"}' <<< "${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 "151900920"}' <<< "'${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.: 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 strings are the same!"
    echo " < -----------------------------------------"
fi
0
répondu Eduardo Lucio 2018-01-31 20:45:59

une autre façon de le faire sans modifier les IFS:

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

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

peut-être que ce sera lent pour de très grandes cordes cependant?

Ceci est basé sur Dennis Williamson réponse.

0
répondu sel-en-ium 2018-05-31 05:56:22

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 ce 'arr' est un tableau à quatre cordes. Cela ne nécessite pas de dealing IFS ou lire ou toute autre chose spéciale, donc beaucoup plus simple et directe.

-1
répondu rsjethani 2016-09-13 16:21:10

mise à JOUR: à ne pas faire, en raison de problèmes avec la fonction eval.

avec un peu moins de cérémonie:

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

p.ex.

string="foo, bar,baz"
IFS=', ' eval 'array=($string)'
echo ${array[1]} # -> bar
-1
répondu user1009908 2017-05-06 23:18:44

une autre façon serait:

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

maintenant vos éléments sont stockés dans le tableau" arr". Pour itérer à travers les éléments:

for i in ${arr[@]}; do echo $i; done
-1
répondu Safter Arslan 2017-08-09 03:21:05