一、目标
- 理解普通命令
- 理解内建命令
- 理解本地变量
- 理解环境变量
- 理解shell的运行原理
二、模拟shell
大部分是软件都是死循环,shell的本质也是死循环
1.打印命令行
可以直接调用系统函数,这里是简单的模拟,就不用系统函数了,直接在环境变量里面提取
cpp
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <unistd.h>
4
5 const char* GetUserName()
6 {
7 char* name = getenv("USER");
8 if(name == NULL)
9 return "None";
10 return name;
11 }
12
13 const char* GetHostName()
14 {
15 char* hostname = getenv("HOSTNAME");
16 if(hostname == NULL)
17 return "None";
18 return hostname;
19 }
20
21 const char* GetPwd()
22 {
23 char* pwd = getenv("PWD");
24 if(pwd == NULL)
25 return "None";
26 return pwd;
27 }
28
29 void PrintCommanLine()
30 {
31 printf("[%s@%s %s]# ",GetUserName(), GetHostName(), GetPwd()); // 用户名 @ 主机名 当前路径11
32 }
33 int main()
34 {
35 while(1)
36 {
37 PrintCommanLine();
38 sleep(1);
39 }
40
41
42 return 0;
43 }
运行会发现,什么都没有打印?
因为,在缓冲区里面没有刷新,但是加上换行就不符合预期了,这里需要自己调用刷新缓冲区的函数。
cpp
9 void PrintCommanLine()
30 {
31 // 打印命令行字符串
32 printf("[%s@%s %s] ",GetUserName(), GetHostName(), GetPwd()); // 用户名 @ 主机名 当前路径11
33 fflush(stdout);
34
35 }
刷新缓冲区可以打印出来,但是一直打印也不符号预期,打印一下之后应该是等待键盘上的输入
2.键盘上输入信息
当然不能使用scanf函数来获取 ,因为有的命令是带有选项的,这里使用fgets函数来获取
**作用:**从指定的文件流中获取一行字符串
**成功:**s的起始地址
**失败:**NULL
cpp
36 int main()
37 {
38 char command_line[MAXSIZE] = {0};
39 while(1)
40 {
41 //1.打印命令行字符串
42 PrintCommanLine();
43 //2.获取用户输入
44 if(NULL == fgets(command_line, sizeof(command_line), stdin))
45 continue;
46 printf("%s\n", command_line);
47
48 sleep(1);
49 }
50
51
52 return 0;
53 }
运行会发现多了一个换行符:
运行结果:
cpp
[zhangsan@hcss-ecs-f571 shell]$ make
gcc -o myshell myshell.c #-std=c++11
[zhangsan@hcss-ecs-f571 shell]$ ./myshell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] ls -a -l
ls -a -l
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell]
这是因为输入是时候会输入一个换行符,这个换行符也被读进去了。
这样解决:
cpp
43 //2.获取用户输入
44 if(NULL == fgets(command_line, sizeof(command_line), stdin))
45 continue;
46 //2.1用户在输入的时候,至少会按一次回车abcd\n,\n是一个字符
47 command_line[strlen(command_line)-1] = '\0';
48 printf("%s\n", command_line);

运行结果符合预期
封装一下:
cpp
37 int GetCommand(char commandline[], int size)
38 {
39
40 if(NULL == fgets(commandline, size, stdin))
41 return 0;
42 //2.1用户在输入的时候,至少会按一次回车abcd\n,\n是一个字符
43 commandline[strlen(commandline)-1] = '\0';
44 return strlen(commandline);
45 }
// main函数里面的:
53 //2.获取用户输入
54
55 if(0 == GetCommand(command_line, sizeof(command_line)))
56 continue;
57 printf("%s\n", command_line);
3.解析字符串
cpp
// 解析字符串 "ls -a -l" -> "ls" "-a" "-l" NULL
// gargv[]下标: 0 1 2 3

关于这个函数的使用,可以看下面这篇文章
解析字符串的代码:
cpp
56 int ParseCommand(char commandline[])
57 {
58
59 // 每次进来清空gargv表
60 gargc = 0;
61 memset(gargv, 0, sizeof gargv);
62
63 // 故意写成=
64 // 单个命令也没有错误,while语句里面会加1的
65 gargv[0] = strtok(commandline, gsep);
66 while((gargv[++gargc] = strtok(NULL, gsep)));
67 printf("gargc:%d\n", gargc);
68 int i = 0;
69 for(;gargv[i]; i++)
70 {
71 printf("gargv[%d]:%s\n",i,gargv[i]);
72 }
73 return gargc;
74 }
4.执行这个命令
当然不可以让bash自己进行程序替换,需要让子进程进行程序替换
cpp
77 int ExecuteCommand()
78 {
79 // 不能让bash执行程序替换函数,这里需要创建子进程
80 pid_t id = fork();
81 if(id < 0)
82 return -1;
83 else if(id == 0)
84 {
85 // 子进程 如何执行?
86 execvp(gargv[0], gargv);
87 exit(1); // 程序替换失败,退出码设为1
88 }
89 else
90 {
91 // 父进程等子进程
92 int status = 0;
93 pid_t rid = waitpid(id, &status, 0);
94 if(rid > 0)
95 {
96 // 等待成功
97 // printf("wait child process success!\n");
98
99 }
100 }
101 return 0;
102 }
运行结果:(可以正常运行)
bash
[zhangsan@hcss-ecs-f571 shell]$ make
gcc -o myshell myshell.c #-std=c++11
[zhangsan@hcss-ecs-f571 shell]$ ./myshell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] ls -a -l
total 36
drwxrwxr-x 3 zhangsan zhangsan 4096 Dec 24 20:13 .
drwxrwxr-x 11 zhangsan zhangsan 4096 Dec 23 20:22 ..
-rw-rw-r-- 1 zhangsan zhangsan 80 Dec 23 20:30 Makefile
-rwxrwxr-x 1 zhangsan zhangsan 13408 Dec 24 20:13 myshell
-rw-rw-r-- 1 zhangsan zhangsan 2747 Dec 24 20:13 myshell.c
drwxrwxr-x 2 zhangsan zhangsan 4096 Dec 23 22:38 test
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell]
细节1:理解命令
**问题:**自己的shell可以再一次调用自己的shall吗
**答案:**百分之百是可以的
原因:
- 系统的bash本身也是一个程序(命令),也是被执行起来的
- 当我们在su - 的时候,本质是系统是在当前的bash上再调用(套)一个bash(让root账号重新登陆一次)
bash和shell的关系:
细节2:理解内建命令
$?不能正常执行(下面7解决)
bash
[zhangsan@hcss-ecs-f571 shell]$ ./myshell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] ls
Makefile myshell myshell.c test
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] $?
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] echo $?
$?
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell]
cd路径切不过去
bash
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] pwd
/home/zhangsan/learn_-linux/shell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] cd ..
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] pwd
/home/zhangsan/learn_-linux/shell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] cd ..
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] pwd
/home/zhangsan/learn_-linux/shell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell]
**原因:**因为当前执行这些命令的是fork后的子进程执行的,不是bash执行的。
这样执行是不对的,需要让bash自己执行这些命令,这些命令就是内建命令
所以在执行命令前需要判断一下这个命令是内建命令还是需要子进程执行的命令
5.处理内建命令
chdir可以改变当前的工作路径(谁调用就该谁的)

bash
// 函数:
77 // return val:
78 // 0 :不是内建命令
79 // 1 :是内建命令 && 执行完毕(没有风险的)
80 int CheckBuiltinExecute()
81 {
82
83 if(strcmp(gargv[0],"cd") == 0 )
84 {
85 // 内建命令
86 if(gargc == 2)
87 {
88 // 新的目标路径:gargv[1]
89 chdir(gargv[1]);
90 }
91 return 1;
92 }
93 return 0;
94 }
// main函数中:
134 //3.解析字符串"ls -a -l" -> "ls" "-a" "-l"
135 //命令行解释器,需要对用户输入的字符串首先进行解析
136 ParseCommand(command_line);
137 //3.5 判断:这个命令是让父进程bash执行(内建命令),还是让子进程执行
138 if(CheckBuiltinExecute() > 0)
139 {
140 // 是内建命令
141 continue;
142 }
143 //4.子进程执行这个命令
144 ExecuteCommand();
但是运行会发现命令行的路径没有改变
原因:这里的路径是从环境变量里面拿的,环境变量被更新(操作系统不会自动更改环境变量,是用户级别的数据)需要bash自己来修改
解决方法1:
用系统调用来获取当前路径。

getcwd怎么获取当前工作路径的?
cpp
36 const char* GetPwd()
37 {
38 //char* pwd = getenv("PWD");
39 char* pwd = getcwd(cwd, sizeof cwd);
40 if(pwd == NULL)
41 return "None";
42 return pwd;
43 }
这样是不会修改环境变量里面的PWD,但是运行起来也不符合预期,预期是环境变量是随着当前工作路径而修改的。
解决方法2:
用环境变量来修改
介绍函数:

**snprintf功能:**可以格式化的将内容打印到指定长度的字符串中
cpp
121 int CheckBuiltinExecute()
122 {
123
124 if(strcmp(gargv[0],"cd") == 0 )
125 {
126 // 内建命令
127 if(gargc == 2)
128 {
129 // 新的目标路径:gargv[1]
130 // 1.更改进程内核中的路径
131 chdir(gargv[1]);
132 // 2.更改环境变量
133 char pwd[1024];
134 getcwd(pwd, sizeof(pwd));
135 // 这里避免"PWD="被覆盖(因为putenv不会拷贝字符串,而是直接使用传入指针指向的内存)
136 // 需要开辟新的空间,这里加1是因为字符串需要以"\0"结尾。
137 char* new_pwd = (char*)malloc(strlen("PWD=") + strlen(pwd) + 1);
138 snprintf(new_pwd, strlen("PWD=") + strlen(pwd) + 1, "PWD=%s", pwd);// PWD=/home/zhangsan
139 putenv(new_pwd);// 导出环境变量
140 // 修改自己的环境变量,这里没有覆盖原来的PWD
141 // genv[genvc++] = new_pwd;
142 // genv[genvc] = NULL;
143 lastcode = 0;
144 }
145 return 1;
146 }
关于为什么这里需要开辟新的空间来存:"PWD=":
如果不开辟环空间来存"PWD=",是这个样子的:


细节:为什么内建命令还有可执行文件

**原因:**命令行有两种执行模式
- 交互式(用户输入一条命令一个回车,输出结果)
- 批处理:用shell脚本运行命令(把命令写到脚本中,脚本一行一行的执行)
运行shell脚本的时候会在bash上创建子进程,这时候运行内建命令就需要执行对应的程序文件 ,因为内建命令在shell内部的。
6.处理命令行的格式问题

这里用c++string的rfind来查找最后一个"/"的位置,使用substr来进行截取
直接实现成函数:
cpp
21 static std::string rfindDir(const std::string& p)
22 {
23 if(p == "/")
24 return p;
25 const std::string psep = "/";
26 auto pos = p.rfind(psep);
27 if(pos == std::string::npos)
28 return std::string();
29 return p.substr(pos + 1);
30 }
在打印函数里面调用即可
cpp
57 void PrintCommanLine()
58 {
59 printf("[%s@%s %s]# ",GetUserName(), GetHostName(),rfindDir(GetPwd()).c_str()); // 用户名 @ 主机名 当前路径
60 fflush(stdout);
61
62 }
成功解决:

7.解决**$?不能正常执行**
定义一个全局变量退出码:lastcode
- 在父进程成功等待后,接收一下退出码
- 在内建命令运行完后,也设置一下退出码
可以保证lastcode是最后一个命令执行时的退出码!
内建命令函数:(添加一个内建命令并且设置lastcode)
cpp
99 int CheckBuiltinExecute()
100 {
101
102 if(strcmp(gargv[0],"cd") == 0 )
103 {
104 // 内建命令
105 if(gargc == 2)
106 {
107 // 新的目标路径:gargv[1]
108 // 1.更改进程内核中的路径
109 chdir(gargv[1]);
110 // 2.更改环境变量
111 char pwd[1024];
112 getcwd(pwd, sizeof(pwd));
113 snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);// PWD=/home/zhangsan
114 putenv(cwd);// 导出环境变量
115 lastcode = 0;
116 }
117 return 1;
118 }
119 else if(strcmp(gargv[0], "echo") == 0)
120 {
121 if(gargc == 2)
122 {
123 if(gargv[1][0] == '$')
124 {
125 // $? ? -> 看做一个变量名
126 if(strcmp(gargv[1] + 1, "?") == 0)
127 {
128 printf("lastcode:%d\n", lastcode);
129 }
130 lastcode = 0;
131 }
132 }
133 return 1;
134 }
135 return 0;
136 }
在父进程的代码:(接收退出码,设置lastcode)
cpp
137 int ExecuteCommand()
138 {
139 // 不能让bash执行程序替换函数,这里需要创建子进程
140 pid_t id = fork();
141 if(id < 0)
142 return -1;
143 else if(id == 0)
144 {
145 // 子进程 如何执行?
146 execvp(gargv[0], gargv);
147 exit(1); // 程序替换失败,退出码设为1
148 }
149 else
150 {
151 // 父进程等子进程
152 int status = 0;
153 pid_t rid = waitpid(id, &status, 0);
154 if(rid > 0)
155 {
156 // 等待成功
157 // printf("wait child process success!\n");
158 lastcode = WEXITSTATUS(status);
159 }
160 }
161 return 0;
162 }
运行结果:
bash
[zhangsan@hcss-ecs-f571 shell]$ ./myshell
[zhangsan@hcss-ecs-f571 shell]# ls faf
ls: cannot access faf: No such file or directory
[zhangsan@hcss-ecs-f571 shell]# echo $?
lastcode:2
[zhangsan@hcss-ecs-f571 shell]# echo $?
lastcode:0
[zhangsan@hcss-ecs-f571 shell]#
8.手动加载环境变量
cpp
19 // 环境变量表
20 char* genv[MAXSIZE];
21 int genvc = 0;
31 void LoadEnv()
32 {
33 // 正常情况下,环境变量表内部是从环境变量里来的
34 // 这里从父进程中取
35 extern char **environ;
36
37 for(;environ[genvc]; genvc++)
38 {
39 genv[genvc] = (char*)malloc(sizeof(char)*4096);
40 // genv[genvc] = new char[4096];
41 strcpy(genv[genvc], environ[genvc]);// 这里必须拷贝过来,不能指向environ里面的地址处
42 }
43 genv[genvc] = NULL;
44
45 }
