Februar 2016

Band 31, Nummer 2

C# – Anpassbare Skripterstellung in C#

Von Vassili Kaplan:

In diesem Artikel zeige ich, wie Sie mit C# eine selbstdefinierte Skripterstellungssprache entwickeln können, ohne auf externe Bibliotheken zurückzugreifen. Die Skriptsprache basiert auf dem Split-and-Merge-Algorithmus zum Analysiere mathematischer Ausdrücke in C#, den ich in der Ausgabe des MSDN Magazine vom Oktober 2015 (msdn.com/magazine/mt573716) vorgestellt habe.

Mithilfe benutzerdefinierter Funktionen kann ich den Split-and-Merge-Algorithmus so erweitern, dass nicht nur ein mathematischer Ausdruck, sondern auch eine anpassbare Skriptsprache analysiert werden kann. Die „standardmäßigen“ Ablaufsteuerungsanweisungen der Sprache (if, else, while, continue, break usw.) können als benutzerdefinierte Funktionen ebenso wie andere typische Skriptsprachenfunktionen (Betriebssystembefehle, Zeichenfolgenbearbeitung, Dateisuche usw.) hinzugefügt werden.

Ich nenne meine Sprache „Customizable Scripting in C#“ (CSCS). Was spricht dafür, eine weitere Skriptsprache zu erstellen? Der Hauptgrund ist die einfache Anpassbarkeit der Sprache. Das Hinzufügen einer neuen Funktion oder Ablaufsteuerungsanweisung, die eine beliebige Anzahl von Parametern verwendet, erfordert nur einige wenige Codezeilen. Darüber hinaus können die Funktionsnamen und Ablaufsteuerungsanweisungen bei nur wenigen Konfigurationsänderungen in einem nicht englischen Sprachszenario genutzt werden, was ich auch in diesem Artikel zeige. Sobald Sie erkennen, wie die Sprache CSCS implementiert wird, sind Sie in der Lage, Ihre eigene benutzerdefinierte Skriptsprache zu entwickeln.

Der Umfang von CSCS

Es ist ziemlich simpel, eine sehr einfache Skriptsprache zu implementieren, aber brutal schwer bei einer Fünf-Sterne-Sprache. Ich werde hier den Umfang von CSCS einschränken, damit Sie wissen, was Sie erwarten können:

  • Die Sprache CSCS bietet die folgenden Ablaufsteuerungsanweisungen: if, else if, else, while, continue, break. Geschachtelte Anweisungen werden ebenfalls unterstützt. Sie erfahren, wie Sie weitere Steuerungsanweisungen dynamisch hinzufügen.
  • Es gibt keine booleschen Werte. Sie schreiben nicht „if (a)“, sondern „if (a == 1)“.
  • Logische Operatoren werden nicht unterstützt. Sie schreiben nicht „if (a ==1 and b == 2)“, sondern geschachtelte IF-Anweisungen: if (a == 1) { if (b == 2) { … } }.
  • Funktionen und Methoden werden in CSCS nicht unterstützt, können aber in C# geschrieben und mit dem Split-And-Merge-Parser für die Verwendung mit CSCS registriert werden.
  • Nur Kommentare im „//“-Stil werden unterstützt.
  • Variablen und eindimensionale Arrays werden unterstützt, die alle auf globaler Ebene definiert werden. Eine Variable kann eine Zahl, eine Zeichenfolge oder ein (als Liste implementiertes) Tupel anderer Variablen enthalten. Mehrdimensionale Arrays werden nicht unterstützt.

Abbildung 1 zeigt ein „Hello, World!“-Programm in CSCS. Aufgrund eines Tippfehlers bei „print“ zeigt das Programm am Ende einen Fehler an: „Couldn’t parse token [pint].“ Wie Sie sehen, wurden alle vorherigen Anweisungen erfolgreich ausgeführt, was heißt, dass CSCS ein Interpreter ist.

„Hello, World!“ in CSCS
Abbildung 1: „Hello, World!“ in CSCS

Modifizierungen am Split-and-Merge-Algorithmus

Ich habe zwei Änderungen an der „Split“-Komponente des Split-and-Merge-Algorithmus vorgenommen. (Die „Merge“-Komponente bleibt unverändert.)

Die erste Änderung ist, dass das Ergebnis der Analyse eines Ausdrucks nun eine Zahl, eine Zeichenfolge oder ein Tupel von Werten (von denen jeder entweder eine Zeichenfolge oder eine Zahl sein kann) anstatt nur eine Zahl sein kann. Ich habe die folgende „Parser.Result“-Klasse zum Speichern des Ergebnisses der Anwendung des Split-and-Merge-Algorithmus erstellt:

public class Result
{
  public Result(double dRes = Double.NaN, 
    string sRes = null, 
    List<Result> tRes = null)
  {
    Value  = dResult;
    String = sResult;
    Tuple  = tResult;
  }
  public double
       Value  { get; set; }
  public string
       String { get; set; }
  public List<Result> Tuple  { get; set; }
}

Die zweite Änderung ist, dass der Split-Teil nun nicht erst erfolgt, wenn ein Zeichen zum Beenden der Analyse gefunden wird, „)“ oder „\n“, sondern bis ein beliebiges Zeichen in einem übergebenen Array von Zeichen zum Beenden der Analyse gefunden wird. Dies ist z. B. erforderlich, wenn das erste Argument einer IF-Anweisung analysiert wird, in der das Trennzeichen ein Zeichen des Typs „<“, „>“ oder „=“ sein kann.

Sie können sich den modifizierten Split-and-Merge-Algorithmus im Quellcode im begleitenden Download ansehen.

Der Interpreter

Die für das Interpretieren des CSCS-Codes zuständige Klasse heißt „Interpreter“. Sie wird als Singleton, d. h. als Klassendefinition implementiert, bei der es nur eine Instanz der Klasse geben kann. In ihrer „Init“-Methode wird der Parser (siehe den zuvor erwähnten Originalartikel) mit allen Funktionen initialisiert, die vom Interpreter verwendet werden:

public void Init()
{
  ParserFunction.AddFunction(Constants.IF,
        new IfStatement(this));
  ParserFunction.AddFunction(Constants.WHILE,
     new WhileStatement(this));
  ParserFunction.AddFunction(Constants.CONTINUE,
  new ContinueStatement());
  ParserFunction.AddFunction(Constants.BREAK,
     new BreakStatement());
  ParserFunction.AddFunction(Constants.SET,
       new SetVarFunction());
...
}

In der Datei „Constants.cs“ werden die in CSCS verwendeten tatsächlichen Namen definiert:

...
public const string IF          = "if";
public const string ELSE        = "else";
public const string ELSE_IF     = "elif";
public const string WHILE       = "while";
public const string CONTINUE    = "continue";
public const string BREAK       = "break";
public const string SET         = "set";

Funktionen, die mit dem Parser registriert werden, müssen als eine Klasse implementiert werden, die von der „ParserFunction“-Klasse abgeleitet ist, und müssen deren „Evaluate“-Methode überschreiben.

Der erste Schritt, den der Interpreter ausführt, wenn er mit der Bearbeitung eines Skripts beginnt, ist dessen Vereinfachung, indem alle Leerzeichen (es sei denn, sie befinden sich innerhalb einer Zeichenfolge) und alle Kommentare entfernt werden. Deshalb können Leerzeichen oder neue Zeilen nicht als Trennzeichen für Operatoren verwendet werden. Das Trennzeichen für Operatoren und die Kommentarzeichenfolge werden auch in „Constants.cs“ definiert:

public const char END_STATEMENT = ';';
public const string COMMENT     = "//";

Variablen und Arrays

CSCS unterstützt Zahlen (mit dem Typ „Double“), Zeichenfolgen oder Tupel (Arrays mit Variablen, die als C#-Liste implementiert werden). Jedes Element eines Tupels kann entweder eine Zeichenfolge oder Zahl, aber kein anderes Tupel sein. Deshalb werden mehrdimensionale Arrays nicht unterstützt. Zum Definieren einer Variablen wird die CSCS-Funktion „set“ verwendet. Die C#-Klasse „SetVarFunction“ implementiert die Funktionalität für das Festlegen eines Variablenwerts (siehe Abbildung 2).

Abbildung 2: Implementierung der Funktion zum Festlegen von Variablen

class SetVarFunction : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string varName = Utils.GetToken(data, ref from, Constants.NEXT_ARG_ARRAY);
    if (from >= data.Length)
    {
      throw new ArgumentException("Couldn't set variable before end of line");
    }
    Parser.Result varValue = Utils.GetItem(data, ref from);
    // Check if the variable to be set has the form of x(i),
    // meaning that this is an array element.
    int arrayIndex = Utils.ExtractArrayElement(ref varName);
    if (arrayIndex >= 0)
    {
      bool exists = ParserFunction.FunctionExists(varName);
      Parser.Result  currentValue = exists ?
            ParserFunction.GetFunction(varName).GetValue(data, ref from) :
            new Parser.Result();
      List<Parser.Result> tuple = currentValue.Tuple == null ?
                                  new List<Parser.Result>() :
                                  currentValue.Tuple;
      if (tuple.Count > arrayIndex)
      {
        tuple[arrayIndex] = varValue;
      }
      else
      {
        for (int i = tuple.Count; i < arrayIndex; i++)
        {
          tuple.Add(new Parser.Result(Double.NaN, string.Empty));
        }
        tuple.Add(varValue);
      }
      varValue = new Parser.Result(Double.NaN, null, tuple);
    }
    ParserFunction.AddFunction(varName, new GetVarFunction(varName, varValue));
    return new Parser.Result(Double.NaN, varName);
  }
}

Es folgen verschiedene Beispiele der Definition einer Variablen in CSCS:

set(a, "2 + 3");  // a will be equal to the string "2 + 3"
set(b, 2 + 3);    // b will be equal to the number 5
set(c(2), "xyz"); // c will be initialized as a tuple of size 3 with c(0) = c(1) = ""

Beachten Sie, dass es keine spezielle Deklaration eines Arrays gibt. Durch das bloße Definieren einer Variablen mit einem Index wird das Array initialisiert, sofern noch nicht geschehen. Außerdem werden ihm bei Bedarf leere Elemente hinzugefügt. Im vorangegangenen Beispiel wurden die Elemente „c(0)“ und „c(1)“ hinzugefügt, die beide als leere Zeichenfolgen initialisiert wurden. Dadurch fällt meiner Ansicht nach der unnötige Schritt weg, der in den meisten Skriptsprachen erforderlich ist, um zunächst ein Array zu deklarieren.

Alle CSCS-Variablen und -Arrays werden mithilfe von CSCS-Funktionen (wie „set“ oder „append“) erstellt. Alle werden mit globalem Geltungsbereich definiert und können später verwendet werden, indem der Variablenname oder eine Variable mit einem Index aufgerufen wird. In C# wird dies in die in Abbildung 3 gezeigte „GetVar“-Funktion implementiert.

Abbildung 3: Implementierung der Funktion zum Abrufen von Variablen

class GetVarFunction : ParserFunction
{
  internal GetVarFunction(Parser.Result value)
  {
    m_value = value;
  }
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    // First check if this element is part of an array:
    if (from < data.Length && data[from - 1] == Constants.START_ARG)
    {
      // There is an index given - it may be for an element of the tuple.
      if (m_value.Tuple == null || m_value.Tuple.Count == 0)
      {
        throw new ArgumentException("No tuple exists for the index");
      }
      Parser.Result index = Utils.GetItem(data, ref from, true /* expectInt */);
      if (index.Value < 0 || index.Value >= m_value.Tuple.Count)
      {
        throw new ArgumentException("Incorrect index [" + index.Value +
          "] for tuple of size " + m_value.Tuple.Count);
      }
      return m_value.Tuple[(int)index.Value];
    }
    // This is the case for a simple variable, not an array:
    return m_value;
  }
  private Parser.Result m_value;
}

Nur die Variablenfunktion „set“ muss mit dem Parser registriert werden:

ParserFunction.AddFunction(Constants.SET, new SetVarFunction());

Die Variablenfunktion „get“ wird innerhalb der Variablenfunktion „set“ im C#-Code registriert (siehe die vorletzte Anweisung in Abbildung 2):

ParserFunction.AddFunction(varName, new GetVarFunction(varName, varValue));

Es folgen einige Beispiele für das Abrufen von Variablen in CSCS:

append(a, "+ 5"); // a will be equal to the string "2 + 3 + 5"
set(b, b * 2);    // b will be equal to the number 10 (if it was 5 before)

Ablaufsteuerung: If, Else If, Else

Die Ablaufsteuerungsanweisungen „If“, „Else If“ und „Else“ werden intern ebenfalls als Parserfunktionen implementiert. Sie werden wie alle anderen Funktionen vom Parser registriert:

ParserFunction.AddFunction(Constants.IF, new IfStatement(this));

Nur das Schlüsselwort IF muss mit dem Parser registriert werden. Die Anweisungen ELSE_IF und ELSE werden innerhalb der „IfStatement“-Implementierung verarbeitet:

class IfStatement : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    m_interpreter.CurrentChar = from;
    Parser.Result result = m_interpreter.ProcessIf();
    return result;
  }
  private Interpreter m_interpreter;
}

Die tatsächliche Implementierung der IF-Anweisung erfolgt in der „Interpreter“-Klasse (siehe Abbildung 4).

Abbildung 4: Implementierung der IF-Anweisung

internal Parser.Result ProcessIf()
{
  int startIfCondition = m_currentChar;
  Parser.Result result = null;
  Parser.Result arg1 = GetNextIfToken();
  string comparison  = Utils.GetComparison(m_data, ref m_currentChar);
  Parser.Result arg2 = GetNextIfToken();
  bool isTrue = EvalCondition(arg1, comparison, arg2);
  if (isTrue)
  {
    result = ProcessBlock();
    if (result is Continue || result is Break)
    {
      // Got here from the middle of the if-block. Skip it.
      m_currentChar = startIfCondition;
      SkipBlock();
    }
    SkipRestBlocks();
    return result;
  }
  // We are in Else. Skip everything in the If statement.
  SkipBlock();
  int endOfToken = m_currentChar;
  string nextToken = Utils.GetNextToken(m_data, ref endOfToken);
  if (ELSE_IF_LIST.Contains(nextToken))
  {
    m_currentChar = endOfToken + 1;
    result = ProcessIf();
  }
  else if (ELSE_LIST.Contains(nextToken))
  {
    m_currentChar = endOfToken + 1;
    result = ProcessBlock();
  }
  return result != null ? result : new Parser.Result();
}

Es ist explizit vorgegeben, dass die „If“-Bedingung folgendes Format hat: Argument 1, Gleichzeichen, Argument 2:

Parser.Result arg1 = GetNextIfToken();
string comparison  = Utils.GetComparison(m_data, ref m_currentChar);
Parser.Result arg2 = GetNextIfToken();
bool isTrue = EvalCondition(arg1, comparison, arg2);

Hier können optionale Anweisungen des Typs AND, OR oder NOT hinzugefügt werden.

Die „EvalCondition“-Funktion vergleicht lediglich die Token gemäß dem Vergleichszeichen:

internal bool EvalCondition(Parser.Result arg1, string comparison, Parser.Result arg2)
{
  bool compare = arg1.String != null ? CompareStrings(arg1.String, comparison, arg2.String) :
                                       CompareNumbers(arg1.Value, comparison, arg2.Value);
  return compare;
}

Hier die Implementierung eines numerischen Vergleichs:

internal bool CompareNumbers(double num1, string comparison, double num2)
{
  switch (comparison) {
    case "==": return num1 == num2;
    case "<>": return num1 != num2;
    case "<=": return num1 <= num2;
    case ">=": return num1 >= num2;
    case "<" : return num1 <  num2;
    case ">" : return num1 >  num2;
    default: throw new ArgumentException("Unknown comparison: " + comparison);
  }
}

Der Zeichenfolgenvergleich ist ähnlich und im Code im begleitenden Download verfügbar. Gleiches gilt für die unkomplizierte Implementierung der „GetNextIfToken“-Funktion.

Wenn eine Bedingung des Typs „if“, „else if“ oder „else“ wahr ist, werden alle Anweisungen innerhalb des Blocks verarbeitet. Dies wird in Abbildung 5 in der „ProcessBlock“-Methode implementiert. Wenn die Bedingung nicht wahr ist, werden alle Anweisungen übersprungen. Dies wird in der „SkipBlock“-Methode implementiert (siehe den begleitenden Quellcode).

Abbildung 5: Implementierung der „ProcessBlock“-Methode

internal Parser.Result ProcessBlock()
{
  int blockStart = m_currentChar;
  Parser.Result result = null;
  while(true)
  {
    int endGroupRead = Utils.GoToNextStatement(m_data, ref m_currentChar);
    if (endGroupRead > 0)
    {
      return result != null ? result : new Parser.Result();
    }
    if (m_currentChar >= m_data.Length)
    {
      throw new ArgumentException("Couldn't process block [" +
                                   m_data.Substring(blockStart) + "]");
    }
    result = Parser.LoadAndCalculate(m_data, ref m_currentChar,
      Constants.END_PARSE_ARRAY);
    if (result is Continue || result is Break)
    {
      return result;
    }
  }
}

Beachten Sie, wie die Anweisungen „Continue“ und „Break“ in der „while“-Schleife verwendet werden. Diese Anweisungen werden ebenfalls als Funktionen implementiert. Hier die „Continue“-Anweisung:

class Continue : Parser.Result  { }
class ContinueStatement : ParserFunction
{
  protected override Parser.Result
    Evaluate(string data, ref int from)
  {
    return new Continue();
  }
}

Die Implementierung der „Break“-Anweisung erfolgt analog. Beide werden wie alle anderen Funktionen mit dem Parser registriert:

ParserFunction.AddFunction(Constants.CONTINUE,  new ContinueStatement());
ParserFunction.AddFunction(Constants.BREAK,     new BreakStatement());

Sie können die „Break“-Funktion verwenden, um geschachtelte „If“-Blöcke oder eine „While“-Schleife zu verlassen.

Ablaufsteuerung: Die „While“-Schleife

Auch die „While“-Schleife wird mit dem Parser als Funktion implementiert und registriert:

ParserFunction.AddFunction(Constants.WHILE,     new WhileStatement(this));

Immer wenn das Schlüsselwort „while“ analysiert wird, wird die „Evaluate“-Methode des „WhileStatement“-Objekts aufgerufen:

class WhileStatement : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string parsing = data.Substring(from);
    m_interpreter.CurrentChar = from;
    m_interpreter.ProcessWhile();
    return new Parser.Result();
  }
  private Interpreter m_interpreter;
}

Deshalb befindet sich die tatsächliche Implementierung der „while“-Schleife in der „Interpreter“-Klasse (siehe Abbildung 6).

Abbildung 6: Implementierung der „While“-Schleife

internal void ProcessWhile()
{
  int startWhileCondition = m_currentChar;
  // A heuristic check against an infinite loop.
  int cycles = 0;
  int START_CHECK_INF_LOOP = CHECK_AFTER_LOOPS / 2;
  Parser.Result argCache1 = null;
  Parser.Result argCache2 = null;
  bool stillValid = true;
  while (stillValid)
  {
    m_currentChar = startWhileCondition;
    Parser.Result arg1 = GetNextIfToken();
    string comparison = Utils.GetComparison(m_data, ref m_currentChar);
    Parser.Result arg2 = GetNextIfToken();
    stillValid = EvalCondition(arg1, comparison, arg2);
    int startSkipOnBreakChar = m_currentChar;
    if (!stillValid)
    {
      break;
    }
    // Check for an infinite loop if same values are compared.
    if (++cycles % START_CHECK_INF_LOOP == 0)
    {
      if (cycles >= MAX_LOOPS || (arg1.IsEqual(argCache1) &&
        arg2.IsEqual(argCache2)))
      {
        throw new ArgumentException("Looks like an infinite loop after " +
          cycles + " cycles.");
      }
      argCache1 = arg1;
      argCache2 = arg2;
    }
    Parser.Result result = ProcessBlock();
    if (result is Break)
    {
      m_currentChar = startSkipOnBreakChar;
      break;
    }
  }
  // The while condition is not true anymore: must skip the whole while
  // block before continuing with next statements.
  SkipBlock();
}

Beachten Sie, dass die „while“-Schleife nach einer bestimmten Anzahl von Iterationen proaktiv nach einer unendlichen Schleife sucht, was in den Konfigurationseinstellungen von der Konstanten CHECK_AFTER_LOOPS definiert wird. Die Heuristik ist, dass wenn über mehrere Schleifen hinweg exakt dieselben Werte in der „while“-Bedingung verglichen werden, dies ein Hinweis auf eine unendliche Schleife sein könnte. Abbildung 7 zeigt eine „while“-Schleife, bei der ich vergessen habe, die Zyklusvariable i innerhalb der „while“-Schleife zu erhöhen.

Erkennen einer unendlichen „while“-Schleife in CSCS
Abbildung 7: Erkennen einer unendlichen „while“-Schleife in CSCS

Funktionen, Funktionen, Funktionen

Damit CSCS nützlichere Dinge erledigen kann, müssen weitere Funktionen implementiert werden. Das Hinzufügen einer neuen Funktion zu CSCS erfolgt ganz einfach: Implementieren Sie zuerst eine von der „ParserFunction“-Klasse abgeleitete Klasse (bei Überschreibung der „Evaluate“-Methode), und registrieren Sie diese dann beim Parser. Hier sehen Sie die Implementierung der „Print“-Funktion:

class PrintFunction : ParserFunction
{
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    List<string> args = Utils.GetFunctionArgs(data, ref from);
    m_interpreter.AppendOutput(string.Join("", args.ToArray()));
    return new Parser.Result();
  }
  private Interpreter m_interpreter;
}

Die Funktion gibt eine beliebige Anzahl von durch ein Komma getrennten Argumenten aus, die an sie übergeben werden. Das tatsächliche Lesen der Argumente erfolgt in der Hilfsfunktion „GetFunctionArgs“, die alle übergebenen Argumente als eine Liste mit Zeichenfolgen zurückgibt. Sie können sich die Funktion im Quellcode im begleitenden Download ansehen.

Der zweite und letzte Schritt ist die Registrierung der „Print“-Funktion beim Parser im Programminitialisierungsteil:

ParserFunction.AddFunction(Constants.PRINT,     new PrintFunction(this));

Die Konstante „Constants.PRINT“ ist als „print“ definiert.

Abbildung 8 zeigt eine Implementierung einer Funktion, die einen neuen Prozess startet.

Abbildung 8: Ausführen der Prozessfunktionsimplementierung

class RunFunction : ParserFunction
{
  internal RunFunction(Interpreter interpreter)
  {
    m_interpreter = interpreter;
  }
  protected override Parser.Result Evaluate(string data, ref int from)
  {
    string processName = Utils.GetItem(data, ref from).String;
    if (string.IsNullOrWhiteSpace(processName))
    {
      throw new ArgumentException("Couldn't extract process name");
    }
    List<string> args = Utils.GetFunctionArgs(data, ref from);
    int processId = -1;
    try
    {
      Process pr = Process.Start(processName, string.Join("", args.ToArray()));
      processId = pr.Id;
    }
    catch (System.ComponentModel.Win32Exception exc)
    {
      throw new ArgumentException("Couldn't start [" + processName + "]:
        " + exc.Message);
    }
    m_interpreter.AppendOutput("Process " + processName + " started, id:
      " + processId);
    return new Parser.Result(processId);
  }
  private Interpreter m_interpreter;
}

Nun zeige ich, wie Sie in CSCS Dateien suchen, einen Prozess beginnen und beenden und verschiedene Werte ausgeben:

 

set(b, findfiles("*.cpp", "*.cs"));
set(i, 0);
while(i < size(b)) {
  print("File ", i, ": ", b(i));
  set(id, run("notepad", b(i)));
  kill(id);
  set(i, i+ 1);
}

Abbildung 9 zeigt die Funktionen, die in den herunterladbaren Quellcode implementiert sind, und eine Kurzbeschreibung. Die meisten Funktionen sind Wrapper für entsprechende C#-Funktionen.

Abbildung 9: CSCS-Funktionen

abs Ruft den absoluten Wert eines Ausdrucks ab
append Fügt eine Zeichenfolge oder Zahl (die anschließend in eine Zeichenfolge umgewandelt wird) an eine Zeichenfolge an
cd Ändert ein Verzeichnis
cd.. Wechselt zur nächst höheren Verzeichnisebene
dir Zeigt den Inhalt des aktuellen Verzeichnisses
enc Ruft den Inhalt einer Umgebungsvariablen ab
exp Exponentialfunktion
findfiles Sucht Dateien mit einem angegebenen Muster
findstr Sucht Dateien, die eine Zeichenfolge mit einem bestimmten Muster enthalten
indexof Gibt einen Index einer Unterzeichenfolge oder -1 zurück, falls nicht gefunden
kill Beendet einen Prozess, der eine angegebene Prozess-ID-Nummer hat
pi Gibt eine Annäherung der Konstante pi zurück
pow Gibt das erste Argument hoch dem zweiten Argument zurück
print Gibt eine angegebene Liste von Argumenten aus (Zahlen und Listen werden in Zeichenfolgen umgewandelt)
psinfo Gibt Prozessinformationen zu einem angegebenen Prozessnamen zurück
pstime Gibt die gesamte Prozessorzeit für diesen Prozess zurück, was für das Messen von Zeiten nützlich ist
pwd Zeigt den Pfadnamen des aktuellen Verzeichnisses
run Startet einen Prozess mit einer angegebenen Argumentliste und gibt die Prozess-ID zurück
setenv Legt den Inhalt einer Umgebungsvariablen fest
set Legt den Wert einer Variablen oder eines Arrayelements fest
sin Gibt den Wert des Sinus des angegebenen Arguments zurück
size Gibt die Länge der Zeichenfolge oder die Größe der Liste zurück
sqrt Gibt die Quadratwurzel der angegebenen Zahl zurück
substr Gibt die Unterzeichenfolge der Zeichenfolge zurück, die am angegebenen Index beginnt
tolower Wandelt eine Zeichenfolge in Kleinschreibung um
toupper Wandelt eine Zeichenfolge in Großschreibung um

Internationalisierung

Beachten Sie, dass Sie mehrere Bezeichnungen (Funktionsnamen) entsprechend derselben Funktion beim Parser registrieren können. Deshalb ist es möglich, eine beliebige Anzahl anderer Sprachen hinzuzufügen.

Das Hinzufügen einer Übersetzung umfasst das Registrieren einer anderen Zeichenfolge beim selben C#-Objekt. Es folgt der entsprechende C#-Code:

var languagesSection =
  ConfigurationManager.GetSection("Languages") as NameValueCollection;
string languages = languagesSection["languages"];
foreach(string language in languages.Split(",".ToCharArray());)
{
  var languageSection =
    ConfigurationManager.GetSection(language) as NameValueCollection;
  AddTranslation(languageSection, Constants.IF);
  AddTranslation(languageSection, Constants.WHILE);
...
}

Die „AddTranslation“-Methode fügt ein Synonym für eine bereits vorhandene Funktion hinzu:

public void AddTranslation(NameValueCollection languageDictionary, string originalName)
{
  string translation = languageDictionary[originalName];
  ParserFunction originalFunction =
    ParserFunction.GetFunction(originalName);
  ParserFunction.AddFunction(translation, originalFunction);
}

Dank der C#-Unterstützung von Unicode können die meisten Sprachen auf diese Weise hinzugefügt werden. Auch die Variablennamen können in Unicode hinzugefügt werden.

Alle Übersetzungen werden in der Konfigurationsdatei angegeben. So sieht die Konfigurationsdatei für Spanisch aus:

<Languages>
  <add key="languages" value="Spanish" />
</Languages>
<Spanish>
    <add key="if"    value ="si" />
    <add key="else"  value ="sino" />
    <add key="elif"  value ="sinosi" />
    <add key="while" value ="mientras" />
    <add key="set"   value ="asignar" />
    <add key="print" value ="imprimir" />
 ...
</Spanish>

Hier ist ein Beispiel des CSCS-Codes in Spanisch:

asignar(a, 5);
mientras(a > 0) {
  asignar(expr, 2*(10 – a*3));
  si (expr > 0) {
    imprimir(expr, " es mayor que cero");
  }
  sino {
    imprimir(expr, " es cero o menor que cero");
  }
  asignar(a, a - 1);
}

Der Parser kann nun Steuerungsanweisungen und Funktionen in Englisch und Spanisch verarbeiten. Die Anzahl von Sprachen, die hinzugefügt werden können, ist nicht beschränkt.

Zusammenfassung

Alle CSCS-Elemente (Ablaufsteuerungsanweisungen, Variablen, Arrays und Funktionen) werden durch die Definition einer C#-Klasse implementiert, die von der Basisklasse „ParserFunction“ abgeleitet ist und deren „Evaluate“-Methode überschreibt. Anschließend registrieren Sie ein Objekt dieser Klasse beim Parser. Dieser Ansatz bietet die folgenden Vorteile:

  • Modularität: Jede CSCS-Funktion und -Ablaufsteuerungsanweisung befindet sich in ihrer eigenen Klasse. Deshalb kann eine neue Funktion oder Ablaufsteuerungsanweisung einfach definiert oder eine vorhandene einfach geändert werden.
  • Flexibilität: CSCS-Schlüsselworte und -Funktionsnamen sind in jeder beliebigen Sprache möglich. Dafür muss nur die Konfigurationsdatei geändert werden. Im Gegensatz zu den meisten anderen Sprachen müssen CSCS-Ablaufsteuerungsanweisungen, -Funktionen und -Variablennamen nicht im ASCII-Format angegeben werden.

Freilich ist die Sprache CSCS in dieser Phase alles andere als vollständig. Hier nun einige Möglichkeiten, ihren Nutzen zu steigern:

  • Erstellen mehrdimensionaler Arrays. Es kann dieselbe C#-Datenstruktur wie für eindimensionale Arrays (List<Ergebnis>) verwendet werden. Mehr Analysefunktionalität muss allerdings hinzugefügt werden, wenn ein Element im multidimensionalen Array abgerufen oder festgelegt werden soll.
  • Ermöglichen, dass Tupel in einer Zeile initialisiert werden.
  • Hinzufügen logischer Operatoren (AND, OR, NOT usw.), was sehr nützlich für „if“- und „while“-Anweisungen wäre.
  • Hinzufügen der Fähigkeit, Funktionen und Methoden in CSCS zu schreiben. Derzeit können nur zuvor in C# geschriebene und kompilierte Funktionen verwendet werden.
  • Hinzufügen der Fähigkeit, CSCS-Quellcode aus anderen Elementen einzuschließen.
  • Hinzufügen weiterer Funktionen, die typische betriebssystembezogene Aufgaben ausführen. Da die meisten dieser Aufgaben einfach in C# implementiert werden können, wären die meisten nur dünne Wrapper für ihre C#-Entsprechungen.
  • Erstellen einer Verknüpfung für die Funktion „set(a, b)“ als „a = b“.

Ich hoffe, dass Ihnen dieser Kurzeinblick in die Sprache CSCS gefallen hat, der die Möglichkeiten zum Erstellen einer eigenen selbstdefinierten Skriptsprache aufzeigt.


Vassili Kaplan ist ein ehemaliger Microsoft Lync-Entwickler. Seine Leidenschaft ist die Programmierung in C# und C++. Er lebt derzeit in Zürich in der Schweiz und arbeitet freiberuflich für verschiedene Banken. Sie erreichen ihn unter iLanguage.ch.

Unser Dank gilt dem folgenden technischen Experten bei Microsoft für die Durchsicht dieses Artikels: James McCaffrey