web-dev-qa-db-fra.com

Fractionner la chaîne en un tableau dans Bash

Dans un script Bash, je voudrais diviser une ligne en morceaux et les stocker dans un tableau.

La ligne:

Paris, France, Europe

Je voudrais les avoir dans un tableau comme celui-ci:

array[0] = Paris
array[1] = France
array[2] = Europe

J'aimerais utiliser un code simple, la vitesse de la commande n'a pas d'importance. Comment puis-je le faire?

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

Notez que les caractères dans $IFS sont traités individuellement comme des 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. Chose intéressante, les champs vides ne sont pas créés lorsque des virgules apparaissent dans l’entrée, car l’espace est spécialement traité.

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

echo "${array[0]}"

Pour parcourir les éléments:

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

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

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

Le dernier exemple est utile car les tableaux de Bash sont rares. En d'autres termes, vous pouvez supprimer un élément ou ajouter un élément, puis les index ne sont pas contigus.

unset "array[1]"
array[42]=Earth

Pour obtenir le nombre d'éléments dans un tableau:

echo "${#array[@]}"

Comme mentionné ci-dessus, les tableaux peuvent être rares, vous ne devez donc pas utiliser la longueur pour obtenir le dernier élément. Voici comment vous pouvez utiliser Bash 4.2 et versions ultérieures:

echo "${array[-1]}"

dans n'importe quelle version de Bash (à partir de 2.05b):

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

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

895

Voici un moyen sans configurer IFS:

string="1:2:3:4:5"
set -f                      # avoid globbing (expansion of *).
array=(${string//:/ })
for i in "${!array[@]}"
do
    echo "$i=>${array[i]}"
done

L'idée est d'utiliser le remplacement de chaîne:

${string//substring/replacement}

pour remplacer toutes les correspondances de $ sous-chaîne par des espaces, puis en utilisant la chaîne substituée pour initialiser un tableau:

(element1 element2 ... elementN)

Remarque: cette réponse utilise l’opérateur split + glob . Par conséquent, pour empêcher le développement de certains caractères (tels que *), il est conseillé de suspendre la lecture pour ce script.

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

Imprime trois

59
Jmoney38

Il m'est parfois arrivé que la méthode décrite dans la réponse acceptée ne fonctionne pas, surtout si le séparateur est un retour à la ligne.
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
31
Luca Borrione

Ceci est similaire à l'approche de Jmoney38, mais en utilisant sed:

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

Impressions 1

4
ssanch

La clé permettant de scinder votre chaîne en un tableau est le délimiteur multi-caractères de ", ". Toute solution utilisant IFS pour les délimiteurs multi-caractères est intrinsèquement fausse, car IFS est un ensemble de ces caractères et non une chaîne. 

Si vous affectez IFS=", ", la chaîne sera rompue par EITHER "," OR " " ou toute combinaison de ceux-ci, ce qui ne représente pas le délimiteur à deux caractères de ", "

Vous pouvez utiliser awk ou sed pour scinder la chaîne, avec substitution de processus:

#!/bin/bash

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

Il est plus efficace d’utiliser un regex directement dans Bash:

#!/bin/bash

str="Paris, France, Europe"

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

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

Avec le deuxième formulaire, il n'y a pas de sous-shell et ce sera intrinsèquement plus rapide.


Edit by bgoldst: Voici quelques points de repère comparant ma solution readarray à la solution regex de dawg, et j'ai également inclus la solution read pour le plaisir (remarque: j'ai légèrement modifié la solution regex pour une plus grande harmonie avec ma solution) ( voir aussi mes commentaires sous le post):

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

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

function testAll {
    local funcs=();
    local args=();
    local func='';
    local -i rc=-1;
    while [[ "$1" != ':' ]]; do
        func="$1";
        if [[ ! "$func" =~ ^[_a-zA-Z][_a-zA-Z0-9]*$ ]]; then
            echo "bad function name: $func" >&2;
            return 2;
        fi;
        funcs+=("$func");
        shift;
    done;
    shift;
    args=("$@");
    for func in "${funcs[@]}"; do
        echo -n "$func ";
        { time $func "${args[@]}" >/dev/null 2>&1; } 2>&1| tr '\n' '/';
        rc=${PIPESTATUS[0]}; if [[ $rc -ne 0 ]]; then echo "[$rc]"; else echo; fi;
    done| column -ts/;
}; ## end testAll()

function makeStringToSplit {
    local -i n=$1; ## number of fields
    if [[ $n -lt 0 ]]; then echo "bad field count: $n" >&2; return 2; fi;
    if [[ $n -eq 0 ]]; then
        echo;
    Elif [[ $n -eq 1 ]]; then
        echo 'first field';
    Elif [[ "$n" -eq 2 ]]; then
        echo 'first field, last field';
    else
        echo "first field, $(rep $[$1-2] 'mid field, ')last field";
    fi;
}; ## end makeStringToSplit()

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

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

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

Comme d'autres l'ont souligné dans ce fil de discussion, la question du PO donnait l'exemple d'une chaîne délimitée par des virgules à analyser dans un tableau, mais n'indiquait pas s'il s'intéressait uniquement aux délimiteurs de virgule, aux délimiteurs à un caractère ou aux caractères multiples. les délimiteurs. 

Étant donné que Google a tendance à classer cette réponse au sommet des résultats de recherche ou presque, je souhaitais fournir aux lecteurs une réponse claire à la question des délimiteurs multiples, car celle-ci est également mentionnée dans au moins une réponse.

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

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

Lien vers commentaire cité/article référencé

Lien vers la question citée: Comment diviser une chaîne sur un délimiteur multi-caractères en bash?

1
MrPotatoHead

Essaye ça

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

C'est simple. Si vous le souhaitez, vous pouvez également ajouter une déclaration (et également supprimer les virgules):

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

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

1
Geoff Lee

Cela fonctionne pour moi sur OSX:

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

Si votre chaîne a un délimiteur différent, remplacez-les d'abord par des espaces:

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

Simple :-)

0
To Kra

UPDATE: Ne faites pas cela, à cause de problèmes avec eval.

Avec un peu moins de cérémonie:

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

par exemple.

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

Je suis tombé sur ce post en cherchant à analyser une entrée du type: Word1, Word2, ...

aucune de ces réponses ne m'a aidé. résolu en utilisant awk. Si cela aide quelqu'un: 

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

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

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

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

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

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

0
sel-en-ium

Voici mon bidouillage!

Fractionner des chaînes par des chaînes est une chose assez ennuyeuse à faire avec bash. Ce qui se passe, c’est que nous avons des approches limitées qui ne fonctionnent que dans quelques cas (divisées par ";", "/", "." Et ainsi de suite) ou que nous avons divers effets secondaires sur les résultats.

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

#!/bin/bash

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

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

    Args:
        TARGET_P (str): Target string to "split".
        DELIMITER_P (Optional[str]): Delimiter used to "split". If not 
    informed the split will be done by spaces.

    Returns:
        F_SPLIT_R (array): Array with the provided string separated by the 
    informed delimiter.
    '

    F_SPLIT_R=()
    TARGET_P=$1
    DELIMITER_P=$2
    if [ -z "$DELIMITER_P" ] ; then
        DELIMITER_P=" "
    fi

    REMOVE_N=1
    if [ "$DELIMITER_P" == "\n" ] ; then
        REMOVE_N=0
    fi

    # NOTE: This was the only parameter that has been a problem so far! 
    # By Questor
    # [Ref.: https://unix.stackexchange.com/a/390732/61742]
    if [ "$DELIMITER_P" == "./" ] ; then
        DELIMITER_P="[.]/"
    fi

    if [ ${REMOVE_N} -eq 1 ] ; then

        # NOTE: Due to bash limitations we have some problems getting the 
        # output of a split by awk inside an array and so we need to use 
        # "line break" (\n) to succeed. Seen this, we remove the line breaks 
        # momentarily afterwards we reintegrate them. The problem is that if 
        # there is a line break in the "string" informed, this line break will 
        # be lost, that is, it is erroneously removed in the output! 
        # By Questor
        TARGET_P=$(awk 'BEGIN {RS="dn"} {gsub("\n", "3F2C417D448C46918289218B7337FCAF"); printf $0}' <<< "${TARGET_P}")

    fi

    # NOTE: The replace of "\n" by "3F2C417D448C46918289218B7337FCAF" results 
    # in more occurrences of "3F2C417D448C46918289218B7337FCAF" than the 
    # amount of "\n" that there was originally in the string (one more 
    # occurrence at the end of the string)! We can not explain the reason for 
    # this side effect. The line below corrects this problem! By Questor
    TARGET_P=${TARGET_P%????????????????????????????????}

    SPLIT_NOW=$(awk -F"$DELIMITER_P" '{for(i=1; i<=NF; i++){printf "%s\n", $i}}' <<< "${TARGET_P}")

    while IFS= read -r LINE_NOW ; do
        if [ ${REMOVE_N} -eq 1 ] ; then

            # NOTE: We use "'" to prevent blank lines with no other characters 
            # in the sequence being erroneously removed! We do not know the 
            # reason for this side effect! By Questor
            LN_NOW_WITH_N=$(awk 'BEGIN {RS="dn"} {gsub("3F2C417D448C46918289218B7337FCAF", "\n"); printf $0}' <<< "'${LINE_NOW}'")

            # NOTE: We use the commands below to revert the intervention made 
            # immediately above! By Questor
            LN_NOW_WITH_N=${LN_NOW_WITH_N%?}
            LN_NOW_WITH_N=${LN_NOW_WITH_N#?}

            F_SPLIT_R+=("$LN_NOW_WITH_N")
        else
            F_SPLIT_R+=("$LINE_NOW")
        fi
    done <<< "$SPLIT_NOW"
}

# --------------------------------------
# HOW TO USE
# ----------------

STRING_TO_SPLIT="
 * How do I list all databases and tables using psql?

\"
Sudo -u postgres /usr/pgsql-9.4/bin/psql -c \"\l\"
Sudo -u postgres /usr/pgsql-9.4/bin/psql <DB_NAME> -c \"\dt\"
\"

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

[Ref.: 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
Eduardo Lucio

Utilisez ceci:

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

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