本篇博客,来教大家用C写一个简易的linux shell,帮助理解之前学习的进程控制相关知识
演示系统:CentOS7.6
[TOC]
前言
之所以说是简易的shell,是因为我们现在的水平肯定写不出来linux系统里面那么复杂的shell。
我们的目的仅仅是为了学习父子进程、进程替换、内建命令等等知识,并把这些知识的作用通过这个小shell体现出来
源码仓库:gitee
1.基础框架
之前的学习中有提到过,我们在linux命令行内运行的很多进程,都是以子进程的方式运行的。说白了就是bash进程里面给我们fork创建了其他子进程,再用子进程进行进程替换,指向对应的可执行文件
而需要做到这一点,我们要一步一步来
- bash首先要显示命令行的提示符用户名@主机名 路径(参考之前vim博客中的进度条程序)
- 获取用户的输入内容
- 从用户的输入中,以" "空格为分割,分离出命令和参数
- fork创建子进程,子进程执行进程替换,父进程等待子进程结束
这一切都是在一个while(1)的死循环里面执行的,bash本质上就是一个死循环的父进程
2.开整一个
2.1 打印命令行提示符
先来试试打印出命令行的提示符吧!
| 12
 
 | printf("[慕雪@FS-1041 当前路径]# ");fflush(stdout);
 
 | 

如果不这么弄,而使用\n换行,就会出现命令行提示符一直在闪动打印。这不是我们想要的结果
光是打印一个基本的路径可不太够哦,我们还可以试着获取环境变量的PWD得到当前的路径,再打印出来
| 12
 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函数来解决这个问题!
| 12
 3
 4
 5
 
 | #define NUM 1024char cmd_line[NUM];
 
 memset(cmd_line, '\0', sizeof(cmd_line) * sizeof(char));
 fgets(cmd_line, NUM, stdin);
 
 | 
获取了之后先打印一下cmd_line,可以看到成功获取了我们输入的结果
| 12
 3
 4
 
 | [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# test i k dtest 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
| 12
 3
 4
 5
 6
 
 | if(strlen(cmd_line)==1){
 continue;
 }
 
 cmd_line[strlen(cmd_line) - 1] = '\0';
 
 | 
这样我们的bash就和linux自己的bash一样,敲回车会直接新起一行,不做任何操作

如果不这么处理,就会引发段错误导致bash直接终止
| 12
 
 | [慕雪@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了好久才勉强搞出来,太笨蛋了
| 12
 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就会接受到=的返回值,从而停止循环
| 12
 3
 4
 
 | for(int j=0;j<cmd_args_num;j++){
 printf("args[%d] %s\n",j,cmd_args[j]);
 }
 
 | 
通过打印,可以看到它成功分离出来了我们的参数
| 12
 3
 
 | [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ls -largs[0] ls
 args[1] -l
 
 | 
单独处理ls
在linux的bash下,我们执行的ls都是带颜色的。这是因为centos的配置文件中,将ls设置成了ls --color=auto的别名,要想我们自己bash里面的ls也带上颜色,则需要单独处理一下ls
| 12
 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分离出来的参数如下
| 12
 3
 4
 
 | [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# ls -largs[0] ls
 args[1] --color=auto
 args[2] -l
 
 | 
2.4 进程替换
参数分离出来了,下一步要做的,便是进程替换了
我们需要使用的是exec函数里面的哪一个呢?
- 带p的exec函数,它会自动去PATH里面查找可执行文件
- 带v的,函数,因为我们的传参已经分离在了一个字符指针数组里面
基本的代码如下,父进程打印内容是为了测试,实际的bash肯定是没有这个打印的~
| 12
 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的
| 12
 3
 4
 5
 6
 7
 8
 
 | [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# python3 test.pyargs[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试一下?
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 11
 12
 
 | [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# lsmakefile  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语言中都有现成的函数供我们使用,还是很方便的
| 12
 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函数里面的内容,完整代码请去我的代码仓库查看
| 12
 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还没有修改,我没想好要怎么操作捏
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# lsmakefile  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,也没问题呢
| 12
 3
 4
 5
 6
 7
 8
 9
 
 | #include<iostream>
 #include<stdlib.h>
 using namespace std;
 int main()
 {
 cout << "ts= " << getenv("ts") <<endl;
 return 0;
 }
 
 | 
| 12
 3
 4
 5
 6
 7
 8
 9
 10
 
 | [慕雪@FS-1041 ~/git/linux/code/22-10-15_myshell]# make testg++ 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的设置就需要我们手写啦
| 12
 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了非常久才写出来,都带了注释,大家可以看看
| 12
 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命令来测试以下,替换成功!修改已有的别名也是没有问题的
| 12
 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里面提到的那几个,不过我们学习的目的已经达到了~也没必要死磕在这里