Leitura de caractere por caractere com leitura do bash

8

Eu tenho tentado usar o bash para ler um arquivo caractere por caractere.

Após muitas tentativas e erros, descobri que isso 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

Ou seja, eu posso ler linha por linha e, em seguida, percorrer cada linha char por char.

Antes de fazer isso, eu tentei: exec 4<file.txt && while read -r -n1 ch <&4; do; echo -n "$ch"; done mas pularia todos os espaços em branco no arquivo .

Poderia explicar por que? Existe uma maneira de fazer a segunda estratégia (isto é, ler char por char com a leitura do bash) funcionar?

PSkocik
fonte
4
Defina IFScomo nada para que os espaços em branco sobrevivam à divisão de palavras.
manatwork
Tentei isso com IFS = '', mas acho que tinha que ser apenas IFS =. Obrigado!
PSKocik 01/10/12

Respostas:

12

Você precisa remover os caracteres de espaço em branco do $IFSparâmetro para readparar de ignorar caracteres iniciais e finais (com -n1, o caractere de espaço em branco, se houver algum, seria inicial e final, portanto, ignorado):

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

Mas, mesmo assim, o bash readignora os caracteres de nova linha, com os quais você pode contornar:

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

Embora você possa usar em IFS= read -d '' -rn1vez disso ou até melhor IFS= read -N1(adicionado em 4.1, copiado de ksh93(adicionado o))) que é o comando para ler um caractere.

Observe que o bash readnão pode lidar com caracteres NUL. E o ksh93 tem os mesmos problemas que o bash.

Com zsh:

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

(zsh pode lidar com caracteres NUL).

Observe que aqueles read -k/n/Nleem vários caracteres , não bytes . Portanto, para caracteres multibyte, eles podem precisar ler vários bytes até que um caractere completo seja lido. Se a entrada contiver caracteres inválidos, você poderá acabar com uma variável que contém uma sequência de bytes que não forma caracteres válidos e que o shell pode acabar contando como vários caracteres . Por exemplo, em um código de idioma UTF-8:

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

Isso \375introduziria um caractere UTF-8 de 6 bytes. No entanto, o sexto ( A) acima é inválido para um caractere UTF-8. Você ainda termina com \375\200\200\200\200Ain $a, que bashconta como 6 caracteres, embora os 5 primeiros não sejam realmente caracteres, apenas 5 bytes não fazem parte de nenhum caractere.

Stéphane Chazelas
fonte
Obrigado. Simples e bonito. Na verdade, eu tentei algo para esse fim (modificando a variável IFS), mas meio que não funcionou para mim, então acabei com essa mistura minha (reprodução desnecessária de descritores de arquivo etc.).
PSKocik 01/10/12
1
Curiosamente, parece que o uso read -rN1resolve o problema da nova linha e, assim, elimina a necessidade de fornecer uma nova linha como padrão durante a impressão $a.
krb686
Apenas FTR eu estou lendo 4118 linha 20 MB arquivo. O uso read -n1(caractere por caractere) leva 4 minutos e 51 segundos e aquece o laptop a 90 graus. O uso read -r(linha por linha) leva 1,3 segundos e o laptop fica a 54 graus com o ventilador duplo silencioso.
WinEunuuchs2Unix
2

Este é um exemplo simples usando cut, um forloop & wc:

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

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

BEIJO, não é?

Gilles Quenot
fonte
Se esse é o KISS, então o que é uma bashsolução pura file="$(</etc/passwd)"; bytes="${#file}"; for ((i=0;i<bytes;i++)); do echo "${file:i:1}"; done:?
Manatwork # 1/12
Graças a ambos. Sim, se eu tiver que recorrer à obtenção desses caracteres de linhas, é melhor obtê-los de todo o arquivo. Acho que a solução de sch é a mais KISS, no entanto.
PSKocik 01/10/12
@manatwork Essa é uma solução boa e simples. Mesmo assim, parece-me que a resposta acima, usando um loop de leitura, é um pouco mais rápida por algum motivo. Talvez substrings no bash sejam bastante lentos?
krb686
@ krb686, na verdade todo bash"É muito grande e muito lento". de acordo com a seção BUGS da sua página de manual. Mas, mesmo assim, ainda é mais rápido dividir uma sequência na memória do que ler um arquivo repetidamente para cada caractere. Pelo menos na minha máquina: pastebin.com/zH5trQQs
manatwork