=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() |
拓展
这里的命令行处理并没有考虑输入/输出重定向
,所以仍有较大的需要完善的地方