如何写一个简单的shell
看完《UNIX环境高级编程》后我就一直想写一个简单的shell来作为练习,因为有事断断续续的写了好几个月,如今写了差不多来总结一下。
源代码放在了Github: https://github.com/Broglie/Oh-Shell
简单的分析
我们的shell不像bash那样复杂全面,只是实现其中的一小部分功能:命令历史,命令补全,支持IO重定向和管道。一共分成几个部分:主
函数文件,输出出错信息,解析命令等。
我们打开bash对照着做,首先bash有命令提示符,我们要做的和bash的命令提示符一样。然后我们读取用户输入的命令并使用readline库
将命令添加到历史命令列表。然后我们将命令传递给解析命令函数,解析命令并执行。
输出命令提示符
打开bash后我们看到初始的命令提示符如下图所示:
以root登录后用cd
命令切换到别的工作目录后如下图所示:
我们发现bash的命令提示符格式为[用户名]@[主机名]:[当前工作目录][$或#]
。可以看出用户的home目录以~表示,而结尾部分的$或#是
指如果当前用户是普通用户,则命令提示符是$;如果当前用户是root用户,则命令提示符为#。
那么该如何实现呢?我们用getpwuid
函数获取用户的信息,包括用户名和用户ID;用gethostname
函数获取主机名;用getcwd
函数获
取当前用户的当前工作目录。有了用户ID我们就能判断当前用户是不是root,因为root用户的用户ID为0。有了当前工作目录我们就能判断
工作目录是不是在用户的home目录下,如果在home目录下我们就将home目录的部分替换成~。这样打印提示符的任务就完成了,此部分代码
的实现在main.c文件中的getPrompt
函数中。
命令历史和命令补全
Linux默认保存最后输入的500个命令历史。我们可以用GNU的readline库实现命令历史和命令补全。安装就按其中的说明的步骤进
行就行。readline的库有很多功能,但我们只需要其中的readline
函数和add_history
函数就行了。readline
函数以一个字符串为参
数作为提示符,返回用户输入的一行命令;add_history
函数以一个字符串作为参数,并将此字符串添加到历史命令列表里。
打印出错消息
在err.h头文件中声明了3个错误处理函数:err_ret
函数打印出错消息并返回,err_quit
和err_sys
函数打印出错消息并退出程序。其
后两个函数在发生致命错误时调用,而err_ret
函数在发生非致命错误时调用。其实现在err.c文件中。其实现是参照《UNIX环境高级编程》
中的出错处理函数。
实现IO重定向和管道
处理诸如cat < in.data | grep str | sort > out.data
这样的命令需要实现IO重定向和管道。默认情况下在shell中运行的程序其标准输
入(描述符为0)和标准输出(描述符为1)都关联到终端,IO重定向是指将程序的标准输入和标准输出重定向到文件或其他设备。shell从描
述符0读,从描述符1写。如果我们想将标准输入重定向就得关闭描述符0,再将其他文件或设备关联到描述符0。对于标准输出也一样。
管道是将进程的标准输入和/或标准输出通过一个数据通道与另一个进程关联。以本节开头的那个例子来说,grep
的标准输入来自cat
程
序的输出,标准输出则作为sort
程序的标准输入。我们可以用pipe
函数创建一个管道,其参数是一个int型数组(假设为fd[2]),数组有
两个元素。经由数组返回两个文件描述符,fd[0]为读而打开,fd[1]为写而打开。fd[1]的输出是fd[0]的输入。如果想重定向标准输入,则将
标准输入与fd[0]相关联,如果想重定向标准输出,则将标准输出与fd[1]相关联。
命令解析
我认为命令解析是这个程序中最难的部分了,可能是我没学过编译原理的原因。所以我死扣了好长时间还是不会怎样解析shll命令(现在也没
搞通)。我偶然发现在xv6课程主页上有一个homework是关于这个的,它给出了解析命令的框架,将关键的部分空出来
让学生补全,所以我就照搬出来用了。所以实现的功能也有限,还有命令列表和后台命令不会解析。等以后我看了编译原理后再把这个坑填了。
总结
这个shell虽然简单,但也让我学到了很多知识,查了很多资料。看完《UNIX环境高级编程》后收获了很多,最高兴的莫过于将学到的知识用在
实际的程序中。就写到这里吧。
Reference:
《UNIX环境高级编程 第三版》