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

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 pour que je puisse:

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

Je n’ai pas nécessairement besoin des variables ADDR1 et 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 suit:

 #!/usr/bin/env bash IN="bla@some.com;john@home.com" mails=$(echo $IN | tr ";" "\n") for addr in $mails do echo "> [$addr]" done 

Sortie:

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

Il y avait une solution impliquant la définition de Internal_field_separator (IFS) à ; . Je ne suis pas sûr de ce qui s’est passé avec cette réponse, comment réinitialiser IFS à la valeur par défaut?

RE: solution IFS , j’ai essayé ceci et ça marche, je conserve l’ancien IFS puis 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 que la première chaîne lors de l’impression en boucle, sans les crochets autour de $IN cela fonctionne.

Vous pouvez définir la variable IFS ( Internal Field Separator), puis la laisser parsingr dans un tableau. Lorsque cela se produit dans une commande, l’affectation à IFS n’a lieu que dans l’environnement de cette commande (à read ). Il parsing ensuite l’entrée en fonction de la valeur de la variable IFS dans un tableau, que nous pouvons ensuite parcourir.

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

Il va parsingr une ligne d'éléments séparés par ; , en le poussant dans un tableau. Des trucs pour traiter l'intégralité de $IN , chaque fois qu'une ligne de saisie est séparée par ; :

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

Tiré du tableau fractionné du script shell Bash :

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

Explication:

Cette construction remplace toutes les occurrences de ';' (l’initiale // signifie remplacement global) dans la chaîne IN avec ' ' (un seul espace), puis interprète la chaîne délimitée par un espace comme un tableau (c’est ce que font les parenthèses).

La syntaxe utilisée à l’intérieur des accolades pour remplacer chacune ';' elles ';' caractère avec un caractère ' ' est appelé extension de paramètre .

Il y a des pièges courants:

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

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

 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 aide, cependant.

Réponse compatible

Pour cette question, il y a déjà beaucoup de manières différentes de le faire en bash . Mais bash possède de nombreuses fonctionnalités spéciales , appelées bashism, qui fonctionnent bien, mais qui ne fonctionneront dans aucun autre shell .

En particulier, les tableaux , les tableaux associatifs et les substitutions de motifs sont de simples poils et peuvent ne pas fonctionner sous d’autres shells .

Sur Debian GNU / Linux , il existe un shell standard appelé dash , mais je connais beaucoup de personnes qui aiment utiliser ksh .

Enfin, dans une très petite situation, il existe un outil spécial appelé busybox avec son propre interpréteur de shell ( ash ).

Chaîne demandée

L’échantillon de chaîne dans SO question est:

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

Comme cela pourrait être utile avec les espaces blancs et que les espaces blancs pourraient modifier le résultat de la routine, je préfère utiliser cet exemple de chaîne:

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

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

Sous pure bash, nous pouvons utiliser des tableaux et des IFS :

 var="bla@some.com;john@home.com;Full Name " 

 oIFS="$IFS" IFS=";" declare -a fields=($var) IFS="$oIFS" unset oIFS 

 IFS=\; read -a fields < <<"$var" 

En utilisant cette syntaxe sous bash récent, ne modifiez pas $IFS pour la session en cours, mais uniquement pour la commande en cours:

 set | grep ^IFS= IFS=$' \t\n' 

Maintenant, la chaîne var est divisée et stockée dans un tableau ( fields nommés):

 set | grep ^fields=\\\|^var= fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name ") var='bla@some.com;john@home.com;Full Name ' 

Nous pourrions demander un contenu variable avec declare -p :

 declare -p var fields declare -- var="bla@some.com;john@home.com;Full Name " declare -a fields=([0]="bla@some.com" [1]="john@home.com" [2]="Full Name ") 

read est le moyen le plus rapide de faire le fractionnement, car il n'y a pas de fourchettes et aucune ressource externe appelée.

De là, vous pouvez utiliser la syntaxe que vous connaissez déjà pour traiter chaque champ:

 for x in "${fields[@]}";do echo "> [$x]" done > [bla@some.com] > [john@home.com] > [Full Name ] 

ou laisser tomber chaque champ après traitement (j'aime cette approche changeante ):

 while [ "$fields" ] ;do echo "> [$fields]" fields=("${fields[@]:1}") done > [bla@some.com] > [john@home.com] > [Full Name ] 

ou même pour une impression simple (syntaxe plus courte):

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

Chaîne divisée basée sur le délimiteur en shell

Mais si vous écrivez quelque chose utilisable sous de nombreux shells, vous ne devez pas utiliser de bashismes .

Il existe une syntaxe, utilisée dans de nombreux shells, pour diviser une chaîne en première ou en dernière occurrence d'une sous-chaîne:

 ${var#*SubStr} # will drop begin of ssortingng up to first occur of `SubStr` ${var##*SubStr} # will drop begin of ssortingng up to last occur of `SubStr` ${var%SubStr*} # will drop part of ssortingng from last occur of `SubStr` to the end ${var%%SubStr*} # will drop part of ssortingng from first occur of `SubStr` to the end 

(Le manque de ceci est la raison principale de ma publication de réponse;)

Comme souligné par Score_Under :

# et % suppriment la chaîne correspondante la plus courte possible, et

## et %% suppriment le plus longtemps possible.

Ce petit exemple de script fonctionne bien sous bash , dash , ksh , busybox et a également été testé sous bash:

 var="bla@some.com;john@home.com;Full Name " while [ "$var" ] ;do iter=${var%%;*} echo "> [$iter]" [ "$var" = "$iter" ] && \ var='' || \ var="${var#*;}" done > [bla@some.com] > [john@home.com] > [Full Name ] 

S'amuser!

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

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

Dans le cas de la division de cet exemple spécifique en un tableau de script bash, tr est probablement plus efficace, mais cut peut ê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 dans une boucle, et itérer le paramètre -f pour extraire chaque champ indépendamment.

Cela devient plus utile lorsque vous avez un fichier journal délimité avec des lignes comme ceci:

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

cut est très pratique pour pouvoir traiter ce fichier et sélectionner un champ particulier pour un traitement ultérieur.

Cela a fonctionné pour moi:

 ssortingng="1;2" echo $ssortingng | cut -d';' -f1 # output is 1 echo $ssortingng | cut -d';' -f2 # output is 2 
 echo "bla@some.com;john@home.com" | sed -e 's/;/\n/g' bla@some.com john@home.com 

Cela fonctionne aussi:

 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 ne transmettez que “bla@some.com”, il sera atsortingbué à la fois à ADD1 et à ADD2.

Je pense que AWK est la commande la plus efficace pour résoudre votre problème. AWK est inclus dans Bash par défaut dans presque toutes les dissortingbutions 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.

Une autre approche de la réponse de Darron , c’est comment je le fais:

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

Dans Bash, un moyen à l’épreuve des balles, cela fonctionnera même si votre variable contient des nouvelles lignes:

 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'option -d de read (délimiteur) avec un délimiteur vide, de sorte que read soit obligé de lire tout ce qu'il est alimenté. Et nous alimentons en read avec exactement le contenu de la variable, sans ligne de retour grâce à printf . Notez que nous mettons également le délimiteur dans printf pour nous assurer que la chaîne transmise à read a un délimiteur final. Sans elle, read réduirait les éventuels champs vides à la fin:

 $ 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 final est conservé.


Mise à jour pour Bash≥4.4

Depuis Bash 4.4, le mapfile intégré (aka readarray ) prend en charge l’option -d pour spécifier un délimiteur. Une autre voie canonique est donc:

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

Que diriez-vous de cette ligne, si vous n’utilisez pas de tableaux:

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

Sans définir l’IFS

Si vous avez juste un colon, vous pouvez le faire:

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

tu auras:

 b = foo c = bar 

Voici un 3 lignes propres:

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

IFS délimite les mots basés sur le séparateur et () est utilisé pour créer un tableau . Ensuite, [@] 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 .

Il existe une manière simple et intelligente comme celle-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 {} 

C’est le moyen le plus simple de le faire.

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

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

 split() { local ssortingng="$1" local delimiter="$2" if [ -n "$ssortingng" ]; 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:

  • IFS n'est pas surchargé: en raison de la scope dynamic des variables, même locales, le remplacement de l' IFS par une boucle entraîne la fuite de la nouvelle valeur dans les appels de fonctions effectués depuis la boucle.

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

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

 #!/usr/bin/env bash split() { # ... } split "$@" 
 IN="bla@some.com;john@home.com" IFS=';' read -a IN_arr < << "${IN}" for entry in "${IN_arr[@]}" do echo $entry done 

Sortie

 bla@some.com john@home.com 

Système: Ubuntu 12.04.1

vous pouvez appliquer de nombreuses situations

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

aussi vous pouvez l’utiliser

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

Si pas d’espace, pourquoi pas ça?

 IN="bla@some.com;john@home.com" arr=(`echo $IN | tr ';' ' '`) echo ${arr[0]} echo ${arr[1]} 

Il y a quelques bonnes réponses ici (errator esp.), Mais pour que quelque chose d’analogue se scinde dans d’autres langues – ce qui est ce que j’ai pris la question initiale pour signifier – j’ai choisi ceci:

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

Maintenant, ${a[0]} , ${a[1]} , etc. 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. Aller avec la solution $IFS dans ce cas.

Utilisez le set intégré pour charger le tableau $@ :

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

Alors, que la fête commence:

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

Deux alternatives Bourne-ish où ni les tableaux bash ne sont nécessaires:

Cas 1 : Restez simple: utilisez une NewLine comme séparateur d’enregistrements … 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 créé pour faciliter la manipulation des listes.

Idée: Peut-être vaut-il la peine d'utiliser NL en interne et de ne convertir que vers un autre RS lorsque le résultat final est généré en externe .

Cas 2 : Utiliser un ";" en tant que 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 pouvant être composée dans 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 rest calme et continue B-)}

Outre les réponses fantastiques déjà fournies, il suffit d’imprimer les données que vous envisagez d’utiliser avec awk :

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

Cela définit le séparateur de champ à ; , afin qu'il puisse parcourir les champs avec une boucle for 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;cd;e_;f" > [a] > [b] > [cd] > [e_] > [f] 

Dans Android shell, 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 

Qu'est-ce que le travail est:

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

// signifie remplacement global.

Ok 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 pas de problème avec les espaces vides . La valeur sera correctement séparée dans le tableau!

[]

Un one-liner 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 définit uniquement IFS dans un sous-shell, vous n’avez donc pas à vous soucier de sauvegarder et de restaurer sa valeur.

 IN='bla@some.com;john@home.com;Charlie Brown  

Sortie:

 bla@some.com john@home.com Charlie Brown  

Explanation: Simple assignment using parenthesis () converts semicolon separated list into an array provided you have correct IFS while doing that. Standard FOR loop handles individual items in that array as usual. Notice that the list given for IN variable must be "hard" quoted, that is, with single ticks.

IFS must be saved and restored since Bash does not treat an assignment the same way as a command. An alternate workaround is to wrap the assignment inside a function and call that function with a modified IFS. In that case separate saving/restoring of IFS is not needed. Thanks for "Bize" for pointing that out.

Maybe not the most elegant solution, but works with * and spaces:

 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 

Outputs

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

Other example (delimiters at beginning and end):

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

Basically it removes every character other than ; making delims eg. ;;; . Then it does for loop from 1 to number-of-delimiters as counted by ${#delims} . The final step is to safely get the $i th part using cut .