Icon Ressource

[C] Escapesequenzen in einem String parsen wie in einem Literal

In Stringliteralen werden bestimmte Sequenzen mit vorangestelltem Backslash vom Compiler in das repräsentierte Zeichen konvertiert. Das Betrifft bspw. Steuerzeichen wie \t und \n, aber auch oktale Sequenzen (am bekanntesten \0) oder hexadezimale Sequenzen (z.B. \xFF). Sind solche Zeichenfolgen aber nicht als Literal im Code definiert, sondern werden als String aus einer Datei oder als Argument empfangen, dann passiert diese Konvertierung natürlich nicht.
Nun ist das in der Regel auch gar nicht gewollt. Wer aber beispielsweise versuchen will, spezielle Zeichen, wie '\0' oder Zeichen außerhalb des 7 Bit ASCII Bereiches in einem Kommandozeilenargument an sein Programm zu übergeben, wird damit Probleme bekommen. Eine Möglichkeit wäre hier, dieselben Escapesequenzen zu nutzen und die Funktionalität des Compilers in einer Funktion nachzubauen.

Die folgende ParseEscSeq Funktion tut dies.
escseqparser.h:
#ifndef ESCSEQPARSER_H_INCLUDED__
#define ESCSEQPARSER_H_INCLUDED__

#ifdef __cplusplus
extern "C"
#endif

/** \brief  Replace escape sequences, as used in integer character constants,
*           with their associated characters.
* \param  str     Null-terminated C-string which is modified by replacing escape
*                 sequences with their associated characters.
* \param  endptr  Pointer to an object of type char*, whose value is set by the
*                 function to the address after the end of str. This address
*                 can be used to determine the end of the used memory if the
*                 original string contains "\0" escape sequences and str points
*                 to the first string of several adjacent null-terminated
*                 strings or to binary data. Accessing the memory that the
*                 object points to causes undefined behavior.
*                 This parameter can be NULL, in which case it is not used.
* \param  ignore  A zero value causes the function to fail if an invalid escape
*                 sequence is found. Flags IGNORE_UNKNOWN and IGNORE_RANGE cause
*                 the function to ignore certain invalid escape sequences.
*                 IGNORE_UNKNOWN - The leading backslash of unknown escape
*                  sequences is truncated.
*                 IGNORE_RANGE - Hex and octal values which are out of range are
*                  narrowed to the size of a char.
*                 These flags can be combined using bitwise OR (|).
*  \return  char*  If the function succeeds str is returned which points to the
*           updated string. If the function fails NULL is returned and both the
*           string that str points to and the object that endptr points to
*           remain unchanged. */
char *ParseEscSeq(char *const str, char **const endptr, const unsigned ignore);

/** \brief  Flags used to be passed to the "ignore" parameter of function
*         "ParseEscSeq". */
#define IGNORE_UNKNOWN (0x1U)
#define IGNORE_RANGE (0x2U)

#endif // ESCSEQPARSER_H_INCLUDED__


escseqparser.c:
#include "escseqparser.h"
#include <ctype.h>
#include <errno.h>
#include <limits.h>
#include <stddef.h>
#include <stdlib.h>
#include <string.h>

char *ParseEscSeq(char *const str, char **const endptr, const unsigned ignore)
{
  char *buf = NULL, *ocursor = NULL;
  if (str == NULL || (buf = ocursor = (char *)malloc(strlen(str) + 1)) == NULL)
    return NULL;
  for (char *icursor = str; *icursor != '\0';)
  {
    if (*icursor == '\\')
    {
      switch ((int)(*(++icursor)))
      {
        case '"': case '\'': case '?': case '\\':
        {
          *(ocursor++) = *(icursor++);
        } break;
        case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7':
        {
          char oct[4] = {0}, *stop = NULL;
          *oct = *icursor;
          for (int idx = 1; idx < 3 && (oct[idx] = icursor[idx]) != '\0'; ++idx);
          errno = 0;
          const unsigned long chval = strtoul(oct, &stop, 8);
          if (errno != 0 || stop == oct || ((ignore & IGNORE_RANGE) == 0U && chval > (const unsigned long)UCHAR_MAX))
          {
            free(buf);
            return NULL;
          }
          *(ocursor++) = (const char)(const unsigned char)chval;
          icursor += stop - oct;
        } break;
        case 'a':
        {
          *(ocursor++) = '\a';
          ++icursor;
        } break;
        case 'b':
        {
          *(ocursor++) = '\b';
          ++icursor;
        } break;
        case 'f':
        {
          *(ocursor++) = '\f';
          ++icursor;
        } break;
        case 'n':
        {
          *(ocursor++) = '\n';
          ++icursor;
        } break;
        case 'r':
        {
          *(ocursor++) = '\r';
          ++icursor;
        } break;
        case 't':
        {
          *(ocursor++) = '\t';
          ++icursor;
        } break;
        case 'v':
        {
          *(ocursor++) = '\v';
          ++icursor;
        } break;
        case 'x':
        {
          char *stop = NULL;
          errno = 0;
          const unsigned long chval = strtoul(++icursor, &stop, 16);
          if (errno != 0 || stop == icursor || isxdigit((int)(*icursor)) == 0 || ((ignore & IGNORE_RANGE) == 0U && chval > (const unsigned long)UCHAR_MAX))
          {
            free(buf);
            return NULL;
          }
          *(ocursor++) = (const char)(const unsigned char)chval;
          icursor += stop - icursor;
        } break;
        default:
        {
          if ((ignore & IGNORE_UNKNOWN) == 0U)
          {
            free(buf);
            return NULL;
          }
          else
            *(ocursor++) = *(icursor++);
        } break;
      }
    }
    else
      *(ocursor++) = *(icursor++);
  }
  *(ocursor++) = '\0';
  const ptrdiff_t ospan = ocursor - buf;
  memcpy(str, buf, (const size_t)ospan);
  free(buf);
  if (endptr != NULL)
    *endptr = str + ospan;
  return str;
}

Wie zu sehen ist, ist es durch endptr möglich zu erkennen, wo die geparste Zeichenfolge endet, auch wenn diese '\0' Zeichen beinhaltet.
Weiterhin können 2 Flags zum ignorieren bestimmter Fehler gesetzt werden. Die meisten Compiler ignorieren unbekannte Escapesequenzen (ggf. mit einer Warnung). Das IGNORE_UNKNOWN Flag wäre für ein vergleichbares Verhalten der Funktion zu übergeben.



Der folgende Testcode gibt das empfangene Argument *) aus, parst danach mögliche Escapesequenzen und gibt das Ergebnis aus. Der Code behandelt dabei '\0' Zeichen als Nullterminierungen von Stringelementen eines Arrays.
*) Es ist zu beachten, dass einige Shells Backslashes bereits ebenso als Escapezeichen interpretieren und ggf. schon vor der Übergabe des Arguments ersetzen könnten. Ebenso scheinen die vorgeschalteten Kommandozeilenparser, die das argv Array erstellen unter Linux und Windows unterschiedliche Algorithmen zu verwenden. Insbesondere was Anführungszeichen mit vorangestelltem Backslash anbelangt.

main.c:
#include "escseqparser.h"
#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
  int ret = 1;
  puts("====================");
  if (argc == 2)
  {
    puts(argv[1]);
    char *end = NULL;
    const char *ptr = ParseEscSeq(argv[1], &end, IGNORE_UNKNOWN);
    if (ptr != NULL)
    {
      for (const char *substr = ptr; substr < end; substr += strlen(substr) + 1) // +1 is for the terminating null which needs to be skipped, too
      {
        puts("----------");
        puts(substr);
      }
      ret = 0;
    }
    puts("~~~~~~~~~~");
  }
  puts(ret == 0 ? "SUCCESS" : "ERROR");
  puts("====================");
  return ret;
}

Wird das Programm z.B. mit Argument "a\tb\0abc" aufgerufen, so erhält man folgende Ausgabe:
Testausgabe:
====================
a\tb\0cde
----------
a       b
----------
cde
~~~~~~~~~~
SUCCESS
====================
Autor
german
Aufrufe
2.900
Erstellt am
Letzte Bearbeitung
Bewertung
0,00 Stern(e) 0 Bewertung(en)

Weitere Ressourcen von german

Zurück
Oben Unten