Ein Problem mit string



  • @Fragender sagte in Ein Problem mit string:

    Die Win 11-Konsole hatte einfach keine Fehlermeldung, dass dll's fehlen, ausgegeben. 😞

    Ja, das ist mich auch schon öfter negativ aufgefallen. Oft gibt es jedoch ein Fehler-Fenster, das zumindest die erste nicht gefundene DLL meldet. Ich vermute das hängt damit zusammen, ob es sich um eine Konsolen- oder eine GUI-Anwendung handelt (GCC+Clang -mconsole / MSVC -subsystem:console vs. GCC+Clang -mwindows / MSVC -subsystem:windows). Bei programmatisch geladenen DLLs (die nicht explizit gelinkt wurden, z.B. via dlopen/LoadLibrary) hat man das auch öfter (wenn nicht im Programmcode gehandhabt). Auch DLLs können schonmal andere DLLs "manuell" laden, da wundert man sich manchmal, dass bei der ersten DLL eine Fehlermeldung kommt, aber für weitere benötigte DLLs keine mehr.



  • Danke @Finnegan , es funktioniert ja jetzt erst mal... und ich kann damit Dateien umbenennen, die komische Zeichen enthalten (sodass beide Systeme damit umgehen können sollten).

    Wo sind eigentlich die Unterschiede zwischen
    auto
    const auto
    und const auto & ?



  • @Fragender sagte in Ein Problem mit string:

    Wo sind eigentlich die Unterschiede zwischen
    auto
    const auto
    und const auto & ?

    Simple Antwort: auto verwendet die selben Deduktions-Regeln wie für Template-Parameter, wobei Referenzen und const/volatile nicht mit abgeleitet werden. Wenn der resultierende Typ const oder eben eine Referenz sein soll, dann fügt man das so an das auto, genau so wie an einen reinen Typ wie z.B. int. Wenn ich eine Referenz auf einen konstanten int will, dann schreibe ich auch const int&.

    Es gibt auch noch decltype(auto), wobei die Regeln für decltype(<Ausdruck>) angewendet und Referenzen wie auch const/volatile mit übernommen werden. Das braucht man aber eher selten, im Zweifel ist ein einfaches auto das, was man will.



  • Eiii, das ist ja die reinste char-/string-Hölle. 😞 Ich habe es jetzt noch einmal umgeschrieben, und

    1. ich kann es mit x86_64-w64-mingw32-g++ -static rename1.cpp ohne Fehler kompilieren,
    2. es scheint mit allen Dateinamen in Windows zu funktionieren.

    Ob es allerdings "gut"/ "schön"/ "richtig" ist, das ist noch einmal eine ganz andere Frage:

    #include <filesystem>
    #include <iostream>
    #include <set>
    #include <string>
    #include <vector>
    namespace fs = std::filesystem;
    
    int main(int argc, char *argv[]) {
      std::set<char16_t> allowed1;
      for (char c = '0'; c <= '9'; c++) {
        allowed1.insert(c);
      }
      for (char c = 'a'; c <= 'z'; c++) {
        allowed1.insert(c);
      }
      for (char c = 'A'; c <= 'Z'; c++) {
        allowed1.insert(c);
      }
      allowed1.insert('(');
      allowed1.insert(')');
      // allowed1.insert('[');
      // allowed1.insert(']');
      allowed1.insert('-');
      allowed1.insert('_');
      std::vector<std::u16string> vec;
      auto path = fs::current_path();
      for (const auto &entry : fs::directory_iterator(path)) {
        auto p1 = entry.path().filename().u16string();
        auto p2 = entry.path().filename().u16string();
        auto last = p2.find_last_of(u".");
        for (int i = 0; i < last; i++) {
          if (p2[i] == '&') {
            p2[i] = 'n';
          }
          if (allowed1.count(p2[i]) == 0) {
            p2[i] = '_';
          }
        }
        for (int i = 0; i < p2.size() - 1; i++) {
          if (p2[i] == '_' && p2[i + 1] == '_') {
            p2.erase(i, 1);
            i--;
          }
        }
        vec.push_back(p1);
        vec.push_back(p2);
      }
      for (int i = 0; i < vec.size(); i += 2) {
        const auto p1 = vec[i];
        const auto p2 = vec[i + 1];
        if (p1 != p2) {
          for (const auto &c : p1)
            std::cout << static_cast<char>(c);
          std::cout << " -> ";
          for (const auto &c : p2)
            std::cout << static_cast<char>(c);
          std::cout << "\n";
          fs::rename(p1, p2);
        }
      }
      return 0;
    }
    
    


  • @Fragender sagte in Ein Problem mit string:

    Eiii, das ist ja die reinste char-/string-Hölle. 😞 Ich habe es jetzt noch einmal umgeschrieben, und

    1. ich kann es mit x86_64-w64-mingw32-g++ -static rename1.cpp ohne Fehler kompilieren,
    2. es scheint mit allen Dateinamen in Windows zu funktionieren.

    Probier auch mal ԺДヤغΪրά൬ཛ.txt. Ich weiss nicht, was du genau vorhast, aber in einem UTF-16-Codierten String via Indizes rumzufummeln ist erstmal auffällig. ich hoffe du weisst was du tust. Willst du alle nicht erlaubten Zeichen aus dem Dateinamen löschen?

    Ob es allerdings "gut"/ "schön"/ "richtig" ist, das ist noch einmal eine ganz andere Frage:

    Allerdings. Da gibts schon ein paar Dinge anzumerken:

    std::set<char16_t> allowed1;
    

    Ein std::set ist O(logn)\mathcal{O}(\log n) für die Query, ein std::unordered_set nur O(1)\mathcal{O}(1) und daher wahrscheinlich eine bessere Wahl...

    allowed1.insert(...);
    ...
    for (int i = 0; i < last; i++) {
    ...
       if (allowed1.count(p2[i]) == 0)
       ...
    

    ... besonders, wenn du auch noch ein std::set::count für jedes Zeichen machst. Verwende lieber std::(unordered_)set::find oder std::(unordered_)set::contains (ab C++20), dann kommt nicht noch ein linerarer Aufwand bezüglich der Anzahl der Elemente obendrauf.

    Der Code schreit allerdings nahezu nach einem simplen std::regex_replace (siehe unten). Vielleicht solltest du mal schauen, ob das nicht eventuell eine bessere Alternative ist - vorausgesetzt deine Implementierung kann mit UTF-8/16 umgehen.

    auto last = p2.find_last_of(u".");
    for (int i = 0; i < last; i++) {
        if (p2[i] == '&') {
            p2[i] = 'n';
        }
        ...
    

    Nicht jeder Dateiname hat einen Punkt! Dieser Code läuft bei einer Datei dateiname mit einem Out-of-Bounds-Read und potentiell Write vor die Wand. Behandle den Fall, dass std::u16string::find_last_of das gesuchte Zeichen nicht findet.

        for (int i = 0; i < p2.size() - 1; i++) {
          if (p2[i] == '_' && p2[i + 1] == '_') {
            p2.erase(i, 1);
            i--;
          }
        }
    

    Aufeinanderfolgende Unterstriche zu einem kollabieren? Ganz schön umständlich und vor allem wegen dem i-- in der i++-Schleife nicht leicht zu verstehen. Wie wäre stattdessen folgender Ansatz: Anstatt den String zweimal zu kopieren und den Ziel-String "zurechtzufummeln" iterierst du stattdessen einfach durch die Zeichen des Dateinamens und konstruierst währenddessen den Ziel-Dateinamen, z.B. via std::string::append. Dabei übersetzt du deine Zeichen und kannst auch gleich die mehrfachen Unterstriche überspringen, wenn du in der vorherigen Iteration bereits einen eingefügt hast.

    ...
          for (const auto &c : p1)
            std::cout << static_cast<char>(c);
          std::cout << " -> ";
          for (const auto &c : p2)
            std::cout << static_cast<char>(c);
    ...
    

    Ich habe mich gerade gefragt, was das für ne komische Ausgabe-Methode ist und nach etwas herumspielen festgestellt, dass weder cout- noch wcout-Ausgabe für u16string und auch nicht für u8string unterstützt werden. Auch haben viele Funktionen der Standardbibliothek keine Overloads für diese Typen (WTF?) ...

    Die C++-Unterstützung für diese Strings scheint mir bemitleidenswert (lasse mich gerne eines besseren belehren). Ich würde daher empfehlen, auf diese zu verzichten und sich auf std::string/std::wstring zu beschränken. Die Pfade kannst du so in einen (w)string kopieren:

    auto u8path = entry.path().filename().u8string();
    // std::string hat keinen Konstruktor, der einen std::u8string entgegennimmt, 
    // aber DAS geht witzigerweise:
    std::string path{ std::begin(u8path),  std::end(u8path) };
    std::cout << path << std::endl;
    

    bzw.

    auto u16path = entry.path().filename().u16string();
    std::wstring path{ std::begin(u16path),  std::end(u16path) };
    std::wcout << path << std::endl;
    ...
    

    Man kann UTF-8/16 auch durchaus in einem (w)string ablegen, man muss sich nur bewusst sein, dass bei beiden Codierungen ein "Zeichen" aus mehreren Code Points (char/char8_t/wchar_t/char16_t) bestehen kann. Direkte Manipulation dieser via s[i] sollte man daher vermeiden wenn man nicht genau weiss was man tut. Nur dass das klar ist: Auch UTF-16 ist eine Multibyte-Codierung, bei der ein "Zeichen" mehr als einen "char" haben kann.

    Windows und MinGW+GCC ist auch noch eine weitere unheilige Allianz, was Zeichencodierungen angeht - kein Wunder wenn du da auf Probleme stösst. Unter Linux würde das alles ein bisschen mehr zumindest "gefühlt out-of-the-box" funktionieren, da dort Konsole und System-APIs UTF-8 verwenden und man das meistens nicht einmal merkt, wenn man einfach stur std::string verwendet.

    Ich persönlich bevorzuge auch aus diesen wie auch aus Portabilitätsgründen UTF-8. Deine Dateinamen-Übersetzung würde ich daher so lösen - auch wenn man (leider) der Windows-Konsole erstmal etwas umständlich verklickern muss, dass man UTF-8 ausgeben möchte (Wichtig: Quellcode-Datei muss als UTF-8 gespeichert werden, damit das so wie hier funktioniert):

    #include <iostream>
    #include <string>
    #include <regex>
    
    #if defined(_WIN32)
        #if !defined(WIN32_LEAN_AND_MEAN)
            #define WIN32_LEAN_AND_MEAN
        #endif
        #if !defined(NOMINMAX)
            #define NOMINMAX
        #endif    
        #include <Windows.h>
    #endif
    
    auto translate(const std::string& file_name) -> std::string
    {
        static const std::regex regex{ "[^a-zA-Z0-9\\(\\)-_]+" };
        return std::regex_replace(file_name, regex, "_");
    }
    
    auto main() -> int
    {
        #ifdef _WIN32
            auto previous_cp = GetConsoleOutputCP();
            SetConsoleOutputCP(CP_UTF8);
        #endif
        std::string file_name = "צּՁぉŌ両dateinameצּՁぉŌ両";
        std::cout << file_name << " -> " << translate(file_name) << std::endl;
        #ifdef _WIN32
            SetConsoleOutputCP(previous_cp);
        #endif
    }
    

    Demo: https://godbolt.org/z/KfbGPcddT

    Falls du ausschliesslich ASCII-Zeichen erlauben willst (die alle keine Multibyte-Zeichen sind und durch nur je einen char repräsentiert werden), sollte dieser-Ansatz mindestens so gut sein wie eine selbstgestrickte Implementierung, auch wenn std::regex kein UTF-8 unterstützen sollte. Du solltest nur darauf verzichten, im Regex-Ausdruck irgendwelche Nicht-ASCII-Zeichen zu verwenden, das macht dann eventuell nicht das, was du erwartest.

    Die Directory-Schleife in deinem Programm könnte dann z.B. so aussehen:

    ...
    for (const auto &entry : fs::directory_iterator(path)) 
    {
        auto entry_path_u8string = entry.path().filename().u8string();
        std::string entry_path_string{ std::begin(entry_path_u8string), std::end(entry_path_u8string) };
        std::string translated_entry_path_string = translate(entry_path_string);
        std::cout << entry_path_string << " -> " << translated_entry_path_string << std::endl;
        fs::rename(
            entry.path(),
            std::u8string{ 
                std::begin(translated_entry_path_string),
                std::end(translated_entry_path_string)
            }
        );
    }
    

    Beachte, dass ich hier den übersetzten Dateinamen wieder explizit in einen std::u8string konvertiere, damit std::filesystem::path auch weiss, dass der UTF-8-kodiert ist und keine komischen Sachen veranstaltet.



  • Danke erst mal ...

    Die Idee ist folgende: Alle "Sonderzeichen" durch _ ersetzen, die im Dateinamen (ohne Dateiendung dabei) vorkommen; &-Zeichen durch n ersetzen, und mehrere aufeinanderfolgende _ durch genau ein _ ersetzen (also fast eine Art trim ...). Danach soll der Dateiname umbenannt werden, insofern es eine Änderung gibt.

    ... Dann werde ich mir mal unordered_set bzw. regex_replace genauer ansehen.

    Ja, das mit den u16strings ist sehr merkwürdig ... Charles Petzold hat in "Windows-Programmierung" dem ein eigenes Kapitel gewidmet, in dem es ausschließlich um die Zeichenkodierung geht. (Hab ich aber (noch) nicht gelesen)



  • Haaa, weil heute Karneval ist, und mir die Närrinnen und Narren etwas auf den Sack gehen ... mal eine etwas abwegige Frage: Weshalb hat mal die unordered_set eigentlich unordered_set genannt - und nicht etwa "hash_set", womit ich intuitiv eher gerechnet hätte? Ich hatte eigentlich nur auf die set (bzw. map) "zurückgegriffen", weil ich eine "hash_set" nicht ad hoc gefunden hatte ... Sind denn alle "Kollektionen/Behälter" nach ihrer abstrakt größtmöglichen unique Eigenschaft benannt worden - oder ist hier etwas logisch "inkosequent"? ... Oder ist das das Resultat des historisch gewachsenen Breis?



  • Haaa, weil heute Karneval ist

    Was ist Karneval?

    Weshalb hat mal die unordered_set eigentlich unordered_set genannt - und nicht etwa "hash_set", womit ich intuitiv eher gerechnet hätte?

    Ich vermute mal, damit besser klar wird, dass eine Iteration keine garantierte Reihenfolge hat - genau wie bei der unordered_map. Damit kommt dann hoffentlich niemand auf die Idee, dass eine Reihenfolge vielleicht doch immer gleich ist (z.B. weil das in einer bestimmten Implementierung so sein könnte). Python hat zum Beispiel die Klasse dict - und OrderedDict. Allerdings sind seit Version 3.6 auch normale dict "ordered", d.h. behalten ihre Reihenfolge. Durch das unordered im Namen ist unmissverständlich klar, dass das nicht garantiert wird.



  • @Fragender sagte in Ein Problem mit string:

    Weshalb hat mal die unordered_set eigentlich unordered_set genannt - und nicht etwa "hash_set", womit ich intuitiv eher gerechnet hätte?

    Ja, den Namen finde ich auch etwas verschroben. Ich hätte die beiden wahrscheinlich einfach tree_set und hash_set genannt. Wie @wob schon sagte, der Name weist vermutlich darauf hin, dass ein Hash Set im Vergleich zu einem, das auf einem Suchbaum basiert, keine intrinsische Ordnung hat. Einen Suchbaum kann man via Tiefensuche abschreiten (wie es dessen Iterator z.B. vermutlich tut) und erhält damit die Elemente in der Reihenfolge ihres Ordnungskriteriums für diesen Baum.

    Vielleicht wollte man damit auch hervorheben, dass unordered_set auch für Objekte geeignet ist, die sich nicht in eine sinnvolle lineare Ordnung bringen lassen oder für die es aus anderen gründen keinen operator< oder std::less gibt. Das ist eine Voraussetzung, um einen Typ mit std::set verwenden zu können.



  • Ok, Danke für eure Erklärungen. 🙂

    Ich bin jetzt einen ganz anderen Weg gegangen, und verwende für das File-Renaming ein Java-Programm... Das kann ich ja auch auf Linux und auf Windows ausführen... Das ist vielleicht nicht ganz Sinn der Sache, aber es funktioniert auch und es ist etwas "robuster" bzw. "resilienter", hinsichtlich der Dateinamen. 😕

    import javax.swing.*;
    import java.awt.*;
    import java.io.File;
    import java.io.IOException;
    import java.nio.file.Files;
    import java.nio.file.Path;
    import java.nio.file.StandardCopyOption;
    import java.util.ArrayList;
    import java.util.Arrays;
    
    public class Main {
        private static void dryRun(JTextArea area, boolean dry) {
            final String fn1 = "C:\\Users\\x\\Music\\yt-dlp\\";
            final String fn2 = "D:\\yt-dlp\\";
            ArrayList<String[]> map = new ArrayList<>();
            File[] files = new File(fn1).listFiles();
            assert files != null;
            for (File f : files) {
                String fn = f.getName();
                if (fn.endsWith(".mp3")) {
                    String prefix = fn.substring(0, fn.lastIndexOf('.'));
                    prefix = prefix.replaceAll("&", "n");
                    prefix = prefix.replaceAll("[^\\-a-zA-Z0-9()_]", "_");
                    prefix = prefix.replaceAll("_{2,}", "_");
                    map.add(new String[]{f.getAbsolutePath(), fn2 + prefix + ".mp3"});
                }
            }
            for (String[] sa : map) {
                area.append(Arrays.toString(sa) + "\n");
            }
            area.append("\n");
    
            if (!dry) {
                try {
                    for (String[] sa : map) {
                        if (!new File(sa[1]).exists()) {
                            area.append(String.format("Copy %s to %s ...%n", sa[0], sa[1]));
                            Files.copy(Path.of(sa[0]), Path.of(sa[1]), StandardCopyOption.REPLACE_EXISTING);
                        }
                    }
                    area.append("Success!\n\n");
                } catch (IOException ioException) {
                    area.append("An io exception occurred!\n\n");
                }
            }
        }
    
        public static void main(String[] args) {
            JFrame frame = new JFrame("FileCopy and FileSync 1");
            JTextArea area = new JTextArea("Hallo\n\n");
            JPanel panel = new JPanel(new GridLayout(1, 2));
            JButton button1 = new JButton("Dry run");
            JButton button2 = new JButton("Copy!");
            panel.add(button1);
            panel.add(button2);
            frame.add(new JScrollPane(area));
            frame.add(panel, BorderLayout.SOUTH);
            frame.setSize(400, 400);
            frame.setDefaultCloseOperation(JFrame.DISPOSE_ON_CLOSE);
            frame.setVisible(true);
    
            button1.addActionListener(e -> dryRun(area, true));
            button2.addActionListener(e -> dryRun(area, false));
        }
    }
    

    Eigentlich ist nur Zeile 22 bis 24 interessant. Wenn ich das in C++ hinbekommen würde, wäre das super.



  • @Fragender sagte in Ein Problem mit string:

    Eigentlich ist nur Zeile 22 bis 24 interessant. Wenn ich das in C++ hinbekommen würde, wäre das super.

    Hab ich dir nicht das Beispiel mit std::regex geschrieben? Das ist die C++-Version davon. Übrigens hatte mein Regex noch ein + am Ende, das matcht dann beliebig viele (mehr als 0) aufeinanderfolgende unzulässige Zeichen. Damit kannst du dir die Ersetzung der mehrfachen Unterstriche in der nächsten Zeile sparen, da eben beliebig viele durch nur einen Unterstrich ersetzt werden - es werden also eh keine mehrfachen Unterstriche erzeugt.

    Damit das funktioniert, würde ich das Regex wie folgt umschreiben: "[^\\-a-zA-Z0-9\\(\\)]+" (ich glaube Klammern müssen auch regex-escaped werden). Das hat das + am Ende und der Unterstrich ist im Regex kein zulässiges Zeichen. Das hat den Effekt, dass auch bereits im Dateinamen vorhandene Unterstriche zu nur einem einzigen kollabiert werden. Also z.B. ÄÖÜ____datei -> _datei direkt beim ersten replaceAll bzw. std::regex_replace in C++.


Anmelden zum Antworten