Lecture caractère par caractère avec lecture bash

8

J'ai essayé d'utiliser bash pour lire un fichier caractère par caractère.

Après de nombreux essais et erreurs, j'ai découvert que cela fonctionne:

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

C'est-à-dire que je peux le lire ligne par ligne, puis parcourir chaque ligne char par char.

Avant de faire cela, j'avais essayé: exec 4<file.txt && while read -r -n1 ch <&4; do; echo -n "$ch"; done mais cela ignorerait tous les espaces dans le fichier .

Pourriez-vous expliquer pourquoi? Existe-t-il un moyen de faire fonctionner la deuxième stratégie (c'est-à-dire lire caractère par caractère avec la lecture de bash)?

PSkocik
la source
4
Mis IFSà rien pour que les espaces blancs survivent à la séparation des mots.
manatwork du
J'ai essayé avec IFS = '', mais je suppose que ce devait être juste IFS =. Merci!
PSkocik

Réponses:

12

Vous devez supprimer les caractères d'espacement du $IFSparamètre pour readarrêter de sauter les caractères de début et de fin (avec -n1, le caractère d'espacement s'il y en a serait à la fois de début et de fin, donc ignoré):

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

Mais même dans ce cas, bash readignorera les caractères de nouvelle ligne, avec lesquels vous pouvez contourner:

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

Bien que vous puissiez utiliser à la IFS= read -d '' -rn1place ou même mieux IFS= read -N1(ajouté en 4.1, copié depuis ksh93(ajouté en o)) qui est la commande pour lire un caractère.

Notez que bash ne readpeut pas gérer les caractères NUL. Et ksh93 a les mêmes problèmes que bash.

Avec zsh:

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

(zsh peut gérer les caractères NUL).

Notez que ceux-ci read -k/n/Nlisent un certain nombre de caractères , pas d' octets . Ainsi, pour les caractères multi-octets, ils peuvent avoir à lire plusieurs octets jusqu'à ce qu'un caractère complet soit lu. Si l'entrée contient des caractères non valides, vous pouvez vous retrouver avec une variable qui contient une séquence d'octets qui ne forme pas de caractères valides et que le shell peut finir par compter comme plusieurs caractères . Par exemple, dans un environnement local UTF-8:

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

Cela \375introduirait un caractère UTF-8 de 6 octets. Cependant, le 6ème ( A) ci-dessus n'est pas valide pour un caractère UTF-8. Vous vous retrouvez toujours avec \375\200\200\200\200Ain $a, qui bashcompte pour 6 caractères bien que les 5 premiers ne soient pas vraiment des caractères, seulement 5 octets ne faisant partie d'aucun caractère.

Stéphane Chazelas
la source
Merci. Simple et beau. J'ai en fait essayé quelque chose à cette fin (en modifiant la variable IFS), mais cela n'a pas fonctionné pour moi, donc j'ai fini avec ma concoction (jeu inutile avec les descripteurs de fichiers, etc.).
PSkocik
1
Fait intéressant, il semble que l'utilisation read -rN1résout le problème de la nouvelle ligne et élimine ainsi la nécessité de fournir une nouvelle ligne par défaut lors de l'impression $a.
krb686
Juste FTR, je lis le fichier 4118 ligne 20 Mo. L'utilisation de read -n1(caractère par caractère) prend 4 min 51 secondes et chauffe l'ordinateur portable à 90 degrés. L'utilisation read -r(ligne par ligne) prend 1,3 seconde et l'ordinateur portable reste à 54 degrés avec deux ventilateurs silencieux.
WinEunuuchs2Unix
2

Ceci est un exemple simple utilisant cutune forboucle & wc:

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

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

KISS n'est-ce pas?

Gilles Quenot
la source
Si tel est KISS, alors ce qui est pure bashsolution: file="$(</etc/passwd)"; bytes="${#file}"; for ((i=0;i<bytes;i++)); do echo "${file:i:1}"; done?
manatwork
Merci aux deux. Ouais, si je dois recourir à l'obtention de ces caractères à partir de lignes, je ferais aussi bien de les obtenir à partir du fichier entier. Je trouve que la solution de sch est la plus KISS, cependant.
PSkocik
@manatwork C'est une bonne solution simple. Même ainsi, il me semble que la réponse ci-dessus en utilisant une boucle de lecture est un peu plus rapide pour une raison quelconque. Peut-être que les sous-chaînes en bash sont assez lentes?
krb686
@ krb686, en fait le tout bash"C'est trop grand et trop lent." selon la section BUGS de sa page de manuel. Mais même ainsi, il est toujours plus rapide de découper une chaîne en mémoire que de lire un fichier encore et encore pour chaque caractère. Au moins sur ma machine: pastebin.com/zH5trQQs
manatwork