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.
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
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.
${#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
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.
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 .
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
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
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.
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
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
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
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