Zeichen für Zeichen mit Bash lesen

8

Ich habe versucht, mit bash eine Datei zeichenweise zu lesen.

Nach langem Ausprobieren habe ich festgestellt, dass dies funktioniert:

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

Das heißt, ich kann es Zeile für Zeile lesen und dann jede Zeile char für char durchlaufen.

Vorher hatte ich es versucht: exec 4<file.txt && while read -r -n1 ch <&4; do; echo -n "$ch"; done aber es würde alle Leerzeichen in der Datei überspringen .

Könnten Sie bitte erklären, warum? Gibt es eine Möglichkeit, die zweite Strategie (dh das Lesen von Zeichen für Zeichen mit Bashs Lesen) zum Funktionieren zu bringen?

PSkocik
quelle
4
Stellen Sie IFSnichts ein, damit Leerzeichen die Wortaufteilung überleben.
Manatwork
Versuchte das mit IFS = '', aber ich denke, es musste nur IFS = sein. Vielen Dank!
PSkocik

Antworten:

12

Sie müssen Leerzeichen aus dem $IFSParameter entfernen read, um das Überspringen führender und nachfolgender Zeichen zu beenden (wobei -n1das Leerzeichen, falls vorhanden, sowohl führende als auch nachfolgende Zeichen ist, also übersprungen):

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

Aber selbst dann readüberspringen Bashs Zeilenumbruchzeichen, mit denen Sie umgehen können:

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

Obwohl Sie IFS= read -d '' -rn1stattdessen oder sogar besser IFS= read -N1(hinzugefügt in 4.1, kopiert von ksh93(hinzugefügt in o)) verwenden können, ist dies der Befehl zum Lesen eines Zeichens.

Beachten Sie, dass Bashs readnicht mit NUL-Zeichen umgehen können. Und ksh93 hat die gleichen Probleme wie bash.

Mit zsh:

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

(zsh kann mit NUL-Zeichen umgehen).

Beachten Sie, dass diese read -k/n/Neine Anzahl von Zeichen und keine Bytes lesen . Bei Multibyte-Zeichen müssen sie möglicherweise mehrere Bytes lesen, bis ein vollständiges Zeichen gelesen wird. Wenn die Eingabe ungültige Zeichen enthält, erhalten Sie möglicherweise eine Variable, die eine Folge von Bytes enthält, die keine gültigen Zeichen bilden und die die Shell möglicherweise als mehrere Zeichen zählt . Zum Beispiel in einem UTF-8-Gebietsschema:

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

Das \375würde ein 6-Byte-UTF-8-Zeichen einführen. Das sechste ( A) oben ist jedoch für ein UTF-8-Zeichen ungültig. Sie erhalten immer noch \375\200\200\200\200Ain $a, was bashals 6 Zeichen zählt, obwohl die ersten 5 keine wirklichen Zeichen sind, sondern nur 5 Bytes, die nicht Teil eines Zeichens sind.

Stéphane Chazelas
quelle
Vielen Dank. Einfach und schön. Ich habe tatsächlich etwas zu diesem Zweck versucht (Ändern der IFS-Variablen), aber es hat bei mir irgendwie nicht funktioniert, und so kam ich zu meiner Erfindung (unnötiges Spielen mit Dateideskriptoren usw.).
PSkocik
1
Interessanterweise sieht es so aus, als würde die Verwendung read -rN1stattdessen das Newline-Problem lösen und somit die Notwendigkeit beseitigen, beim Drucken standardmäßig eine Newline bereitzustellen $a.
krb686
Nur FTR Ich lese 4118 Zeile 20 MB Datei. Die Verwendung von read -n1(char by char) dauert 4 Minuten und 51 Sekunden und heizt den Laptop auf 90 Grad auf. Die Verwendung read -r(Zeile für Zeile) dauert 1,3 Sekunden und der Laptop bleibt bei 54 Grad, während zwei Lüfter leise sind.
WinEunuuchs2Unix
2

Dies ist ein einfaches Beispiel mit cuteiner forSchleife & wc:

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

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

KISS, nicht wahr?

Gilles Quenot
quelle
Wenn das KISS ist, was ist dann eine reine bashLösung : file="$(</etc/passwd)"; bytes="${#file}"; for ((i=0;i<bytes;i++)); do echo "${file:i:1}"; done?
Manatwork
Danke an beide. Ja, wenn ich diese Zeichen aus Zeilen abrufen muss, kann ich sie auch aus der gesamten Datei abrufen. Ich finde die Lösung von sch jedoch am meisten KISS.
PSkocik
@manatwork Das ist eine gute, einfache Lösung. Trotzdem scheint mir die obige Antwort mit einer Leseschleife aus irgendeinem Grund etwas schneller zu sein. Vielleicht sind Teilzeichenfolgen in Bash ziemlich langsam?
krb686
@ krb686, eigentlich das ganze bash"Es ist zu groß und zu langsam." gemäß dem Abschnitt BUGS der Manpage. Trotzdem ist es immer noch schneller, eine Zeichenfolge in den Speicher zu schneiden, als für jedes Zeichen immer wieder eine Datei zu lesen. Zumindest auf meinem Rechner: pastebin.com/zH5trQQs
Manatwork