本篇博客,来教大家用C写一个简易的linux shell
,帮助理解之前学习的进程控制相关知识
演示系统:CentOS7.6
[TOC]
前言
之所以说是简易的shell,是因为我们现在的水平肯定写不出来linux系统里面那么复杂的shell。
我们的目的仅仅是为了学习父子进程、进程替换、内建命令等等知识,并把这些知识的作用通过这个小shell体现出来
源码仓库:gitee
1.基础框架
之前的学习中有提到过,我们在linux命令行内运行的很多进程,都是以子进程的方式运行的。说白了就是bash进程里面给我们fork创建了其他子进程,再用子进程进行进程替换
,指向对应的可执行文件
而需要做到这一点,我们要一步一步来
- bash首先要显示命令行的提示符
用户名@主机名 路径
(参考之前vim博客中的进度条程序) - 获取用户的输入内容
- 从用户的输入中,以
" "
空格为分割,分离出命令和参数 - fork创建子进程,子进程执行进程替换,父进程等待子进程结束
这一切都是在一个while(1)
的死循环里面执行的,bash本质上就是一个死循环的父进程
2.开整一个
2.1 打印命令行提示符
先来试试打印出命令行的提示符吧!
1 2
| printf("[慕雪@FS-1041 当前路径]# "); fflush(stdout);
|
如果不这么弄,而使用\n
换行,就会出现命令行提示符一直在闪动打印。这不是我们想要的结果
光是打印一个基本的路径可不太够哦,我们还可以试着获取环境变量的PWD
得到当前的路径,再打印出来
1 2 3 4 5 6
| char cur_pwd[SIZE] = "~"; int sz_pwd = strlen(getenv("HOME")); strcat(cur_pwd, getenv("PWD") + sz_pwd); printf("[慕雪@FS-1041 %s]# ", cur_pwd);
fflush(stdout);
|
这里我们必须要去掉PWD前面/home/用户名
的内容,将其替换成~
打印出来的效果如下,是不是和我们linux的命令行很像啦!
1
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]#
|
你还可以从环境变量中获取HOSTNAME
和USER
来替换掉前面的内容
这里为了和linux自己的shell区分一下,我就不替换了
2.2 获取用户输入
C语言获取用户输入,我们一般用的是scanf
但是这个函数在现在这个地方可不那么好用喽!我们输入命令的时候需要用空格分开命令行参数。scanf会因为空格而停止接受
我们可以用gets
函数来解决这个问题!
1 2 3 4 5
| #define NUM 1024 char cmd_line[NUM];
memset(cmd_line, '\0', sizeof(cmd_line) * sizeof(char)); fgets(cmd_line, NUM, stdin);
|
获取了之后先打印一下cmd_line
,可以看到成功获取了我们输入的结果
1 2 3 4
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# test i k d test i k d
[慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]#
|
但为什么多打了一个换行呢?
这是因为fgets
在接受输入的时候,把我们输入结束的回车也给收起来辣
1
| cmd_line[strlen(cmd_line) - 1] = '\0';
|
光是去掉回车还是有点问题,如果我们只敲了一个回车,后续我们分离参数的时候,总不能对一个空的字符串进行处理吧?
所以还需要单独判断strlen(cmd_line)==1
的情况,直接continue
1 2 3 4 5 6
| if(strlen(cmd_line)==1) { continue; }
cmd_line[strlen(cmd_line) - 1] = '\0';
|
这样我们的bash就和linux自己的bash一样,敲回车会直接新起一行,不做任何操作
如果不这么处理,就会引发段错误导致bash直接终止
1 2
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# Segmentation fault
|
2.3 分离参数
获取好用户输入啦,下一步就是分离参数了!
这里面我们直接使用strtok
这个函数即可!
1
| char * strtok ( char * str, const char * sep );
|
它的作用是根据分隔符返回这个分隔符在字符串里面的起始位置;如果传入的是一个NULL,则从上一次处理的位置继续往后处理。
- strtok函数找到str中的下一个标记,并将其用
\0
结尾,返回一个指向这个标记的指针 - 如果字符串中不存在更多的标记,则返回 NULL 指针
该函数的详解参考我的博客 点我
😥最开始的时候我忘记了这个函数,直接自己写了一个分离算法,debug了好久才勉强搞出来,太笨蛋了
1 2 3 4 5 6 7 8
| #define SEP " " size_t cmd_args_num = 0; char *cmd_args[SIZE];
cmd_args[0] = strtok(cmd_line, SEP); cmd_args_num = 1; while (cmd_args[cmd_args_num++] = strtok(NULL, SEP)); cmd_args_num--;
|
注意!=
赋值操作符是有返回值的!它的返回值是我们的左值,也就是每一次获取到的strtok
的结果,这个结果被cmd_args[cmd_args_num]
所接受
那么,当strtok
返回NULL的时候,while就会接受到=
的返回值,从而停止循环
1 2 3 4
| for(int j=0;j<cmd_args_num;j++) { printf("args[%d] %s\n",j,cmd_args[j]); }
|
通过打印,可以看到它成功分离出来了我们的参数
1 2 3
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ls -l args[0] ls args[1] -l
|
单独处理ls
在linux的bash下,我们执行的ls都是带颜色的。这是因为centos
的配置文件中,将ls设置成了ls --color=auto
的别名,要想我们自己bash里面的ls也带上颜色,则需要单独处理一下ls
1 2 3 4 5 6 7 8
| cmd_args[0] = strtok(cmd_line, SEP); cmd_args_num = 1;
if (strcmp(cmd_args[0], "ls") == 0) cmd_args[cmd_args_num++] = (char *)"--color=auto"; while (cmd_args[cmd_args_num++] = strtok(NULL, SEP)); cmd_args_num--;
|
最终ls -l
分离出来的参数如下
1 2 3 4
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ls -l args[0] ls args[1] --color=auto args[2] -l
|
2.4 进程替换
参数分离出来了,下一步要做的,便是进程替换了
我们需要使用的是exec
函数里面的哪一个呢?
- 带
p
的exec
函数,它会自动去PATH
里面查找可执行文件 - 带
v
的,函数,因为我们的传参已经分离在了一个字符指针数组里面
基本的代码如下,父进程打印内容是为了测试,实际的bash肯定是没有这个打印的~
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| pid_t ret_id = fork(); if (ret_id == 0) { execvp(cmd_args[0], cmd_args); exit(134); }
int status = 0; pid_t ret = waitpid(ret_id, &status, 0); printf("\n"); if (ret > 0) { printf("bash等待子进程成功!code: %d, sig: %d\n", WEXITSTATUS(status), WTERMSIG(status)); }
|
运行成功!
执行python3的文件也是ok的
1 2 3 4 5 6 7 8
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# python3 test.py args[0] python3 args[1] test.py
hello python
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]#
|
3.内建命令
完成了上面的几步后,一个基础的bash就搞定了
但是这样还不够,不信cd试一下?
1 2 3 4 5 6 7 8 9 10 11 12
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ls makefile myshell myshell.c myshell_err.c test test.cpp test.py
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# cd test
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ls makefile myshell myshell.c myshell_err.c test test.cpp test.py
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]#
|
诶,为什么cd了之后,再次ls,路径没有变化呢?
这是因为我们的cd是被子进程执行的,切换的是子进程的工作目录。可子进程执行完cd之后就结束运行了,它根本没有影响到父进程bash!
之前学习的时候,我们提到过内建命令这一个概念。有一些命令不应该是子进程执行的,而应该是bash自己执行的,比如这里的cd
,还有导入环境变量的export
其实说白了就是bash检测到内建命令,就执行他自己的一个函数呗
3.1 cd和export命令
cd/export
命令,c语言中都有现成的函数供我们使用,还是很方便的
1 2 3 4 5 6 7 8 9 10 11 12 13
|
int PutEnvIn(char *new_env) { putenv(new_env); return 0; }
int ChangeDir(const char *new_path) { chdir(new_path); return 0; }
|
以下是main
函数里面的内容,完整代码请去我的代码仓库查看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| if (strcmp(cmd_args[0], "cd") == 0 && cmd_args[1] != NULL) { ChangeDir(cmd_args[1]); continue; }
char env_buffer[SIZE][NUM]; size_t env_num = 0; if (strcmp(cmd_args[0], "export") == 0 && cmd_args[1] != NULL) { strcpy(env_buffer[env_num], cmd_args[1]); PutEnvIn(env_buffer[env_num]); env_num++; continue; }
|
这时候cd就能正常执行了,不过pwd还没有修改,我没想好要怎么操作捏
1 2 3 4 5 6 7 8 9
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ls makefile myshell myshell.c myshell_err.c test test.cpp test.py
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# cd test [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ls
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]#
|
试一试export
,也没问题呢
1 2 3 4 5 6 7 8 9
| #include<iostream> #include<stdlib.h> using namespace std; int main() { cout << "ts= " << getenv("ts") <<endl; return 0; }
|
1 2 3 4 5 6 7 8 9 10
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# make test g++ test.cpp -o test -std=c++11
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# export ts=12341 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ./test ts= 12341
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]#
|
3.2 alias别名设置
上面两个命令有现成的,alias
的设置就需要我们手写啦
1 2 3 4 5 6 7 8 9 10
| #define NUM 1024 #define SIZE 128
typedef struct alias_cmd { char _cmd[SIZE]; char _acmd[SIZE]; } alias; alias cmd_alias[SIZE]; size_t alias_num = 0;
|
这里我先定义了一个结构体,用来存放变量别名的键值对,方便我们进行替换
然后就是漫长的替换步骤,这部分我debug了非常久才写出来,都带了注释,大家可以看看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59
| void set_alias(char *cmd, char *acmd) { for (int i = 0; i < alias_num; i++) { if (strcmp(cmd_alias[i]._cmd, cmd) == 0) { strcpy(cmd_alias[i]._acmd, acmd); return; } }
strcpy(cmd_alias[alias_num]._cmd, cmd); strcpy(cmd_alias[alias_num]._acmd, acmd); alias_num++; }
bool is_alias(char *cmd_args[], int sz) { int i = 0; for (i = 0; i < alias_num; i++) { if (strcmp(cmd_alias[i]._cmd, cmd_args[0]) == 0) { size_t index = 1, j; char *cmd_args_temp[SIZE]; memset(cmd_line_alias, '\0', sizeof(cmd_line_alias) * sizeof(char)); strcpy(cmd_line_alias, cmd_alias[i]._acmd); cmd_args_temp[0] = strtok(cmd_line_alias, SEP); if (strcmp(cmd_args_temp[0], "ls") == 0 && strcmp(cmd_args[0], "ls") != 0) cmd_args_temp[index++] = (char *)"--color=auto"; while (cmd_args_temp[index++] = strtok(NULL, SEP)) ; index--; for (j = 1; j < cmd_args_num; j++) { cmd_args_temp[index++] = cmd_args[j]; } cmd_args_num = index; for (j = 0; j < cmd_args_num; j++) { cmd_args[j] = cmd_args_temp[j]; } cmd_args[j] = NULL; return true; } } return false; }
|
其实肯定是有更好的方案的,但是我还没想出来咋弄。现在这个能跑就OK,哈哈
以最基本的ll
命令来测试以下,替换成功!修改已有的别名也是没有问题的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# alias ll='ls -l' [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ll total 60 -rw-rw-r-- 1 muxue muxue 136 Oct 15 23:21 makefile -rwxrwxr-x 1 muxue muxue 14040 Oct 16 17:05 myshell -rw-rw-r-- 1 muxue muxue 8217 Oct 16 16:59 myshell.c -rw-rw-r-- 1 muxue muxue 6942 Oct 15 22:38 myshell_err.c -rwxrwxr-x 1 muxue muxue 9072 Oct 16 17:08 test -rw-rw-r-- 1 muxue muxue 130 Oct 15 23:22 test.cpp -rw-rw-r-- 1 muxue muxue 21 Oct 16 00:11 test.py
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# alias ll='ls -l -a' [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ll total 68 drwxrwxr-x 2 muxue muxue 4096 Oct 16 17:08 . drwxrwxr-x 13 muxue muxue 4096 Oct 15 17:31 .. -rw-rw-r-- 1 muxue muxue 136 Oct 15 23:21 makefile -rwxrwxr-x 1 muxue muxue 14040 Oct 16 17:05 myshell -rw-rw-r-- 1 muxue muxue 8217 Oct 16 16:59 myshell.c -rw-rw-r-- 1 muxue muxue 6942 Oct 15 22:38 myshell_err.c -rwxrwxr-x 1 muxue muxue 9072 Oct 16 17:08 test -rw-rw-r-- 1 muxue muxue 130 Oct 15 23:22 test.cpp -rw-rw-r-- 1 muxue muxue 21 Oct 16 00:11 test.py
bash等待子进程成功!code: 0, sig: 0 [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]#
|
结语
就这样,一个最基本的bash或者说shell就被我们搞定啦
其实内建命令远不止3里面提到的那几个,不过我们学习的目的已经达到了~也没必要死磕在这里