Suchen

Mit Pyparsing eine eigene Skriptsprache zur Embedded-Entwicklung definieren

| Autor / Redakteur: Tobias Schaffner * / Sebastian Gerstl

Python ist auch in der Embedded-Entwicklung zur Generierung praktischer Scripts beliebt. Mit Pyparsing besteht zudem die Möglichkeit, mit wenig Aufwand eine auf die eigenen Bedürfnisse zugeschnittene Skriptsprache zu definieren, die innerhalb des Python-Kontextes läuft.

Firmen zum Thema

Python bietet mit dem Modul PyParsing eine relativ wenig bekannte Möglichkeit, eine eigene Skriptsprache zu definieren, die innerhalb des Python-Kontextes läuft und die auf die eigenen Bedürfnisse zugeschnitten werden kann – mit vergleichsweise geringem Aufwand.
Python bietet mit dem Modul PyParsing eine relativ wenig bekannte Möglichkeit, eine eigene Skriptsprache zu definieren, die innerhalb des Python-Kontextes läuft und die auf die eigenen Bedürfnisse zugeschnitten werden kann – mit vergleichsweise geringem Aufwand.
(Bild: gemeinfrei / CC0 )

Ein mit Python realisiertes System soll in der Lage sein, eine Reihe von einem Nutzer übergebener Befehle selbstständig ausführen zu können. Das System soll die Befehle in Form eines Skripts entgegennehmen. Der Nutzer soll in der Lage sein, Bedingungen definieren zu können.

Diese zunächst einfach klingende Forderung logischer Elemente in der Kommunikation führt bei falscher Umsetzung schnell zu einer instabilen oder unsicheren Schnittstelle. Neben zwei klassischen Ansätzen wird PyParsing als Lösungsansatz für ein typisches Problem im Embedded-Umfeld vorgestellt. PyParsing ermöglicht es, mit niedrigerem Aufwand eine eigene Sprache aus der Taufe zu heben, die auf das vorliegende Problem zugeschnitten ist.

Eigene Skriptsprache definieren: Ansatz statisches Datenformat

Oft werden für den Datenaustausch mit einem zu steuernden Gerät Datenformate wie JSON oder XML verwendet. JSON und XML wurden für den Austausch statischer Daten entwickelt. Zwar gibt es Projekte, die versuchen, einfache Logik zu JSON hinzuzufügen. Sie resultieren jedoch schnell in komplex verschachtelten Dictionaries und Listen. Das Ergebnis ist nicht nur schlecht lesbar, sondern auch schwer um weitere Funktionalitäten erweiterbar.

Ansatz mit regulärem Python-Einsatz

Eine weitere Möglichkeit ist die Verwendung von Python selbst. Denkbar ist die Übergabe eines Pythonskripts, das direkt ausgeführt wird. Dieser Ansatz stellt dem Sender des Skripts jedoch einen unnötig großen und schwer kontrollierbaren Sprachraum zur Verfügung. Es müsste sichergestellt werden, dass das Skript in einem sehr eingeschränkten Kontext ausgeführt wird, um den Import beliebiger Bibliotheken oder den Zugriff auf Daten zu verhindern.

Ansatz mit PyParsing

PyParsing ist ein Pythonmodul mit Unterstützung von Python 3.x zur Erstellung einfacher Grammatiken. Es ist damit ein Gegenentwurf zum wesentlich komplexeren, klassischen Lex/Yacc-Ansatz. Das Erstellen einer eigenen Sprache ermöglicht es, einen Sprachraum mit genau dem benötigten Funktionsumfang definieren zu können. Dank Hilfsfunktionen wie "Forward" für Vorwärtsdeklarationen und Typen wie "alphas" für alphanumerische Zeichen des Moduls bleibt die Definition der Grammatik auch bei wachsendem Funktionsumfang gut lesbar.

Da sowohl das Parsen als auch die Interpretation eines übergebenen Skripts im Pythonkontext abläuft, kann auf Pythonvariablen und -methoden direkt zugegriffen werden. So kann zum Beispiel beim Erkennen eines Funktionsaufrufes in der neu erstellten Skriptsprache direkt eine Pythonfunktion aufgerufen werden.

Aufrufe von Pythonfunktionen im Beispiel

Im folgenden Beispiel sollen Funktionen nach dem Pythonschema in etwas vereinfachter Form eingeführt werden. An der Stelle der Parameter des Funktionsaufrufs können ein oder mehrere Ausdrücke stehen. Ein Ausdruck kann entweder ein Literal, eine Variable oder selbst ein Funktionsaufruf sein:

sum(5, var, diff(4, 4))

Ein Literal ist entweder ein String oder eine Zahl:

23 literal = quotedString ^ pyparsing_common.number

Für Variablen werden die gleichen Wörter wie in Python erlaubt:

24 variable = Word(alphas+'_', alphanums+'_')

Da eine Funktionen ein Ausdruck ist, jedoch selbst wieder einen Ausdruck als Parameter erwartet, wird an dieser Stelle eine Vorwärtsdeklaration benötigt:

25 function = Forward().setName('function')

Alle drei Komponenten bilden einen Ausdruck:

26 expression = (function ^ variable ^ literal).setName('expression')

Anschliesßend lässt sich der Ausdruck wieder für die Definition des Funktionsaufrufs verwenden:

28 function << Group(variable +
29     Literal('(').suppress() +
30     Optional(delimitedList(expression)) +
31     Literal(')').suppress()).setParseAction(function_call)

Wird eine Funktion geparst, wird function_call() aufgerufen, was ein FunctionCall-Objekt zurückgibt, das den Namen der Funktion sowie eine Liste der Argumente beinhaltet. Die übergebene Liste an Tokens beinhaltet alle Elemente der Funktionsdefinition, die nicht mit suppress markiert wurden.

15 class FunctionCall():
16     def __init__(self, name, args):
17          elf.name = name
18          elf.args = args
19
20 def function_call(tokens):
21     return FunctionCall(tokens[0][0], tokens[0][1:])

Zuletzt wird das Semikolon als Separator festgelegt:

33 lang = delimitedList(expression, delim=';')

Der Sprache soll als erstes commandline Argument ein zu parsender String übergeben werden können:

34 parsed = lang.parseString(sys.argv[1], parseAll=True)

Über die Funktionsaufrufe lässt sich anschließend iterieren. Das Ergebnis wird nach der Auswertung ausgegeben:

57 for expr in parsed:
58    print(eval_expression(expr))

Um Funktionsaufrufe als Parameter von Funktionsaufrufen zu ermöglichen, muss bei der Auswertung rekursiv vorgegangen werden.

41 def eval_expression(expr):
42     if isinstance(expr, str):
43         if expr[0] in '"\'':
44             return expr[1:-1]
45         if expr not in variables:
46             aise KeyError(f"Variable {expr} not found")
47         return variables.get(expr)
48     elif isinstance(expr, FunctionCall):
49         if expr.name not in functions:
50             raise KeyError(f"Unknown function {expr.name}")
51         a = []
52         for arg in expr.args:
53             a.append(eval_expression(arg))
54         return functions[expr.name](*a)
55     return expr

Die Variablen und Funktionen werden aus dem Pythonkontext über eine Hashmap zur Verfügung gestellt. Der Einfachheit halber werden zwei Funktionen von operator übergeben.

36 functions = {'sum': operator.add,
37     'diff': operator.sub}
38 variables = {'var1': 2,
39     'var2': 4}

Unser Parser kann nun mit Hilfe der Kommandozeile getestet werden:

$ python parser.py "sum(var1, var2); diff(sum(3, 2), 5); sum(\"hello\", \"world\")"
6
0
helloworld

PyParsing ermöglicht es also, eine einfache, durch Semikolon separierte Skriptsprache mit Variablen und Funktionsaufrufen mit Argumenten, If-Else sowie While-Funktionen in weniger als 100 Zeilen zu beschreiben. Die Interpretation im Pythonkontext ermöglicht eine gute Integration in Python-Code.

Der Autor: Tobias Schaffner, Softwareentwickler bei der Mixed Mode GmbH in Gräfelfing bei München.
Der Autor: Tobias Schaffner, Softwareentwickler bei der Mixed Mode GmbH in Gräfelfing bei München.
(Bild: Mixed Mode)

Der Autor

*Tobias Schaffner ist Open Source-Enthusiast und als Software Entwickler mit Schwerpunkt embedded Linux bei Mixed Mode tätig. Sein großes Interesse an intelligenten Systemen spiegelt sich auch in seiner Ausbildung zum B.Sc. Informatik und B.Sc. Psychologie wider.

Weiterführende Quellen:

Pyparsing Quick reference guide, by John W. Shipman.

Creating Languages For Dummies, by Roberto Alsina

(ID:45443300)