bash - sous - shell découper une chaine de caractere




Comment séparer une chaîne sur un délimiteur dans Bash? (20)

Réponse compatible

Pour cette question SO, il y a déjà beaucoup de façons différentes de le faire dans bash . Mais bash a beaucoup de fonctionnalités spéciales , ce que l'on appelle le bashism qui fonctionne bien, mais cela ne fonctionnera pas dans n'importe quel autre shell .

En particulier, les tableaux , les tableaux associatifs et les substitutions de motifs sont de purs bashismes et peuvent ne pas fonctionner sous d'autres shells .

Sur mon Debian GNU / Linux , il y a un shell standard appelé dash , mais je connais beaucoup de gens 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'exemple de chaîne dans la question SO est:

IN="[email protected];[email protected]"

Comme cela peut être utile avec les espaces et comme les espaces peuvent modifier le résultat de la routine, je préfère utiliser cette chaîne d'exemple:

 IN="[email protected];[email protected];Full Name <[email protected]>"

Séparer les chaînes en fonction du délimiteur dans bash (version> = 4.2)

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

var="[email protected];[email protected];Full Name <[email protected]>"

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

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

L'utilisation de cette syntaxe sous bash ne modifie 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]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")
var='[email protected];[email protected];Full Name <[email protected]>'

Nous pourrions demander un contenu variable avec declare -p :

declare -p var fields
declare -- var="[email protected];[email protected];Full Name <[email protected]>"
declare -a fields=([0]="[email protected]" [1]="[email protected]" [2]="Full Name <[email protected]>")

read est le moyen le plus rapide de faire la séparation, car il n'y a pas de fourches et pas de ressources externes appelées.

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

for x in "${fields[@]}";do
    echo "> [$x]"
    done
> [[email protected]]
> [[email protected]]
> [Full Name <[email protected]>]

ou laisser tomber chaque champ après le traitement (j'aime cette approche de décalage ):

while [ "$fields" ] ;do
    echo "> [$fields]"
    fields=("${fields[@]:1}")
    done
> [[email protected]]
> [[email protected]]
> [Full Name <[email protected]>]

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

printf "> [%s]\n" "${fields[@]}"
> [[email protected]]
> [[email protected]]
> [Full Name <[email protected]>]

Séparer la chaîne en fonction du séparateur dans la shell

Mais si vous écrivez quelque chose utilisable sous beaucoup de coquilles, vous ne devez pas utiliser des bashismes .

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

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

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

Comme indiqué 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 été testé sous Mac OS:

var="[email protected];[email protected];Full Name <[email protected]>"
while [ "$var" ] ;do
    iter=${var%%;*}
    echo "> [$iter]"
    [ "$var" = "$iter" ] && \
        var='' || \
        var="${var#*;}"
  done
> [[email protected]]
> [[email protected]]
> [Full Name <[email protected]>]

S'amuser!

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

IN="[email protected];[email protected]"

Maintenant, je voudrais diviser les chaînes par ; délimiteur de sorte que j'ai:

ADDR1="[email protected]"
ADDR2="[email protected]"

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 ce que j'étais après:

#!/usr/bin/env bash

IN="[email protected];[email protected]"

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

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

Sortie:

> [[email protected]]
> [[email protected]]

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é cela et ça marche, je garde l'ancien IFS et ensuite le restaurer:

IN="[email protected];[email protected]"

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

IFS=$OIFS

BTW, quand j'ai essayé

mails2=($IN)

J'ai seulement obtenu la première chaîne lors de l'impression en boucle, sans les parenthèses autour de $IN cela fonctionne.


C'est la manière la plus simple de le faire.

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

Cela fonctionne aussi:

IN="[email protected];[email protected]"
echo ADD1=`echo $IN | cut -d \; -f 1`
echo ADD2=`echo $IN | cut -d \; -f 2`

Attention, cette solution n'est pas toujours correcte. Dans le cas où vous passez "[email protected]" seulement, il l'assignera à ADD1 et ADD2.


Dans Bash, une méthode à l'épreuve des balles, qui 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'option -d de read (délimiteur) avec un délimiteur vide, de sorte que read soit obligé de lire tout ce qu'il contient. Et nous alimentons read avec exactement le contenu de la variable, sans retour chariot grâce à printf . Notez que nous mettons également le délimiteur dans printf pour nous assurer que la chaîne passée en read possède un délimiteur de fin. Sans cela, la read réduirait les champs vides potentiels suivants:

$ 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 ) supporte l'option -d pour spécifier un délimiteur. D'où une autre voie canonique est:

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

Deux alternatives bourne-ish où aucun des deux ne nécessite de tables bash:

Cas 1 : Keep it nice and simple: Utilisez un NewLine comme le Record-Separator ... par exemple.

IN="[email protected]
[email protected]"

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

Remarque: dans ce premier cas, aucun sous-processus n'est forké pour faciliter 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 ne convertir que vers un RS différent lors de la génération du résultat final en externe .

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

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

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

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

IN="[email protected];[email protected]"
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 garder son calme et continuer B-)}


Extrait du tableau de partage de script shell Bash :

IN="[email protected];[email protected]"
arrIN=(${IN//;/ })

Explication:

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

La syntaxe utilisée à l'intérieur des accolades pour remplacer chaque ';' Un caractère avec un caractère ' ' s'appelle Expansion de paramètre .

Il y a quelques 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;

Il y a un moyen simple et intelligent comme ceci:

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

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 développé cela, parce que je pense que c'est l'une des commandes les plus utiles pour faire ce genre de chose, en particulier pour l'analyse 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 voulez tirer des champs spécifiques du milieu.

Exemple:

$ echo "[email protected];[email protected]" | cut -d ";" -f 1
[email protected]
$ echo "[email protected];[email protected]" | cut -d ";" -f 2
[email protected]

Vous pouvez évidemment mettre cela dans une boucle, et itérer le paramètre -f pour tirer 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 attribute|meta data

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


La fonction Bash / zsh suivante divise son premier argument sur le délimiteur donné par le second 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' ';'

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 données, celle-ci présente les avantages suivants:

  • IFS n'est pas redéfini: En raison de la portée dynamique des variables locales, le remplacement de IFS sur une boucle entraîne la fuite de la nouvelle valeur dans les appels de fonction exécutés depuis la boucle.

  • Les tableaux ne sont pas utilisés: La lecture d'une chaîne dans un tableau en utilisant read nécessite l'option -a dans Bash et -A dans zsh.

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

#!/usr/bin/env bash

split() {
    # ...
}

split "[email protected]"

Mis à part les réponses fantastiques déjà fournies, s'il ne s'agit que d'imprimer les données, vous pouvez envisager d'utiliser awk :

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

Ceci définit le séparateur de champ sur ; , de sorte qu'il peut boucler à travers les champs avec une boucle for et imprimer en conséquence.

Tester

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

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]


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


Un one-liner pour séparer une chaîne séparée par ';' dans un tableau est:

IN="[email protected];[email protected]"
ADDRS=( $(IFS=";" echo "$IN") )
echo ${ADDRS[0]}
echo ${ADDRS[1]}

Cela ne fait que définir IFS dans un sous-shell, donc vous n'avez pas à vous soucier de sauvegarder et de restaurer sa valeur.



Voici un 3-liner propre:

in="[email protected];[email protected];[email protected];[email protected]"
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 array . Puis [@] est utilisé pour renvoyer chaque élément en tant que mot séparé.

Si vous avez un code par la suite, vous devez également restaurer $IFS , par exemple unset IFS .


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

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

Outputs

> [[email protected] me.com]
> [*]
> [[email protected]]

Other example (delimiters at beginning and end):

IN=";[email protected] me.com;*;[email protected];"
> []
> [[email protected] me.com]
> [*]
> [[email protected]]
> []

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 .


Okay guys!

Here's my answer!

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

Why this approach is "the best" for me?

Because of two reasons:

  1. You do not need to escape the delimiter;
  2. You will not have problem with blank spaces . The value will be properly separated in the array!

[]'s



IN='[email protected];[email protected];Charlie Brown <[email protected];!"#$%&/()[]{}*? 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

Sortie:

[email protected]
[email protected]
Charlie Brown <[email protected]
!"#$%&/()[]{}*? 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 ayez des IFS corrects. 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 "hard", c'est-à-dire avec des graduations uniques.

IFS doit être sauvegardé et restauré puisque Bash ne traite pas une assignation de la même manière qu'une commande. Une autre solution consiste à envelopper l'affectation dans une fonction et à appeler cette fonction avec un IFS modifié. Dans ce cas, la sauvegarde / restauration séparée de l'IFS n'est pas nécessaire. Merci pour "Bize" pour le signaler.







scripting