¿Cómo realizar un bucle for en cada carácter en una cadena en Bash?

Tengo una variable como esta:

words="这是一条狗。" 

Quiero hacer un ciclo para cada uno de los caracteres, uno a la vez, por ejemplo, primer character="这" , luego character="是" , character="一" , etc.

La única forma que conozco es dar salida a cada carácter para separar la línea en un archivo, luego usarlo while read line , pero esto parece muy ineficiente.

  • ¿Cómo puedo procesar cada carácter en una cadena a través de un ciclo for?

Con sed en el shell de LANG=en_US.UTF-8 , obtuve los siguientes resultados:

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

y

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

Por lo tanto, la salida se puede enlazar con while read ... ; do ... ; done while read ... ; do ... ; done

editado para texto de muestra traducido al inglés:

 "你好嗎 新年好。全型句號" 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 

Puede usar un estilo C for bucle:

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

${#foo} expande a la longitud de foo . ${foo:$i:1} expande a la subcadena que comienza en la posición $i de longitud 1.

${#var} devuelve la longitud de var

${var:pos:N} devuelve N caracteres desde pos adelante

Ejemplos:

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

por lo que es fácil de iterar.

de otra manera:

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

o

 $ 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 

Estoy sorprendido de que nadie haya mencionado la solución de bash obvio utilizando solo while y read .

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

Tenga en cuenta el uso de echo -n para evitar la nueva línea extraña al final. printf es otra buena opción y puede ser más adecuada para sus necesidades particulares. Si quiere ignorar el espacio en blanco, reemplace "$words" por "${words// /}" .

Otra opción es fold . Sin embargo, tenga en cuenta que nunca se debe alimentar a un bucle for. Más bien, use un ciclo while de la siguiente manera:

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

El principal beneficio de usar el comando fold externo (del paquete coreutils ) sería breve. Puede enviar su salida a otro comando como xargs (parte del paquete findutils ) de la siguiente manera:

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

Deberá reemplazar el comando de echo utilizado en el ejemplo anterior con el comando que desea ejecutar contra cada carácter. Tenga en cuenta que xargs descartará espacios en blanco de forma predeterminada. Puede usar -d '\n' para desactivar ese comportamiento.


Internacionalización

Acabo de probar fold con algunos de los personajes asiáticos y me di cuenta de que no tiene soporte Unicode. Entonces, aunque está bien para las necesidades de ASCII, no funcionará para todos. En ese caso hay algunas alternativas.

Probablemente reemplazaría fold -w1 con una matriz awk:

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

O el comando grep mencionado en otra respuesta:

 grep -o . 

Actuación

FYI, comparé las 3 opciones mencionadas. Las dos primeras fueron rápidas, casi vinculadas, con el doblez ligeramente más rápido que el ciclo while. Como era de esperar, xargs fue el más lento ... 75 veces más lento.

Aquí está el código de prueba (abreviado):

 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 

Aquí están los resultados:

 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 

Solo he probado esto con ascii strings, pero podrías hacer algo como:

 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 

Creo que todavía no existe una solución ideal que preserve correctamente todos los caracteres de espacios en blanco y sea lo suficientemente rápida, por lo que publicaré mi respuesta. El uso de ${foo:$i:1} funciona, pero es muy lento, lo que es especialmente notable con cadenas grandes, como mostraré a continuación.

Mi idea es una expansión de un método propuesto por Six , que implica read -n1 , con algunos cambios para mantener todos los caracteres y funcionar correctamente para cualquier cadena:

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

Cómo funciona:

  • IFS='' - La redefinición del separador de campo interno a una cadena vacía evita la eliminación de espacios y tabs. Hacerlo en la misma línea que la read significa que no afectará a otros comandos de la carcasa.
  • -r - Significa "en bruto", lo que impide que la read trate \ al final de la línea como un carácter de concatenación de línea especial.
  • -d '' - Al pasar una cadena vacía como un delimitador se evita que la read elimine los caracteres de nueva línea. En realidad significa que el byte nulo se usa como un delimitador. -d '' es igual a -d $'\0' .
  • -n 1 - Significa que se leerá un carácter a la vez.
  • printf %s "$string" - Usar printf lugar de echo -n es más seguro, porque echo trata las opciones -n y -e como. Si pasa "-e" como una cadena, echo no imprimirá nada.
  • < <(...) - Pasar la cadena al ciclo usando la sustitución del proceso. Si utiliza aquí-cadenas en su lugar ( done <<< "$string" ), un carácter de línea nueva adicional se agrega al final. Además, pasar una cuerda a través de una tubería ( printf %s "$string" | while ... ) haría que el ciclo se ejecute en una subcadena, lo que significa que todas las operaciones variables son locales dentro del ciclo.

Ahora, probemos el rendimiento con una cadena enorme. Utilicé el siguiente archivo como fuente:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
El siguiente script fue llamado a través del comando de 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") 

Y el resultado es:

 $ time ./test.sh real 0m1.161s user 0m1.036s sys 0m0.116s 

Como podemos ver, es bastante rápido.
Luego, reemplacé el bucle con uno que usa expansión de parámetros:

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

El resultado muestra exactamente qué tan mala es la pérdida de rendimiento:

 $ time ./test.sh real 2m38.540s user 2m34.916s sys 0m3.576s 

Los números exactos pueden ser muy diferentes en los sistemas, pero la imagen general debería ser similar.

También es posible dividir la cadena en una matriz de caracteres usando fold y luego iterar sobre esta matriz:

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

Otro enfoque, si no te importa que se ignore el espacio en blanco:

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

Otra forma es:

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

Comparto mi solución:

 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 

donde {1..N} es un rango inclusivo

${#TEXT} es una cantidad de letras en una cadena

${TEXT[i]} – puede obtener un carácter de cadena como un elemento de una matriz