如何编写命令解释器/解析器?


22

问题:以字符串形式运行命令。

  • 命令示例:

    /user/files/ list all; 相当于: /user/files/ ls -la;

  • 另一个:

    post tw fb "HOW DO YOU STOP THE TICKLE MONSTER?;"

相当于: post -tf "HOW DO YOU STOP THE TICKLE MONSTER?;"

当前解决方案:

tokenize string(string, array);

switch(first item in array) {
    case "command":
        if ( argument1 > stuff) {
           // do the actual work;
        }
}

我在此解决方案中看到的问题是:

  • 除了在每种情况下嵌套ifs-else外,没有其他错误检查。该脚本变得非常庞大且难以维护。
  • 命令和响应是硬编码的。
  • 无法知道标志是否正确或缺少参数。
  • 缺乏智能提示“您可能要运行$ command”。

我无法解决的最后一件事是使用不同编码的同义词,例如:

case command:
case command_in_hebrew:
    do stuff;
break;

最后一个也许是微不足道的,但是,我想看到的是这种计划的坚实基础。

我目前正在用PHP编程,但可能会在PERL中做到这一点。


我完全看不到它与PHP的关系。关于SO和SE的解释器/编译器主题已经有很多线程。
拉斐尔

3
没人提到getopt吗?
安东·巴科夫斯基

@AntonBarkovsky:我做到了。查看我的链接。我认为,像Ubermensch这样的回答对于OP试图做的事情来说过于复杂。
2011年

1
我还引用了一个使用RegExp的简单方法。回答也是更新
超人

没有提及任何具体进度。郎。您可以添加“ c”标签,“ ruby​​”标签,“ php”标签,也许有一个开源库,标准库或“常用的,还不是标准库”。为您的进步。郎。
umlcat 2011年

Answers:


14

坦率地说,构建解析器是一项繁琐的工作,与编译器技术非常接近,但是构建解析器却是一次很好的冒险。解析器带有解释器。因此,您必须同时构建两者。

解析器和解释器快速入门

这不是太技术性。因此,专家们不必为我烦恼。

当您将某些输入馈送到终端时,终端会将输入拆分为多个单元。输入称为表达式,多个单元称为令牌。这些令牌可以是运算符或符号。因此,如果您在计算器中输入4 + 5,则此表达式将被拆分为三个标记4,+,5。加号被视为4和5个符号时的运算符。这被传递到包含操作员定义的程序(将其视为解释器)。根据定义(在我们的示例中为add),它将两个符号相加并将结果返回到终端。所有编译器均基于该技术。将表达式拆分为多个标记的程序称为词法分析器,将这些标记转换为标记以进行进一步处理和执行的程序称为解析器。

Lex和Yacc是在C下基于BNF语法构建词法分析器和语法分析器的规范形式,建议使用此选项。大多数解析器是Lex和Yacc的克隆。

构建解析器/解释器的步骤

  1. 将令牌分类为符号,运算符和关键字(关键字是运算符)
  2. 使用BNF表单建立语法
  3. 为您的操作编写解析器功能
  4. 编译为程序运行

因此,在上述情况下,您的加成令牌将是任意数字和加号,并定义了如何处理词法分析器中的加号

注意事项

  • 从求值从左向右选择一个解析器技术LALR
  • 阅读有关编译器的这本龙书以了解它。我个人还没有完成这本书
  • 链接将使您可以快速了解Python下的Lex和Yacc

一个简单的方法

如果您只需要具有有限功能的简单解析机制,请将您的需求转换为正则表达式,然后创建一整套功能。为了说明,假设对四个算术函数使用一个简单的解析器。所以,你将是第一个,然后调用操作的功能,在风格(类似于LISP)列表(+ 4 5)(add [4,5])那么你可以使用一个简单的正则表达式来获得运营商和要操作的符号列表。

通过这种方法可以轻松解决大多数常见情况。缺点是您不能使用语法清晰的嵌套表达式,也不能使用简单的高阶函数。


2
这是最困难的方法之一。分离词法分析和解析过程等-对于非常复杂但古老的语言实现高性能解析器可能很有用。在现代世界中,无词法分析是最简单的默认选项。解析组合器或eDSL比Yacc等专用预处理器更易于使用。
SK-logic

同意SK-logic,但由于需要一个一般的详细答案,因此我建议Lex和Yacc以及一些解析器基础知识。Anton建议的getopts也是一个更简单的选择。
Ubermensch 2011年

这就是我所说的-lex和yacc是最困难的解析方式之一,甚至不够通用。在一般情况下,无Lexer分析(例如packrat或类似Parsec的简单分析)要简单得多。而且《龙书》对于解析不再是一个非常有用的介绍,因为它已经过时了。
SK-logic

@ SK-logic您能推荐一本更好的更新书吗?它似乎涵盖了试图理解解析的人的所有基本知识(至少在我看来)。关于lex和yacc,尽管很难,但已被广泛使用,并且许多编程语言都提供了它的实现。
Ubermensch 2011年

1
@ alfa64:当您实际根据此答案编写解决方案代码时,请务必告知我们
quentin-starin 2011年

7

首先,涉及语法或如何指定参数时,请不要发明自己的语法。在GNU风格的标准已经是非常流行的和众所周知的。

其次,由于您使用的是公认的标准,所以不要重新发明轮子。使用现有的库来为您做。如果您使用GNU样式参数,几乎可以肯定,您选择的语言已经有一个成熟的库。例如:c#phpc

一个好的选项解析库甚至会为您提供可用选项的格式化帮助。

编辑12/27

看来您正在做的事情比实际情况要复杂得多。

当您查看命令行时,它确实非常简单。只是选项和这些选项的参数。很少有复杂的问题。选项可以有别名。参数可以是参数列表。

这个问题的一个问题是,您实际上没有为要处理的命令行类型指定任何规则。我已经建议使用GNU标准,您的示例也接近于此(尽管我不太了解您的第一个示例,其中路径作为第一项?)。

如果我们在谈论GNU,则任何单个选项都只能具有长格式和短格式(单个字符)作为别名。包含空格的任何参数都必须用引号引起来。可以链接多个短格式选项。短格式选项必须以单破折号开头,长格式必须以两个破折号开头。只有最后一个链接的简短形式选项可以带有参数。

一切都很简单。都非常普遍。还可以用您能找到的每种语言来实现,大概五倍了。

不要写 使用已写的内容。

除非您有标准命令行参数以外的其他想法,否则请使用许多已经存在且经过测试的库中的一个来执行此操作。

有什么并发症?


3
始终要始终利用开源社区。
Spencer Rathbun

您是否尝试过getoptionkit?
alfa64 2011年

不,我已经有好几年没有用PHP了。可能还有其他php库。我使用了链接到的c#命令行解析器库。
2011年

4

您是否已经尝试过http://qntm.org/loco之类的东西?这种方法比任何手写的临时方法都要干净得多,但不需要像Lemon这样的独立代码生成工具。

编辑:处理具有复杂语法的命令行的一般技巧是将参数组合回单个空格分隔的字符串,然后将其正确解析,就好像它是某些特定于域的语言的表达一样。


+1不错的链接,我想知道它是否可以在github或其他上使用。那使用条款呢?
hakre 2011年

1

您没有提供有关语法的许多细节,仅是一些示例。我看到的是,有一些字符串,空格和(用双引号括起来的字符串,然后是一个“;”(可能对您的问题无所谓)。在最后。

看起来这可能类似于PHP语法。如果是这样,PHP附带有一个解析器,您可以重复使用,然后更具体地进行验证。最后,您需要处理令牌,但是看起来这只是从左到右,因此实际上只是所有令牌的迭代。

token_get_all下列问题的答案给出了一些重用PHP令牌解析器()的示例:

这两个示例也都包含一个简单的解析器,可能类似于适合您的方案的那些解析器。


是的,我匆忙处理了语法内容,现在将其添加。
alfa64 2011年

1

如果您的需求很简单,并且你们都有时间并且对此感兴趣,那么我会反对这里,并说不要回避编写自己的解析器。如果没有别的,这是一个很好的学习经验。如果您有更复杂的要求-嵌套函数调用,数组等-请注意,这样做可能会花费大量时间。滚动自己的游戏的一大好处是,与系统集成不会有问题。不利的一面当然是所有的麻烦都是你的错。

但是,请不要使用硬编码命令来处理令牌。然后,使用类似的探测命令的问题就消失了。

每个人都总是推荐龙的书,但我总是发现Ronald Mak的书面编译器和口译员”是更好的介绍。


0

我写过这样的程序。一种是具有类似命令语法的IRC机器人。有一个很大的文件,它是一个很大的switch语句。它可以工作-速度很快-但是很难维护。

OOP旋转更多的另一种选择是使用事件处理程序。您可以使用命令及其专用功能创建键值数组。给出命令后,您将检查数组是否具有给定键。如果是这样,请调用该函数。这是我对新代码的建议。


我已经阅读了您的代码,它与我的代码完全相同,但是正如我所说,如果您想让其他人使用,则需要添加错误检查和内容
alfa64 2011年

1
@ alfa64请在问题中添加所有说明,而不要添加评论。尽管您正在寻找真正特定的内容,但是并不清楚您到底想要什么。如果是这样,请告诉我们确切的含义。我不认为它很容易从去I think my implementation is very crude and faultybut as i stated, if you want other people to use, you need to add error checking and stuff...告诉我们什么是原油一下,什么是错误的,它会帮助你得到更好的答案。
yannis 2011年

当然,我会重做这个问题
alfa64年

0

我建议使用工具,而不要自己实现编译器或解释器。Irony使用C#表示目标语言语法(您命令行的语法)。CodePlex上的描述说:“ Irony是用于在.NET平台上实现语言的开发套件。”

参见CodePlex上的Irony官方主页:Irony-.NET语言实现工具包


您将如何在PHP中使用它?
SK-logic

我在问题中没有看到任何PHP标记或对PHP的引用。
Olivier Jacot-Descombes

我知道,它原本是关于PHP的,但是现在被重写了。
SK-logic

0

我的建议是为Google寻找一个可以解决您问题的图书馆。

我最近经常使用NodeJS,而Optimist是我用于命令行处理的东西。我鼓励您搜索一种可以用于自己选择的语言的语言。如果没有,请编写一个并开源:D您甚至可以通读Optimist的源代码并将其移植到您选择的语言。


0

您为什么不简化一点,您的要求呢?

不要使用完整的解析器,它过于复杂,甚至对于您的案例来说都是不必要的。

进行循环,写一条表示“提示”的消息,可以作为您当前的路径。

等待一个字符串,“解析”该字符串,然后根据字符串的内容执行某些操作。

该字符串可以像预期的一行一样“解析”,其中的空格是分隔符(“ tokenizer”),其余字符被分组。

例。

程序输出(并保持在同一行中):/ user / files /用户写入(在同一行中)所有列表;

您的程序将生成一个列表,集合或数组,例如

list

all;

或者如果 ”;” 被认为是空格之类的分隔符

/user/files/

list

all

您的程序可以从期望一条指令开始,而没有Unix风格的“管道”,也没有windowze风格的重定向。

您的程序可以制作一个指令字典,每个指令可能都有一个参数列表。

命令设计模式适用于您的情况:

http://en.wikipedia.org/wiki/Command_pattern

这是一个“纯c”伪代码,未经测试或完成,仅是关于如何完成的构想。

您还可以使其更面向对象,并且可以使用编程语言。

例:


// "global function" pointer type declaration
typedef
  void (*ActionProc) ();

struct Command
{
  char[512] Identifier;
  ActionProc Action; 
};

// global var declarations

list<char*> CommandList = new list<char*>();
list<char*> Tokens = new list<char*>();

void Action_ListDirectory()
{
  // code to list directory
} // Action_ListDirectory()

void Action_ChangeDirectory()
{
  // code to change directory
} // Action_ChangeDirectory()

void Action_CreateDirectory()
{
  // code to create new directory
} // Action_CreateDirectory()

void PrepareCommandList()
{
  CommandList->Add("ls", &Action_ListDirectory);
  CommandList->Add("cd", &Action_ChangeDirectory);
  CommandList->Add("mkdir", &Action_CreateDirectory);

  // register more commands
} // void PrepareCommandList()

void interpret(char* args, int *ArgIndex)
{
  char* Separator = " ";
  Tokens = YourSeparateInTokensFunction(args, Separator);

  // "LocateCommand" may be case sensitive
  int AIndex = LocateCommand(CommandList, args[ArgIndex]);
  if (AIndex >= 0)
  {
    // the command

    move to the next parameter
    *ArgIndex = (*ArgIndex + 1);

    // obtain already registered command
    Command = CommandList[AIndex];

    // execute action
    Command.Action();
  }
  else
  {
    puts("some kind of command not found error, or, error syntax");
  }  
} // void interpret()

void main(...)
{
  bool CanContinue = false;
  char* Prompt = "c\:>";

  char Buffer[512];

  // which command line parameter string is been processed
  int ArgsIndex = 0;

  PrepareCommandList();

  do
  {
    // display "prompt"
    puts(Prompt);
    // wait for user input
      fgets(Buffer, sizeof(Buffer), stdin);

    interpret(buffer, &ArgsIndex);

  } while (CanContinue);

} // void main()

您没有提到您的编程语言。您还可以提及任何编程语言,但最好是“ XYZ”。


0

您前面有几项任务。

看你的要求...

  • 您需要分析命令。这是一个相当容易的任务
  • 您需要具有可扩展的命令语言。
  • 您需要有错误检查和建议。

可扩展命令语言指示需要DSL。如果扩展很简单,我建议不要自己动手,而应使用JSON。如果它们很复杂,则使用s表达式语法是不错的选择。

错误检查意味着您的系统也知道可能的命令。那将是后指挥系统的一部分。

如果是从头开始实现这样的系统,则可以将Common Lisp与精简的阅读器一起使用。每个命令令牌将映射到一个符号,该符号将在s表达式RC文件中指定。标记化后,将在有限的上下文中对其进行评估/扩展,以捕获错误,任何可识别的错误模式都将返回建议。之后,实际命令将被分派到OS。


0

函数编程中有一个不错的功能,您可能会感兴趣。

这称为模式匹配

这是ScalaF#中模式匹配的一些示例的两个链接。

我同意您的观点,使用switch结构有点繁琐,并且我特别喜欢在Pascal中使用模式匹配在Scala中实现编译器。

特别是,我建议您研究Scala网站的lambda演算示例。

我认为这是最明智的处理方式,但是如果您必须严格遵循PHP,那么您将陷入“老派” switch


0

查看Apache CLI,它的全部目的似乎是完全按照您的意愿进行操作,因此,即使您无法使用它,也可以查看其架构并进行复制。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.