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

J’ai une variable comme celle-ci:

words="这是一条狗。" 

Je veux faire une boucle for sur chacun des caractères, un à la fois, par exemple premier character="这" , puis character="是" , character="一" , etc.

La seule façon que je connaisse est de sortir chaque caractère pour séparer les lignes dans un fichier, puis d’utiliser les while read line , mais cela semble très inefficace.

  • Comment puis-je traiter chaque caractère d’une chaîne par une boucle for?

Avec sed sur le dash de dash de LANG=en_US.UTF-8 , les opérations suivantes ont été effectuées:

 $ 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 while read ... ; do ... ; done

édité pour exemple de texte traduire 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 un style C for boucle:

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

${#foo} développe sur la longueur de foo . ${foo:$i:1} développe en 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 la pos

Exemples:

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

il est donc facile d’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 de base évidente en utilisant seulement le while et read .

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

Notez l'utilisation de echo -n pour éviter la nouvelle ligne à la fin. printf est une autre bonne option et peut convenir davantage à vos besoins particuliers. Si vous voulez ignorer les espaces, remplacez "$words" par "${words// /}" .

Une autre option est le fold . Veuillez noter toutefois qu'il ne faut jamais l'introduire 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 par une autre commande telle que xargs (faisant partie du paquet 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 sur chaque caractère. Notez que xargs supprimera les espaces par défaut. Vous pouvez utiliser -d '\n' pour désactiver ce comportement.


Internationalisation

Je viens de tester fold avec certains des caractères asiatiques et je me suis rendu compte qu'il n'y avait pas de support Unicode. Donc, même si cela convient aux besoins 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 info, j'ai comparé les 3 options susmentionnées. Les deux premiers ont été rapides, presque liés, avec la boucle de pliage légèrement plus rapide que la boucle while. Sans surprise, xargs était le plus lent ... 75x plus lent.

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

 words=$(python -c 'from ssortingng 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 l’ai seulement testé avec des chaînes ASCII, mais vous pourriez faire quelque chose comme:

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

Je crois qu’il n’existe toujours pas de solution idéale pour préserver correctement tous les caractères d’espace et qu’elle est suffisamment 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 une extension d’une méthode proposée par Six , qui consiste à read -n1 , avec quelques modifications pour conserver tous les caractères et fonctionner correctement pour n’importe quelle chaîne:

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

Comment ça marche:

  • IFS='' - Redéfinir le séparateur de champs internes en une chaîne vide empêche la suppression des espaces et des tabulations. Le faire sur une même ligne en read signifie que cela n'affectera pas les autres commandes du shell.
  • -r - Signifie "raw", ce qui empêche de read traitant \ à la fin de la ligne comme un caractère de concaténation de ligne spécial.
  • -d '' - Passer une chaîne vide en tant que délimiteur empêche de read caractères de nouvelle ligne. En fait, cela signifie qu'un 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 "$ssortingng" - L'utilisation de printf place de echo -n est plus sûre, car echo traite -n et -e comme options. Si vous passez "-e" en tant que chaîne, echo n'imprimera rien.
  • < <(...) - Passer une chaîne à la boucle en utilisant la substitution de processus. Si vous utilisez ici-ssortingngs ( done <<< "$string" ), un caractère de nouvelle ligne supplémentaire est ajouté à la fin. En outre, faire passer une chaîne à travers un tube ( printf %s "$ssortingng" | while ... ) rendrait la boucle exécutée 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é via la commande time :

 #!/bin/bash # Saving contents of the file into a variable named `ssortingng'. # 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 voir, c'est assez rapide.
Ensuite, j'ai remplacé la boucle par une boucle qui utilise l'extension des parameters:

 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 selon les systèmes, mais l’ensemble doit être similaire.

Il est également possible de diviser la chaîne en un tableau de caractères en utilisant fold et ensuite itérer sur ce tableau:

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

Une autre approche, si vous ne vous souciez pas des espaces blancs ignorés:

 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 

Je partage ma solution:

 read word for char in $(grep -o . <<<"$word") ; do echo $char done 
 TEXT="hello world" for i in {1..${#TEXT}}; do echo ${TEXT[i]} done 

{1..N} est une plage inclusive

${#TEXT} est un nombre de lettres dans une chaîne

${TEXT[i]} – vous pouvez obtenir un caractère de chaîne comme un élément d’un tableau