=Linux=一步步自己写一个shell程序
系统:阿里云服务器Linux CentOs 7
编辑器: vim
编译器: gcc (支持C99)
文件
本次写的程序较为简单,所以只使用一个源文件
所以在shell中touch一个makefile和一个myshell.c
shell
1 | touch makefile |
然后编辑makefile文件
makefile
1 | 1 myshell:myshell.c |
头文件
本程序因函数较杂,会include较多头文件
myshell.c
1 |
宏定义
为了统一修改部分参数,以及使参数更易读,这里使用部分宏定义
myshell.c
1 |
全局变量
我们需要用全局的变量来存储命令行(command line)和参数包
myshell.c
1 | char cline[LINE_SIZE]; |
用Interact函数实现交互功能
打印命令行头部
为了打印命令行头部,我们需要知道三样东西:用户,主机,工作路径,这里包装了三个函数来分别调用getenv函数
myshell.c
1 | const char* getusername()//获取用户名 |
因此打印的代码为
1 | printf(LEFT "%s@%s %s" RIGHT LABLE,getusername(),gethostname(),getpwd()); |
获取命令行
使用Linux的终端时,我们会打命令+空格+参数...,因此我们的myshell程序也要支持连空格一起读入,读入一整行命令
所以scanf并不适合用来读入命令,这次我们使用fgets函数,这个函数可以从文件流中整行读入,而正好在终端输入的字符都储存在标准输入流,即stdin中,因此可以用一行代码获取命令行
为安全考虑,这里使用一个临时变量s来接受fgets的返回值并用assert判空,但在release版本中assert不被编译,导致变量s未被调用,而报警告(甚至报错),所以还要再加一句(void) s,只为了调用一下s,没有更多用处
之后便完成了文件流的读取

但此时获得的命令行在\0前以\n结尾,所以要把\n替换为\0
1 | char *s = fgets(cline,size,stdin); |
整个函数体
myshell.c
1 | void Interact(char* cline,int size) |
测试
先在main函数里调用一次Interact函数测试一下
我的测试结果如下

可以看到达到了预期效果,但是工作路径太长了,还是学一学Linux的展示方式吧,我们来把getpwd函数重写一下
重写getpwd()
1 | const char* getpwd() |
这样打印出的工作路径仅为当前文件夹,可以缩短很多长度
分割命令行
现在的cline中的命令行还是完整的一串,需要分割出命令和参数包,因此我们也封装一个函数Splitcline
这里使用的是string.h中的strtok函数,可以用特定的单个或多个字符将字符串分割
myshell.c
1 | int Splitcline(char*cline,char** argv) |
再写一段测试代码
myshell.c
1 | int main() |
执行外部命令
通过fork函数创建子进程,然后用execvp替换子进程,通过环境变量PATH找到外部命令并替换到子进程执行,同时父进程myshell调用waitpid函数等待子进程结束,保证myshell程序正常运行
myshell.c
1 | void ExternalCommand() |
执行内建命令
shell中并不是所有的命令都由子进程完成的,比如用cd命令改变工作路径,就不能让子进程去执行(否则只是改了子进程的路径),因此我们还需要加一个内建命令接口
myshell.c
1 | int BuildCommand(char* _argv[],int _argv_n)//处理内建命令 |
完成框架
至此,把main函数组织好后,一个简单的shell代码框架就搭好了,可以根据需要继续扩展内建命令的内容,比如导出环境变量,实现echo指令等(略写)。
myshell.c
1 | int main() |
拓展
这里的命令行处理并没有考虑输入/输出重定向,所以仍有较大的需要完善的地方