web-dev-qa-db-fra.com

Comment effectuer une boucle for sur chaque caractère d'une chaîne dans Bash?

J'ai une variable comme celle-ci:

words="这是一条狗。"

Je souhaite créer une boucle for sur chacun des caractères, l'un après l'autre, par exemple. d'abord character="这", puis character="是", character="一", etc.

Le seul moyen que je connaisse consiste à écrire chaque caractère dans une ligne, puis à utiliser while read line, mais cela semble très inefficace.

  • Comment puis-je traiter chaque caractère d'une chaîne via une boucle for? 
54
Village

Avec sed sur dash Shell de LANG=en_US.UTF-8, les éléments suivants fonctionnent correctement:

$ echo "你好嗎 新年好。全型句號" | sed -e 's/\(.\)/\1\n/g'
你
好
嗎

新
年
好
。
全
型
句
號

et

$ echo "Hello world" | sed -e 's/\(.\)/\1\n/g'
H
e
l
l
o

w
o
r
l
d

Ainsi, la sortie peut être bouclée avec while read ... ; do ... ; done

édité pour un exemple de texte traduit en anglais:

"你好嗎 新年好。全型句號" is zh_TW.UTF-8 encoding for:
"你好嗎"     = How are you[ doing]
" "         = a normal space character
"新年好"     = Happy new year
"。全型空格" = a double-byte-sized full-stop followed by text description
36
Rony

Vous pouvez utiliser une boucle for de style C:

foo=string
for (( i=0; i<${#foo}; i++ )); do
  echo "${foo:$i:1}"
done

${#foo} se développe à la longueur de foo. ${foo:$i:1} se développe dans la sous-chaîne à partir de la position $i de longueur 1.

177
chepner

${#var} renvoie la longueur de var

${var:pos:N} renvoie N caractères à partir de pos

Exemples:

$ words="abc"
$ echo ${words:0:1}
a
$ echo ${words:1:1}
b
$ echo ${words:2:1}
c

il est donc facile à itérer.

autrement:

$ grep -o . <<< "abc"
a
b
c

ou

$ grep -o . <<< "abc" | while read letter;  do echo "my letter is $letter" ; done 

my letter is a
my letter is b
my letter is c
26
Tiago Peczenyj

Je suis surpris que personne n'ait mentionné la solution évidente bash utilisant uniquement while et read

while read -n1 character; do
    echo "$character"
done < <(echo -n "$words")

Notez l'utilisation de echo -n pour éviter les retours à la ligne superflus à la fin. printf est une autre bonne option et peut être plus adaptée à vos besoins particuliers. Si vous souhaitez ignorer les espaces, remplacez "$words" par "${words// /}".

Une autre option est fold. S'il vous plaît noter cependant qu'il ne devrait jamais être introduit dans une boucle for. Utilisez plutôt une boucle while comme suit:

while read char; do
    echo "$char"
done < <(fold -w1 <<<"$words")

Le principal avantage de l’utilisation de la commande fold externe (du package coreutils) serait la brièveté. Vous pouvez alimenter sa sortie avec une autre commande telle que xargs (composant du package findutils), comme suit:

fold -w1 <<<"$words" | xargs -I% -- echo %

Vous voudrez remplacer la commande echo utilisée dans l'exemple ci-dessus par la commande que vous souhaitez exécuter contre chaque caractère. Notez que xargs éliminera les espaces par défaut. Vous pouvez utiliser -d '\n' pour désactiver ce comportement.


Internationalisation

Je viens de tester fold avec certains caractères asiatiques et je me suis rendu compte qu'il ne prend pas en charge le format Unicode. Ainsi, même si cela convient aux besoins de ASCII, cela ne fonctionnera pas pour tout le monde. Dans ce cas, il existe des alternatives.

Je remplacerais probablement fold -w1 par un tableau awk:

awk 'BEGIN{FS=""} {for (i=1;i<=NF;i++) print $i}'

Ou la commande grep mentionnée dans une autre réponse:

grep -o .


Performance

Pour votre information, j'ai comparé les 3 options susmentionnées. Les deux premiers étaient rapides, presque liés, la boucle de pliage étant légèrement plus rapide que la boucle while. Sans surprise, xargs était la plus lente ... 75x plus lente.

Voici le code de test (abrégé):

words=$(python -c 'from string import ascii_letters as l; print(l * 100)')

testrunner(){
    for test in test_while_loop test_fold_loop test_fold_xargs test_awk_loop test_grep_loop; do
        echo "$test"
        (time for (( i=1; i<$((${1:-100} + 1)); i++ )); do "$test"; done >/dev/null) 2>&1 | sed '/^$/d'
        echo
    done
}

testrunner 100

Voici les résultats:

test_while_loop
real    0m5.821s
user    0m5.322s
sys     0m0.526s

test_fold_loop
real    0m6.051s
user    0m5.260s
sys     0m0.822s

test_fold_xargs
real    7m13.444s
user    0m24.531s
sys     6m44.704s

test_awk_loop
real    0m6.507s
user    0m5.858s
sys     0m0.788s

test_grep_loop
real    0m6.179s
user    0m5.409s
sys     0m0.921s
18
Six

Je n'ai testé cela qu'avec des chaînes ascii, mais vous pouvez faire quelque chose comme:

while test -n "$words"; do
   c=${words:0:1}     # Get the first character
   echo character is "'$c'"
   words=${words:1}   # trim the first character
done
12
William Pursell

Je crois qu’il n’existe toujours pas de solution idéale permettant de conserver correctement tous les caractères d’espace et d’être assez rapide. Je posterai donc ma réponse. Utiliser ${foo:$i:1} fonctionne, mais est très lent, ce qui est particulièrement visible avec les grandes chaînes, comme je le montrerai ci-dessous.

Mon idée est un développement d'une méthode proposée par Six, qui implique read -n1, avec quelques modifications pour conserver tous les caractères et fonctionner correctement pour toute chaîne:

while IFS='' read -r -d '' -n 1 char; do
        # do something with $char
done < <(printf %s "$string")

Comment ça marche:

  • IFS='' - La redéfinition du séparateur de champ interne en chaîne vide empêche la suppression d'espaces et de tabulations. Le faire sur la même ligne que read signifie que cela n’affectera pas les autres commandes du shell.
  • -r - signifie "raw", ce qui empêche read de traiter \ à la fin de la ligne comme un caractère spécial de concaténation de ligne.
  • -d '' - Le fait de passer une chaîne vide en tant que délimiteur empêche read de supprimer les caractères de nouvelle ligne. En réalité, cela signifie que l'octet nul est utilisé comme délimiteur. -d '' est égal à -d $'\0'.
  • -n 1 - signifie qu'un caractère à la fois sera lu.
  • printf %s "$string" - Utiliser printf au lieu de echo -n est plus sûr, car echo traite les options -n et -e. Si vous transmettez "-e" en tant que chaîne, echo n'imprimera rien.
  • < <(...) - Passage de chaîne dans la boucle en utilisant la substitution de processus. Si vous utilisez plutôt here-strings (done <<< "$string"), un caractère de nouvelle ligne supplémentaire est ajouté à la fin. En outre, le fait de passer une chaîne de caractères dans un tube (printf %s "$string" | while ...) ferait exécuter la boucle dans un sous-shell, ce qui signifie que toutes les opérations sur les variables sont locales dans la boucle.

Maintenant, testons les performances avec une énorme chaîne .J'ai utilisé le fichier suivant comme source:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
Le script suivant a été appelé par la commande time:

#!/bin/bash

# Saving contents of the file into a variable named `string'.
# This is for test purposes only. In real code, you should use
# `done < "filename"' construct if you wish to read from a file.
# Using `string="$(cat makefiles.txt)"' would strip trailing newlines.
IFS='' read -r -d '' string < makefiles.txt

while IFS='' read -r -d '' -n 1 char; do
        # remake the string by adding one character at a time
        new_string+="$char"
done < <(printf %s "$string")

# confirm that new string is identical to the original
diff -u makefiles.txt <(printf %s "$new_string")

Et le résultat est:

$ time ./test.sh

real    0m1.161s
user    0m1.036s
sys     0m0.116s

Comme on peut le constater, c'est assez rapide.
Ensuite, j'ai remplacé la boucle par une boucle utilisant le développement de paramètres:

for (( i=0 ; i<${#string}; i++ )); do
    new_string+="${string:$i:1}"
done

La sortie montre exactement à quel point la perte de performance est mauvaise:

$ time ./test.sh

real    2m38.540s
user    2m34.916s
sys     0m3.576s

Les chiffres exacts peuvent être très différents sur des systèmes différents, mais la vue d'ensemble devrait être similaire.

12
Thunderbeef

Il est également possible de scinder la chaîne en un tableau de caractères à l'aide de fold, puis d'itérer ce tableau:

for char in `echo "这是一条狗。" | fold -w1`; do
    echo $char
done
5
sebix

La boucle de style C dans la réponse de @ chepner se trouve dans la fonction Shell update_terminal_cwd, et la solution grep -o . est astucieuse, mais j'ai été surpris de ne pas voir de solution utilisant seq. Voilà le mien:

read Word
for i in $(seq 1 ${#Word}); do
  echo "${Word:i-1:1}"
done
1
De Novo

Une autre approche, si vous ne vous souciez pas de l’ignorance des espaces:

for char in $(sed -E s/'(.)'/'\1 '/g <<<"$your_string"); do
    # Handle $char here
done
0
anon

Une autre façon est:

Characters="TESTING"
index=1
while [ $index -le ${#Characters} ]
do
    echo ${Characters} | cut -c${index}-${index}
    index=$(expr $index + 1)
done
0
Javier Salas