一点号编程派昨天
平常工作中经常用到 shell 吧?好不好奇 shell 的具体执行方式?今天推送的这两篇文章,将利用 Python 实现一些简单的 shell 功能。
本文原作者为 Supasate Choochaisri ,由 PythonTG 翻译组的 Justin 翻译,校对为 EarlGrey。译者简介:Justin,python工程师一枚,对go、docker感兴趣,还在成长ing。
我很好奇 shell(比如 bash、cash等)内部的工作原理,所以我用 Python 实现了yosh(Your Own Shell)来满足自己的好奇心。我在本文中阐释的概念同样适用于其他语言。
yosh_project |-- yosh |-- __init__.py |-- shell.pyyosh_project是项目根文件夹(你也可以用yosh来命名)。
yosh是包文件夹,__init__.py会让包与文件夹同名(如果你不写 python,可以忽略这点)。
shell.py是主要的 shell 文件。启动 shell 时,它会立刻展示命令提示符并等待输入。在接收到命令并执行完毕(细节会在后面讲到)后,shell 会再次回到等待循环,准备接收下一条命令。
shell.pyshell_loop然后在shell_loop函数中,使用status标志来表示循环是否应该继续。在循环开始时,shell 将立即显示命令提示符,并等待输入。importsys SHELL_STATUS_RUN =1SHELL_STATUS_STOP =0defshell_loop: status = SHELL_STATUS_RUN whilestatus == SHELL_STATUS_RUN: # Display a command prompt sys.stdout.write('> ') sys.stdout.flush # Read command input cmd = sys.stdin.readline接下来,对输入的命令进行切分(tokenize),并执行(稍后会实现tokenize和execute函数)。现在,importsys SHELL_STATUS_RUN =1SHELL_STATUS_STOP =0defshell_loop: status = SHELL_STATUS_RUN whilestatus == SHELL_STATUS_RUN: # Display a command prompt sys.stdout.write('> ') sys.stdout.flush # Read command input cmd = sys.stdin.readline # Tokenize the command input cmd_tokens = tokenize(cmd) # Execute the command and retrieve new status status = execute(cmd_tokens)上述就是整个的 shell 循环。如果通过python shell.py启动 shell,会立即显示命令提示符。但是如果我们输入命令并回车,就会抛出错误,因为我们还没有定义tokenize函数。要退出 shell,可以使用ctrl-c,稍后我会介绍如何优雅地终止 shell。用户在 shell 中键入命令并按下回车时,输入的命令是一条长长的字符串,其中包含了命令名以及参数。因此,我们必须将其切分(将字符串拆分成多个 token)。
字符串切分乍一看很简单。我们可能会使用cmd.split根据空格来分割输入的命令。对于形如ls -a my_folder的命令是奏效的,因为cmd.split会将其拆分为一个列表 ―['ls', '-a', 'my_folder’],这样我们使用起来就比较容易了。但是,某些情况下,某些参数会带有单引号或者双引号,比如echo "Hello World”或者echo 'Hello World’。如果我们使用cmd.split, 将会得到一个包含三个 token 的列表 ―['echo', '"Hello', 'World”’]['echo', 'Hello World’]幸运地是,Python 提供了一个叫做shlex的库,可以很好地帮助我们分词(注:我们也可以使用正则表达式,但这不是本文的重点)。importsys importshlex ... deftokenize(string): returnshlex.split(string) ...然后,我们将这些 token 传给执行进程。
这是 shell 核心,而且也是有趣的部分。shell 执行mkdir test_dir时会发生什么呢?(注:mkdir是一个程序,传递test_dir参数执行后会创建名为test_dir的目录)。这一步中涉及的第一个函数是execvp。在解释execvp的功能之前,让我们先来实际使用一下。importos ... defexecute(cmd_tokens): # Execute command os.execvp(cmd_tokens[0], cmd_tokens) # Return status indicating to wait for next command in shell_loop returnSHELL_STATUS_RUN ...尝试再次运行 shell 并输入命令mkdir test_dir,然后回车。现在的问题是,我们敲击回车后,shell 并未等待下一条命令,而是终止了。但是目录成功被创建。
所以,execvp到底做了什么呢? execvp是系统调用exec的一个变种。第一个参数是程序名。v表示第二个参数是程序参数列表(可变的参数个数)。p表示PATH环境将用于搜索给定的程序名。在我们之前的尝试中,mkdir程序就是基于PATH环境变量。(exec还有其他变种,比如execv、execvpe、execl、execlp、execlpe等,你可以google一下获取更多信息。)exec会将当前调用进程的内存,替换为一个即将执行的进程。在我们的示例中,shell 进程的内存被mkdir程序替换。然后mkdir变为主进程,并创建test_dir目录,最后进程终止。这里最主要的一点是,shell 进程已经被mkdir进程取代。这也是为什么 shell 会终止而不是等待下一条命令。所以,我们需要另外一个系统调用fork来解决这个问题。fork会分配新的内存,并将现有进程拷贝到新的进程,我们称这个新进程为子进程,调用进程为父进程。之后,执行过exec的程序将会取代子进程。因此,作为父进程的 shell 就不用进行内存替换了。下面是修改后的代码:
...defexecute(cmd_tokens): # Fork a child shell process # If the current process is a child process, its `pid` is set to `0` # else the current process is a parent process and the value of `pid` # is the process id of its child process. pid = os.fork ifpid ==0: # Child process # Replace the child shell process with the program called with exec os.execvp(cmd_tokens[0], cmd_tokens) elifpid >0: # Parent process whileTrue: # Wait response status from its child process (identified with pid) wpid, status = os.waitpid(pid,0) # Finish waiting if its child process exits normally # or is terminated by a signal ifos.WIFEXITED(status)oros.WIFSIGNALED(status): break # Return status indicating to wait for next command in shell_loop returnSHELL_STATUS_RUN ...os.fork运行的代码若属于子进程,pid为 0;若属于父进程,pid为子进程的 id。 os.execvp在子进程中被调用,子进程的所有源码将被调用程序的源码替换掉。但是,父进程的代码没有改变。父进程等待子进程终止后,会返回继续 shell 循环的状态。
现在你可以尝试运行我们自己打造的 shell ,并键入mkdir test_dir2。shell应该会正常运转,而且 shell 进程会继续等待你输入下一条命令。尝试ls命令就会看到已创建的目录。但是,还是存在一些问题。
第一点,输入cd test_dir2,然后再输入ls,应该会进入到test_dir2这个空目录中,但是目录并没有切换到test_dir2。第二点,我们仍不能优雅地退出 shell。
上述问题将会在 Part2 中解决。
Python 翻译组是EarlGrey@编程派发起成立的一个专注于 Python 技术内容翻译的小组,目前已有近 30 名 Python 技术爱好者加入。
翻译组出品的内容(包括教程、文档、书籍、视频)将在编程派微信公众号首发,欢迎各位 Python 爱好者推荐相关线索。
推荐线索,可直接在编程派微信公众号推文下留言即可。
欢迎转发至朋友圈。如无特殊注明,本公号所发文章均为原创或编译,如需转载,请联系「编程派」获得授权。
扫码关注编程派