Hlavní navigace

GLib: Lexikální scanner

22. 3. 2001
Doba čtení: 7 minut

Sdílet

Dnešním dílem začíná v našem popisu funkcí knihovny GLib poněkud rozsáhlejší kapitola: lexikální scanner. Lexikální scanner je propracovaný mechanismus usnadňující programování úloh spočívajících ve čtení textu. Užitečný je zejména při vyhodnocování konfiguračních souborů aplikací.

Základní pojmy

V následujícím textu se to bude jenom hemžit symboly, identifikátory, tokeny a podobně, proto bude jistě vhodné tyto pojmy nějakým způsobem vysvětlit.

  • Identifikátor – identifikátorem je pro GScanner jakýkoliv řetězec vyhovující pravidlům pro rozpoznávání identifikátorů. Implicitně toto pravidlo zní: identifikátorem je každý řetězec, jež začíná písmenem abecedy (bez interpunkce) nebo znakem podtržítka („_“) a následuje libovolná kombinace písmen, číslic a podtržítek. Toto pravidlo lze samozřejmě změnit podle vlastních potřeb. V programovacích jazycích jsou identifikátory třeba názvy proměnných.
  • Symbol – symbolem je v řeči GScanner u uži­vatelem definovaný identifikátor speciálního významu. Použiji-li znovu jako příklad programovací jazyky, symboly by zde byla klíčová slova.
  • Komentář – poznámka ve zpracovávaném souboru, která slouží ke zpřehlednění textu a kterou se GScanner vůbec nebude zaobírat – přeskočí ji jako by to byly bílé znaky.
  • Token – zpracovaná část textu, výsledek práce GScanner u. Pod tímto heslem rozumějme jednu primitivní položku textu, kterou GScanner rozpoznal a předal jako návratovou hodnotu. Tokeny reprezentují jednotlivé přečtené symboly, identifikátory, ale i číselné konstanty a ostatní znaky. Jelikož ve své prapodstatě jsou to obyčejné integerové hodnoty, neměl by být problém jejich význam rozpoznávat a příslušně na ně reagovat.

Jak GScanner pracuje

Lexikální scanner GScanner převádí (tokenizuje) čtený text na jednotlivá slova/symboly (tokeny), tj. rozpoznává jednotlivé atomické prvky textu a předává je v předupravené formě k dalšímu zpracování. Při tomto procesu se uplatňují různá nastavitelná pravidla, která celý proces řídí a omezují programátorovo úsilí pouze na interpretaci načtených dat.

Jinými slovy, GScanner na žádost programátora postupně čte vstupní textová data tak, že výsledkem každé operace čtení je jeden token (identifikátor, symbol, znak, číslo…) – viz obrázek 1. Jak se ale má na jednotlivé tokeny reagovat, to musí programátor vymyslet sám.

Obrázek 1 - Práce GScanneru

Obrázek 1 – Práce GScanner u

Dá se říci, že zpracovává textu pomocí GScanner u probíhá zhruba podle následujícího zjednodušeného scénáře:

  1. vytvoření nového GScanner u,
  2. konfigurace chování GScanner u při zpracovávání textu a načtení symbolů do tabulky symbolů GScanner u,
  3. příprava na čtení textu,
  4. čtení a interpretace všech jednotlivých tokenů,
  5. uvolnění GScanner u z pa­měti.

Při čtení zpracovávaného textu se rozpoznávají jednotlivá lexikální primitiva a vracejí se ve formě tokenů. Mějme například text: „ Letos je rok 2001.“. Jak jej GScanner rozloží na tokeny?

Letos je rok 2001.
|     |  |   |   |
|     |  |   |   V
|     |  |   V   G_TOKEN_CHAR, hodnota: '.'
|     |  V   G_TOKEN_INT, hodnota: 2001
|     V  G_TOKEN_IDENTIFIER, hodnota: "rok"
V     G_TOKEN_IDENTIFIER, hodnota: "je"
G_TOKEN_IDENTIFIER, hodnota: "Letos"

Pokud bychom však GScanner nastavili tak, aby integerové hodnoty převáděl na float a do tabulky symbolů GScanner u bychom pod názvem „ symbol_je“ vložili identifikátor „ je“, byl by výstup následující:

Letos je rok 2001.
|     |  |   |   |
|     |  |   |   V
|     |  |   V   G_TOKEN_CHAR, hodnota: '.'
|     |  V   G_TOKEN_FLOAT, hodnota: 2001.0
|     V  G_TOKEN_IDENTIFIER, hodnota: "rok"
V     G_TOKEN_SYMBOL, hodnota: "symbol_je"
G_TOKEN_IDENTIFIER, hodnota: "Letos"

Ale dost už teoretického tlachání, pojďme si na nějakém příkladu ukázat, jak se s  GScanner em pracuje.

Ukázka práce s  GScanner em

#include <glib.h>

/* nejaky text, ktery dame GScanneru ke zpracovani */
static const gchar *test_text = (
  "/* Toto je testovaci textovy retezec. */ "
  "delka = 350; "
  "vyska = 120.5; "
  "sirka = 2.17; "
  " "
  "/* zmenime hodnotu sirky */ "
  "sirka = 3; "
);

/* definice vyctovych konstant reprezentujicich specialni symboly */
enum {
  SYMBOL_DELKA = G_TOKEN_LAST + 1,
  SYMBOL_VYSKA = G_TOKEN_LAST + 2,
  SYMBOL_SIRKA = G_TOKEN_LAST + 3
};

/* promenne, jejichz hodnoty se prectou z textu */
static gfloat delka = 0;
static gfloat vyska = 0;
static gfloat sirka = 0;


/* precte a zpracuje jeden radek */
static guint cti(GScanner *scanner)
{
  guint symbol;

  /* ocekavame symbol */
  g_scanner_get_next_token(scanner);
  symbol = scanner->token;
  if ((symbol < SYMBOL_DELKA) || (symbol > SYMBOL_SIRKA)) {
    /* neprecetl se symbol - vrat token,
     * ktery jsme ocekavali, ale na vstupu nebyl */
    return G_TOKEN_SYMBOL;
  }

  /* ocekavame '=' */
  g_scanner_get_next_token(scanner);
  if (scanner->token != '=') {
    /* neprecetlo se rovnitko - vrat token,
     * ktery jsme ocekavali, ale na vstupu nebyl */
    return '=';
  }

  /* ocekavame float hodnotu (integerove hodnoty
   * jsou konvertovany na float automaticky) */
  g_scanner_get_next_token(scanner);
  if (scanner->token != G_TOKEN_FLOAT) {
    /* neprecetl se float - vrat token,
     * ktery jsme ocekavali, ale na vstupu nebyl */
    return G_TOKEN_FLOAT;
  }

  /* koukneme se dale, je-li tam ';' */
  if (g_scanner_peek_next_token (scanner) != ';')
    {
      /* strednik nenalezen, precteme spatny token
       * a koncime s chybou - vracime token,
       * ktery jsme ocekavali, ale na vstupu nebyl */
      g_scanner_get_next_token(scanner);
      return ';';
    }

  /* vsechno dopadlo dobre - priradime 
   * prectenou hodnotu prislusne promenne */
  switch (symbol) {
    case SYMBOL_DELKA:
      delka = scanner->value.v_float;
      break;
    case SYMBOL_VYSKA:
      vyska = scanner->value.v_float;
      break;
    case SYMBOL_SIRKA:
      sirka = scanner->value.v_float;
      break;
  }

  /* na vstupu je jeste porad ';' - musime ho precist */
  g_scanner_get_next_token (scanner);

  /* koncime s uspechem */
  return G_TOKEN_NONE;
}


/* hlavni program */
int main(void)
{
  GScanner *scanner;
  guint ocekavany_token;

  /* vytvoreni GScanneru */
  scanner = g_scanner_new(NULL);

  /* nastaveni vlastnosti GScanneru podle nasich potreb: */

  /* automaticky prevadej oktalni, hexadecimalni
   * hodnoty na G_TOKEN_INT */
  scanner->config->numbers_2_int = TRUE;

  /* automaticky prevadej integerove hodnoty na float */
  scanner->config->int_2_float = TRUE;
  
  /* nevracej G_TOKEN_SYMBOL, ale primo hodnotu symbolu
   * (tedy napr. primo SYMBOL_VYSKA apod.) */
  scanner->config->symbol_2_token = TRUE;

  /* nacteni symbolu do tabulky symbolu */
  g_scanner_add_symbol(scanner, "delka",
      GINT_TO_POINTER(SYMBOL_DELKA));
  g_scanner_add_symbol(scanner, "vyska",
      GINT_TO_POINTER(SYMBOL_VYSKA));
  g_scanner_add_symbol(scanner, "sirka", 
      GINT_TO_POINTER(SYMBOL_SIRKA));

  /* budeme cist z textoveho bufferu */
  g_scanner_input_text(scanner, test_text, strlen(test_text));

  /* pojmenujeme vstup pro obsluhu chybovych hlaseni */
  scanner->input_name = "testovaci text";


  /* scanovaci cyklus - neustale cteme vstup az do konce
   * nebo dokud scanner neohlasi chybu nebo dokud nase
   * rutina parse() neohlasi chybnou syntaxi */
  do {
    /* cteme jeden radek */
    ocekavany_token = cti(scanner);
    
    /* koukneme se co je dal 
     * (neni-li tam konec souboru (G_TOKEN_EOF)) */
    g_scanner_peek_next_token(scanner);
  } while (ocekavany_token == G_TOKEN_NONE &&
           scanner->next_token != G_TOKEN_EOF &&
           scanner->next_token != G_TOKEN_ERROR);

  /* v pripade neuspechu vypis chybove hlaseni */
  if (ocekavany_token != G_TOKEN_NONE) {
    g_scanner_unexp_token(scanner, ocekavany_token,
        NULL, "symbol", NULL, NULL, TRUE);
  }

  /* koncime - uvolneni GScanneru z pameti */
  g_scanner_destroy(scanner);

  /* tisk vysledku */
  g_print("delka: %f ", delka);
  g_print("vyska: %f ", vyska);
  g_print("sirka: %f ", sirka);

  return 0;
}

Ačkoli je ukázkový příklad docela dlouhý, není zase tak obtížný. Nedělejte si těžkou hlavu, pokud hned všechno nepochopíte. Veškerým aspektům práce s  GScanner em se ještě budeme věnovat.

Program má na starosti čtení konfiguračního záznamu (pro jednoduchost je to textový buffer test_text), ve kterém jsou informace uspořádány do tvaru „ <SYMBOL> = <HODNOTA>;“. GScanner bude rozpoznávat tři symboly:

  • delka“ ( SYMBOL_DELKA)
  • vyska“ ( SYMBOL_VYSKA)
  • sirka“ ( SYMBOL_SIRKA)

Zavedeme si pro ně výčtové konstanty a statické proměnné, do kterých se budou ukládat získané hodnoty.

Na začátku programu se vytvoří nová instance GScanner u a provedou se různá nastavení. Dojde také k „zaregistrování“ symbolů do tabulky symbolů GScanner u (voláním funkce  g_scanner_add_symbol()).

Voláním funkce g_scanner_input_text() scanneru sdělíme, že chceme, aby četl z textového bufferu test_text, a vrhneme se na čtení.

Čtení a zpracovávání textu je vykonáváno pomocí cyklu do/while a funkce cti(). Tuto funkci jsme sestavili tak, aby zpracovala vždy jednu konstrukci „ <SYMBOL> = <HODNOTA>;“ a vrátila G_TOKEN_NONE jako indikaci, že vše proběhlo v pořádku. V případě syntaktické chyby bude naopak vracet token, který byl očekáván, ale na vstupu se neobjevil.

Cyklus do/while tedy zavolá funkci cti() a její výsledek uloží do proměnné ocekavany_token. Voláním g_scanner_peek_nex­t_token() se podíváme, jaký je následující token. Pokud je různý od G_TOKEN_EOF (konec souboru), G_TOKEN_ERROR (chyba při čtení) a dokud je ocekavany_token roven G_TOKEN_NONE (funkce cti() proběhla bez problémů), dojde k opakování cyklu a tedy zpracování dalšího „příkazu“.

Nyní se podívejme, jak pracuje samotná cti(). Postupně čte jednotlivé tokeny a kontroluje, jestli jsou to ty, které očekává. V případě chyby okamžitě končí a vrací token, který na vstupu marně hledala. Postupně se tak načte nějaký symbol, rovnítko „ =“, float-hodnota a středník. Nakonec se podle druhu symbolu přiřadí přečtená hodnota patřičné proměnné.

Při práci s  GScanner em jsem používal ke čtení dvou funkcí: g_scanner_get_next_token() a g_scanner_peek_nex­t_token(). Jaký je v nich rozdíl? Zatímco ta první „skutečně“ přečte token a nastaví vnitřní ukazovátko pozice čtení za něj, druhá funkce se jen „podívá“, jaký je následující token, ale ukazovátko ponechá před ním.

root_podpora

Konec programu je už triviální. Po skončení čtecího cyklu do/while se v případě chyby vypíše hlášení, scanner se uvolní a vypíšou se hodnoty proměnných.

A to by mohlo pro začátek stačit. Doufám, že jste si na uvedeném příkladu udělali obrázek, jak se se scannerem pracuje a příště se na něj podíváme trochu více zblízka.

Byl pro vás článek přínosný?

Autor článku

Michal Burda vystudoval informatiku a aplikovanou matematiku a nyní pracuje na Ostravské univerzitě jako odborný asistent. Zajímá se o data mining, Javu a Linux.