it-swarm.com.de

String in ein Array in Bash aufteilen

In einem Bash-Skript möchte ich eine Linie in Stücke teilen und sie in einem Array speichern.

Die Linie:

Paris, France, Europe

Ich möchte sie in einem Array wie folgt haben:

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

Ich möchte einfachen Code verwenden, die Geschwindigkeit des Befehls spielt keine Rolle. Wie kann ich es tun?

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

Beachten Sie, dass die Zeichen in $IFS einzeln als Trennzeichen behandelt werden, sodass die Felder in diesem Fall durch entweder ein Komma oder ein Leerzeichen und nicht durch die Reihenfolge der beiden Zeichen getrennt werden können. Interessanterweise werden jedoch keine leeren Felder erstellt, wenn ein Komma-Leerzeichen in der Eingabe angezeigt wird, da das Leerzeichen speziell behandelt wird.

Zugriff auf ein einzelnes Element:

echo "${array[0]}"

Um die Elemente zu durchlaufen:

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

Um sowohl den Index als auch den Wert zu erhalten:

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

Das letzte Beispiel ist nützlich, da Bash-Arrays spärlich sind. Sie können also ein Element löschen oder ein Element hinzufügen. Dann sind die Indizes nicht zusammenhängend.

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

So erhalten Sie die Anzahl der Elemente in einem Array:

echo "${#array[@]}"

Wie oben erwähnt, können Arrays spärlich sein, sodass Sie nicht die Länge verwenden sollten, um das letzte Element zu erhalten. So können Sie in Bash 4.2 und höher:

echo "${array[-1]}"

in jeder Version von Bash (von irgendwo nach 2.05b):

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

Größere negative Offsets werden weiter vom Ende des Arrays entfernt. Beachten Sie das Leerzeichen vor dem Minuszeichen im älteren Formular. Es ist notwendig.

895

Hier ist ein Weg, ohne IFS einzustellen:

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

Die Idee verwendet den String-Ersatz:

${string//substring/replacement}

um alle Übereinstimmungen von $ substring durch Leerzeichen zu ersetzen und dann die ersetzte Zeichenfolge zum Initialisieren eines Arrays zu verwenden:

(element1 element2 ... elementN)

Hinweis: Diese Antwort verwendet den Operator split + glob . Um die Erweiterung einiger Zeichen (z. B. *) zu verhindern, empfiehlt es sich daher, das Globbing für dieses Skript anzuhalten.

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

Druckt drei

59
Jmoney38

Manchmal kam es mir vor, dass die in der akzeptierten Antwort beschriebene Methode nicht funktionierte, insbesondere wenn das Trennzeichen ein Wagenrücklauf ist.
In diesen Fällen habe ich auf folgende Weise gelöst:

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

Die akzeptierte Antwort gilt für Werte in einer Zeile.
Wenn die Variable mehrere Zeilen hat:

string='first line
        second line
        third line'

Wir brauchen einen ganz anderen Befehl, um alle Zeilen zu bekommen:

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

Oder die viel einfachere Bash Readarray:

readarray -t lines <<<"$string"

Das Drucken aller Zeilen ist mit der Funktion printf sehr einfach:

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

>[first line]
>[        second line]
>[        third line]
27
user2350426

Dies ähnelt dem Ansatz von Jmoney38, verwendet jedoch sed:

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

Drucke 1

4
ssanch

Der Schlüssel zum Aufteilen Ihrer Zeichenfolge in ein Array ist das aus mehreren Zeichen bestehende Trennzeichen von ", ". Jede Lösung, die IFS für Trennzeichen mit mehreren Zeichen verwendet, ist grundsätzlich falsch, da IFS eine Menge dieser Zeichen und keine Zeichenfolge ist. 

Wenn Sie IFS=", " zuweisen, wird die Zeichenfolge in EITHER "," OR " " oder in einer beliebigen Kombination von Zeichen unterbrochen, die keine genaue Darstellung des aus zwei Zeichen bestehenden Begrenzungszeichens von ", " darstellt. 

Sie können awk oder sed verwenden, um die Zeichenfolge mit Prozessersetzung zu teilen:

#!/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

Es ist effizienter, einen Regex direkt in Bash zu verwenden:

#!/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...

Mit dem zweiten Formular gibt es keine Unter-Shell und es wird von Natur aus schneller.


Edit by bgoldst: Hier sind einige Benchmarks, die meine readarray-Lösung mit der Regex-Lösung von dawg vergleichen, und ich habe auch die read-Lösung für dieses Problem hinzugefügt (Hinweis: Ich habe die Regex-Lösung etwas modifiziert, um mit meiner Lösung mehr Harmonie zu erzielen) ( Siehe auch meine Kommentare unter dem Beitrag):

## 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=("[email protected]");
    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

Pure-Bash-Lösung mit mehreren Zeichen als Trennzeichen.

Wie andere in diesem Thread hervorgehoben haben, gab die Frage des OP ein Beispiel für eine durch Kommas getrennte Zeichenfolge, die in ein Array geparst werden sollte, gab jedoch nicht an, ob er/sie sich nur für Kommas als Trennzeichen, für einzelne Zeichen oder für mehrere Zeichen interessierte Trennzeichen. 

Da Google diese Antwort tendenziell an oder in der Nähe der Spitze der Suchergebnisse einordnet, wollte ich den Lesern eine starke Antwort auf die Frage nach mehreren Trennzeichen geben, da dies auch in mindestens einer Antwort erwähnt wird.

Wenn Sie auf der Suche nach einer Lösung für ein Trennzeichenproblem mit mehreren Zeichen sind, schlage ich vor, den Beitrag von Mallikarjun M zu überprüfen, insbesondere die Antwort von gniourf_gniourf , Die diese elegante reine BASH-Lösung bereitstellt mit Parametererweiterung:

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

Link zu zitierter Kommentar/referenzierter Beitrag

Link zu der zitierten Frage: Wie kann ich eine Zeichenfolge in einem Trennzeichen mit mehreren Zeichen in Bash teilen?

1
MrPotatoHead

Versuche dies

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

Es ist einfach. Wenn Sie möchten, können Sie auch eine Deklaration hinzufügen (und auch die Kommas entfernen):

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

Das IFS wird hinzugefügt, um das obige rückgängig zu machen, funktioniert jedoch auch in einer frischen Bash-Instanz

1
Geoff Lee

Das funktioniert für mich unter OSX:

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

Wenn Ihre Zeichenfolge ein anderes Trennzeichen hat, ersetzen Sie diese zunächst durch Leerzeichen:

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

Einfach :-)

0
To Kra

UPDATE: Tun Sie dies nicht, wegen Problemen mit eval.

Mit etwas weniger Zeremonie:

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

z.B.

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

Ich bin auf diesen Beitrag gestoßen, als ich eine Eingabe wie: Word1, Word2, ... analysieren wollte.

nichts davon hat mir geholfen. gelöst mit awk. Wenn es jemandem hilft: 

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

Eine andere Möglichkeit, dies zu tun, ohne IFS zu ändern:

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

Anstatt das IFS so zu ändern, dass es zu unserem gewünschten Trennzeichen passt, können wir alle Vorkommen unseres gewünschten Trennzeichens ", " durch den Inhalt von $IFS über "${string//, /$IFS}" ersetzen.  

Vielleicht wird dies für sehr große Saiten jedoch langsam sein?

Dies basiert auf Dennis Williamsons Antwort.

0
sel-en-ium

Hier ist mein Hack!

Das Aufteilen von Strings durch Strings ist mit Bash ziemlich langweilig. Was passiert, ist, dass wir nur begrenzte Ansätze haben, die nur in wenigen Fällen funktionieren (getrennt durch ";", "/", "." Usw.), oder dass wir eine Reihe von Nebenwirkungen in den Ausgaben haben.

Die unten beschriebene Vorgehensweise hat eine Reihe von Manövern erfordert, aber ich glaube, dass dies für die meisten unserer Bedürfnisse funktionieren wird!

#!/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

Benutze das:

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

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