Shell的基本生命周期
无论使用什么语言实现,Shell 的生命周期都是一样的,主要做三件事:
- 初始化:Shell 会在初始化时读入和执行配置文件。这会改变 Shell 接下来各方面的行为。
- 解释:Shell 在解释阶段(也就是等待用户输入的阶段)读入标准输入的命令并解释执行。
- 终结:在用户输入 shutdown 命令后,Shell 会释放掉占用的内存并终结自己。
使用Python实现的 Shell 主函数如下:
1 2 3 4 5 6 7
| def main(): init()
shell_loop()
|
和 C 中的主函数相比,多了一个init()
函数,该函数是用来注册函数关系映射表的,在 Shell 正式启动前将一些内置的命令先加载好,各个内置的命令写在主目录下的 func 文件夹下,每一个命令都对应一个.py文件。这个功能在 C 中的 Shell也有实现,只不过是通过字符串数组和函数指针数组实现的,且全部写在 main.c 文件里。在 Python 中这样实现的目的是为了降低代码的耦合度,使得增添命令变得更加灵活。如果想为我们的 Shell 增加或减少一个命令,只需要在 func 文件下增加 或减少一个.py文件即可,不需要频繁改动主函数。
init()
函数的实现如下(暂时只实现了这四个命令):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| def register_command(name, func): """ 注册命令,使命令与相应的处理函数建立映射关系 @Param name:命令名 @param func:函数名 """ built_in_cmds[name] = func def init(): """ 注册所有命令 """ register_command("cd", cd) register_command("exit", exit) register_command("getenv", getenv) register_command("history", history)
|
Shell的基本循环
Shell 的基本循环也是通用的以下三点:
- 读入:从标准输入中读入命令
- 解析:将执行的命令和其参数解析出来输入程序执行
- 执行:根据命令和参数执行程序
用 Python 的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| def shell_loop(): status = SHELL_STATUS_RUN while status == SHELL_STATUS_RUN: display_cmd_prompt()
ignore_signals()
try: cmd = sys.stdin.readline()
cmd_tokens = tokenize(cmd)
cmd_tokens = preprocess(cmd_tokens)
status = execute(cmd_tokens) except: _, err, _ = sys.exc_info() print(err)
|
虽然没有 C 中实现的那么简洁清楚,但我增加了更多的功能。比如:打印命令提示符,在 C 中只是打印一个’>’;忽略 Ctrl-z 和 Ctrl-c 终止信号;在循环内加入了异常检测语句。这样的 Shell 比起 C 里的 Shell 看起来就显得高大上多了。
显示命令提示符的代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| def display_cmd_prompt(): user = getpass.getuser()
hostname = socket.gethostname()
cwd = os.getcwd()
base_dir = os.path.basename(cwd)
home_dir = os.path.expanduser('~') if cwd == home_dir: base_dir = '~'
if platform.system() != "Windows": sys.stdout.write("[\033[1;33m%s\033[0;0m@%s \033[1;36m%s\033[0;0m] $"%(user, hostname, base_dir)) else: sys.stdout.write("[%s@%s %s]$ " % (user, hostname, base_dir)) sys.stdout.flush()
|
分别通过 Python 标准库中的函数获取了当前用户名、主机名和工作路径。并判断如果是用户目录则用’~’替代。对于非 Windows 系统输出带颜色的代码。
忽略终止信号的代码如下:
1 2 3 4 5 6 7 8
| def ignore_signals(): if platform.system() != "Window": signal.signal(signal.SIGTSTP, signal.SIG_IGN) signal.signal(signal.SIGINT, signal.SIG_IGN)
|
在读入命令时直接使用了 Python 中的标准读入,所以就没有单独为读入写一个函数了。
解析一行
首先将读到的行进行分词,我直接使用了 Python 库 shlex 中的 split()
方法,该方法按 Shell 的语法规则对字符串进行分割。
1 2 3 4 5 6 7
| def tokenize(string): return shlex.split(string)
|
相较于 C 中的 Shell 分词之后还增加了一个预处理函数,将输入的环境变量进行替换,这样就可以输入变量了。
预处理函数如下:
1 2 3 4 5 6 7 8 9 10 11 12
| def preprocess(tokens): processed_token = [] for token in tokens: if token.startswith('$'): processed_token.append(os.getenv(token[1:])) else: processed_token.append(token)
return processed_token
|
解释执行
在读取了预处理后的命令行参数后,便可以进行解释执行了,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| def execute(cmd_tokens): with open(HISTORY_PATH, 'a') as history_file: history_file.write(' '.join(cmd_tokens) + os.linesep)
if cmd_tokens: cmd_name = cmd_tokens[0] cmd_args = cmd_tokens[1:]
if cmd_name in built_in_cmds: return built_in_cmds[cmd_name](cmd_args)
signal.signal(signal.SIGINT, handler_kill)
if platform.system() != "Windows": p = subprocess.Popen(cmd_tokens)
p.communicate() else: command = "" command = ' '.join(cmd_tokens) os.system(command) return SHELL_STATUS_RUN
|
由于实现了 history 命令,所以在每一次执行命令时都会将该命令写入保存历史命令的文件 history_file 中,方便调用 history()
时读取。接下来判断输入的命令是否是内置实现的命令,如果是,则直接调用。不是的话则会判断当前操作系统类型,并根据操作系统类型调用不同的执行命令的函数。
总结
至此,用 Python 实现的 Shell 就全部介绍完毕了。相较于 C 中的实现,我做了以下几点优化:
- 优化了命令提示符功能,看起来更专业
- 低耦合的设计,可以动态地添加删除命令
- 根据不同的操作系统类型进行了适配(C中的实现仅支持 Linux)
- 可以在输入中使用变量
总之,Shell 就是这么一个读取用户命令,并将其解释执行的程序。通过实现这么一个简单的 Shell 让我对平时编程最常打交道的程序有了更清楚的认识,十分有趣。强烈建议大家自己动手实现一遍。
最后是各个命令的具体实现代码:
cd:
1 2 3 4 5 6 7 8
| from .constants import *
def cd(args): if len(args) > 0: os.chdir(args[0]) else: os.chdir(os.getenv('HOME')) return SHELL_STATUS_RUN
|
exit:
1 2 3 4
| from .constants import *
def exit(args): return SHELL_STATUS_STOP
|
getenv:
1 2 3 4 5 6
| from .constants import *
def getenv(args): if len(args) > 0: print(os.getenv(args[0])) return SHELL_STATUS_RUN
|
history:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import sys from .constants import *
def history(args): with open(HISTORY_PATH, 'r') as history_file: lines = history_file.readlines() limit = len(lines) if len(args) > 0: limit = int(args[0]) start = len(lines) - limit for line_num, line in enumerate(lines): if line_num >= start: sys.stdout.write('%d %s' %(line_num + 1, line))
sys.stdout.flush()
return SHELL_STATUS_RUN
|