shell
- 功能描述
- 思路介绍
- 1.实现常驻进程功能
- 2.实现命令读取功能
- 3. 实现命令解析功能
- 4.实现子进程执行命令功能
- 5.完善功能
- 补充内容
- 让父进程运行内置命令
- 实现子进程能够获得父进程的环境变量功能(export命令)
- shell实现重定向功能
- 全部代码如下:
功能描述
实现一个类似于shell的命令行解释器。通过让子进程执行命令,父进程等待等待并解析命令,从而可以执行类似于“ls”,“ls -a -l -i”,'pwd"等linux指令。
思路介绍
1.实现常驻进程功能
在这里将要实现一个死循环,并且打印出提示信息。
while(1)
{
//命令行解释器一定是一个常驻内存的进程,不退出
//打印出提示信息[]
23 printf("[xty@localhost myshell]#] ");
24 fflush(stdout);
}
运行如图:
2.实现命令读取功能
使用fgets函数读取输入的内容,注意要注意把回车给删除(因为fgets会把回车也读取进来)
#define NUM 1024
//保存完整的命令字符串
char cmd_line[NUM];
//2.获取用户键盘的输入[输入的是各种指令和选项:"ls -a -l -i"]
memset(cmd_line, '\0', sizeof(cmd_line));
if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
{
continue;
}
cmd_line[strlen(cmd_line)-1] = '\0';
//发现有"ls -a -l\n'\o'" 回车换行,我们应该删除这个
//printf("echo:%s \n", cmd_line);
3. 实现命令解析功能
需要将我们输入的命令行字符串,变成shell能理解的语言。
将命令行选项使用strtok分开,把 "ls -a -l"变成 “ls”, “-a”, “-l”, “NULL”。
为后面的execvp作准备。
#define SIZE 32
//保存打散之后的命令行字符串
char *g_argv[SIZE];
//3. 将命令行选项使用strtok分开, "ls -a -l"-> "ls", "-a", "-l", "NULL"
g_argv[0] = strtok(cmd_line, " ");
int index = 1;
while(g_argv[index++] = strtok(NULL, " "));
//检查一下g_argv对不对
for(index = 0; g_argv[index];index++)
{
//虽然存入的是地址,但是%s,会将它看成字符串打印出来
printf("g_argv[%d] = %s\n", index, g_argv[index]);
}
结果如下:
4.实现子进程执行命令功能
子进程执行命令,父进程等待子进程返回。
//4.让子进程执行命令,执行完后给父进程返回值
pid_t id = fork();
if(id==0)
{
//子进程,执行命令
printf("子进程开始执行任务\n");
execvp(g_argv[0], g_argv);
exit(1);
}
//father
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret>0) printf("exit code:%d\n", WEXITSTATUS(status));
5.完善功能
让"ls"有颜色,并且让shell认识"ls"命令。
完整代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<sys/wait.h>
6 #include<sys/types.h>
7
8 #define NUM 1024
9 #define SIZE 32
10 //保存完整的命令字符串
11 char cmd_line[NUM];
12 //保存打散之后的命令行字符串
13 char *g_argv[SIZE];
14
15
16 //shell运行原理:通过让子进程执行命令,父进程等待&&解析命令
17 int main()
18 {
19 //0.命令行解释器一定是一个常驻内存的进程,不退出
20 while(1)
21 {
22 //1.打印出提示信息[]
23 printf("[xty@localhost myshell]#] ");
24 fflush(stdout);
25
26 //2.获取用户键盘的输入[输入的是各种指令和选项:"ls -a -l -i"]
27 memset(cmd_line, '\0', sizeof(cmd_line));
28 if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
29 {
30 continue;
31 }
32 if(cmd_line[0] == '\n')
33 {
34 continue;
35 }
36 cmd_line[strlen(cmd_line)-1] = '\0';
37 //发现有"ls -a -l\n'\o'" 回车换行,我们应该删除这个
38 //printf("echo:%s \n", cmd_line);
39
40 //3. 将命令行选项使用strtok分开, "ls -a -l"-> "ls", "-a", "-l", "NULL"
41 g_argv[0] = strtok(cmd_line, " ");
42 int index = 1;
43
44 if(strcmp(g_argv[0], "ls")==0)
45 {
46 g_argv[index++]="--color=auto";
47 }
48 //还可以让编译器支"ll"
49 if(strcmp(g_argv[0],"ll")==0)
50 {
51 g_argv[0] = "ls";
52 g_argv[index++] = "-l";
53 g_argv[index++] = "--color=auto";
54 }
55
56 //让ls命令有颜色
57 while(g_argv[index++] = strtok(NULL, " "));
58
59 //检查一下g_argv对不对
60 for(index = 0; g_argv[index];index++)
61 {
62 //虽然存入的是地址,但是%s,会将它看成字符串打印出来
63 printf("g_argv[%d] = %s\n", index, g_argv[index]);
64 }
65
66 //4.让子进程执行命令,执行完后给父进程返回值
67 pid_t id = fork();
68 if(id==0)
69 {
70 //子进程,执行命令
71 printf("子进程开始执行任务\n");
72 execvp(g_argv[0], g_argv);
73 exit(1);
74 }
75
76 //father
77 int status = 0;
78 pid_t ret = waitpid(id, &status, 0);
79 if(ret>0) printf("exit code:%d\n", WEXITSTATUS(status));
80 }
81 return 0;
82 }
这样我们一个简易的shell就完成了。
补充内容
shell执行的命令,通常有两种:
- 第三方提供的对应在磁盘中有具体二进制文件的可执行程序(由子进程执行)
- shell内部,自己实现的方法,由(父进程)来执行,有些命令会影响到shell本身,比如:“cd”,"export"命令等。
让父进程运行内置命令
当我们执行cd命令时,发现程序并没有改变目录。如下图:
原因是:cd命令被子进程执行了,子进程的当前目录被修改了,但是子进程立马就退出了。但是并没有影响到父进程的当前目录,所以第二次再次运行的时候,子进程还是在父进程的目录创建的,因此没有改变。
使用chdir函数改变父进程的工作目录。
代码如下:
66 //让父进程执行cd 命令
67 if(strcmp(g_argv[0], "cd")==0)
68 {
69 if(g_argv[1]!=NULL) chdir(g_argv[1]); // cd .., cd path
70 continue;
71 }
结果如下图:
实现子进程能够获得父进程的环境变量功能(export命令)
因export和其他的命令行功能不一样,所以需要再判断一下该功能。并且需要创建新数组保存一下环境变量,因为cmd_line再第二次读取时会清空,会导致getenv时得不到被清空的环境变量(因为存的是指针地址,清空后变成了野指针),因此需要创建数组存储一下!
代码如下:
//myshell.c
//写一个环境变量的bufer,用来存储临时的环境变量,防止cmd_line被清空
char my_num[64];
// export MYNUM=111222333
if(strcmp(g_argv[0], "export")==0 && g_argv[1]!=NULL)
{
strcpy(my_num, g_argv[1]);
int ret = putenv(my_num);
if (ret == 0) printf("%s export sucess\n", my_num); //查看一下导入的是什么
continue;
}
//env_test.c
1 #include<stdio.h>
2 #include<stdlib.h>
3
4 int main()
5 {
6 printf("我是测试环境变量是否成功导入进程\n");
7 printf("MYNUM= %s \n",getenv("MYNUM"));
8
9 }
shell实现重定向功能
先检查是否有重定向的功能,然后再执行命令。
//检查命令模块
//定义标志位的含义
22 #define INPUT_REDIR 1
23 #define OUTPUT_REDIR 2
24 #define APPEND_REDIR 3
25 #define NONE_REDIR 0
26 int redir_status = NONE_REDIR;
27
28 char *CheckRedir(char *start)
29 {
30 assert(start);
31 char *end = start + strlen(start) - 1;// ls -a -l
32
33 //从后往前找
34 while(end >= start)
35 {
36 if(*end == '>')
37 {
38 if(*(end - 1) == '>')
39 {
40 redir_status = APPEND_REDIR;
41 *(end - 1) = '\0';
42 end++; //重定向后字符串的首地址
43 break;
44 }
45 redir_status = OUTPUT_REDIR;
46 *end = '\0';
47 end++; // ls -a>myfile.txt ->ls -a\0myflie.txt
48 break;
49 }
50 else if(*end == '<')
51 {
52 redir_status = INPUT_REDIR;
53 *end = '\0';
54 end++;
55 break;
56 }
57 else{
58 end--;
59 }
60 }
61 if(end>=start)
62 {
63 return end; //要打开的文件名
64 }
65 else{
66 return NULL; //没有重定向功能
67 }
68 }
//main函数内部,子进程执行命令前,多一个重定向打开文件的逻辑:
142 //4.让子进程执行命令,执行完后给父进程返回值
143 pid_t id = fork();
144 if(id==0)
145 {
146 //子进程,执行命令
147 printf("子进程开始执行任务\n");
148 if(sep!=NULL)
149 {
150 int fd = -1;
151 //有重定向
152 switch(redir_status)
153 {
154 case INPUT_REDIR:
155 fd = open(sep, O_RDONLY);
156 dup2(fd, 0);
157 break;
158 case OUTPUT_REDIR:
159 fd = open(sep, O_WRONLY | O_TRUNC | O_CREAT, 0666);
160 dup2(fd, 1);
161 break;
162 case APPEND_REDIR:
163 fd = open(sep, O_WRONLY | O_APPEND | O_CREAT, 0666);
164 dup2(fd, 1);
165 break;
166 default:
167 printf("erro????????\n");
168 break;
169 }
170 }
171
172 execvp(g_argv[0], g_argv);
173 exit(1);
174 }
175
全部代码如下:
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4 #include<string.h>
5 #include<assert.h>
6 #include<fcntl.h>
7 #include<sys/wait.h>
8 #include<sys/stat.h>
9 #include<sys/types.h>
10
11 #define NUM 1024
12 #define SIZE 32
13 //保存完整的命令字符串
14 char cmd_line[NUM];
15 //保存打散之后的命令行字符串
16 char *g_argv[SIZE];
17
18 //写一个环境变量的bufer,用来存储临时的环境变量,防止cmd_line被清空
19 char my_num[64];
20
21 //定义标志位的含义
22 #define INPUT_REDIR 1
23 #define OUTPUT_REDIR 2
24 #define APPEND_REDIR 3
25 #define NONE_REDIR 0
26 int redir_status = NONE_REDIR;
27
28 char *CheckRedir(char *start)
29 {
30 assert(start);
31 char *end = start + strlen(start) - 1;// ls -a -l
32
33 //从后往前找
34 while(end >= start)
35 {
36 if(*end == '>')
37 {
38 if(*(end - 1) == '>')
39 {
40 redir_status = APPEND_REDIR;
41 *(end - 1) = '\0';
42 end++; //重定向后字符串的首地址
43 break;
44 }
45 redir_status = OUTPUT_REDIR;
46 *end = '\0';
47 end++; // ls -a>myfile.txt ->ls -a\0myflie.txt
48 break;
49 }
50 else if(*end == '<')
51 {
52 redir_status = INPUT_REDIR;
53 *end = '\0';
54 end++;
55 break;
56 }
57 else{
58 end--;
59 }
60 }
61 if(end>=start)
62 {
63 return end; //要打开的文件名
64 }
65 else{
66 return NULL; //没有重定向功能
67 }
68 }
69
70 //shell运行原理:通过让子进程执行命令,父进程等待&&解析命令
71 int main()
72 {
73 //0.命令行解释器一定是一个常驻内存的进程,不退出
74 while(1)
75 {
76 //1.打印出提示信息[]
77 printf("[xty@localhost myshell]#] ");
78 fflush(stdout);
79
80 //2.获取用户键盘的输入[输入的是各种指令和选项:"ls -a -l -i"]
81 memset(cmd_line, '\0', sizeof(cmd_line));
82 if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
83 {
84 continue;
85 }
86 if(cmd_line[0] == '\n')
87 {
88 continue;
89 }
90 cmd_line[strlen(cmd_line)-1] = '\0';
91 //发现有"ls -a -l\n'\o'" 回车换行,我们应该删除这个
92 //printf("echo:%s \n", cmd_line);
93
94
95 //在分析指令之前就检查有没有重定向
96 char* sep = CheckRedir(cmd_line);
97
98 //3. 将命令行选项使用strtok分开, "ls -a -l"-> "ls", "-a", "-l", "NULL"
99 g_argv[0] = strtok(cmd_line, " ");
100 int index = 1;
101
102 if(strcmp(g_argv[0], "ls")==0)
103 {
W>104 g_argv[index++]="--color=auto";
105 }
106 //还可以让编译器支"ll"
107 if(strcmp(g_argv[0],"ll")==0)
108 {
W>109 g_argv[0] = "ls";
W>110 g_argv[index++] = "-l";
W>111 g_argv[index++] = "--color=auto";
112 }
113
114 //让ls命令有颜色
W>115 while(g_argv[index++] = strtok(NULL, " "));
116
117 检查一下g_argv对不对
118 //for(index = 0; g_argv[index];index++)
119 //{
120 // //虽然存入的是地址,但是%s,会将它看成字符串打印出来
121 // printf("g_argv[%d] = %s\n", index, g_argv[index]);
122 //}
123
124
125 // export MYNUM=111222333
126 if(strcmp(g_argv[0], "export")==0 && g_argv[1]!=NULL)
127 {
128 strcpy(my_num, g_argv[1]);
129 int ret = putenv(my_num);
130 if (ret == 0) printf("%s export sucess\n", my_num); //查看一下导入的是什么
131 continue;
132 }
133
134
135
136 //让父进程执行cd 命令
137 if(strcmp(g_argv[0], "cd")==0)
138 {
139 if(g_argv[1]!=NULL) chdir(g_argv[1]); // cd .., cd path
140 continue;
141 }
142 //4.让子进程执行命令,执行完后给父进程返回值
143 pid_t id = fork();
144 if(id==0)
145 {
146 //子进程,执行命令
147 printf("子进程开始执行任务\n");
148 if(sep!=NULL)
149 {
150 int fd = -1;
151 //有重定向
152 switch(redir_status)
153 {
154 case INPUT_REDIR:
155 fd = open(sep, O_RDONLY);
156 dup2(fd, 0);
157 break;
158 case OUTPUT_REDIR:
159 fd = open(sep, O_WRONLY | O_TRUNC | O_CREAT, 0666);
160 dup2(fd, 1);
161 break;
162 case APPEND_REDIR:
163 fd = open(sep, O_WRONLY | O_APPEND | O_CREAT, 0666);
164 dup2(fd, 1);
165 break;
166 default:
167 printf("erro????????\n");
168 break;
169 }
170 }
171
172 execvp(g_argv[0], g_argv);
173 exit(1);
174 }
175
176 //father
177 int status = 0;
178 pid_t ret = waitpid(id, &status, 0);
179 if(ret>0) printf("exit code:%d\n", WEXITSTATUS(status));
180 }
181 return 0;
182 }