Lectura caracter por caracter con bash read

8

He estado tratando de usar bash para leer un archivo carácter por carácter.

Después de mucho ensayo y error, descubrí que esto funciona:

exec 4<file.txt 
declare -i n
while read -r ch <&4; 
     n=0
     while [ ! $n -eq ${#ch} ]
           do  echo -n "${ch:$n:1}"
               (( n++ ))
          done
     echo "" 
     done

Es decir, puedo leerlo línea por línea y luego recorrer cada línea char por char.

Antes de hacer esto, lo intenté, exec 4<file.txt && while read -r -n1 ch <&4; do; echo -n "$ch"; done pero omitiría todos los espacios en blanco en el archivo .

¿Podría explicar por qué? ¿Hay alguna manera de hacer que la segunda estrategia (es decir, leer char por char con bash read) funcione?

PSkocik
fuente
44
Establecido IFSen nada para que los espacios en blanco sobrevivan a la división de palabras.
manatwork el
Intenté eso con IFS = '', pero supongo que tenía que ser solo IFS =. ¡Gracias!
PSkocik

Respuestas:

12

Debe eliminar los caracteres de espacio en blanco del $IFSparámetro para readdejar de omitir los caracteres iniciales y finales (con -n1el carácter de espacio en blanco si alguno sería tanto inicial como final, por lo que se omite):

while IFS= read -rn1 a; do printf %s "$a"; done

Pero incluso entonces, bash readomitirá los caracteres de nueva línea, con los que puede trabajar:

while IFS= read -rn1 a; do printf %s "${a:-$'\n'}"; done

Aunque podría usar IFS= read -d '' -rn1en su lugar o incluso mejor IFS= read -N1(agregado en 4.1, copiado de ksh93(agregado o)), que es el comando para leer un carácter.

Tenga en cuenta que bash readno puede hacer frente a los caracteres NUL. Y ksh93 tiene los mismos problemas que bash.

Con zsh:

while read -ku0 a; do print -rn -- "$a"; done

(zsh puede hacer frente a caracteres NUL).

Tenga en cuenta que esos read -k/n/Nleen una cantidad de caracteres , no bytes . Entonces, para los caracteres multibyte, pueden tener que leer varios bytes hasta que se lea un carácter completo. Si la entrada contiene caracteres no válidos, puede terminar con una variable que contiene una secuencia de bytes que no forma caracteres válidos y que el shell puede contar como varios caracteres . Por ejemplo, en un entorno local UTF-8:

$ printf '\375\200\200\200\200ABC' | bash -c '
    IFS= read  -rN1 a; echo "${#a}"'
6

Eso \375introduciría un carácter UTF-8 de 6 bytes. Sin embargo, el sexto ( A) anterior no es válido para un carácter UTF-8. Todavía terminas con \375\200\200\200\200Ain $a, que bashcuenta como 6 caracteres, aunque los primeros 5 no son realmente caracteres, solo 5 bytes no forman parte de ningún carácter.

Stéphane Chazelas
fuente
Gracias. Simple y hermoso Realmente intenté algo con este fin (modificar la variable IFS), pero no funcionó para mí, así que terminé con esa mezcla mía (juego innecesario con descriptores de archivos, etc.).
PSkocik
1
Curiosamente, parece que el uso read -rN1resuelve el problema de la nueva línea y, por lo tanto, elimina la necesidad de proporcionar una nueva línea por defecto al imprimir $a.
krb686
Solo FTR estoy leyendo 4118 línea 20 MB archivo. Usar read -n1(char por char) toma 4 min 51 segundos y calienta la computadora portátil a 90 grados. El uso read -r(línea por línea) toma 1.3 segundos y la computadora portátil se mantiene a 54 grados con dos ventiladores silenciosos.
WinEunuuchs2Unix
2

Este es un ejemplo simple usando cut, un forbucle & wc:

bytes=$(wc -c < /etc/passwd)
file=$(</etc/passwd)

for ((i=0; i<bytes; i++)); do
    echo $file | cut -c $i
done

BESO no es así?

Gilles Quenot
fuente
Si eso es KISS, ¿cuál es una bashsolución pura file="$(</etc/passwd)"; bytes="${#file}"; for ((i=0;i<bytes;i++)); do echo "${file:i:1}"; done?
manatwork
Gracias a los dos. Sí, si tengo que recurrir a obtener esos caracteres de las líneas, también podría obtenerlos de todo el archivo. Sin embargo, creo que la solución de sch es la más BESADA.
PSkocik
@manatwork Esa es una solución buena y simple. Aun así, me parece que la respuesta anterior usando un ciclo de lectura es bastante más rápida por alguna razón. ¿Quizás las subcadenas en bash son bastante lentas?
krb686
@ krb686, en realidad todo bash"Es demasiado grande y demasiado lento". de acuerdo con la sección BUGS de su página de manual. Pero aun así, es más rápido cortar una cadena en la memoria que leer un archivo una y otra vez para cada carácter. Al menos en mi máquina: pastebin.com/zH5trQQs
manatwork