Hallo,
ich bin gerade dabei einen SQL-Lexer(Scanner?)/Parser zu bauen, eigentlich nur, weil ich einen OR-Mapper (für mich) baue, der auch DDL lesen können soll. Und zwar richtig und nicht (ausschließlich/hauptsächlich) per RegExp.
Kurzer Quellcodeausschnitt:
Anti_StringParser übernimmt das Parsen (von einem einfachen CREATE TABLE-Statement) anhand der Token-Definition von Anti_StringParser_Mysql. Als Identifier habe ich u.a. einfach mal eine RegExp-Klasse benutzt, die bestimmte Zeichenketten als zusammengehörig erkennt. Die Problematik besteht jetzt eigentlich wenn variable Codeteile (SQL-Identifier, also Tabellennamen oder sogar Strings) kommen:
Als Ergebnis bekomme ich relativ fix eine halbwegs brauchbare Ausgabe:
(naja usw.).
Leider nur halbwegs brauchbar. Siehe object#57. Schön und gut, dass der Identifier erkannt wurde, allerdings wären es ja richtiger Token "`" + Token String "benchmark" + Token "`".
Ich denke hier habe ich das Vorgehen von einem Scanner (oder heißt es Lexer?) nicht verstanden. Er muss ja, sobald er ein "`" findet auf einen anderen Modus schalten (lies alles ein, (fast) egal was kommt bis zum nächsten "`"). Ursprünglich dachte ich, dass ein Lexer erstmal alles ganz dumm zerlegt und erst der Parser dann sagt, he da ist ein Token "`" das niemals geschlossen wird. Wenn das der Fall wäre, könnte mein Lexer ja aber nicht wissen, dass nach dem öffnenden "`" nur stupide alles bis zum nächsten "`" einlesen muss. Hier ist mein Lexer offenbar noch zu blöd für, also vermutlich falsch konzipiert.
Im Internet konnte ich leider keine gute Anleitung für den Lexer/Parserbau finden. Der Wikiartikel dazu ist auch sehr knapp.
Oder bin ich jetz schon auf einem guten Weg und muss nurnoch die "identifier" oder "string" Tokens intern auf mehrere verteilen (Aggregation)?
ich bin gerade dabei einen SQL-Lexer(Scanner?)/Parser zu bauen, eigentlich nur, weil ich einen OR-Mapper (für mich) baue, der auch DDL lesen können soll. Und zwar richtig und nicht (ausschließlich/hauptsächlich) per RegExp.
Kurzer Quellcodeausschnitt:
Anti_StringParser übernimmt das Parsen (von einem einfachen CREATE TABLE-Statement) anhand der Token-Definition von Anti_StringParser_Mysql. Als Identifier habe ich u.a. einfach mal eine RegExp-Klasse benutzt, die bestimmte Zeichenketten als zusammengehörig erkennt. Die Problematik besteht jetzt eigentlich wenn variable Codeteile (SQL-Identifier, also Tabellennamen oder sogar Strings) kommen:
PHP-Code:
<?php
class Anti_StringParser
{
// ..
public function parse($source) // z.B. = "CREATE TABLE .."
{
$position = 0;
$source = (string)$source;
$length = mb_strlen($source); // Länge, damit ich weiß wann zu Ende geparset ist
$tokens = array(); // meine Liste der erzeugten Tokens
$maxLoop = 10000; // infinite loop Schutz ;)
do {
$current = mb_substr($source, $position); // relevanter Quellcodeabschnitt
$found = false;
foreach ($this->_identifiers as $name => $identifier) {
// durchlaufe alle Identifier (siehe nächstes Quellcodebeispiel für die Definitionen)
list ($match, $offset, $token) = $identifier->matches($current, $source, $position); // hat ein Identifier einen Token gefunden?
if (!$match) {
continue; // nicht? dann frag den nächsten
}
$found = true;
$position += $offset; // OK offset auf die aktuelle Position rechnen, damit das nächste Stück Code gelesen werden kann
$tokens[] = $token;
break;
}
if (!$found) {
// keiner Identifier hat was gefunden, Quellcode wurde nicht erkannt/ist nicht gültig
throw new Exception("unknown identifier at position [$position]:\n<var>$current</var>");
}
$parsed = ($position == $length); // sind wir fertig?
if (--$maxLoop <= 0) {
throw new Exception("infinite loop at position [$position]: $current");
}
} while (!$parsed);
Debug::stop($tokens); // ~ var_dump
}
}
?>
PHP-Code:
<?php
class Anti_StringParser_Mysql extends Anti_StringParser
{
public function init()
{
$this->_addIdentifier('regexp', 'whitespace', '\s+'); // whitespace Erkennung per RegExp
$this->_addIdentifier('regexp', 'keyword', '\b[A-Za-z][A-Za-z0-9\_]*\b'); // Keyword erkennung
$this->_addIdentifier('char', '(');
$this->_addIdentifier('char', ')');
$this->_addIdentifier('char', '=');
$this->_addIdentifier('char', ';');
$this->_addIdentifier('char', ',');
$this->_addIdentifier('regexp', 'string', "'[^']*'"); // Strings, erstmal ganz einfach
$this->_addIdentifier('regexp', 'number', "\b[0-9]+\b"); // Zahlen (für z.B. "int(10)")
$this->_addIdentifier('regexp', 'identifier', "`[^`]*`"); // DB-Identifier (nicht zu verwechseln mit dem Klassentyp, der die Tokens erstellt), doofe Namenswahl vlt.
}
}
?>
PHP-Code:
<?php
class Anti_StringParser_Identifier_RegExp extends Anti_StringParser_Identifier_Abstract
{
// ..
public function matches($current, $source, $position)
{
try {
$pattern = $this->_delimiter . '^' . $this->_regExp . $this->_delimiter . $this->_modifiers; // regexp bauen
$success = (bool)preg_match($pattern, $current, $matches);
if (!$success) {
return array(false, null, null); // nix gefunden
}
$match = $matches[0];
$offset = mb_strlen($matches[0]); // offset berechnen
$token = new Anti_StringParser_Token($this->_name, $match); // token bauen
$result = array(true, $offset, $token);
return array(true, $offset, $token); // Treffer zurückliefern
} catch (Exception $e) {
throw new Exception("match with regular expression failed", 0, $e);
}
}
}
?>
Code:
CREATE TABLE `benchmark` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) COLLATE utf8_bin NOT NULL, `description` text COLLATE utf8_bin NOT NULL, `last_modified` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, `created` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00', PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_bin
Code:
DEBUG ARG 1: array(104) { [0]=> object(Anti_StringParser_Token)#52 (2) { ["_name"]=> string(10) "whitespace" ["_value"]=> string(29) " " } [1]=> object(Anti_StringParser_Token)#53 (2) { ["_name"]=> string(7) "keyword" ["_value"]=> string(6) "CREATE" } [2]=> object(Anti_StringParser_Token)#54 (2) { ["_name"]=> string(10) "whitespace" ["_value"]=> string(1) " " } [3]=> object(Anti_StringParser_Token)#55 (2) { ["_name"]=> string(7) "keyword" ["_value"]=> string(5) "TABLE" } [4]=> object(Anti_StringParser_Token)#56 (2) { ["_name"]=> string(10) "whitespace" ["_value"]=> string(1) " " } [5]=> object(Anti_StringParser_Token)#57 (2) { ["_name"]=> string(10) "identifier" ["_value"]=> string(11) "`benchmark`" } [6]=> object(Anti_StringParser_Token)#58 (2) { ["_name"]=> string(10) "whitespace" ["_value"]=> string(1) " " } [7]=> object(Anti_StringParser_Token)#59 (2) { ["_name"]=> string(1) "(" ["_value"]=> string(1) "(" }
Leider nur halbwegs brauchbar. Siehe object#57. Schön und gut, dass der Identifier erkannt wurde, allerdings wären es ja richtiger Token "`" + Token String "benchmark" + Token "`".
Ich denke hier habe ich das Vorgehen von einem Scanner (oder heißt es Lexer?) nicht verstanden. Er muss ja, sobald er ein "`" findet auf einen anderen Modus schalten (lies alles ein, (fast) egal was kommt bis zum nächsten "`"). Ursprünglich dachte ich, dass ein Lexer erstmal alles ganz dumm zerlegt und erst der Parser dann sagt, he da ist ein Token "`" das niemals geschlossen wird. Wenn das der Fall wäre, könnte mein Lexer ja aber nicht wissen, dass nach dem öffnenden "`" nur stupide alles bis zum nächsten "`" einlesen muss. Hier ist mein Lexer offenbar noch zu blöd für, also vermutlich falsch konzipiert.
Im Internet konnte ich leider keine gute Anleitung für den Lexer/Parserbau finden. Der Wikiartikel dazu ist auch sehr knapp.
Oder bin ich jetz schon auf einem guten Weg und muss nurnoch die "identifier" oder "string" Tokens intern auf mehrere verteilen (Aggregation)?
Kommentar