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?
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.
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.
t="one,two,three"
a=($(echo "$t" | tr ',' '\n'))
echo "${a[2]}"
Imprime trois
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
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
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
##
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?
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
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 :-)
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
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
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.
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
Utilisez ceci:
countries='Paris, France, Europe'
OIFS="$IFS"
IFS=', ' array=($countries)
IFS="$OIFS"
#${array[1]} == Paris
#${array[2]} == France
#${array[3]} == Europe