【Linux】进程控制(4)自主shell命令行解释器
hello~ 很高兴见到大家! 这次带来的是Linux系统中关于进程控制这部分的一些知识点,如果对你有所帮助的话,可否留下你宝贵的三连呢?
个 人 主 页: 默|笙

文章目录
- 一、搭建基本框架
- 1.0 导入环境变量表
- 1.1 输出shell命令行函数PrintCommandLine()
- 1.2 获取用户输入字符串函数GetCommand()
- 1.3 解析字符串函数
- strtok函数
- 1.4 执行命令函数
- 1. 普通命令vs内建命令
- 2. 父进程执行内建命令函数CheckBuiltinExecute()
- cd
- chdir函数和getcwd函数
- echo
- 3. 子进程执行普通命令函数ExecuteCommand()
- 二、完整代码
一、搭建基本框架
- 首先bash是一个进程,而且是一个需要一直运行的进程,所以主函数一定是一个死循环。while(1){}。
156 int main()
157 {
158 Loadenv();
159 char command_line[MAXSIZE] = {0};
160 while(1)
161 {
162 //打印命令行字符串
163 PrintCommandLine();
164 //获取用户输入的字符串
165 if (0 ==GetCommand(command_line, sizeof(command_line)))
166 {
167 continue;
168 }
169
170 //解析字符串
171 ParseCommand(command_line);
172 //检查一个命令是要由bash执行的内建命令,还是要让子进程执行的普通命令
173 if(CheckBuiltinExecute() > 0)
174 continue;
175 //执行解析出来的字符串
176 ExecuteCommand();
177 }
178 return 0;
179 }
- 首先就是要导入环境变量表,之后进入while循环,不断执行输出命令行字符串,获取用户输入字符串,解析字符串,和执行命令这几步。
1.0 导入环境变量表
16 //环境变量表
17 int genvc = 0;
18 char* genv[MAXARGS];
23 void Loadenv()
24 {
25 //正常情况下这个Shell它是从配置文件里面读取环境变量
26 //但这里我们没办法实现,因为非常复杂
27 //所以就直接从bash里面读取了
28 extern char** environ;
29 for(; environ[genvc]; genvc++)
30 {
31 genv[genvc] = (char*)malloc(sizeof(char) * 4096);
32 strcpy(genv[genvc], environ[genvc]);
33 }
34 genv[genvc] = NULL;
35
36 for (int i = 0; genv[i]; i++)
37 {
38 printf("genv[%d]: %s
", i, genv[i]);
39 }
40 }
1.1 输出shell命令行函数PrintCommandLine()

- 在我们什么都不输入的时候,bash会输出这样一串字符串,这个字符串可以拆分为[用户名@主机名 当前文件的名字]$/#。其中$代表普通用户,#代表root用户。
- 我们就可以根据这个格式来写出这个shell命令行的函数PrintCommandLine()。其中用户名、主机名以及当前文件的名字都可以用从环境变量表里面进行获取,需要用到的函数是getenv()。
//后面添加的函数,用于提取路径最后的那个文件名
43 std::string rfindDir(const std::string &p)
44 {
45 if(p == "/")
46 return p;
47 const std::string psep = "/";
48 auto pos = p.rfind(psep);
49 if(pos == std::string::npos)
50 return std::string();
51 return p.substr(pos+1); // /home/whb
52 }
9 //获取用户名
10 const char* GetUsername()
11 {
12 char* name = getenv("USER");
13 if (name == NULL)
14 return "None";
15 return name;
16 }
17 //获取主机名
18 const char* GetHostname()
19 {
20 char* hostname = getenv("HOSTNAME");
21 if (hostname == NULL)
22 return "None";
23 return hostname;
24 }
25 //获取pwd当前路径
26 const char* GetPwd()
27 {
28 char* pwd = getenv("PWD");
29 if (pwd == NULL)
30 return "None";
31 return pwd;
32 }
33 //打印命令行字符串函数
34 void PrintCommandLine()
35 {
36 printf("[%s@%s %s]#", GetUsername(), GetHostname(), rfindDir(GetPwd()).c_str());
37 fflush(stdout);
38 }
- 打印出这样一串shell命令行,由于后面需要用户来输入命令,所以不能加换行符 ,但是不加 的话这串字符就只能一直呆在缓冲区里面,所以需要fflush函数来刷新一下缓冲区。
1.2 获取用户输入字符串函数GetCommand()
- 获取字符串的时候,就会卡住,也就是卡在打印出的命令行后面。
- 用来获取用户输入的字符串函数有很多,比如scanf,fgets,这里我们用fgets读取一整行,因为scanf遇到空格就不会读取了,这不符合我们的要求,我们要把空格一起读取到。
41 #define MAXSIZE 128
49 int GetCommand(char* commandline, int size)
50 {
51 if(NULL == fgets(commandline, size, stdin))
52 return 0;
53 //用户输入的时候至少会摁一下回车键,把回车键所在位置置为' '
54 commandline[strlen(commandline) - 1] = ' ';
55 //printf("%s
", commandline);
56 return strlen(commandline);
57 }
//while循环外
92 char command_line[MAXSIZE] = {0};
//main函数while循环里
97 //获取用户输入的字符串
98 if (0 ==GetCommand(command_line, sizeof(command_line)))
99 {
100 continue;
101 }
- 首先我在while循环外定义了一个command_line命令行数组用来存储用户输入的命令行,它的大小定为128字节。

- 之后是Getcommand函数,这个函数通过fgets函数读取用户所输入的命令行。由于用户在每次命令行输入之后都会摁一下回车键,这个回车键也会被当成字符读取到commandline数组里面,所以这里需要做一些处理,让commandline[strlen(commandline) - 1] = ‘ ’,手动将最后一个回车字符置为结束符’ ’。
- 如果用户没有输入,那么fgets读取失败Getcommand函数的返回值就是0,会执行continue语句,不会执行接下来的代码,重新进入循环。
1.3 解析字符串函数
- 首先是解析之后的字符串要放在哪里,根据空格进行分割后的字符串就是一个个的命令行参数,当然是要放在命令行参数表里面,我设置一个char*类型的数组gargv来进行存储。同时还有记录命令行参数个数的gargc变量。
strtok函数

- 再就是用来解析字符串的strtok函数,它的第一个参数是需要进行切割的字符串,第二个参数是分隔符字符串。它的返回值是切割后的目标字符串首字母地址。
- 切割下第一个目标字符串第一个参数传入需要进行切割的字符串str,如果要接着进行切割的话第一个参数就不能也传入str了,而是需要传入NULL。这样strtok函数才会知道是需要接着进行上一次的切割,否则它仍旧会切割下str的第一个目标字符串。
- strtok函数支持传入多个分隔符,比如传入" #!"它就会按照空格、#、! 这三个字符作为分隔符来对传入的字符串进行切割。
- 至于为什么strtok跟其他函数不同,可以继续进行上一次函数的切割,这是因为它的实现运用了static变量。
//全局
42 #define MAXARGS 32
43
44 //命令行参数表
45 int gargc = 0;
46 char* gargv[MAXARGS];
47 const char* gsep = " ";
59 void ParseCommand(char* commandline)
60 {
61 gargc = 0;
62 memset(gargv, 0, sizeof(gargv));
63
64 gargv[0] = strtok(commandline, gsep);
//strtok先切割返回一个值,再把这个值存入gargv[++gargc]里面,再检测这个值
65 while ((gargv[++gargc] = strtok(NULL, gsep)));
69 }
- while ((gargv[++gargc] = strtok(NULL, gsep)));代码逻辑是strtok先切割返回一个值,再把这个值存入gargv[++gargc]里面,再检测这个值
- 代码还有一点,那就是strtok函数在没有可以切割的字符串了之后会返回NULL,这个NULL会存入gargv[++gargc]里面,gargc自增1,所以gargc的个数不会少,而是刚刚好。
- 每次进行切割的时候都要把原来的gargv也就是命令行参数列表清空,gargc命令行参数个数清零。
1.4 执行命令函数
1. 普通命令vs内建命令
- 像是ls这样的二进制文件,一般需要通过创建子进程然后让子进程进行程序切换来完成调用。这种就叫做普通命令。
- 而cd和echo这种,前者所要改变的是当前bash的路径而不是子进程的路径所以不能通过子进程进行程序切换来完成调用,不然改变的只是子进程的路径,而不会影响到它的父进程bash的路径。而echo有一个作用$?可以打印出上一个所执行的二进制文件的退出码。这个文件是由一般子进程执行的,谁能获得子进程的退出码?只能是父进程。像是这种不能够由子进程通过程序替换来执行的命令,需要由Shell自行执行的命令就叫做内建命令。
- 内建命令是 Shell 自身实现的功能(无独立二进制文件),无需通过「创建子进程 + 程序替换」执行,直接在 Shell 进程内运行,这是与普通命令的核心区别。
2. 父进程执行内建命令函数CheckBuiltinExecute()
151 //检查一个命令是要由bash执行的内建命令,还是要让子进程执行的普通命令
152 if(CheckBuiltinExecute() > 0)
153 continue;
- 在让子进程执行命令之前,首先需要判断该命令是否为内建命令,如果是内建命令就让自主Shell运行,不是则通过创建子进程+程序替换执行。
- 内建命令有很多,这里只实现cd和echo命令。如果是需要父进程执行的命令则返回1,执行后续的continue语句,如果是要子进程执行的命令则返回0,会继续执行接下来的代码。
cd
- 需要切换Shell的路径,需要用到函数chdir。
chdir函数和getcwd函数

- 使用chdir函数需要包含头文件unistd.h头文件。
- 这个函数的作用是更改当前所执行这个函数的进程的路径。会将当前进程的工作路径修改为传入的path。
- 成功则返回0,失败则返回-1。

-
chdir用于修改进程的工作目录,而getcwd用于获取进程当前的工作目录,执行成功则返回指向buf的指针,buf存储的是以’ ’为结尾的绝对路径字符串。
-
用户使用cd时传入的第二个命令行参数就是我们的目标路径,所以只需要给chdir函数传入gargv[1]就好。
-
但是会存在一个问题,那就是通过chdir修改Shell的路径,系统不会自动给我们修改环境变量里面的PWD,这会导致我们的Shell打印的命令行字符串后面的路径不会改变。所以这里我们需要手动的更改一下环境变量PWD。
49 //我们的Shell自己所处的工作路径
50 char cwd[MAXSIZE];
75 if (strcmp(gargv[0], "cd") == 0)
76 {
77 if (gargc == 2)
78 {
79 chdir(gargv[1]);
80
81 //修改环境变量
82 char pwd[1024];
83 //存储当前获取到的工作路径到pwd里面
84 getcwd(pwd, sizeof(pwd));
85 //拼接PWD环境变量
86 snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);
87 //导入环境变量cwd
88 putenv(cwd);
89 return 1;
90 }
echo
- 需要处理echo $这种内建命令特殊情况和echo 语句这种普通情况,后面的普通情况交给子进程去处理就好,我们处理前面的特殊情况。
51 //上一次命令执行完毕后的退出码
52 int lastcode = 0;
94 else if (strcmp(gargv[0], "echo") == 0)
95 {
96 if (gargc == 2)
97 {
98 if (gargv[1][0] == '$')
99 {
100 if (strcmp(gargv[1]+1, "?") == 0)
101 {
102 printf("%d
", lastcode);
103 }
104 else if (strcmp(gargv[1]+1, "PATH") == 0)
105 {
106 printf("%s
", getenv("PATH"));
107 }
108 lastcode = 0;
109 return 1;
110 }
111 }
112 }
- 解释一下gargv[1] + 1,这个gargv[1]它实际上是一个指针,指向这个gargv[1]存储的字符串的首字母,+1这个指针就会往后移动一个字节,就会跳过’$'字符,指向它后面的字符来作为新字符串的首字符。
- 然后新增全局变量lastcode,这个变量是用来记录上一个命令执行完之后的退出码。Shell进程执行成功之后也要更新lastcode为0。
3. 子进程执行普通命令函数ExecuteCommand()
71 int ExecuteCommand()
72 {
73 pid_t id = fork();
74 if (id == 0)
75 {
76 execvp(gargv[0], gargv);
77 exit(0);
78 }
79 else if (id < 0)
80 return -1;
81 else
82 {
83 int status = 0;
84 pid_t sid = waitpid(id, &status, 0);
85 if (sid > 0)
lastcode = WEXITSTATUS(status);
86 //printf("wait childprocess sucess!!!
");
87 }
88 return 0;
89 }
- 也就是创建子进程然后让子进程进行程序切换执行命令,父进程就等待回收子进程,这样父进程就能够得到子进程的退出码,最后如果回收成功则更新退出码。创建子进程成功则返回0,创建失败则返回-1。
二、完整代码
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7 #include<iostream>
8 #include<string>
9
10 #define MAXSIZE 128
11 #define MAXARGS 32
12
13 //命令行参数表
14 int gargc = 0;
15 char* gargv[MAXARGS];
16 const char* gsep = " ";
17 //环境变量表
18 int genvc = 0;
19 char* genv[MAXARGS];
20 //我们的Shell自己所处的工作路径
21 char cwd[MAXSIZE];
22 //上一次命令执行完毕后的退出码
23 int lastcode = 0;
24
25 void Loadenv()
26 {
27 //正常情况下这个Shell它是从配置文件里面读取环境变量
28 //但这里我们没办法实现,因为非常复杂
29 //所以就直接从bash里面读取了
30 extern char** environ;
31 for(; environ[genvc]; genvc++)
32 {
33 genv[genvc] = (char*)malloc(sizeof(char) * 4096);
34 strcpy(genv[genvc], environ[genvc]);
35 }
36 genv[genvc] = NULL;
37
38 for (int i = 0; genv[i]; i++)
39 {
40 printf("genv[%d]: %s
", i, genv[i]);
41 }
42 }
43 std::string rfindDir(const std::string &p)
44 {
45 if(p == "/")
46 return p;
47 const std::string psep = "/";
48 auto pos = p.rfind(psep);
49 if(pos == std::string::npos)
50 return std::string();
51 return p.substr(pos+1); // /home/whb
52 }
53 //获取用户名
54 const char* GetUsername()
55 {
56 char* name = getenv("USER");
57 if (name == NULL)
58 return "None";
59 return name;
60 }
61 //获取主机名
62 const char* GetHostname()
63 {
64 char* hostname = getenv("HOSTNAME");
65 if (hostname == NULL)
66 return "None";
67 return hostname;
68 }
69 //获取pwd当前路径
70 const char* GetPwd()
71 {
72 char* pwd = getenv("PWD");
73 if (pwd == NULL)
74 return "None";
75 return pwd;
76 }
77 //打印命令行字符串函数
78 void PrintCommandLine()
79 {
80 printf("[%s@%s %s]#", GetUsername(), GetHostname(), rfindDir(GetPwd()).c_str());
81 fflush(stdout);
82 }
83
84
85 int GetCommand(char* commandline, int size)
86 {
87 if(NULL == fgets(commandline, size, stdin))
88 return 0;
89 //用户输入的时候至少会摁一下回车键,把回车键所在位置置为' '
90 commandline[strlen(commandline) - 1] = ' ';
91 //printf("%s
", commandline);
92 return strlen(commandline);
93 }
94
95 void ParseCommand(char* commandline)
96 {
97 gargc = 0;
98 memset(gargv, 0, sizeof(gargv));
99
100 gargv[0] = strtok(commandline, gsep);
101 while ((gargv[++gargc] = strtok(NULL, gsep)));
102 //int i = 0;
103 //for (i = 0; i < gargc; i++)
104 // printf("%s
", gargv[i]);
105 }
106
107 int CheckBuiltinExecute()
108 {
109 if (strcmp(gargv[0], "cd") == 0)
110 {
111 if (gargc == 2)
112 {
113 chdir(gargv[1]);
114
115 //修改环境变量
116 char pwd[1024];
117 //存储当前获取到的工作路径到pwd里面
118 getcwd(pwd, sizeof(pwd));
119 //拼接PWD环境变量
120 snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);
121 //导入环境变量cwd
122 putenv(cwd);
123 }
124 return 1;
125 }
126 else if (strcmp(gargv[0], "echo") == 0)
127 {
128 if (gargc == 2)
129 {
130 if (gargv[1][0] == '$')
131 {
132 if (strcmp(gargv[1]+1, "?") == 0)
133 {
134 printf("%d
", lastcode);
135 }
136 else if (strcmp(gargv[1]+1, "PATH") == 0)
137 {
138 printf("%s
", getenv("PATH"));
139 }
140 lastcode = 0;
141 return 1;
142 }
143 }
144 }
145
146 return 0;
147 }
148 int ExecuteCommand()
149 {
150 pid_t id = fork();
151 if (id == 0)
152 {
153 execvp(gargv[0], gargv);
154 exit(0);
155 }
156 else if (id < 0)
157 return -1;
158 else
159 {
160 int status = 0;
161 pid_t sid = waitpid(id, &status, 0);
162 if (sid > 0)
163 lastcode = WEXITSTATUS(status);
164 //printf("wait childprocess sucess!!!
");
165 }
166 return 0;
167 }
168 int main()
169 {
170 Loadenv();
171 char command_line[MAXSIZE] = {0};
172 while(1)
173 {
174 //打印命令行字符串
175 PrintCommandLine();
176 //获取用户输入的字符串
177 if (0 ==GetCommand(command_line, sizeof(command_line)))
178 {
179 continue;
180 }
181
182 //解析字符串
183 ParseCommand(command_line);
184 //检查一个命令是要由bash执行的内建命令,还是要让子进程执行的普通命令
185 if(CheckBuiltinExecute() > 0)
186 continue;
187 //执行解析出来的字符串
188 ExecuteCommand();
189 }
190 return 0;
191 }
今天的分享就到此结束啦,如果对读者朋友们有所帮助的话,可否留下宝贵的三连呢~~
让我们共同努力, 一起走下去!











