Zeichenzählen mit Bash in UTF-8

Vom einfachen Programm zum fertigen Debian-Paket, Fragen rund um Programmiersprachen, Scripting und Lizenzierung.
Antworten
Benutzeravatar
Lohengrin
Beiträge: 3139
Registriert: 29.08.2004 00:01:05
Wohnort: Montsalvat

Zeichenzählen mit Bash in UTF-8

Beitrag von Lohengrin » 14.10.2018 03:07:05

Ich habe vor ein paar Tagen sehr überrascht festgestellt, dass bei Variablen in Bash gar nicht Bytes, sondern Zeichen gezählt werden.

Code: Alles auswählen

$ foo=Blödmann ; echo ${#foo} ${foo:4} ; echo -n $foo | hd
8 mann
00000000  42 6c c3 b6 64 6d 61 6e  6e                       |Bl..dmann|
00000009
Die nächste Frage ist dann, was mit kaputten UTF-8 gemacht wird.
Durch Ausprobieren bin ich auf folgendes Phänomen gekommen.

Code: Alles auswählen

$ foo=$(echo -ne "\xf4\x8f\xbf\xbf") ; echo ${#foo} $foo
1 􏿿
$ foo=$(echo -ne "\xf4\x8f\xbf\x4f") ; echo ${#foo} $foo
4 ���O
$ foo=$(echo -ne "\xf4\x90\x80\x80") ; echo ${#foo} $foo
1 ����
$ foo=$(echo -ne "\xf7\xbf\xbf\xbf") ; echo ${#foo} $foo
1 ����
$ foo=$(echo -ne "\xfd\xbf\xbf\xbf\xbf\xbf") ; echo ${#foo} $foo
1 ������
$ foo=$(echo -ne "\xfe\x82\x80\x80\x80\x80\x80") ; echo ${#foo} $foo
7 �������
Beispiel0: Das ist das größte definierte Unicode. Ein Zeichen. Wie erwartet.
Beispiel1: Das soll ein 4-Byte-Zeichen werden, aber das 3. (von 0 gezählt) ist ein Ascii -> 0. Zeichen kaputt. Das nächste Byte ist ein Folgebyte -> 1. Zeichen kaputt. Das nächste Byte ist ein Folgebyte -> 2. Zeichen kaputt. Das nächste Byte ist Ascii. Macht 4 Zeichen. Wie erwartet.
Beispiel2: Das ist nicht mehr Unicode. Das sollten, wie im Beispiel1, vier kaputte werden. Aber ${#foo} zählt 1.
Beispiel3: ${#foo} zählt alle 4-Byte-Zeichen durch, egal ob es dazu Unicode gibt.
Beispiel4: ${#foo} erkennt auch 6-Byte-Zeichen an.
Beispiel5: Bei 7-Byte-Zeichen macht auch ${#foo} nicht mehr mit.

Wenn ${#foo} so zählt, dann wird das wohl auch bei Substring so sein.

Code: Alles auswählen

$ foo=$(echo -ne "\xfd\xbf\xbf\xbf\xbf\xbf") ; echo ${#foo} $foo ; bar=${foo:2:2} ; echo ${#bar} $bar|hd
1 ������
00000000  30 0a                                             |0.|
00000002
$ foo=$(echo -ne "\xfe\x82\x80\x80\x80\x80\x80") ; echo ${#foo} $foo ; bar=${foo:2:2} ; echo ${#bar} $bar|hd
7 �������
00000000  32 20 80 80 0a                                    |2 ...|
00000005
Wie erwartet.

Ist ${#foo} und ${foo:2:2} falsch, oder ist die Ausgabe auf dem Terminal falsch?
Was ist korrekt? Gibt es U+110000 bis U+3FFFFFFF in UTF-8 oder nicht?
Wenn man schon zur Definition von Zeichen das UTF-8-Schema nimmt, dann soll man doch bitteschön das ganze Spektrum nehmen, und das geht bis \xfe\xbf\xbf\xbf\xbf\xbf\xbf. Damit wäre sowohl ${#foo} und ${foo:2:2} als auch die Ausgabe auf dem Terminal falsch.

Das Ganze spielt eine fundamentale Rolle bei Regex.
Wenn Bash mit kaputten UTF-8-Zeichen nicht konsistent umgeht, dann muss man vorher sicher sein, dass keine kaputten UTF-8-Zeichen drin sind. Das wäre ja fürchterlich.

Für was ist das Zählen in Zeichen statt in Bytes überhaupt gut?
Für mich ist ein chinesisches Zeichen, das 4 Bytes lang ist, oder ein deutscher Umlaut, der zwei Byte lang ist, so etwas wie die Bytefolge keit oder schaft. Und genau so würde ich das in einer Regex verwendet, nämlich in Klammern.
Mit welcher Begründung ist das ph in Stephan etwas anderes als das ß in Fußball? Beides sind zwei Bytes, die zusammengehören.
Hier ist das Pferd von hinten aufgezäumt worden. Die Bytefolgen, die im UTF-8-Schema zusammengehören, bilden ein Zeichen.
Jetzt kann man den Mandarin-Frieden durch das Inuktitut-mu in Regex ohne Klammern ersetzen, aber man hat die Kontrolle über die Datengröße in Bytes verloren.
Jetzt kann man mit nur einem ^H ein ganzes ä entfernen. Dafür hat man keine Möglichkeit mehr, nur das \xa4 vom ä zu entfernen. Was spricht eigentlich dagegen, zum Entfernen von einem zwei-Bytes-Zeichen, ^H^H zu machen?
Harry, hol schon mal das Rasiermesser!

Benutzeravatar
format_c
Beiträge: 185
Registriert: 23.01.2008 14:24:17
Kontaktdaten:

Re: Zeichenzählen mit Bash in UTF-8

Beitrag von format_c » 14.10.2018 11:06:03

Komisch.
Bei mir kommt da etwas anderes raus:

Code: Alles auswählen

$ echo $SHELL
/bin/bash
$ echo $TERM
xterm-256color
$ foo=$(echo -ne "\xf4\x90\x80\x80") ; echo ${#foo} $foo
4 ????
$ foo=$(echo -ne "\xfd\xbf\xbf\xbf\xbf\xbf") ; echo ${#foo} $foo
6 ??????
$ foo="äöüß" ; echo ${#foo} $foo
4 äöüß
$ 
Hab mal zwei falsch-zähl Beispiele von dir rausgegriffen.
[[ Black Holes are where God devided by 0 ]]

Benutzeravatar
niemand
Beiträge: 11962
Registriert: 18.07.2004 16:43:29

Re: Zeichenzählen mit Bash in UTF-8

Beitrag von niemand » 14.10.2018 11:12:09

Hier ebenfalls. Kaputte/falsche locale-Konfiguration?
Hardware: dunkelgrauer Laptop mit buntem Display (zum Aufklappen!) und eingebauter Tastatur; beiger Klotz mit allerhand Kabeln und auch mit Farbdisplay und Tastatur (je zum auf’n Tisch Stellen)
Am 7. Tag aber sprach der HERR: „QWNoIFNjaGVpw58sIGtlaW5lbiBCb2NrIG1laHIgYXVmIGRlbiBNaXN0ISBJY2ggbGFzcyBkYXMgamV0enQgZWluZmFjaCBzbyDigKYK“

Benutzeravatar
Lohengrin
Beiträge: 3139
Registriert: 29.08.2004 00:01:05
Wohnort: Montsalvat

Re: Zeichenzählen mit Bash in UTF-8

Beitrag von Lohengrin » 14.10.2018 16:56:27

niemand hat geschrieben: ↑ zum Beitrag ↑
14.10.2018 11:12:09
Hier ebenfalls. Kaputte/falsche locale-Konfiguration?
Faszinierend!
Vllt habt ihr eine neuere Bash-Version als ich. Bei mir ist

Code: Alles auswählen

$ bash --version
GNU bash, Version 4.4.12(1)-release (x86_64-pc-linux-gnu)
...
$ set | grep LANG
GDM_LANG=de_DE.UTF-8
LANG=de_DE.UTF-8
$ set | grep LC
MAILCHECK=60
So wie es bei euch läuft, ist es konsistent. Unicode endet bei U+10FFFF, und utf-8, was größeres als 10FFFF kodieren soll, ist ungültig.

Da bleibt dann nur noch das Phänomen, dass beim Zusammenfügen von zwei Strings mit 2 und 3 Zeichen ein String mit 3 Zeichen herauskommen kann.

Code: Alles auswählen

$ foo1=$(echo -ne "A\xe2\x82") ; foo2=$(echo -ne "\xacB"); bar="${foo1}${foo2}" ; echo "${#foo1} $foo1  ${#foo2} $foo2  ${#bar} $bar" 
3 A��  2 �B  3 A€B
Noch eine Variante wäre, wenn beim Zuweisen von etwas in eine Variable kaputte Zeichen gefressen würden. So geschieht es ja auch mit \x00.

Code: Alles auswählen

foo=$(echo -ne "A\x00B") ; echo ${#foo} ${foo} ; echo -n $foo | hd 
bash: Warnung: command substitution: ignored null byte in input
2 AB
00000000  41 42                                             |AB|
00000002
Harry, hol schon mal das Rasiermesser!

Benutzeravatar
niemand
Beiträge: 11962
Registriert: 18.07.2004 16:43:29

Re: Zeichenzählen mit Bash in UTF-8

Beitrag von niemand » 14.10.2018 17:01:21

Lohengrin hat geschrieben: ↑ zum Beitrag ↑
14.10.2018 16:56:27
Vllt habt ihr eine neuere Bash-Version als ich.
Getestet mit 4.4.23 und 4.3.30.

Im Gegensatz zu dir ist bei mir auch LC_ALL=de_DE.UTF-8 gesetzt. Komplette locale-Ausgabe:

Code: Alles auswählen

LANG=de_DE.UTF-8
LC_CTYPE="de_DE.UTF-8"
LC_NUMERIC="de_DE.UTF-8"
LC_TIME="de_DE.UTF-8"
LC_COLLATE="de_DE.UTF-8"
LC_MONETARY="de_DE.UTF-8"
LC_MESSAGES="de_DE.UTF-8"
LC_PAPER="de_DE.UTF-8"
LC_NAME="de_DE.UTF-8"
LC_ADDRESS="de_DE.UTF-8"
LC_TELEPHONE="de_DE.UTF-8"
LC_MEASUREMENT="de_DE.UTF-8"
LC_IDENTIFICATION="de_DE.UTF-8"
LC_ALL=de_DE.UTF-8
Hardware: dunkelgrauer Laptop mit buntem Display (zum Aufklappen!) und eingebauter Tastatur; beiger Klotz mit allerhand Kabeln und auch mit Farbdisplay und Tastatur (je zum auf’n Tisch Stellen)
Am 7. Tag aber sprach der HERR: „QWNoIFNjaGVpw58sIGtlaW5lbiBCb2NrIG1laHIgYXVmIGRlbiBNaXN0ISBJY2ggbGFzcyBkYXMgamV0enQgZWluZmFjaCBzbyDigKYK“

Benutzeravatar
Lohengrin
Beiträge: 3139
Registriert: 29.08.2004 00:01:05
Wohnort: Montsalvat

Re: Zeichenzählen mit Bash in UTF-8

Beitrag von Lohengrin » 14.10.2018 17:27:56

niemand hat geschrieben: ↑ zum Beitrag ↑
14.10.2018 17:01:21
Im Gegensatz zu dir ist bei mir auch LC_ALL=de_DE.UTF-8 gesetzt. Komplette locale-Ausgabe:

Code: Alles auswählen

LANG=de_DE.UTF-8
LC_CTYPE="de_DE.UTF-8"
LC_NUMERIC="de_DE.UTF-8"
LC_TIME="de_DE.UTF-8"
LC_COLLATE="de_DE.UTF-8"
LC_MONETARY="de_DE.UTF-8"
LC_MESSAGES="de_DE.UTF-8"
LC_PAPER="de_DE.UTF-8"
LC_NAME="de_DE.UTF-8"
LC_ADDRESS="de_DE.UTF-8"
LC_TELEPHONE="de_DE.UTF-8"
LC_MEASUREMENT="de_DE.UTF-8"
LC_IDENTIFICATION="de_DE.UTF-8"
LC_ALL=de_DE.UTF-8
Das wird mir immer unverständlicher.
Bei mir ist

Code: Alles auswählen

$ locale
LANG=de_DE.UTF-8
LANGUAGE=
LC_CTYPE="de_DE.UTF-8"
LC_NUMERIC="de_DE.UTF-8"
LC_TIME="de_DE.UTF-8"
LC_COLLATE="de_DE.UTF-8"
LC_MONETARY="de_DE.UTF-8"
LC_MESSAGES="de_DE.UTF-8"
LC_PAPER="de_DE.UTF-8"
LC_NAME="de_DE.UTF-8"
LC_ADDRESS="de_DE.UTF-8"
LC_TELEPHONE="de_DE.UTF-8"
LC_MEASUREMENT="de_DE.UTF-8"
LC_IDENTIFICATION="de_DE.UTF-8"
LC_ALL=
$ set | grep LC
MAILCHECK=60
Ist jetzt LC_COLLATE und LC_ALL gesetzt oder nicht?
Ich habe die mal von Hand gesetzt, aber keine Änderung.

Code: Alles auswählen

$ export LC_ALL=de_DE.UTF-8
$ export LC_COLLATE=de_DE.UTF-8
$ foo=$(echo -ne "\xf4\x90\x80\x80") ; echo ${#foo} $foo
1 ����
$ foo=$(echo -ne "\xfd\xbf\xbf\xbf\xbf\xbf") ; echo ${#foo} $foo
1 ������
Harry, hol schon mal das Rasiermesser!

Benutzeravatar
Lohengrin
Beiträge: 3139
Registriert: 29.08.2004 00:01:05
Wohnort: Montsalvat

Re: Zeichenzählen mit Bash in UTF-8

Beitrag von Lohengrin » 15.10.2018 13:51:46

niemand hat geschrieben: ↑ zum Beitrag ↑
14.10.2018 17:01:21
Im Gegensatz zu dir ist bei mir auch LC_ALL=de_DE.UTF-8 gesetzt.
Was sagt bei dir set | grep ^LC ? Sind bei dir LC_ALL und LC_COLLATE aufgeführt?
Bei mir sind sie es nicht, obwohl sie aktiv sind.

Code: Alles auswählen

$ set|grep ^LC
$ echo -en "Affe\nBiene\nalso\nCebra\nähnlich\n"|sort
Affe
ähnlich
also
Biene
Cebra
$ export LC_COLLATE=C.UTF-8
$ echo -en "Affe\nBiene\nalso\nCebra\nähnlich\n"|sort
Affe
Biene
Cebra
also
ähnlich
$ export LC_COLLATE=
$ set|grep ^LC
LC_COLLATE=
$ echo -en "Affe\nBiene\nalso\nCebra\nähnlich\n"|sort
Affe
ähnlich
also
Biene
Cebra
# dpkg-reconfigure locales hat nicht dazu geführt, dass LC_ALL und LC_COLLATE bei mir durch set | grep ^LC aufgeführt werden.
Harry, hol schon mal das Rasiermesser!

Antworten