当前位置: 首页 > 科技观察

使用Python创建您自己的Shell(第1部分)

时间:2023-03-13 13:51:12 科技观察

我很想知道Shell(如bash、csh等)在内部是如何工作的。于是为了满足自己的好奇心,我用Python实现了一个叫yosh(YourOwnShell)的shell。本文中介绍的概念也可以应用于其他编程语言。(提示:你可以在这里找到这篇博文中使用的源代码,它是在MIT许可证下发布的。我在MacOSX10.11.5上用Python2.7.10和3.4.3测试了它。它应该可以在其他Unix上运行-like环境,例如Linux和Windows上的Cygwin。)让我们开始吧。第0步:项目结构对于这个项目,我使用了以下项目结构。yosh_project|--yosh|--__init__.py|--shell.pyyosh_project为项目根目录(也可以简单命名为yosh)。yosh是包目录,__init__.py可以让它成为一个和包同目录名的包(如果不是用Python写的可以省略)shell.py是我们的主脚本文件。第1步:Shell循环当启动一个shell时,它会显示命令提示符并等待您的命令输入。在接收到输入的命令并执行之后(本文后面会详细解释),你的shell会回到这里并循环等待下一个命令。在shell.py中,我们将从一个调用shell_loop()函数的简单主函数开始,如下所示:),为了指示循环是继续还是停止,我们使用状态标志。在循环开始时,我们的shell将显示命令提示符并等待读取命令输入。importsysSHELL_STATUS_RUN=1SHELL_STATUS_STOP=0defshell_loop():status=SHELL_STATUS_RUNwhilestatus==SHELL_STATUS_RUN:###显示命令提示符sys.stdout.write('>')sys.stdout.flush()###读取命令输入cmd=sys.在stdin.readline()之后,我们拆分命令tokenize输入并执行execute(我们将很快实现tokenize和execute功能)。因此,我们的shell_loop()将如下所示:importsysSHELL_STATUS_RUN=1SHELL_STATUS_STOP=0defshell_loop():status=SHELL_STATUS_RUNwhilestatus==SHELL_STATUS_RUN:###显示命令提示符sys.stdout.write('>')sys.stdout.flush()###读取命令输入cmd=sys.stdin.readline()###拆分命令输入cmd_tokens=tokenize(cmd)###执行命令得到新状态status=execute(cmd_tokens)这就是我们整个shell循环.如果我们使用pythonshell.py启动shell,它将显示命令提示符。但是,如果我们键入命令并按enter,它会抛出一个错误,因为我们还没有定义tokenize函数。要退出shell,请尝试键入ctrl-c。稍后我将解释如何优雅地退出shell。第二步:命令分段tokenize当用户在我们的shell中输入一条命令并按下回车键时,该命令将是一个长字符串,其中包含命令名称及其参数。因此,我们必须对字符串进行拆分(将一个字符串拆分为多个元组)。乍一看似乎很简单。我们也许可以使用cmd.split()将输入拆分为空格。它适用于像ls-amy_folder这样的命令,因为它将命令拆分为列表['ls','-a','my_folder']以便我们可以轻松地处理它们。但是,有些情况下,像echo"HelloWorld"或echo'HelloWorld'这样的参数用单引号或双引号引起来。如果我们使用cmd.spilt,我们将得到一个包含3个标记['echo','"Hello','World"']的列表,而不是一个包含2个标记['echo','HelloWorld']的列表。幸运的是,Python提供了一个名为shlex的库,可以帮助我们神奇地拆分命令。(提示:我们也可以使用正则表达式,但这不是本文的重点。)importsysimportshlex...deftokenize(string):returnshlex.split(string)...然后我们将这些元组发送到执行过程。第3步:执行这是shell的核心和有趣部分。当shell执行mkdirtest_dir时到底发生了什么?(提示:mkdir是一个带有test_dir参数的可执行程序,它创建一个名为test_dir的目录。)execvp是此步骤所需的第一个函数。在我们解释execvp做什么之前,让我们看看它的实际应用。importos...defexecute(cmd_tokens):###执行命令os.execvp(cmd_tokens[0],cmd_tokens)###返回状态告诉下一个命令在shell_loop中等待returnSHELL_STATUS_RUN...尝试运行我们的再次shell,并输入mkdirtest_dir命令,然后回车。在我们按下Enter后,问题是我们的shell只是退出而不是等待下一个命令。但是,该目录已正确创建。那么,execvp到底做了什么?execvp是exec系统调用的变体。第一个参数是程序名称。v表示第二个参数是一个程序参数列表(可变数量的参数)。p表示环境变量PATH将用于搜索给定的程序名称。在我们最后一次尝试中,它会根据我们的PATH环境变量查找mkdir程序。(还有其他exec变体,如execv、execvpe、execl、execlp、execlpe;您可以用google搜索它们以获取更多信息。)exec将调用进程的当前内存替换为即将运行的新进程。在我们的例子中,我们的shell进程内存被替换为mkdir程序。接下来,mkdir成为主进程并创建test_dir目录。***进程退出。这里很重要的一点是我们的shell进程已经被mkdir进程所取代。这就是我们的shell消失并且不等待下一个命令的原因。因此,我们需要另一个系统调用来解决这个问题:fork。fork将分配新的内存并将当前进程复制到新进程。我们称这个新进程为子进程,调用者进程为父进程。然后,子进程内存被执行的程序替换。因此,我们的shell,也就是父进程,是安全的,不会有内存替换的危险。让我们看看修改后的代码。...defexecute(cmd_tokens):###Forkasubshel??lprocess###如果当前进程是子进程,则其`pid`设置为`0`###否则,如果当前进程是父进程,`pid`###的值是其子进程的进程ID。pid=os.fork()ifpid==0:###子进程###用exec调用的程序替换子进程os.execvp(cmd_tokens[0],cmd_tokens)elifpid>0:###父进程whileTrue:###等待其子进程的响应状态(按进程ID查找)wpid,status=os.waitpid(pid,0)###其子进程正常退出时###或者被中断时一个信号,结束等待状态ifos.WIFEXITED(status)oros.WIFSIGNALED(status):break###Returnstatus通知等待shell_loop中的下一个命令returnSHELL_STATUS_RUN...当我们的父进程调用os.fork()时,你它可以想象所有的源代码都被复制到新的子进程中。此时,父进程和子进程看到的是同一段代码,并行运行。如果正在运行的代码属于子进程,则pid将为0。否则,如果正在运行的代码属于父进程,则pid将为子进程的进程id。当在子进程中调用os.execvp时,可以想象子进程的所有源代码都被调用的程序代码替换了。但是,父进程的代码不会改变。当父进程完成等待子进程退出或终止时,它返回一个指示继续shell循环的状态。现在,您可以尝试运行我们的shell并键入mkdirtest_dir2。它应该正确执行。我们的主shell进程仍然存在并等待下一个命令。尝试ls,您可以看到创建的目录。但是,这里仍然存在一些问题。***,尝试cdtest_dir2然后是ls。它应该进入一个空的test_dir2目录。但是,您会看到目录没有更改为test_dir2。其次,我们仍然没有办法优雅地退出我们的shell。