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

82

Tengo una variable como esta:

words="这是一条狗。"

Quiero hacer un bucle en cada uno de los personajes, uno a la vez, por ejemplo, en primer lugar character="这", a continuación character="是", character="一", etc.

La única forma que conozco es generar cada carácter en una línea separada en un archivo y luego usarlo while read line, pero esto parece muy ineficiente.

  • ¿Cómo puedo procesar cada carácter de una cadena a través de un bucle for?
Pueblo
fuente
3
Vale la pena mencionar que vemos muchas preguntas de novatos en las que el OP piensa que esto es lo que quieren hacer. Muy a menudo, es posible una solución mejor que no requiera que cada carácter se procese individualmente. Esto se conoce como un problema XY y la solución adecuada es explicar lo que realmente desea lograr en su pregunta, no solo cómo ejecutar los pasos que cree que lo ayudarán a llegar allí.
tripleee

Respuestas:

45

Con sedel dashcaparazón de LANG=en_US.UTF-8, Tengo las siguientes funcione bien:

$ 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

editado para texto de muestra traducir 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
Rony
fuente
4
Buen esfuerzo en UTF-8. No lo necesitaba, pero obtienes mi voto a favor de todos modos.
Jordania
+1 Puede usar el bucle for en la cadena resultante de sed.
Tyzoid
233

Puede utilizar un forbucle de estilo C :

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

${#foo}se expande a la longitud de foo. ${foo:$i:1}se expande a la subcadena comenzando en la posición $ide longitud 1.

chepner
fuente
¿Por qué necesita dos conjuntos de corchetes alrededor de la declaración for para que funcione?
tgun926
Esa es la sintaxis que bashrequiere.
chepner
3
Sé que esto es antiguo, pero los dos paréntesis son necesarios porque permiten operaciones aritméticas. Ver aquí => tldp.org/LDP/abs/html/dblparens.html
Hannibal
8
@Hannibal Solo quería señalar que este uso particular de paréntesis dobles es en realidad la construcción de bash: for (( _expr_ ; _expr_ ; _expr_ )) ; do _command_ ; doney no es lo mismo que $ (( expr )) ni (( expr )). En las tres construcciones de bash, expr se trata de la misma manera y $ (( expr )) también es POSIX.
nabin-info
1
@codeforester Eso no tiene nada que ver con matrices; es solo una de las muchas expresiones bashque se evalúan en un contexto aritmético.
chepner
36

${#var} devuelve la longitud de var

${var:pos:N}devuelve N caracteres en posadelante

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
Tiago Peczenyj
fuente
1
¿qué pasa con los espacios en blanco?
Leandro
¿Qué pasa con los espacios en blanco? Un carácter de espacio en blanco es un carácter y se repite sobre todos los caracteres. (Aunque usted debe tener cuidado de usar comillas dobles alrededor de cualquier variable o una cadena que contiene espacios en blanco significativa De manera más general, siempre hay que indicar todo lo menos. Usted sabe lo que está haciendo. )
tripleee
23

Me sorprende que nadie haya mencionado la bashsolución obvia utilizando solo whiley read.

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

Tenga en cuenta el uso de echo -npara evitar el salto de línea extraño al final. printfes otra buena opción y puede ser más adecuada para sus necesidades particulares. Si desea ignorar los espacios en blanco, reemplace "$words"con "${words// /}".

Otra opción es fold. Sin embargo, tenga en cuenta que nunca debe introducirse en 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 beneficio principal de usar el foldcomando externo (del paquete coreutils ) sería la brevedad. Puede alimentar su salida a otro comando como xargs(parte del paquete findutils ) de la siguiente manera:

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

Querrá reemplazar el echocomando utilizado en el ejemplo anterior con el comando que le gustaría ejecutar contra cada personaje. Tenga en cuenta que xargsdescartará los espacios en blanco de forma predeterminada. Puede utilizar -d '\n'para deshabilitar ese comportamiento.


Internacionalización

Acabo de probar foldcon algunos de los caracteres asiáticos y me di cuenta de que no es compatible con Unicode. Entonces, aunque está bien para las necesidades de ASCII, no funcionará para todos. En ese caso existen algunas alternativas.

Probablemente lo reemplazaría fold -w1con una matriz awk:

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

O el grepcomando mencionado en otra respuesta:

grep -o .


Actuación

FYI, comparé las 3 opciones mencionadas anteriormente. Los dos primeros fueron rápidos, casi atados, con el bucle de plegado un poco más rápido que el bucle while. Como xargsera de esperar, 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
Seis
fuente
characterestá vacío para espacios en blanco con la while readsolución simple , lo que puede ser problemático si se deben distinguir diferentes tipos de espacios en blanco.
pkfm
Buena solucion. Descubrí que era necesario cambiar read -n1a read -N1para manejar los caracteres de espacio correctamente.
Nielsen
16

Creo que todavía no existe una solución ideal que preserve correctamente todos los caracteres de espacio en blanco y sea lo suficientemente rápida, así que publicaré mi respuesta. El uso ${foo:$i:1}funciona, pero es muy lento, lo que se nota especialmente con cuerdas 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 pestañas. Hacerlo en la misma línea readsignifica que no afectará a otros comandos de shell.
  • -r- Significa "raw", lo que evita que se readtrate \al final de la línea como un carácter de concatenación de línea especial.
  • -d ''- Pasar una cadena vacía como delimitador evita que se eliminen los readcaracteres de nueva línea. En realidad, significa que el byte nulo se utiliza como delimitador. -d ''es igual a -d $'\0'.
  • -n 1 - Significa que se leerá un carácter a la vez.
  • printf %s "$string"- Usar en printflugar de echo -nes más seguro, porque echotrata -ny -ecomo opciones. Si pasa "-e" como una cadena, echono imprimirá nada.
  • < <(...)- Pasar cadena al bucle mediante sustitución de procesos. Si usa here-strings en su lugar ( done <<< "$string"), se agrega un carácter de nueva línea adicional al final. Además, pasar una cadena a través de una tubería ( printf %s "$string" | while ...) haría que el bucle se ejecutara en una subcapa, lo que significa que todas las operaciones de variables son locales dentro del bucle.

Ahora, probemos el rendimiento con una cuerda enorme. Usé el siguiente archivo como fuente:
https://www.kernel.org/doc/Documentation/kbuild/makefiles.txt
Se llamó al siguiente script a través del timecomando:

#!/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.
A continuación, reemplacé el bucle con uno que usa la 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 variar en diferentes sistemas, pero la imagen general debería ser similar.

Thunderbeef
fuente
13

Solo probé esto con cadenas ascii, 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
William Pursell
fuente
8

El bucle de estilo C en la respuesta de @ chepner está en la función de shell update_terminal_cwd, y la grep -o .solución es inteligente, pero me sorprendió no ver una solución usando seq. Aquí está el mío:

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

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

for char in `echo "这是一条狗。" | fold -w1`; do
    echo $char
done
sebix
fuente
1
#!/bin/bash

word=$(echo 'Your Message' |fold -w 1)

for letter in ${word} ; do echo "${letter} is a letter"; done

Aquí está el resultado:

Y es una letra o es una letra u es una letra r es una letra M es una letra e es una letra s es una letra s es una letra a es una letra g es una letra e es una letra

usuario13765771
fuente
0

Otro enfoque, si no le importa que se ignoren los espacios en blanco:

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

fuente
0

Otra forma es:

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

Comparto mi solución:

read word

for char in $(grep -o . <<<"$word") ; do
    echo $char
done
Dani Ballesteros
fuente
Esto tiene muchos errores: intente con una cadena que contenga un *, obtendrá archivos en el directorio actual.
Charles Duffy
-3
TEXT="hello world"
for i in {1..${#TEXT}}; do
   echo ${TEXT[i]}
done

donde {1..N}es un rango inclusivo

${#TEXT} es un número de letras en una cadena

${TEXT[i]} - puede obtener char de una cadena como un elemento de una matriz

Dmitri Emeliov
fuente
5
Shellcheck informa "Bash no admite variables en expansiones de rango de llaves" Por lo tanto, esto no funcionará en Bash
Bren
@Bren Me parece un error.
Sapphire_Brick