Char-array: Alternative zu strtok, das nur auf den delimiter reagiert

Hallo Zusammen,

in einem ESP empfange ich per UDP Texte in folgenden Formaten:

controlword|no|text
controlword|no|
controlword|

Z. B. "name|1|hallo" oder "date|2|" oder "clear|"

Das char-array packetBuffer beinhaltet den gesamten Text.
Zum Teilen in die drei Teiltexte nutze ich strtok.

    char* controlword = strtok(packetBuffer, "|");
    char* number = strtok(NULL, "|");
    char* text = strtok(NULL, "");

Leider prüft "strtok" nicht nur auf den Teiler, sondern auch auf \0.
So dass z. B. bei dem Text "name|1" (also ohne letzten Trennzeichen), auch als controlword "name" und als number "1" ausgelesen wird.

Um festzustellen ob und wo sich die Trenner befinden, such ich mir zuvor die Trenner im packetBuffer.
(natürlich vor dem strtok-Block, weil danach packetBuffer leer wäre)

    int pos_sep_1 = 0;
    int pos_sep_2 = 0;
    bool last_char_is_sep = false;

    const char* sep_1 = strchr(packetBuffer, '|');
    pos_sep_1 = sep_1 - packetBuffer + 1;

    if (pos_sep_1 > 0)
    {
        pos_sep_2 = strchr(sep_1 + 1, '|') - packetBuffer + 1;
        last_char_is_sep = strlen(packetBuffer) == pos_sep_2;
    }

Dementsprechend wird dann im Nachgang geprüft, ob die Trenner vorhanden waren.
Falls nicht, wird der Teiltext geleert

    if (pos_sep_1 <= 0)
    {
        memset(controlword, NULL, sizeof(controlword));   //clear array
    }

    if (text == NULL)
    {
        if ((number != NULL) && !last_char_is_sep)
            memset(number, NULL, sizeof(number));   //clear array
    }

Das Ganze könnte "Trenner suchen", "Array leeren", etc. könnte ich mir sparen, würde strtok nur nach dem Trenner suchen.
Aber (leider wird auch ein "\0" wird zum Trennen verwendet.
(strtok ersetzt den Trenner gegen "\0", und gibt den Teil des Arrays bis zum "\0" aus und merkt sich den Pointer für spätere Aufrufe)

Gibt es eine Alternative zu strtok die NUR den Trenner nutzt und nicht auch noch "\0"?

viele Grüße

\0 ist in C das Kennzeichen für ein Stringende.

Ich versteh Dein Problem nicht.
Das Char-Array hat ein

  • Label: "name"
  • einen Trenner
  • Value: "1"

Damit ist klar, dass Du name und Value trennen kannst

Bei

  • label: "clear"
  • trenner
  • no value
    wird der Trenner zu NULL und number bleibt leer.

Kann es sein, dass Du nie weisst, wieviele Trenner in Deinem Puffer sind?
Dann ist STROK der ungünstigste Ansatz.

Du könntest...
... auf das Label reagieren:

    char* controlword = strtok(packetBuffer, "|");
if ((!strncmp(controlword, labeltext, _size) ||
   (!strncmp(controlword, labeltext2, _size2))
{
    char* number = strtok(NULL, "|");
}
if (!strncmp(controlword, labeltext, _size)
{
    char* text = strtok(NULL, "");
}

und das ganze beim splitten bereits zuordnen.

(Von unterwegs geschrieben - ist halt nur eine Idee, die mir da im Kopfe rumgeht)

Es sind maximal 2 Trenner, die es geben kann.

Wen aber z. B. nur das controlword kommt, also ohne Trenner, so bekomm ich von strtok trotzdem den Text (aus dem Beispiel: name").
In diesem Falle möcht ich aber keinen Text bekommen.
Dazu suche ich mittels strchr den Trenner, damit ich weiß, ob er da ist.
Das würd ich mir halt gerne ersparen.

strok() macht als erstes aus dem Trenner ein \0 und damit ist das nicht zu ändern.
Du bekommst doch fertige Zeichenketten.
Lege Dir ein Array mit Deinen Controlwörtern an.
Prüfe mit strncmp() ob die Zeichenkette mit einem Controlword (oder einem Teil davon) beginnt.
Wenn ja, verwerfe das splitten.
(Ist wie da oben gezeigt, nur eben nicht nach dem ersten strok() sondern schon davor)

Ich hab noch ne Idee.
Du könntest mit strlen() die Länge ermitteln und darauf prüfen, ob buffer[strlen()] Dein Trennzeichen ist.

In dem Fall (kein Trenner im Text) solltest Du aber beim zweiten strtok()-Aufruf einen Zeiger auf einen leeren Text NULL-Pointer bekommen. Genügt das nicht, um eindeutig festzustellen, dass es nur das Controlword war?

Statt nach dem Trenner zu suchen, number testen:

    char* controlword = strtok(packetBuffer, "|");
    char* number = strtok(NULL, "|");
    if (number != NULL) {
        // es ist wirklich was da
        char* text = strtok(NULL, "");
        ...

Hallo,

ist schon eine Weile her mit Parser.

Hiermit prüfst du auf "nichts"
char* text = strtok(NULL, "");
also wird es von strtok ausgewertet.

Lasse strtok nur auf den Delimiter "|" prüfen. Das Ergebnis prüfst du dann immer! auf ungleich NULL für den Abbruch, weil der Null-Terminator erreicht wurde.

Das geänderte hier ...

#include <stdio.h>
#include <string.h>

int main() {
  char myStr[] = "Learn|C++|at|W3schools";
  char *myPtr = strtok(myStr, "|");
  while (myPtr != NULL) {
    printf("%s\n", myPtr);
    myPtr = strtok(NULL, "|");
  }
  return 0;
}

… auf w3schools einmal anschauen.

Das heißt du musst nach jeder Trennung/Aufruf mit strtok eine Ergebnisprüfung ungleich NULL durchführen.

das würde gehen, wenn number immer vorhanden wäre
aber wie ich schon im ersten Post geschrieben habe, gibt es drei verschiedene Formate:

  1. controlword - trenner
  2. controlword - trenner - number - trenner
  3. controlword - trenner - number - trenner - text

Bei den ersten beiden möchte ich mit Sicherheit wissen, dass am Ende der Trenner vorhanden ist:
Im ersten Fall würde "controlword" auch ohne Trenner ausgelesen werden (über das erste strtok) und im zweiten Fall "number" ohne Trenner (über das zweite strtok).

Nur möchte ich, dass "controlword" und "number" nur ausgelesen werden, wenn diese auch ein Trenner jeweils abgeschlossen wurden.
Und das geht leider mit allein mit strtok nicht, da ein \0 am Ende des arrays den fehlenden Trenner am Ende ersetzt.

Daher würde folgendes auch fehlerfrei ausgelesen werden:

  1. controlword
  2. controlword - trenner - number

Also ohne fehlenden Trenner am Ende.

Auf das Nicht-vorhanden sein von "number" kann ich nicht prüfen, da es diese Konstellation geben kann.
Ebenso nicht auf das Nicht-vorhanden sein von "text", da auch dieser nicht immer vorhanden sein muss.

Deswegen lese ich mir ja die Trenner aus.
Das wollt ich aber weglassen.
Aber

Hmmm - ja, verstanden.
Das Protokoll Deines Senders ist inkonsequent | unlogisch.
Korrekt wäre, auch nach text noch einen Trenner zu senden oder - noch besser - einen Trenner vor der abschließenden \0 grundsätzlich zu vermeiden. Lässt sich das ändern?

Sonst fällt mir jetzt auch nix mehr ein.

Hallo,

lies alles komplett ein. Bestimme die Länge vom gesamten String. Prüfe ob das letzte Zeichen ein Delimiter ist. Wenn ja, lasse alles verarbeiten, wenn nein, verwirf alles.

Habe ich ihm in #6 schon angeboten.

Damit bin ich dann hier auch raus.

Hallo,

upps, Entschuldigung, dass hatte ich übersehen.
Das ist/wäre die einfachste Lösung.

Die gesamte Handhabung kann sich der TO auch mittels String statt char Array auf dem ESP leichter machen. Ausreichend große (Länge) String Variable mit reserve() anlegen.

ich muss @wno158 recht geben. Wenn das dritte Token auch mit einem Trenner abgeschlossen würde, würde das die Aufgabe etwas vereinfachen.

Da ein ESP verwendet wird, steht einem ja auch die Standard Library zur Verfügung, dann kann man die ja auch benutzen.

#include <sstream>
#include <iostream>
#include <vector>

int checkTokens(std::vector<std::string>& list, const char* buffer, const char delemitter) {
  constexpr unsigned maxToken {3};

  std::stringstream check(buffer);
  std::string intermediate;
  unsigned numDelemitters {0};

  // count number of delemitters
  while (*buffer) {
    (*buffer == delemitter) && ++numDelemitters;
    ++buffer;
  };
  while (std::getline(check, intermediate, delemitter)) { list.push_back(intermediate); }

  // compare number of tokens with delemitters
  if (numDelemitters != list.size() && list.size() < maxToken) { list.pop_back(); }
  return list.size();
}

void setup() {
  const char* packetBuffer {"name|1|hallo"};
  std::vector<std::string> tokens;

  checkTokens(tokens, packetBuffer, '|');
  for (auto& tk : tokens) { std::cout << tk << "\n"; }
}

void loop() {}

Bei diesem Beispiel kann man sich die Tokens aus dem Vektor holen wie man es braucht.
Ein Token bleibt nur im Vektor, wenn er von einem vorgegebenen Trennzeichen abgeschlossen wird, außer der dritte Token. Der wird auch übernommen, wenn kein oder ein Trennzeichen folgt.

Hallo,

naja, die Aufgabe ist, nur wenn das letzte Zeichen ein '|' ist, soll überhaupt verarbeitet werden. Deswegen kann man alles verwerfen wenn das nicht der Fall ist.

JA eben nicht ... der TO will ja das letzte (dritte) Token nicht mit einem Trennzeichen abschließen, was halt logisch etwas inkonsistent ist. Nach deiner Aussage dürfte das dritte Token dann nicht verarbeitet werden.

oder:

??

womit wird der Trenner abgeschlossen?

Ich glaube, dass das selbst der Letze hier begriffen hat.

Mein Rat:
Ein Parser ist ein einfacher endlicher Automat.

Sobald du das Problem so beschreiben kannst, dass es zur Losung führt, kannst du auch die Lösung hinschreiben, bzw, in C++ gießen.

Hallo,

ich verstehe ihn so. Das letzte Trennzeichen '|' kann übertragen werden. Wenn ja, ist das okay und die Übertragung gültig. Wenn das fehlt ist die Übertragung ungültig. So verstehe ich ihn. Es müssen auch nicht alle Variablen des Protokolls übertragen werden. Wichtig ist, dass das letzte Zeichen ein '|' ist, sonst ist alles ungültig.
Das Protokoll ist zugegebenermaßen Gaga aber machbar.
wno158 hat schon recht. Ich würde wenn möglich das Protokoll überdenken.
Für meine Modelleisenbahn habe ich ein Protokoll mit einem Kommando und optional ein oder zwei Integer, müssen aber nicht. Trennzeichen ist ein Leerzeichen.

Ich verstehe das ein bisschen anders. Nach „text“ kommt kein Trenner mehr.
Aber jeder, außer dem TO, ist der Meinung, dass es anders besser wäre :wink:

Nach nochmaligem Durchlesen des Eingangsposts habe ich noch einen.
Das hier beschreibt das "Protokoll":

Dann ist doch eigentlich das beschriebene Problem ein Verstoß gegen die Vorschrift, denn ein String nur aus 'controlword' und 'no' ohne abschließenden Trenner ist gar nicht vorgesehen:

Dann ist doch vielleicht die Bedingung für eine solche ungültige Nachricht zweiteilig:

  1. Das letzte Zeichen der Übertragung ist nicht '|'
  2. text ist NULL
Code


void checkPacket(char* packetBuffer) {
  Serial.println();
  Serial.print("test result for '");
  Serial.print(packetBuffer);
  Serial.println("':");

  bool isLastCharTrenner = packetBuffer[strlen(packetBuffer)-1] == '|';
  char* controlword = strtok(packetBuffer, "|");
  char* number = strtok(NULL, "|");
  char* text = strtok(NULL, "");

  Serial.print("   controlword: ");
  Serial.println(controlword);
  Serial.print("        number: ");
  Serial.println(number);
  Serial.print("          text: ");
  Serial.println(text);

  Serial.print("--> ");
  if (!isLastCharTrenner && text == NULL)
    Serial.print("NOT ");
  Serial.println("valid");
}

void setup() {
  char buffer[64];
  Serial.begin(115200);

  strcpy(buffer, "controlword|no|text");
  checkPacket(buffer);

  strcpy(buffer, "controlword|no|");
  checkPacket(buffer);

  strcpy(buffer, "controlword|");
  checkPacket(buffer);

  strcpy(buffer, "controlword|no|text|");
  checkPacket(buffer);

  strcpy(buffer, "controlword|no");
  checkPacket(buffer);

  strcpy(buffer, "controlword");
  checkPacket(buffer);
}

void loop() {
  // put your main code here, to run repeatedly:
}
Ausgabe

test result for 'controlword|no|text':
   controlword: controlword
        number: no
          text: text
--> valid

test result for 'controlword|no|':
   controlword: controlword
        number: no
          text: 
--> valid

test result for 'controlword|':
   controlword: controlword
        number: 
          text: 
--> valid

test result for 'controlword|no|text|':
   controlword: controlword
        number: no
          text: text|
--> valid

test result for 'controlword|no':
   controlword: controlword
        number: no
          text: 
--> NOT valid

test result for 'controlword':
   controlword: controlword
        number: 
          text: 
--> NOT valid

Edit: Mein Versuch hat einen m.E. tolerablen Schönheitsfehler: Auch ein Trenner am Ende von text wird akzeptiert. Code und Ausgabe angepasst.