用Python创建一个简单的脚本语言

10 投票
3 回答
1489 浏览
提问于 2025-04-16 20:35

我正在创建一个图形界面应用程序,用来监控和处理一串消息。我想给用户提供一个简单的方法,让他们可以编写一些功能的脚本,现在我在寻找合适的方案。最开始我想用XML,因为它可以很自然地处理嵌入的代码:

<if>
   <condition>
      <recv>
         <MesgTypeA/>
      </recv>
   </condition>
   <loop count=10>
      <send>
         <MesgTypeB>
            <param1>12</param1>
            <param2>52</param2>
         </MesgTypeB>
      </send>
   </loop>
</if>

在解析方面,我打算使用ElementTree来构建代码的状态。不过,写和读XML并不是一件简单的事,尤其是我不能指望编写脚本的人有任何经验。我在想有没有其他更容易读写和在Python中处理的替代方案。我也考虑过JSON,但因为这是一个脚本,顺序是很重要的。

有没有人能推荐一些可能的替代方案呢?

谢谢。

3 个回答

3

可能是Python或者Lisp,因为它们的语法很简单,容易理解。

5

你可以使用 pyparsing 来定义你自己的脚本语言的语法。

16

那Python本身怎么样呢?

举个例子:

>>> import code
>>> def host_func():
...     print("Hello old chap!")
...
>>> c = code.compile_command("print(\"Script says hello!\"); host_func()")
>>> exec(c)
Script says hello!
Hello old chap!

exec 让你可以明确指定想要通过两个可选参数 localsglobals 暴露哪些内容给脚本使用。

在这个例子中,我明确指出了脚本可以访问哪些全局变量。注意,我可以在这里“创建”变量,或者给已有的函数起个新名字。它其实是一个指向函数和数据的字典。

>>> import code
>>> def secret():
...     print("What?! I don't even... get out of here.")
...
>>> def public():
...     print("Hello stranger.")
...
>>> c = code.compile_command("secret(); public()")

如果我用包含两个函数的全局变量来调用这个,指向已经存在的函数,结果是:

>>> exec(c, {"secret": secret, "public": public})
What?! I don't even... get out of here.
Hello stranger.

现在当我省略 secret 时,脚本就找不到它了。

>>> exec(c, {"public": public})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<input>", line 1, in <module>
NameError: name 'secret' is not defined

在这里我完全重新定义了 secret

>>> exec(c, {"public": public, "secret":lambda: print("Haha! Doppelganger.")})
Haha! Doppelganger.
Hello stranger.

正如lazyr 在评论中提到的,这里有安全隐患。上面的例子让脚本几乎可以随意操作。在某些情况下,这样是不被允许的。

有一些方法可以减少这种风险:

  • 限制 __builtins__,只允许“白名单”中的内置函数。
  • 让导入模块变得困难。

例如,下面是如何搞乱 import 语句的(在Py2.*中,内置函数是 __builtins__):

>>> import builtins
>>> def no_import(*args, **kwargs):
...     raise ImportError("I cannot let you do that, Dave.")
...
>>> builtins.__import__ = no_import
>>> import os
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in no_import
ImportError: I cannot let you do that, Dave.

由此可见,我们可以在全局参数中传入自己的 builtins

>>> import code
>>> evil_code = "import os; import stat; os.chmod(\"passwords.txt\", stat.S_IROT
H);"
>>> compiled = code.compile_command(evil_code)
>>> def no_import(*args, **kwargs):
...    raise ImportError("I cannot let you do that, Dave.")
...
>>> exec(compiled, {"__builtins__": {"__import__": no_import}})
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<input>", line 1, in <module>
  File "<stdin>", line 2, in no_import
ImportError: I cannot let you do that, Dave.

不过要注意,这样会搞乱之后的所有导入操作。可能更好的做法是替换成一个允许导入白名单模块的版本。

最后,我不确定这是否能完全保护你。有些聪明的人可能会绕过这个限制。但至少可以让最明显的违规行为受到抑制。

撰写回答