ubuntuusers.de

Merkwürdige Locale bei bash

Status: Ungelöst | Ubuntu-Version: Ubuntu 24.04 (Noble Numbat)
Antworten |

kB Team-Icon

Supporter, Wikiteam
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 10197

Wohnort: Münster

Für ein Bash-Skript benötige ich eine boolsche Funktion, die eine eingegebene Zeichenkette prüft, ob das ein zulässiger Name für eine benutzerdefinierte Variable ist. Bei der Bash muss ein zulässiger Name einer benutzerdefinierten Variable mit einem ASCII-Buchstaben oder dem Unterstrich beginnen und darf nur ASCII-Buchstaben, den Unterstrich und Ziffern enthalten.

Mit einem regulären Ausdruck ist das auch bequem und knackig kurz machbar. Meine Funktion sieht so aus:

isVAR() { [[ ${1^^} =~ ^[A-Z_][A-Z_0-9]*$ ]] ;} 

Sie funktioniert auch, wie man leicht testet:

$ isVAR a ; echo $?
0
$ isVAR 7 ; echo $?
1
$ isVAR 7a ; echo $?
1
$ isVAR a7 ; echo $?
0

Allerdings funktioniert sie nur fast perfekt:

 isVAR äöü ; echo $?
0

Die deutschen Umlaute sind natürlich keine ASCII-Zeichen und damit in Variablennamen ungültig. Offenbar interpretiert aber Bash den Ausdruck [A-Z] nicht als „ASCII-Zeichen A bis Z“, sondern als „alle Buchstaben gemäß der geltenden Locale“, denn ein

$ LANG= isVAR ä ; echo $?
1

beendet den Spuk. Dies ist schon die erste Überraschung.

Aber es kommt noch schlimmer:

$  isVAR æØſðđŋħ ; echo $?
0

Auch das nordische ä und ö, also æ und ø, werden als zur deutschen Lokale (die bei mir eingestellt ist) gehörig behandelt, und ebenso eine Menge weiterer Zeichen. Es wird also nicht nur die deutsche Lokale benutzt, sondern eine falsche deutsche Locale, denn die nordischen Zeichen gehören natürlich nicht zum Deutschen.

Mein Problem: Meine Funktion funktioniert wie gewünscht, wenn ich ihrem Aufruf ein LANG= voranstelle. Das will ich natürlich nicht bei jedem Aufruf machen, sondern lieber in die Definition der Funktion aufnehmen. Wie mache ich das?

TK87

Anmeldungsdatum:
8. Juli 2019

Beiträge: 301

Wohnort: Aachen

Moin,

mit grep und dem Parameter --perl-regexp wird [A-Z] korrekt als ASCII-Zeichen 0x41 bis 0x5a interpretiert.

1
isVar() { grep -iqP "^[A-Z_][A-Z_0-9]*$" <<<$@ ;}
$ isVar äöü ; echo $?
1
$ isVar æØſðđŋħ ; echo $?
1
$ isVar _abc ; echo $?
0

Gruß Thomas

shiro Team-Icon

Supporter

Anmeldungsdatum:
20. Juli 2020

Beiträge: 1449

... sondern lieber in die Definition der Funktion aufnehmen. Wie mache ich das?

Ich würde in der Funktion eine "local" Definition vornehmen, also:

$ isVAR() { local LANG= && [[ ${1^^} =~ ^[A-Z_][A-Z_0-9]*$ ]] ; } 
$ isVAR ä7 ; echo $?
1
$ locale
LANG=de_DE.UTF-8
LANGUAGE=de_DE:en
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=
$ 

TK87

Anmeldungsdatum:
8. Juli 2019

Beiträge: 301

Wohnort: Aachen

Ich habe mal ein wenig weiter geforscht...

kB schrieb:

Offenbar interpretiert aber Bash den Ausdruck [A-Z] nicht als „ASCII-Zeichen A bis Z“, sondern als „alle Buchstaben gemäß der geltenden Locale“

Nein. Bash scheint ä als Sonderform von a, ö als Sonderform von o usw. zu interpretieren. Und ä liegt für Bash genau zwischen a und b, ö genau zwischen o und p, usw.

$ [[ ö =~ [o] ]] ; echo $?
1
$ [[ ö =~ [a-o] ]] ; echo $?
1
$ [[ ö =~ [o-p] ]] ; echo $?
0
$ [[ ß =~ [s-t] ]] ; echo $?
0
$ [[ äöü =~ [ab-op-uv-z] ]] ; echo $?
1

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4773

Wohnort: Berlin

@kB: Es gibt keine „dieser Buchstabe gehört oder gehört nicht zu einer Gebiets-Locale”-Beziehung. Zeichen gehören zu einer Kodierung oder nicht, und wenn da UTF-8 steht, dann gehören alle in UTF-8 kodierbaren Zeichen zu der Locale, egal welches Gebiet. Was das Gebiet regelt, ist wie all die Zeichen die kodierbar sind, sortiert werden. Und bei [A-Z] sind zwischen A und Z alle Zeichen aus Unicode, die wenn man alle Zeichen sortiert, halt so dazwischen sind.

Das dürften folgende 607 Zeichen von A bis Z sein:

AáÁàÀăắằẵẳặĂẮẰẴẲẶâấầẫẩậÂẤẦẪẨẬǎǍåǻÅǺäǟÄǞãÃȧǡȦǠąĄāĀảẢȁȀȃȂạẠḁḀẚªæǽǣÆǼǢbBḃ
ḂḅḄḇḆɓƁcCćĆĉĈčČċĊçḉÇḈƈƇdDďĎḋḊđĐḑḐḍḌḓḒḏḎɖɗƊðÐdzDzDZdžDžDŽeEéÉèÈĕĔêếềễểệÊẾỀỄỂỆ
ěĚëËẽẼėĖȩḝȨḜęĘēḗḕĒḖḔẻẺȅȄȇȆẹẸḙḘḛḚǝəɛƎƏƐfFḟḞgGǵǴğĞĝĜǧǦġĠǥǤģĢḡḠɠƓƣƢhHĥĤȟȞ
ḧḦḣḢħĦḩḨḥḤḫḪẖƕǶiIíÍìÌĭĬîÎǐǏïḯÏḮĩĨįĮīĪỉỈȉȈȋȊịỊḭḬıİijIJjJĵĴǰkKḱḰǩǨķĶḳḲḵḴƙƘ
lLĺĹľĽŀĿłŁļĻḷḹḶḸḽḼḻḺljLjLJmMḿḾṁṀṃṂnNńŃǹǸňŇñÑṅṄņŅṇṆṋṊṉṈʼnŋŊnjNjNJoOóÓòÒŏŎôốồỗổ
ộÔỐỒỖỔỘǒǑöɵȫÖƟȪőŐõṍṏȭÕṌṎȬȯȱȮȰøǿØǾǫǭǪǬōṓṑŌṒṐỏỎȍȌȏȎọỌơớờỡởợƠỚỜỠỞỢɔºƆœŒpP
ṕṔṗṖqQĸrRŕŔřŘṙṘŗŖȑȐȓȒṛṝṚṜṟṞsSśṥŚṤŝŜšṧŠṦṡṠşșŞȘṣṩṢṨſẛßtTťŤẗṫṪŧŦţțŢȚṭṬṱṰṯ
ṮuUúÚùÙŭŬûÛǔǓůŮüǘǜǚǖÜǗǛǙǕűŰũṹŨṸųŲūṻŪṺủỦȕȗụỤṳṲṷṶṵṴưứừữửựƯỨỪỮỬỰvVṽṼṿṾwWẃ
ẂẁẀŵŴẘẅẄẇẆẉẈƿǷxXẍẌẋẊyYýÝỳỲŷŶẙÿŸỹỸẏẎȳȲỷỶỵỴƴƳȝȜzZ

Das Argument, dass Zeichen davon nicht “deutsch“ sind, wäre IMHO komisch, denn wenn ich beispielsweise in einem deutschsprachigen Text eine Liste mit Brücken in Europa sortiere, dann möchte ich doch die Øresund-Brücke in der Nähe von der Oderbaumbrücke sortiert haben, und nicht irgendwo hinter Z oder vor A, weil Ø nicht im deutschsprachigen Alphabet vorkommt. Es gibt ja trotzdem Worte und Eigennamen mit solchen Buchstaben die sinnvoll behandelt werden müssen.

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6572

Wohnort: Hamburg

... darf nur ASCII-Buchstaben, den Unterstrich und Ziffern enthalten.

Könnte man da nicht zuerst prüfen, ob in dem Namen ein Byte enthalten ist, dass größer als 127 ist?

Marc_BlackJack_Rintsch Team-Icon

Ehemalige
Avatar von Marc_BlackJack_Rintsch

Anmeldungsdatum:
16. Juni 2006

Beiträge: 4773

Wohnort: Berlin

Die Zeichenliste weiter oben hatte ich mit Python erstellt: Einfach alle Unicode-Zeichen gemäss aktueller locale sortiert, und dann die von A bis Z ausgegeben:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
import sys
import textwrap
from locale import LC_ALL, setlocale, strxfrm


def main():
    setlocale(LC_ALL, "")
    all_characters = "".join(
        sorted(map(chr, range(1, sys.maxunicode)), key=strxfrm)
    )
    a_to_z = all_characters[
        all_characters.index("A") : all_characters.index("Z") + 1
    ]
    print(textwrap.fill(a_to_z))
    print(len(a_to_z), "characters.")


if __name__ == "__main__":
    main()

Etwas ähnliches wollte ich dann doch noch mal mit der Bash machen: Alle Unicode-Zeichen gegen den regulären Ausdruck [A-Z] prüfen:

1
2
3
4
5
6
#!/bin/bash

for ((i = 1; i < 1114111; i++)); do
    c=$(printf %b "$(printf '\\U%08x' $i)")
    [[ $c =~ [A-Z] ]] && printf %s "$c"
done

Das dauert aber eeeeeewig, so dass ich das abgebrochen habe, und mal geschaut habe was die Bash da macht, und ob man die Grundidee einfach schnell in C nachprogrammieren kann. Damit das keine Geduldsprobe wird.

Das Handbuch von Bash sagt zum =~-Operator:

An additional binary operator, =~, is available, with the same precedence as == and !=. When it is used, the string to the right of the operator is considered an extended regular expression and matched accordingly (as in regex(3)).

regex(7), wo reguläre Ausdrücke nach POSIX-Standard beschrieben werden, sagt zu „bracket expressions“:

A bracket expression is a list of characters enclosed in []. […] If two characters in the list are separated by -, this is shorthand for the full range of characters between those two (inclusive) in the collating sequence, […]

Und:

Ranges are very collating-sequence-dependent, and portable programs should avoid relying on them.

Hier nun das C-Programm, das alle Unicode-Zeichen gegen das Muster [A-Z] testet:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <inttypes.h>
#include <limits.h>
#include <locale.h>
#include <regex.h>
#include <stdio.h>
#include <stdlib.h>
#include <uchar.h>

#define MAX_CHAR32  0x10ffff

int main(void)
{
    setlocale(LC_ALL, "");

    regex_t re;
    int error_code;
    if ((error_code = regcomp(&re, "[A-Z]", REG_EXTENDED | REG_NOSUB))) {
        return error_code;
    }

    uint32_t count = 0;
    char *mbs = malloc(MB_LEN_MAX);
    for (char32_t c = 1; c < MAX_CHAR32; c++) {
        mbstate_t state = {0};
        size_t byte_count = c32rtomb(mbs, c, &state);
        if (byte_count != (size_t) -1) {
            mbs[byte_count] = '\0';
            if (regexec(&re, mbs, 0, NULL, 0) == 0) {
                printf("%s", mbs);
                if (++count % 70 == 0) putchar('\n');
            }
        }
    }
    printf("\n%"PRIu32" characters.\n", count);

    free(mbs);
    regfree(&re);
    return 0;
}

Testläufe:

$ LC_ALL=C ./all_letters
ABCDEFGHIJKLMNOPQRSTUVWXYZ
26 characters.
$ LC_ALL=de_DE.UTF-8 ./all_letters
ABCDEFGHIJKLMNOPQRSTUVWXYZÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝĀĂĄĆĈĊČĎĐĒĔĖĘĚĜ
ĞĠĢĤĦĨĪĬĮİIJĴĶĹĻĽĿŁŃŅŇŊŌŎŐŒŔŖŘŚŜŞŠŢŤŦŨŪŬŮŰŲŴŶŸƁƆƇƊƎƏƐƓƘƟƠƢƯƳDŽDžLJLjNJNjǍǏǑǓǕ
ǗǙǛǞǠǢǤǦǨǪǬDZDzǴǶǷǸǺǼǾȀȂȄȆȈȊȌȎȐȒȘȚȜȞȦȨȪȬȮȰȲḀḂḄḆḈḊḌḎḐḒḔḖḘḚḜḞḠḢḤḦḨḪḬḮḰḲḴḶḸ
ḺḼḾṀṂṄṆṈṊṌṎṐṒṔṖṘṚṜṞṠṢṤṦṨṪṬṮṰṲṴṶṸṺṼṾẀẂẄẆẈẊẌẎẠẢẤẦẨẪẬẮẰẲẴẶẸẺẼẾỀỂỄỆỈỊỌỎỐỒỔ
ỖỘỚỜỞỠỢỤỦỨỪỬỮỰỲỴỶỸ
298 characters.

seahawk1986

Anmeldungsdatum:
27. Oktober 2006

Beiträge: 11300

Wohnort: München

kB schrieb:

Das will ich natürlich nicht bei jedem Aufruf machen, sondern lieber in die Definition der Funktion aufnehmen. Wie mache ich das?

Die Bash kennt lokale Variablen:

1
isVAR() { local LANG=C; [[ ${1^^} =~ ^[A-Z_][A-Z_0-9]*$ ]] ;}

kB Team-Icon

Supporter, Wikiteam
(Themenstarter)
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 10197

Wohnort: Münster

TK87 schrieb:

[…] mit grep und dem Parameter --perl-regexp wird [A-Z] korrekt als ASCII-Zeichen 0x41 bis 0x5a interpretiert

Danke für den Hinweis auf grep und Perl!

Tatsächlich verhält sich grep genauso (oder zumindest ähnlich) wie bash, solange man Basic- oder Extended-Basic-Regexp benutzt und anders, nämlich mit dem von mir erhofften Verhalten, wenn man Perl-Regexp benutzt. Damit liegt die Ursache vermutlich in der Definition der Regexp bei POSIX, und das Verhalten beschränkt sich nicht auf die Bash.

kB Team-Icon

Supporter, Wikiteam
(Themenstarter)
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 10197

Wohnort: Münster

shiro schrieb:

[…] in der Funktion eine "local" Definition vornehmen, also:

$ isVAR() { local LANG= && [[ ${1^^} =~ ^[A-Z_][A-Z_0-9]*$ ]] ; } 

Danke! Das ist eine elegante Lösung mit den Sprachmitteln von bash, wie von mir erhofft.

Ich bin aber inzwischen, auch durch diese Diskussion zur Überzeugung gelangt, dass die Verwendung von regulären Ausdrücken, insbesondere mit Bereichen wie [A-Z] in Programmen keine gute Idee ist, eben weil deren Interpretation extrem von Parametern (locale) der Umgebung abhängt, in der das Programm ausgeführt wird. Das erfordert vom Programmierer erhöhte Beachtung.

kB Team-Icon

Supporter, Wikiteam
(Themenstarter)
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 10197

Wohnort: Münster

Marc_BlackJack_Rintsch schrieb:

[…] bei [A-Z] sind zwischen A und Z alle Zeichen aus Unicode, die wenn man [gemäß der geltenden Locale] alle Zeichen sortiert, halt so dazwischen sind.

Das mag zutreffen, was Du da schreibst, aber ein solches Verhalten ist halt nicht das, was ein naiver Programmierer erwartet.

Ich benötige für meine Aufgabe eine Prüfung auf ASCII-Buchstaben von A-Z, unabhängig von der geltenden Locale. In der Bash leistet das der Glob [A-Z]. Sobald man aber diesen Ausdruck in einem regulären Ausdruck verwendet, wird er anders interpretiert, nämlich wohl so (oder ähnlich) wie Du es beschreibst. So etwas gehört bei mir in die Kategorie „Böse Falle“.

kB Team-Icon

Supporter, Wikiteam
(Themenstarter)
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 10197

Wohnort: Münster

Dakuan schrieb:

[…] Könnte man da nicht zuerst prüfen, ob in dem Namen ein Byte enthalten ist, dass größer als 127 ist?

Auch ein guter Ansatz.

Leider kenne ich aber keine knackige Formulierung, wie man mit der Bash prüft, ob ein einer Zeichenkette, die aus Sicht von bash aus Zeichen, nicht aus Bytes besteht, prüft, ob ein Byte enthalten ist, welches man auch als negative Zahl interpretieren kann.

Mit Schleifen und Typumwandlungen geht das sicherlich, aber ich suche ein knackige Lösung.

kB Team-Icon

Supporter, Wikiteam
(Themenstarter)
Avatar von kB

Anmeldungsdatum:
4. Oktober 2007

Beiträge: 10197

Wohnort: Münster

Marc_BlackJack_Rintsch schrieb:

Die Zeichenliste weiter oben hatte ich mit Python erstellt […]

Etwas ähnliches wollte ich dann doch noch mal mit der Bash machen […]

Das Handbuch von Bash […]

Ranges are very collating-sequence-dependent, and portable programs should avoid relying on them.

Danke. Das ist ein wichtiger Satz. Man sollte ihn fett rot schreiben und dreimal unterstreichen.

[…] C-Programm

Kurze Zusammenfassung: Python, Bash und C verhalten sich bei Bereichen in regulären Ausdrücken bzgl. der Abhängigkeit von der geltenden Locale ähnlich, aber nicht identisch.

TK87

Anmeldungsdatum:
8. Juli 2019

Beiträge: 301

Wohnort: Aachen

Wie wäre es denn mit... {{{#!code bash isVAR() { __0-9*$ ]] ;} }}} Erstaunlicher Weise berücksichtigt der ":alpha:"-Posix tatsächlich nur Zeichen von A-Z (obwohl ich es gerade da anders erwartet hätte).

Dakuan

Avatar von Dakuan

Anmeldungsdatum:
2. November 2004

Beiträge: 6572

Wohnort: Hamburg

Leider kenne ich aber keine knackige Formulierung, wie man mit der Bash prüft, ob ein einer Zeichenkette, ...

Ich leider auch nicht, sonst hätte ich ein Beispiel präsentieren können. Ein Denkansatz wäre, den String als Array aus Bytes zu betrachten (auch wenn das nicht so ist) und dann die Bytes einzeln zu prüfen.

Jedes Byte, das zu einer UTF-8 Sequenz gehört hat das Bit 7 gesetzt (7..0) und kann hier ohne weitere Prüfung als ungültig betrachtet werden.

Antworten |