声明
本人小白,欢迎在评论区提出意见建议。文章未经允许不得转载。
文章首发于我的博客。
一个Linux Shell的基本流程
一个shell启动之后,基本上是按照以下的流程进行工作的。显示提示符(prompt),等待用户输入命令
对命令进行解析(parse)
根据命令执行对应的程序
程序执行完毕,重新从第一步开始,接受新的命令
这篇教程的重点主要是第三步,根据命令执行对应的程序。所以为了简单起见,我们不会实现shell的所有语法。
在Python中创建一个shell
因为我们的目标是实现简化版的shell语法,所以我们可以直接使用Python现有的cmd模组(module)。这是官方文档。
要想使用cmd模组,我们需要先将其导入,然后执行cmd.Cmd().cmdloop()来运行一个简单的互动命令行程序。
import cmd
if __name__ == '__main__':
cmd.Cmd().cmdloop()
但是,我们很快就发现,虽然它确实能够显示提示符,等待并接受用户输入命令,但是无论我们输入什么,它都会给出错误提示。这是因为以上代码建立的是一个空的互动命令行程序。
为了能够自定义这个互动命令行的行为,我们需要以cmd.Cmd作为父类(superclass),通过继承(inheritance),建立我们自己的子类(subclass),然后通过重写(override)特定的方法,来实现我们自己的shell。
import cmd
class PyShell(cmd.Cmd()):
pass # TODO: 重写父类方法
if __name__ == '__main__':
PyShell().cmdloop()
Python中cmd.Cmd类的基本结构
在cmd.Cmd中,通过执行cmdloop方法来进入主循环。在第一次进入主循环时,self.intro中的内容会被显示到屏幕上。随后,self.prompt的内容会被显示到屏幕上,并等待用户输入。
当用户输入完成,cmdloop会对用户的输入进行解析。首先,cmdloop会尝试根据用户的输入,执行对应的do_*方法。举个例子,假设用户输入是exit,那么cmdloop会尝试执行do_exit方法。如果用户的输入有多个词,那么cmdloop会将第一个看成命令,剩下的作为参数。举个例子,假设用户输入的是exit 1,那么cmdloop会尝试执行do_exit这个方法(空格隔开的1会被认为是参数,并作为参数被传入do_exit方法)。如果方法不存在,那么cmdloop会执行default这个方法。default这个方法在父类cmd.Cmd中存粹是显示一条错误信息。这就是为什么一开始我们直接执行cmd.Cmd().cmdloop()的时候无论输入什么命令都会报错。
当do_*或者default方法运行完毕时,cmdloop会进入下一次循环。
因此,我们需要在我们的子类PyShell中对父类的__init__、cmdloop,以及default方法进行重写,并添加一些do_*开头的方法。
实现退出shell的功能
可能细心的读者已经注意到了,这个shell目前为止没有退出功能。只能通过关闭终端(terminal)软件或者按下^C(Control-C)组合键发送键盘中断(Keyboard Interrupt)的方式来结束这个shell。
所以,我们的第一步,是在PyShell中,添加do_exit这个方法。
class PyShell(cmd.Cmd()):
def do_exit(self, arg_str: str) -> None:
exit(0)
do_exit中的参数(parameter)arg_str就是用户的输入中,以第一个空格为分界,右半部分的内容。
举个例子,如果用户输入的是exit,那么arg_str此时就是''。如果用户输入的是exit 1,那么arg_str此时就是字符串'1'。如果用户输入的是exit 1 foo 2 bar,那么arg_str此时就是'1 foo 2 bar'(这部分的空格Python不会自动拆分)。
为了实现可以手动指定以不同退出代码(exit code)退出的功能,我们对do_exit这个方法稍作修改。
import shlex
class PyShell(cmd.Cmd()):
def do_exit(self, arg_str: str) -> None:
"""usage: exit [exitcode]"""
args = shlex.split(arg_str)
if len(args) > 1:
print('exit: too many arguments', file=sys.stderr)
return
if len(args) == 0 or args[0] == '':
exit(0)
try:
exit(int(args[0]))
except ValueError:
print('exit: invalid exit code', file=sys.stderr)
注意,这里我们用shlex.split来处理双引号("")内空格不作为分隔的情况。如果我们直接使用arg_str.split(' '),会出现"hello world"被处理为['"hello', 'world"']的情况。但是如果我们使用的是shlex.split(arg_str),那么"hello world"一定会被正确地处理为['hello world']。
经过修改后的do_exit,只允许最多一个自定的退出代码。如果不提供退出代码,默认为0。有了do_exit方法之后,用户可以使用exit命令来退出我们的shell了。