Comment diviser une chaîne sur un délimiteur dans Bash?


2043

J'ai cette chaîne stockée dans une variable:

IN="bla@some.com;john@home.com"

Maintenant, je voudrais diviser les chaînes par ;délimiteur afin d'avoir:

ADDR1="bla@some.com"
ADDR2="john@home.com"

Je n'ai pas nécessairement besoin des variables ADDR1et ADDR2. Si ce sont des éléments d'un tableau, c'est encore mieux.


Après les suggestions des réponses ci-dessous, je me suis retrouvé avec ce qui était ce que je recherchais:

#!/usr/bin/env bash

IN="bla@some.com;john@home.com"

mails=$(echo $IN | tr ";" "\n")

for addr in $mails
do
    echo "> [$addr]"
done

Production:

> [bla@some.com]
> [john@home.com]

Il y avait une solution impliquant de définir Internal_field_separator (IFS) sur ;. Je ne sais pas ce qui s'est passé avec cette réponse, comment pouvez-vous réinitialiser les IFSparamètres par défaut?

RE: IFSsolution, j'ai essayé ça et ça marche, je garde l'ancien IFSpuis le restaure:

IN="bla@some.com;john@home.com"

OIFS=$IFS
IFS=';'
mails2=$IN
for x in $mails2
do
    echo "> [$x]"
done

IFS=$OIFS

BTW, quand j'ai essayé

mails2=($IN)

Je n'ai obtenu la première chaîne que lors de l'impression en boucle, sans crochets, $INcela fonctionne.


14
En ce qui concerne votre "Edit2": Vous pouvez simplement "désinstaller IFS" et il reviendra à l'état par défaut. Il n'est pas nécessaire de l'enregistrer et de le restaurer explicitement, sauf si vous avez des raisons de vous attendre à ce qu'il ait déjà été défini sur une valeur non définie par défaut. De plus, si vous faites cela à l'intérieur d'une fonction (et si vous ne l'êtes pas, pourquoi pas?), Vous pouvez définir IFS en tant que variable locale et elle reviendra à sa valeur précédente une fois que vous aurez quitté la fonction.
Brooks Moses

19
@BrooksMoses: (a) +1 pour utilisation local IFS=...dans la mesure du possible; (b) -1 pour unset IFS, cela ne réinitialise pas exactement IFS à sa valeur par défaut, bien que je pense qu'un IFS non défini se comporte de la même manière que la valeur par défaut d'IFS ($ '\ t \ n'), mais cela semble une mauvaise pratique de supposer aveuglément que votre code ne sera jamais invoqué avec IFS défini sur une valeur personnalisée; (c) une autre idée est d'invoquer un sous-shell: (IFS=$custom; ...)lorsque le sous-shell quitte IFS retournera à ce qu'il était à l'origine.
dubiousjim

Je veux juste avoir un aperçu rapide des chemins pour décider où jeter un exécutable, alors j'ai eu recours à la course ruby -e "puts ENV.fetch('PATH').split(':')". Si vous voulez rester pur bash n'aidera pas, mais l'utilisation de tout langage de script ayant un split intégré est plus facile.
nicooga

4
for x in $(IFS=';';echo $IN); do echo "> [$x]"; done
user2037659

2
Afin de l'enregistrer en tant que tableau, j'ai dû placer un autre ensemble de parenthèses et changer le \npour juste un espace. Donc, la dernière ligne est mails=($(echo $IN | tr ";" " ")). Alors maintenant, je peux vérifier les éléments de mailsen utilisant la notation de tableau mails[index]ou simplement en itérant dans une boucle
afranques

Réponses:


1236

Vous pouvez définir la variable de séparateur de champ interne (IFS), puis la laisser analyser dans un tableau. Lorsque cela se produit dans une commande, l'affectation à IFSn'a lieu que dans l'environnement de cette commande unique (à read). Il analyse ensuite l'entrée en fonction de la IFSvaleur de la variable dans un tableau, que nous pouvons ensuite parcourir.

IFS=';' read -ra ADDR <<< "$IN"
for i in "${ADDR[@]}"; do
    # process "$i"
done

Il analysera une ligne d'éléments séparés par ;, en la poussant dans un tableau. Trucs pour le traitement de l'ensemble $IN, à chaque fois une ligne d'entrée séparée par ;:

 while IFS=';' read -ra ADDR; do
      for i in "${ADDR[@]}"; do
          # process "$i"
      done
 done <<< "$IN"

22
C'est probablement le meilleur moyen. Combien de temps l'IFS persistera-t-il dans sa valeur actuelle, peut-il gâcher mon code en étant défini alors qu'il ne devrait pas l'être, et comment puis-je le réinitialiser lorsque j'en ai fini?
Chris Lutz

7
maintenant après le correctif appliqué, uniquement pendant la durée de la commande de lecture :)
Johannes Schaub - litb

14
Vous pouvez tout lire en même temps sans utiliser de boucle while: read -r -d '' -a addr <<< "$ in" # Le -d '' est la clé ici, il indique à read de ne pas s'arrêter à la première nouvelle ligne ( qui est la valeur par défaut -d) mais pour continuer jusqu'à EOF ou un octet NULL (qui n'apparaissent que dans les données binaires).
lhunath

56
@LucaBorrione Définir IFSsur la même ligne que le readsans point-virgule ou autre séparateur, par opposition à dans une commande distincte, l'étend à cette commande - il est donc toujours "restauré"; vous n'avez rien à faire manuellement.
Charles Duffy

5
@imagineerThis Il existe un bogue concernant les chaînes héritées et les modifications locales d'IFS qui doit $INêtre cité. Le bug est corrigé dans bash4.3.
chepner

973

Tiré du tableau de division du script shell Bash :

IN="bla@some.com;john@home.com"
arrIN=(${IN//;/ })

Explication:

Cette construction remplace toutes les occurrences de ';'(le premier //signifie le remplacement global) dans la chaîne INpar ' '(un seul espace), puis interprète la chaîne délimitée par des espaces comme un tableau (c'est ce que font les parenthèses environnantes).

La syntaxe utilisée à l'intérieur des accolades pour remplacer chaque ';'caractère par un ' 'caractère est appelée Expansion des paramètres .

Il existe quelques pièges courants:

  1. Si la chaîne d'origine contient des espaces, vous devrez utiliser IFS :
    • IFS=':'; arrIN=($IN); unset IFS;
  2. Si la chaîne d'origine contient des espaces et que le délimiteur est une nouvelle ligne, vous pouvez définir IFS avec:
    • IFS=$'\n'; arrIN=($IN); unset IFS;

84
Je veux juste ajouter: c'est le plus simple de tous, vous pouvez accéder aux éléments du tableau avec $ {arrIN [1]} (à partir des zéros bien sûr)
Oz123

26
Je l'ai trouvé: la technique de modification d'une variable dans un $ {} est connue sous le nom d '«expansion de paramètres».
KomodoDave

23
Non, je ne pense pas que cela fonctionne quand il y a aussi des espaces présents ... il s'agit de convertir le ',' en '' puis de construire un tableau séparé par des espaces.
Ethan

12
Très concis, mais il y a des mises en garde pour une utilisation générale : le shell applique un fractionnement de mots et des extensions à la chaîne, ce qui peut être indésirable; essayez-le avec. IN="bla@some.com;john@home.com;*;broken apart". En bref: cette approche ne fonctionnera pas si vos jetons contiennent des espaces et / ou des caractères intégrés. tels que *cela arrive pour faire correspondre les noms de fichiers des jetons dans le dossier actuel.
mklement0

53
C'est une mauvaise approche pour d'autres raisons: Par exemple, si votre chaîne contient ;*;, alors le *sera étendu à une liste de noms de fichiers dans le répertoire courant. -1
Charles Duffy

249

Si cela ne vous dérange pas de les traiter immédiatement, j'aime faire ceci:

for i in $(echo $IN | tr ";" "\n")
do
  # process
done

Vous pouvez utiliser ce type de boucle pour initialiser un tableau, mais il existe probablement un moyen plus simple de le faire. J'espère que cela vous aidera.


Vous auriez dû garder la réponse IFS. Il m'a appris quelque chose que je ne savais pas, et il a définitivement fait un tableau, alors que cela ne fait qu'un substitut bon marché.
Chris Lutz

Je vois. Ouais, je trouve que faire ces expériences idiotes, je vais apprendre de nouvelles choses chaque fois que j'essaie de répondre. J'ai édité des trucs basés sur les commentaires #bash IRC et non supprimés :)
Johannes Schaub - litb

33
-1, vous n'êtes évidemment pas au courant du dédoublement des mots, car il introduit deux bugs dans votre code. l'une est lorsque vous ne citez pas $ IN et l'autre lorsque vous prétendez qu'une nouvelle ligne est le seul délimiteur utilisé dans le fractionnement de mots. Vous parcourez chaque mot dans IN, pas chaque ligne, et DEFINITIVEMENT pas chaque élément délimité par un point-virgule, bien qu'il puisse sembler avoir l'effet secondaire de ressembler à cela fonctionne.
lhunath

3
Vous pouvez le changer en écho "$ IN" | tr ';' '\ n' | en lecture -r ADDY; do # process "$ ADDY"; fait pour lui faire de la chance, je pense :) Notez que cela va bifurquer, et vous ne pouvez pas changer les variables externes de l'intérieur de la boucle (c'est pourquoi j'ai utilisé la syntaxe <<< "$ IN") puis
Johannes Schaub - litb

8
Pour résumer le débat dans les commentaires: Mises en garde pour une utilisation générale : le shell applique un fractionnement et des extensions de mots à la chaîne, ce qui peut être indésirable; essayez-le avec. IN="bla@some.com;john@home.com;*;broken apart". En bref: cette approche ne fonctionnera pas si vos jetons contiennent des espaces et / ou des caractères intégrés. tels que *cela arrive pour faire correspondre les noms de fichiers des jetons dans le dossier actuel.
mklement0

202

Réponse compatible

Il existe de nombreuses façons de le faire dans .

Cependant, il est important de noter d'abord qu'il basha de nombreuses fonctionnalités spéciales (soi-disant bashismes ) qui ne fonctionneront dans aucune autre.

En particulier, les tableaux , les tableaux associatifs et la substitution de modèles , qui sont utilisés dans les solutions de cet article ainsi que d'autres dans le fil, sont des bashismes et peuvent ne pas fonctionner sous d'autres shells que beaucoup de gens utilisent.

Par exemple: sur mon Debian GNU / Linux , il y a un shell standard appelé; Je connais beaucoup de gens qui aiment utiliser un autre shell appelé; et il y a aussi un outil spécial appelé avec son propre interprète shell ().

Chaîne demandée

La chaîne à séparer dans la question ci-dessus est:

IN="bla@some.com;john@home.com"

J'utiliserai une version modifiée de cette chaîne pour m'assurer que ma solution est robuste aux chaînes contenant des espaces blancs, ce qui pourrait casser d'autres solutions:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

Fractionner la chaîne en fonction du délimiteur dans (version> = 4.2)

En pur bash , on peut créer un tableau avec des éléments divisés par une valeur temporaire pour IFS (le séparateur de champ d'entrée ). L'IFS, entre autres, indique bashquel (s) caractère (s) il doit traiter comme un délimiteur entre les éléments lors de la définition d'un tableau:

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"

# save original IFS value so we can restore it later
oIFS="$IFS"
IFS=";"
declare -a fields=($IN)
IFS="$oIFS"
unset oIFS

Dans les versions plus récentes de bash, le fait de préfixer une commande avec une définition IFS modifie l'IFS pour cette commande uniquement et la réinitialise à la valeur précédente immédiatement après. Cela signifie que nous pouvons faire ce qui précède en une seule ligne:

IFS=\; read -a fields <<<"$IN"
# after this command, the IFS resets back to its previous value (here, the default):
set | grep ^IFS=
# IFS=$' \t\n'

Nous pouvons voir que la chaîne INa été stockée dans un tableau nommé fields, divisé sur les points-virgules:

set | grep ^fields=\\\|^IN=
# fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")
# IN='bla@some.com;john@home.com;Full Name <fulnam@other.org>'

(Nous pouvons également afficher le contenu de ces variables en utilisant declare -p:)

declare -p IN fields
# declare -- IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
# declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name <fulnam@other.org>")

Notez que read c'est le moyen le plus rapide de faire le fractionnement car il n'y a pas de fourches ou de ressources externes appelées.

Une fois le tableau défini, vous pouvez utiliser une simple boucle pour traiter chaque champ (ou, plutôt, chaque élément du tableau que vous avez maintenant défini):

# `"${fields[@]}"` expands to return every element of `fields` array as a separate argument
for x in "${fields[@]}" ;do
    echo "> [$x]"
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Ou vous pouvez supprimer chaque champ du tableau après le traitement à l'aide d'un approche de décalage , que j'aime:

while [ "$fields" ] ;do
    echo "> [$fields]"
    # slice the array 
    fields=("${fields[@]:1}")
    done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Et si vous voulez juste une simple impression du tableau, vous n'avez même pas besoin de le parcourir:

printf "> [%s]\n" "${fields[@]}"
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

Mise à jour: récente > = 4,4

Dans les versions plus récentes de bash, vous pouvez également jouer avec la commande mapfile:

mapfile -td \; fields < <(printf "%s\0" "$IN")

Cette syntaxe préserve les caractères spéciaux, les nouvelles lignes et les champs vides!

Si vous ne souhaitez pas inclure de champs vides, vous pouvez procéder comme suit:

mapfile -td \; fields <<<"$IN"
fields=("${fields[@]%$'\n'}")   # drop '\n' added by '<<<'

Avec mapfile, vous pouvez également sauter la déclaration d'un tableau et implicitement "boucler" sur les éléments délimités, en appelant une fonction sur chacun:

myPubliMail() {
    printf "Seq: %6d: Sending mail to '%s'..." $1 "$2"
    # mail -s "This is not a spam..." "$2" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile < <(printf "%s\0" "$IN") -td \; -c 1 -C myPubliMail

(Remarque: la \0fin de la chaîne de format est inutile si vous ne vous souciez pas des champs vides à la fin de la chaîne ou s'ils ne sont pas présents.)

mapfile < <(echo -n "$IN") -td \; -c 1 -C myPubliMail

# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Ou vous pouvez utiliser <<<, et dans le corps de la fonction, inclure un traitement pour supprimer la nouvelle ligne qu'il ajoute:

myPubliMail() {
    local seq=$1 dest="${2%$'\n'}"
    printf "Seq: %6d: Sending mail to '%s'..." $seq "$dest"
    # mail -s "This is not a spam..." "$dest" </path/to/body
    printf "\e[3D, done.\n"
}

mapfile <<<"$IN" -td \; -c 1 -C myPubliMail

# Renders the same output:
# Seq:      0: Sending mail to 'bla@some.com', done.
# Seq:      1: Sending mail to 'john@home.com', done.
# Seq:      2: Sending mail to 'Full Name <fulnam@other.org>', done.

Fractionner la chaîne en fonction du délimiteur dans

Si vous ne pouvez pas utiliser bash, ou si vous voulez écrire quelque chose qui peut être utilisé dans de nombreux shells différents, vous ne pouvez souvent pas utiliser de bashismes - et cela inclut les tableaux que nous avons utilisés dans les solutions ci-dessus.

Cependant, nous n'avons pas besoin d'utiliser des tableaux pour faire une boucle sur les "éléments" d'une chaîne. Il existe une syntaxe utilisée dans de nombreux shells pour supprimer les sous-chaînes d'une chaîne de la première ou de la dernière occurrence d'un modèle. Notez que* s'agit d'un caractère générique qui représente zéro ou plusieurs caractères:

(L'absence de cette approche dans toute solution publiée jusqu'à présent est la principale raison pour laquelle j'écris cette réponse;)

${var#*SubStr}  # drops substring from start of string up to first occurrence of `SubStr`
${var##*SubStr} # drops substring from start of string up to last occurrence of `SubStr`
${var%SubStr*}  # drops substring from last occurrence of `SubStr` to end of string
${var%%SubStr*} # drops substring from first occurrence of `SubStr` to end of string

Comme expliqué par Score_Under :

#et %supprimer la sous-chaîne correspondante la plus courte possible respectivement au début et à la fin de la chaîne, et

##et %%supprimez la sous-chaîne correspondante la plus longue possible.

En utilisant la syntaxe ci-dessus, nous pouvons créer une approche où nous extrayons des "éléments" de sous-chaîne de la chaîne en supprimant les sous-chaînes jusqu'au délimiteur ou après.

Le bloc de code ci-dessous fonctionne bien dans (y compris Mac OS bash),, , et c'est :

IN="bla@some.com;john@home.com;Full Name <fulnam@other.org>"
while [ "$IN" ] ;do
    # extract the substring from start of string up to delimiter.
    # this is the first "element" of the string.
    iter=${IN%%;*}
    echo "> [$iter]"
    # if there's only one element left, set `IN` to an empty string.
    # this causes us to exit this `while` loop.
    # else, we delete the first "element" of the string from IN, and move onto the next.
    [ "$IN" = "$iter" ] && \
        IN='' || \
        IN="${IN#*;}"
  done
# > [bla@some.com]
# > [john@home.com]
# > [Full Name <fulnam@other.org>]

S'amuser!


15
le # , ##, %et %%substitutions ont ce qui est l' OMI une explication plus facile à retenir (pour combien ils supprimer): #et %supprimer la plus courte chaîne de correspondance possible, et ##et %%supprimer la plus longue possible.
Score_Under

1
Le IFS=\; read -a fields <<<"$var"échoue sur les sauts de ligne et ajoute un saut de ligne de fin. L'autre solution supprime un champ vide de fin.
Isaac

Le délimiteur de coquille est la réponse la plus élégante, point final.
Eric Chen

La dernière alternative pourrait-elle être utilisée avec une liste de séparateurs de champs définie ailleurs? Par exemple, je veux utiliser cela comme un script shell, et passer une liste de séparateurs de champs comme paramètre positionnel.
sancho.s ReinstateMonicaCellio

Oui, en boucle:for sep in "#" "ł" "@" ; do ... var="${var#*$sep}" ...
F. Hauri

184

J'ai vu quelques réponses faisant référence à la cutcommande, mais elles ont toutes été supprimées. C'est un peu étrange que personne n'ait développé cela, car je pense que c'est l'une des commandes les plus utiles pour faire ce genre de chose, en particulier pour analyser des fichiers journaux délimités.

Dans le cas de la division de cet exemple spécifique en un tableau de scripts bash, trest probablement plus efficace, mais cutpeut être utilisé, et est plus efficace si vous souhaitez extraire des champs spécifiques du milieu.

Exemple:

$ echo "bla@some.com;john@home.com" | cut -d ";" -f 1
bla@some.com
$ echo "bla@some.com;john@home.com" | cut -d ";" -f 2
john@home.com

Vous pouvez évidemment mettre cela en boucle et itérer le paramètre -f pour extraire chaque champ indépendamment.

Cela devient plus utile lorsque vous disposez d'un fichier journal délimité avec des lignes comme celle-ci:

2015-04-27|12345|some action|an attribute|meta data

cutest très pratique pour pouvoir accéder à catce fichier et sélectionner un champ particulier pour un traitement ultérieur.


6
Bravo pour l'utilisation cut, c'est le bon outil pour le travail! Beaucoup plus clair que n'importe lequel de ces hacks shell.
MisterMiyagi

4
Cette approche ne fonctionnera que si vous connaissez le nombre d'éléments à l'avance; vous auriez besoin de programmer un peu plus de logique autour de cela. Il exécute également un outil externe pour chaque élément.
uli42

Excatly waht je cherchais pour essayer d'éviter la chaîne vide dans un csv. Maintenant, je peux également pointer la valeur exacte de la «colonne». Travaillez avec IFS déjà utilisé en boucle. Mieux que prévu pour ma situation.
Louis Loudog Trottier

Très utile pour extraire les identifiants et les PID aussi
Milos Grujic

Cette réponse mérite de défiler sur une demi-page :)
Gucu112

124

Cela a fonctionné pour moi:

string="1;2"
echo $string | cut -d';' -f1 # output is 1
echo $string | cut -d';' -f2 # output is 2

1
Bien qu'il ne fonctionne qu'avec un seul délimiteur de caractères, c'est ce que l'OP recherchait (enregistrements délimités par un point-virgule).
GuyPaddock

Répondu il y a environ quatre ans par @Ashok , et aussi, il y a plus d'un an par @DougW , que votre réponse, avec encore plus d'informations. Veuillez poster une solution différente de celle des autres.
MAChitgarha

90

Que diriez-vous de cette approche:

IN="bla@some.com;john@home.com" 
set -- "$IN" 
IFS=";"; declare -a Array=($*) 
echo "${Array[@]}" 
echo "${Array[0]}" 
echo "${Array[1]}" 

La source


7
+1 ... mais je ne nommerais pas la variable "Array" ... pet peev je suppose. Bonne solution.
Yzmir Ramirez

14
+1 ... mais "set" et déclarer -a ne sont pas nécessaires. Vous auriez aussi bien pu utiliser justeIFS";" && Array=($IN)
ata

+1 Juste une remarque: ne devrait-il pas être recommandé de conserver l'ancien IFS, puis de le restaurer? (comme l'a montré stefanB dans son edit3) les gens qui atterrissent ici (parfois simplement en copiant et en collant une solution) pourraient ne pas y penser
Luca Borrione

6
-1: Premièrement, @ata a raison de dire que la plupart des commandes de cette commande ne font rien. Deuxièmement, il utilise la séparation des mots pour former le tableau et ne fait rien pour empêcher l'expansion de glob lors de cette opération (donc si vous avez des caractères glob dans l'un des éléments du tableau, ces éléments sont remplacés par des noms de fichiers correspondants).
Charles Duffy

1
Proposer à l' utilisation $'...': IN=$'bla@some.com;john@home.com;bet <d@\ns* kl.com>'. Imprime ensuite echo "${Array[2]}"une chaîne avec la nouvelle ligne. set -- "$IN"est également nécessaire dans ce cas. Oui, pour empêcher l'expansion globale, la solution doit inclure set -f.
John_West

79

Je pense qu'AWK est la commande la meilleure et la plus efficace pour résoudre votre problème. AWK est inclus par défaut dans presque toutes les distributions Linux.

echo "bla@some.com;john@home.com" | awk -F';' '{print $1,$2}'

va donner

bla@some.com john@home.com

Bien sûr, vous pouvez stocker chaque adresse e-mail en redéfinissant le champ d'impression awk.


3
Ou encore plus simple: echo "bla@some.com; john@home.com" | awk 'BEGIN {RS = ";"} {print}'
Jaro

@Jaro Cela a parfaitement fonctionné pour moi lorsque j'avais une chaîne avec des virgules et que je devais la reformater en lignes. Merci.
Aquarelle

Cela a fonctionné dans ce scénario -> "echo" $ SPLIT_0 "| awk -F 'inode =' '{print $ 1}'"! J'ai eu des problèmes en essayant d'utiliser des atrings ("inode =") au lieu de caractères (";"). $ 1, $ 2, $ 3, $ 4 sont définis comme des positions dans un tableau! S'il existe un moyen de configurer un tableau ... mieux! Merci!
Eduardo Lucio

@EduardoLucio, ce que je pense est peut - être vous au sujet peut tout d' abord remplacer votre delimiter inode=dans ;par exemple sed -i 's/inode\=/\;/g' your_file_to_process, puis définir -F';'quand appliquer awk, l' espoir qui peut vous aider.
Tong

66
echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g'
bla@some.com
john@home.com

4
-1 que faire si la chaîne contient des espaces? par exemple IN="this is first line; this is second line" arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) ), produira un tableau de 8 éléments dans ce cas (un élément pour chaque espace de mots séparé), plutôt que 2 (un élément pour chaque ligne point-virgule séparé)
Luca Borrione

3
@Luca Non, le script sed crée exactement deux lignes. Ce qui crée les multiples entrées pour vous, c'est lorsque vous les placez dans un tableau bash (qui se divise sur l'espace blanc par défaut)
lothar

C'est exactement le point: l'OP doit stocker des entrées dans un tableau pour boucler dessus, comme vous pouvez le voir dans ses modifications. Je pense que votre (bonne) réponse a manqué de mentionner à utiliser arrIN=( $( echo "$IN" | sed -e 's/;/\n/g' ) )pour atteindre cet objectif, et aux conseils pour changer IFS IFS=$'\n'pour ceux qui atterriront ici à l'avenir et doivent diviser une chaîne contenant des espaces. (et pour le restaurer ensuite). :)
Luca Borrione

1
@Luca Bon point. Cependant, l'affectation du tableau n'était pas dans la question initiale lorsque j'ai rédigé cette réponse.
lothar

65

Cela fonctionne également:

IN="bla@some.com;john@home.com"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Attention, cette solution n'est pas toujours correcte. Si vous passez "bla@some.com" uniquement, il l'assignera à la fois à ADD1 et ADD2.


1
Vous pouvez utiliser -s pour éviter le problème mentionné: superuser.com/questions/896800/… "-f, --fields = LIST sélectionnez uniquement ces champs; imprimez également toute ligne qui ne contient aucun caractère de délimitation, sauf si l'option -s est spécifié "
fersarr

34

Une approche différente de la réponse de Darron, voici comment je le fais:

IN="bla@some.com;john@home.com"
read ADDR1 ADDR2 <<<$(IFS=";"; echo $IN)

Je pense que oui! Exécutez les commandes ci-dessus, puis "echo $ ADDR1 ... $ ADDR2" et j'obtiens la sortie "bla@some.com ... john@home.com"
nickjb

1
Cela a VRAIMENT bien fonctionné pour moi ... Je l'ai utilisé pour itérer sur un tableau de chaînes qui contenaient des données DB, SERVER, PORT séparées par des virgules pour utiliser mysqldump.
Nick

5
Diagnostic: l' IFS=";"affectation n'existe que dans la $(...; echo $IN)sous - coque; c'est pourquoi certains lecteurs (dont moi) pensent initialement que cela ne fonctionnera pas. J'ai supposé que tout $ IN était absorbé par ADDR1. Mais nickjb est correct; ça marche. La raison en est que la echo $INcommande analyse ses arguments en utilisant la valeur actuelle de $ IFS, mais les renvoie ensuite à stdout à l'aide d'un délimiteur d'espace, quel que soit le paramètre de $ IFS. L'effet net est donc comme si quelqu'un avait appelé read ADDR1 ADDR2 <<< "bla@some.com john@home.com"(notez que l'entrée est séparée par des espaces et non séparée par des espaces).
dubiousjim

1
Cela échoue sur les espaces et les nouvelles lignes, et étend également les caractères génériques *dans le echo $INavec une expansion de variable non cotée.
Isaac

J'aime vraiment cette solution. Une description des raisons pour lesquelles cela fonctionne serait très utile et en ferait une meilleure réponse globale.
Michael Gaskill

32

Dans Bash, une méthode à l'épreuve des balles, cela fonctionnera même si votre variable contient des retours à la ligne:

IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")

Regardez:

$ in=$'one;two three;*;there is\na newline\nin this field'
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two three" [2]="*" [3]="there is
a newline
in this field")'

L'astuce pour que cela fonctionne est d'utiliser l' -doption de read(délimiteur) avec un délimiteur vide, ce qui readoblige à lire tout ce qui est alimenté. Et nous alimentons readexactement le contenu de la variable in, sans retour à la ligne grâce à printf. Notez que nous mettons également le délimiteur printfpour nous assurer que la chaîne passée à reada un délimiteur de fin. Sans cela, readcouperait les champs vides en fin potentiels:

$ in='one;two;three;'    # there's an empty field
$ IFS=';' read -d '' -ra array < <(printf '%s;\0' "$in")
$ declare -p array
declare -a array='([0]="one" [1]="two" [2]="three" [3]="")'

le champ vide de fin est conservé.


Mise à jour pour Bash≥4.4

Depuis Bash 4.4, la fonction intégrée mapfile(aka readarray) prend en charge l' -doption de spécifier un délimiteur. Par conséquent, une autre voie canonique est:

mapfile -d ';' -t array < <(printf '%s;' "$in")

5
Je l'ai trouvé comme la solution rare sur cette liste qui fonctionne correctement avec \n, les espaces et *simultanément. En outre, pas de boucles; La variable tableau est accessible dans le shell après exécution (contrairement à la réponse la plus élevée). Notez que in=$'...'cela ne fonctionne pas avec les guillemets doubles. Je pense qu'il a besoin de plus de votes positifs.
John_West

28

Que diriez-vous de cette doublure, si vous n'utilisez pas de tableaux:

IFS=';' read ADDR1 ADDR2 <<<$IN

Pensez à utiliser read -r ...pour vous assurer que, par exemple, les deux caractères "\ t" dans l'entrée finissent comme les deux mêmes caractères dans vos variables (au lieu d'un seul caractère de tabulation).
dubiousjim

-1 Cela ne fonctionne pas ici (Ubuntu 12.04). L'ajout echo "ADDR1 $ADDR1"\n echo "ADDR2 $ADDR2"à votre extrait de code produira ADDR1 bla@some.com john@home.com\nADDR2(\ n est une nouvelle ligne)
Luca Borrione

Cela est probablement dû à un bogue impliquant IFSet ici des chaînes qui a été corrigé dans bash4.3. Citer $INdevrait le réparer. (En théorie, $INn'est pas soumis à la séparation ou à la globalisation des mots après son expansion, ce qui signifie que les guillemets ne devraient pas être nécessaires. idée.)
chepner

Cela casse si $ in contient des retours à la ligne même si $ IN est cité. Et ajoute une nouvelle ligne de fin.
Isaac

Un problème avec cela, et de nombreuses autres solutions, est également qu'il suppose qu'il y a EXACTEMENT DEUX éléments dans $ IN - OU que vous êtes prêt à ce que le deuxième élément et les éléments suivants soient écrasés ensemble dans ADDR2. Je comprends que cela répond à la demande, mais c'est une bombe à retardement.
Steven the Easily Amused du

23

Sans paramétrer l'IFS

Si vous n'avez qu'un seul colon, vous pouvez le faire:

a="foo:bar"
b=${a%:*}
c=${a##*:}

tu auras:

b = foo
c = bar

20

Voici un 3-liner propre:

in="foo@bar;bizz@buzz;fizz@buzz;buzz@woof"
IFS=';' list=($in)
for item in "${list[@]}"; do echo $item; done

IFSdélimite les mots en fonction du séparateur et ()est utilisé pour créer un tableau . Puis [@]est utilisé pour renvoyer chaque élément en tant que mot distinct.

Si vous avez du code après cela, vous devez également restaurer $IFS, par exemple unset IFS.


5
L'utilisation de $incaractères non cotés permet d'étendre les caractères génériques.
Isaac

10

La fonction Bash / zsh suivante divise son premier argument sur le délimiteur donné par le deuxième argument:

split() {
    local string="$1"
    local delimiter="$2"
    if [ -n "$string" ]; then
        local part
        while read -d "$delimiter" part; do
            echo $part
        done <<< "$string"
        echo $part
    fi
}

Par exemple, la commande

$ split 'a;b;c' ';'

les rendements

a
b
c

Cette sortie peut, par exemple, être redirigée vers d'autres commandes. Exemple:

$ split 'a;b;c' ';' | cat -n
1   a
2   b
3   c

Par rapport aux autres solutions proposées, celle-ci présente les avantages suivants:

  • IFSn'est pas redéfini: en raison de la portée dynamique des variables locales, même, la redéfinition d' IFSune boucle provoque une fuite de la nouvelle valeur dans les appels de fonction effectués à partir de la boucle.

  • Les tableaux ne sont pas utilisés: la lecture d'une chaîne dans un tableau à l'aide de readnécessite l'indicateur -adans Bash et -Adans zsh.

Si vous le souhaitez, la fonction peut être placée dans un script comme suit:

#!/usr/bin/env bash

split() {
    # ...
}

split "$@"

Ne semble pas fonctionner avec des délimiteurs de plus d'un caractère: split = $ (split "$ content" "file: //")
madprops

Vrai - à partir de help read:-d delim continue until the first character of DELIM is read, rather than newline
Halle Knast

8

vous pouvez appliquer awk à de nombreuses situations

echo "bla@some.com;john@home.com"|awk -F';' '{printf "%s\n%s\n", $1, $2}'

vous pouvez également l'utiliser

echo "bla@some.com;john@home.com"|awk -F';' '{print $1,$2}' OFS="\n"

7

Il existe un moyen simple et intelligent comme celui-ci:

echo "add:sfff" | xargs -d: -i  echo {}

Mais vous devez utiliser gnu xargs, BSD xargs ne supporte pas -d delim. Si vous utilisez Apple Mac comme moi. Vous pouvez installer gnu xargs:

brew install findutils

puis

echo "add:sfff" | gxargs -d: -i  echo {}

4

C'est la façon la plus simple de le faire.

spo='one;two;three'
OIFS=$IFS
IFS=';'
spo_array=($spo)
IFS=$OIFS
echo ${spo_array[*]}

4

Il y a des réponses intéressantes ici (errator en particulier), mais pour quelque chose d'analogue à diviser dans d'autres langues - c'est ce que j'ai pris pour la question initiale - je me suis installé sur ceci:

IN="bla@some.com;john@home.com"
declare -a a="(${IN/;/ })";

Maintenant ${a[0]}, ${a[1]}etc, sont comme vous vous en doutez. Utilisez ${#a[*]}pour le nombre de termes. Ou pour itérer, bien sûr:

for i in ${a[*]}; do echo $i; done

NOTE IMPORTANTE:

Cela fonctionne dans les cas où il n'y a pas d'espace à s'inquiéter, ce qui a résolu mon problème, mais peut ne pas résoudre le vôtre. Allez avec la $IFS(les) solution (s) dans ce cas.


Ne fonctionne pas lorsqu'il INcontient plus de deux adresses de messagerie. Veuillez vous référer à la même idée (mais fixe) dans la réponse de palindrom
olibre

Meilleure utilisation ${IN//;/ }(double barre oblique) pour qu'il fonctionne également avec plus de deux valeurs. Attention, tout caractère générique ( *?[) sera développé. Et un champ vide de fin sera supprimé.
Isaac

3
IN="bla@some.com;john@home.com"
IFS=';'
read -a IN_arr <<< "${IN}"
for entry in "${IN_arr[@]}"
do
    echo $entry
done

Production

bla@some.com
john@home.com

Système: Ubuntu 12.04.1


IFS n'est pas défini dans le contexte spécifique d' readici et peut donc perturber le reste du code, le cas échéant.
codeforester

2

S'il n'y a pas d'espace, pourquoi pas ça?

IN="bla@some.com;john@home.com"
arr=(`echo $IN | tr ';' ' '`)

echo ${arr[0]}
echo ${arr[1]}

2

Utilisez le setintégré pour charger la $@baie:

IN="bla@some.com;john@home.com"
IFS=';'; set $IN; IFS=$' \t\n'

Ensuite, laissez la fête commencer:

echo $#
for a; do echo $a; done
ADDR1=$1 ADDR2=$2

Meilleure utilisation set -- $INpour éviter certains problèmes avec "$ IN" commençant par le tiret. Néanmoins, l'expansion non citée de $INétendra les caractères génériques ( *?[).
Isaac

2

Deux alternatives bourne-ish où aucune ne nécessite de tableaux bash:

Cas 1 : Restez simple et agréable: utilisez un NewLine comme séparateur d'enregistrement ... par exemple.

IN="bla@some.com
john@home.com"

while read i; do
  # process "$i" ... eg.
    echo "[email:$i]"
done <<< "$IN"

Remarque: dans ce premier cas, aucun sous-processus n'est bifurqué pour aider à la manipulation de la liste.

Idée: Peut-être que cela vaut la peine d'utiliser NL de manière intensive en interne , et de convertir uniquement en un RS différent lors de la génération du résultat final en externe .

Cas 2 : utilisation d'un ";" comme séparateur d'enregistrement ... par exemple.

NL="
" IRS=";" ORS=";"

conv_IRS() {
  exec tr "$1" "$NL"
}

conv_ORS() {
  exec tr "$NL" "$1"
}

IN="bla@some.com;john@home.com"
IN="$(conv_IRS ";" <<< "$IN")"

while read i; do
  # process "$i" ... eg.
    echo -n "[email:$i]$ORS"
done <<< "$IN"

Dans les deux cas, une sous-liste peut être composée au sein de la boucle est persistante une fois la boucle terminée. Ceci est utile lors de la manipulation de listes en mémoire, au lieu de stocker des listes dans des fichiers. {ps reste calme et continue B-)}


2

Outre les réponses fantastiques qui ont déjà été fournies, s'il s'agit simplement d'imprimer les données, vous pouvez envisager d'utiliser awk:

awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"

Cela définit le séparateur de champs sur ;, afin qu'il puisse parcourir les champs avec une forboucle et imprimer en conséquence.

Tester

$ IN="bla@some.com;john@home.com"
$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "$IN"
> [bla@some.com]
> [john@home.com]

Avec une autre entrée:

$ awk -F";" '{for (i=1;i<=NF;i++) printf("> [%s]\n", $i)}' <<< "a;b;c   d;e_;f"
> [a]
> [b]
> [c   d]
> [e_]
> [f]

2

Dans le shell Android, la plupart des méthodes proposées ne fonctionnent tout simplement pas:

$ IFS=':' read -ra ADDR <<<"$PATH"                             
/system/bin/sh: can't create temporary file /sqlite_stmt_journals/mksh.EbNoR10629: No such file or directory

Ce qui fonctionne, c'est:

$ for i in ${PATH//:/ }; do echo $i; done
/sbin
/vendor/bin
/system/sbin
/system/bin
/system/xbin

//signifie remplacement global.


1
Échoue si une partie de $ PATH contient des espaces (ou des nouvelles lignes). Développe également les caractères génériques (astérisque *, point d'interrogation? Et accolades […]).
Isaac

2
IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'
set -f
oldifs="$IFS"
IFS=';'; arrayIN=($IN)
IFS="$oldifs"
for i in "${arrayIN[@]}"; do
echo "$i"
done
set +f

Production:

bla@some.com
john@home.com
Charlie Brown <cbrown@acme.com
!"#$%&/()[]{}*? are no problem
simple is beautiful :-)

Explication: L'affectation simple à l'aide de parenthèses () convertit la liste séparée par des points-virgules en un tableau à condition que vous disposiez de l'IFS correct pour ce faire. La boucle FOR standard gère les éléments individuels de ce tableau comme d'habitude. Notez que la liste donnée pour la variable IN doit être "dure" entre guillemets, c'est-à-dire avec des ticks simples.

IFS doit être enregistré et restauré car Bash ne traite pas une affectation de la même manière qu'une commande. Une autre solution consiste à encapsuler l'affectation à l'intérieur d'une fonction et à appeler cette fonction avec un IFS modifié. Dans ce cas, une sauvegarde / restauration séparée d'IFS n'est pas nécessaire. Merci pour "Bize" de l'avoir signalé.


!"#$%&/()[]{}*? are no problembien ... pas tout à fait: []*?sont des caractères glob. Qu'en est-il de la création de ce répertoire et de ce fichier: `mkdir '!" # $% &'; Touch '! "# $% & / () [] {} Vous avez hahahaha - pas de problème' et exécutez votre commande? simple peut être beau, mais quand il est cassé, il est cassé.
gniourf_gniourf

@gniourf_gniourf La chaîne est stockée dans une variable. Veuillez consulter la question d'origine.
ajaaskel

1
@ajaaskel, vous n'avez pas bien compris mon commentaire. Allez dans un répertoire de travail et exécutez les commandes: mkdir '!"#$%&'; touch '!"#$%&/()[]{} got you hahahaha - are no problem'. Ils ne créeront qu'un répertoire et un fichier, avec des noms étranges, je dois l'admettre. Ensuite , exécutez vos commandes avec l'exacte que INvous avez donné: IN='bla@some.com;john@home.com;Charlie Brown <cbrown@acme.com;!"#$%&/()[]{}*? are no problem;simple is beautiful :-)'. Vous verrez que vous n'obtiendrez pas la sortie que vous attendez. Parce que vous utilisez une méthode soumise à des extensions de chemin pour diviser votre chaîne.
gniourf_gniourf

Ceci est de démontrer que les personnages *, ?, [...]et même, si extglobest réglé, !(...), @(...), ?(...), +(...) sont des problèmes avec cette méthode!
gniourf_gniourf

1
@gniourf_gniourf Merci pour vos commentaires détaillés sur la globalisation. J'ai ajusté le code pour désactiver la globalisation. Mon but était cependant juste de montrer qu'une affectation assez simple peut faire le travail de fractionnement.
ajaaskel

1

D'accord les gars!

Voici ma réponse!

DELIMITER_VAL='='

read -d '' F_ABOUT_DISTRO_R <<"EOF"
DISTRIB_ID=Ubuntu
DISTRIB_RELEASE=14.04
DISTRIB_CODENAME=trusty
DISTRIB_DESCRIPTION="Ubuntu 14.04.4 LTS"
NAME="Ubuntu"
VERSION="14.04.4 LTS, Trusty Tahr"
ID=ubuntu
ID_LIKE=debian
PRETTY_NAME="Ubuntu 14.04.4 LTS"
VERSION_ID="14.04"
HOME_URL="http://www.ubuntu.com/"
SUPPORT_URL="http://help.ubuntu.com/"
BUG_REPORT_URL="http://bugs.launchpad.net/ubuntu/"
EOF

SPLIT_NOW=$(awk -F$DELIMITER_VAL '{for(i=1;i<=NF;i++){printf "%s\n", $i}}' <<<"${F_ABOUT_DISTRO_R}")
while read -r line; do
   SPLIT+=("$line")
done <<< "$SPLIT_NOW"
for i in "${SPLIT[@]}"; do
    echo "$i"
done

Pourquoi cette approche est "la meilleure" pour moi?

Pour deux raisons:

  1. Vous n'avez pas besoin d'échapper au délimiteur;
  2. Vous n'aurez aucun problème avec les espaces vides . La valeur sera correctement séparée dans le tableau!

[]


FYI, /etc/os-releaseet /etc/lsb-releasesont censés provenir, et non analysés. Votre méthode est donc vraiment fausse. De plus, vous ne répondez pas tout à fait à la question de la rotation d'une chaîne sur un délimiteur.
gniourf_gniourf

0

Une ligne pour séparer une chaîne séparée par ';' dans un tableau est:

IN="bla@some.com;john@home.com"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

Cela ne définit IFS que dans un sous-shell, vous n'avez donc pas à vous soucier de l'enregistrement et de la restauration de sa valeur.


-1 cela ne fonctionne pas ici (ubuntu 12.04). il imprime uniquement le premier écho avec toute la valeur $ IN, tandis que le second est vide. vous pouvez le voir si vous mettez echo "0:" $ {ADDRS [0]} \ n echo "1:" $ {ADDRS [1]} la sortie est 0: bla@some.com;john@home.com\n 1:(\ n est la nouvelle ligne)
Luca Borrione

1
veuillez vous référer à la réponse de nickjb à pour une alternative de travail à cette idée stackoverflow.com/a/6583589/1032370
Luca Borrione

1
-1, 1. IFS n'est pas défini dans ce sous-shell (il est transmis à l'environnement de "echo", qui est intégré, donc rien ne se passe de toute façon). 2. $INest cité de sorte qu'il n'est pas soumis au fractionnement IFS. 3. La substitution de processus est divisée par des espaces, mais cela peut corrompre les données d'origine.
Score_Under

0

Peut-être pas la solution la plus élégante, mais fonctionne avec *et les espaces:

IN="bla@so me.com;*;john@home.com"
for i in `delims=${IN//[^;]}; seq 1 $((${#delims} + 1))`
do
   echo "> [`echo $IN | cut -d';' -f$i`]"
done

Les sorties

> [bla@so me.com]
> [*]
> [john@home.com]

Autre exemple (délimiteurs au début et à la fin):

IN=";bla@so me.com;*;john@home.com;"
> []
> [bla@so me.com]
> [*]
> [john@home.com]
> []

Fondamentalement, il supprime tous les caractères autres que la ;création, delimspar exemple. ;;;. Ensuite, il forboucle de 1à number-of-delimiterscomme compté par ${#delims}. La dernière étape consiste à obtenir en toute sécurité la $ipièce en utilisant cut.

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.