实现shell
点赞👍👍收藏🌟🌟关注💖💖
你的支持是对我最大的鼓励,我们一起努力吧!😃😃
前面学过了进程创建/终止/等待/替换。现在根据所学的内容实现一个简易版的shell。
//头文件
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<sys/types.h>
5 #include<sys/wait.h>
6 #include<string.h>
1.提示符
这是我们在命令行上首先出现的内容。
注意在命令行输入指令,是在同一行的。
int main()
{
//这里也可以使用环境变量来获取这些内容
printf("用户名@主机名 当前路径#");
//这里printf后面不能带\n,但是要把内容打印到显示器,因此必须刷新缓冲区。
fflush(stdout);
return 0;
}
2.命令行参数
前面说过ls -a -l是命令行参数,是一个整体的字符串,分割层一个个字符串传到main函数中。
因此,我们需要把输入的字符串也要分割成一个个字符串。
2.1输入指令
7 #define NUM 1024
8 #define OPT_NUM 64
9 //全局变量
10 char lineCommand[NUM];//存放输入的指令字符串
11 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
12
这里使用fgets函数,把输入流放在指定数组。
9 #define NUM 1024
10 #define OPT_NUM 64
11
12 char lineCommand[NUM];
13 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
14
15 int main()
16 {
17 //这里也可以使用环境变量来获取这些内容
18 printf("用户名@主机名 当前路径# ");
19 fflush(stdout);
20 //"ls -a -l"-----> "ls" "-a" "-l"
21 //这里减1,是为了极端情况下放\0
22 char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
23 assert(s);
24 //测试一下
25 printf("test:%s\n",lineCommand);
26
27 return 0;
28 }
发现这里空了一格。这是因为我们输入指令的时候,最后一次肯定敲的是\n,因此这里我们消除一下\n。
9 #define NUM 1024
10 #define OPT_NUM 64
11
12 char lineCommand[NUM];
13 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
14
15 int main()
16 {
17 //这里也可以使用环境变量来获取这些内容
18 printf("用户名@主机名 当前路径# ");
19 fflush(stdout);
20 //"ls -a -l"-----> "ls" "-a" "-l"
21 //这里减1,是为了极端情况下放\0
W> 22 char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
23 assert(s);
24 //清除最后一个\n abc\n
25 lineCommand[strlen(lineCommand)-1]=0;
26 //测试一下
27 printf("test:%s\n",lineCommand);
28
29 return 0;
30 }
2.2分割指令
在C语言的时候,学过一个分割字符串的函数,strtok
28 //这里以空格为分隔符
29 myargv[0]=strtok(lineCommand," ");
30 //分割的是同一个字符串,下一次第一个参数就置为NULL就可以了
31 // myargv[1]=strtok(NULL,"");
32 //这里需要实现循环,注意strtok到字符串结束,会返回NULL,myargv[end]=NULL
33 int i=1;
34 while(myargv[i++]=strtok(NULL," "));
36 //条件编译,测试分割是否成功
37 #ifdef DEBUG
38 for(int i=0;myargv[i];++i)
39 {
40 printf("myargv[%d]:%s\n",i,myargv[i]);
41 }
42 #endif
1 myshell:myshell.c
2 gcc -o $@ $^ -std=c99 -DDEBUG //定义宏 不需要就前面加个#注释掉
3
4 .PHONY:clean
5 clean:
6 rm -f myshell
3.创建子进程执行指令
前面我们学了6个替换函数。这里我们选择execvp最合适。
44 //创建子进程
45 pid_t id = fork();
46 assert(id != -1);
47 if(id == 0)
48 {
49 //子进程
50 execvp(myargv[0],myargv);
51 }
52 //父进程
53 //这里先不关心退出码
54 waitpid(id,NULL,0);
但是这里只能实现一次,因此,需要把整体循环起来。
1#include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<assert.h>
5 #include<sys/types.h>
6 #include<sys/wait.h>
7 #include<string.h>
8
9 #define NUM 1024
10 #define OPT_NUM 64
11
12 char lineCommand[NUM];
13 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
14
15 int main()
16 {
17 while(1)
18 {
19 //这里也可以使用环境变量来获取这些内容
20 printf("用户名@主机名 当前路径# ");
21 fflush(stdout);
22 //"ls -a -l"-----> "ls" "-a" "-l"
23 //这里减1,是为了极端情况下放\0
24 char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
25 assert(s);
26 //清除最后一个\n abc\n
27 lineCommand[strlen(lineCommand)-1]=0;
28 //测试一下
29 //printf("test:%s\n",lineCommand);
30 //这里以空格为分隔符
31 myargv[0]=strtok(lineCommand," ");
32 //分割的是同一个字符串,下一次第一个参数就置为NULL就可以了
33 // myargv[1]=strtok(NULL,"");
34 //这里需要实现循环,注意strtok到字符串结束,会返回NULL,myargv[end]=NULL
35 int i=1;
36 while(myargv[i++]=strtok(NULL," "));
37
38 //测试分割是否成功
39#ifdef DEBUG
40 for(int i=0;myargv[i];++i)
41 {
42 printf("myargv[%d]:%s\n",i,myargv[i]);
43 }
44 #endif
45
46 //创建子进程
47 pid_t id = fork();
48 assert(id != -1);
49 if(id == 0)
50 {
51 //子进程
52 execvp(myargv[0],myargv);
53 }
54 //父进程
55 //这里先不关心退出码
56 waitpid(id,NULL,0);
57 }
58 return 0;
59 }
4.三个细节
4.1ls可执行程序问题
自己实现的shell,可执行程序没有颜色,
36 //这里对ls,特殊处理
37 if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
38 {
39 myargv[i++]=(char*)"--color=auto";
40 }
4.2切换路径问题
我们发现,当我切换到上层路径时,发现当前路径没有变。这是为什么呢?
什么是当前路径?
在前面我们学过一个查看进程的命令ls /proc
exe----> 是当前进程执行的是磁盘路径下哪一个程序!
cmd----> 是当前进程的工作目录
什么是当前路径,默认是你在那个路径下把程序跑起来。本质就是当前进程的工作目录。
当前路径知道了,那为什么自己写的shell,cd的时候,路径没有变化呢?
子进程也有自己的工作目录,默认和父进程一样。fork之后,子进程执行cd命令,更改的是子进程的目录!子进程执行完毕之后,被回收了。继续使用的是父进程。然后再创建子进程。默认和父进程工作目录一样,这时执行pwd命令,所以当前目录没有变。
如果就想更改当前工作目录,系统提供chdir函数。
1 #include<stdio.h>
2 #include<unistd.h>
3 #include<stdlib.h>
4
5 int main()
6 {
7 //想让工作目录是谁,就把谁的路径传过来
8 chdir("/home/wdl");
9 while(1)
10 {
11 printf("我是一个进程,我的id是:%d\n",getpid());
12 sleep(1);
13 }
14
15 return 0;
16 }
修改一下自己的代码。
//如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
//像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
42 if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
43 {
44 if(myargv[1] != NULL)
45 {
46 chdir(myargv[1]);
47 continue;
48 }
49 }
4.3进程退出码
echo $? //记录最近一个程序的进程退出码
具体实现
//两个全局变量记录子进程退出信号,退出码
12 int lastCode=0;
13 int lastsig=0;
//特殊处理
57 if(myargv[0] != NULL && strcmp(myargv[0],"echo") == 0)
58 {
59 if(strcmp(myargv[1],"$?") == 0)
60 {
61 printf("%d,%d\n",lastCode,lastsig);
62 }
63 else
64 {
65 printf("%s\n",myargv[1]);
66 }
67 continue;
68 }
78 //创建子进程
79 pid_t id = fork();
80 assert(id != -1);
81 if(id == 0)
82 {
83 //子进程
84 execvp(myargv[0],myargv);
85 exit(1);
86 }
87 //父进程
88 int status=0;
89 pid_t ret= waitpid(id,NULL,0);
90 assert(ret>0);
//记录退出信号,退出码
91 lastCode=(status>>8)&0xFF;
92 lastsig=status&0x7F;
93 return 0;
5.重定向功能
//增加一个分割指令和文件名的函数
commandstrtok(lineCommand);
分割的时候,我们需要知道重定向是什么类型,文件是什么名字,因此增加两个全局变量来记录。
//重定向类型
//第一个为初始重定向
#define DEFAULT_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define ERROR_EDIRR 3
//重定向类型+文件名
int redirType=DEFAULT_REDIR;
char* redirFile=NULL;
分割函数
//这里找文件名没有写成函数,而写个宏
#define trimSpace(start) do{\
while(isspace(*start)) ++start;\
}while(0)
void commandstrtok(char* cmd)
{
assert(cmd);
char* start=cmd;
char* end=cmd+strlen(cmd);
while(start < end)
{
if(*start == '<')
{
*start=0;
++start;
//这里可能 ls -a -l > log.txt,
trimSpace(start);
redirType=INPUT_REDIR;
redirFile=start;
break;
}
else if(*start == '>')
{
*start=0;
++start;
if(*start == '>')
{
redirType=APPEND_REDIR;
++start;
}
else
{
redirType=OUTPUT_REDIR;
}
trimSpace(start);
redirFile=start;
break;
}
else
{
++start;
}
}
因为命令是子进程执行的,真正重定向的功能一定是由子进程来完成的,
但是如何重定向是父进程要告知给子进程的。
//创建子进程
pid_t id = fork();
assert(id != -1);
if(id == 0)
{
switch(redirType)
{
case INPUT_REDIR:
{
int fd=open("log.txt",O_RDONLY);
if(fd <0)
{
perror("open");
return 1;
}
//重定向文件已经打开了
dup2(fd,0);
}
break;
case OUTPUT_REDIR:
case APPEND_REDIR:
{
umask(0);
int flags=O_WRONLY|O_CREAT;
if(redirType == OUTPUT_REDIR) flags|=O_CREAT;
else flags|=O_APPEND;
int fd=open("log.txt",flags,0666);
if(fd < 0)
{
perror("open");
return 1;
}
dup2(fd,1);
}
break;
default:
printf("bug?\n");
break;
}
//程序替换
execvp(myargv[0],myargv);
exit(1);
}
6.myshell完整代码
1 #include<stdio.h>
2 #include<stdlib.h>
3 #include<unistd.h>
4 #include<assert.h>
5 #include<sys/types.h>
6 #include<sys/stat.h>
7 #include<fcntl.h>
8 #include<sys/wait.h>
9 #include<string.h>
10 #include<ctype.h>
11
12 #define NUM 1024
13 #define OPT_NUM 64
14
15 //重定向类型
16 //第一个为初始重定向
17 #define DEFAULT_REDIR 0
18 #define INPUT_REDIR 1
19 #define OUTPUT_REDIR 2
20 #define APPEND_REDIR 3
21
22 //重定向类型+文件名
23 int redirType=DEFAULT_REDIR;
24 char* redirFile=NULL;
25
26 int lastCode=0;
27 int lastsig=0;
28
29 char lineCommand[NUM];
30 char* myargv[OPT_NUM];//指针数组,存放分割的一个个字符串
31
32 //这里找文件名没有写成函数,而写个宏
33 #define trimSpace(start) do{\
34 while(isspace(*start)) ++start;\
35 }while(0)
36
37 void commandstrtok(char* cmd)
38 {
39 assert(cmd);
40 char* start=cmd;
41 char* end=cmd+strlen(cmd);
42 while(start < end)
43 {
44 if(*start == '<')
45 {
46 *start=0;
47 ++start;
48 //这里可能 ls -a -l > log.txt,
49 trimSpace(start);
50 redirType=INPUT_REDIR;
51 redirFile=start;
52 break;
53 }
54 else if(*start == '>')
55 {
56 *start=0;
57 ++start;
58 if(*start == '>')
59 {
60 redirType=APPEND_REDIR;
61 ++start;
62 }
63 else
64 {
65 redirType=OUTPUT_REDIR;
66 }
67 trimSpace(start);
68 redirFile=start;
69 break;
70 }
71 else
72 {
73 ++start;
74 }
75 }
76
77 }
78
79 int main()
80 {
81 while(1)
82 {
83 //防止输入ls -a -l > log.txt,在输入ls -a -l还大概在重定向来执行
84 redirType=DEFAULT_REDIR;
85 redirFile=NULL;
86 //这里也可以使用环境变量来获取这些内容
87 printf("用户名@主机名 当前路径# ");
88 fflush(stdout);
89 //"ls -a -l"-----> "ls" "-a" "-l"
90 //这里减1,是为了极端情况下放\0
W> 91 char* s=fgets(lineCommand,sizeof(lineCommand)-1,stdin);
92 assert(s);
93
94 //增加重定向功能
95 //"ls -a -l > log.txt" "ls -a -l" "log.txt"
96 //"ls -a -l >> log.txt" "ls -a -l" "log.txt"
97 //"cat < log.txt" "cat" "log.txt"
98 //分割指令和文件名的函数
99 commandstrtok(lineCommand);
100
101 //清除最后一个\n abc\n
lineCommand[strlen(lineCommand)-1]=0;
103 //测试一下
104 //printf("test:%s\n",lineCommand);
105 //这里以空格为分隔符
106 myargv[0]=strtok(lineCommand," ");
107 //分割的是同一个字符串,下一次第一个参数就置为NULL就可以了
108 // myargv[1]=strtok(NULL,"");
109 //这里需要实现循环,注意strtok到字符串结束,会返回NULL,myargv[end]=NULL
110 int i=1;
111 //这里对ls,特殊处理
112 if(myargv[0] != NULL && strcmp(myargv[0],"ls") == 0)
113 {
114 myargv[i++]=(char*)"--color=auto";
115 }
116
W>117 while(myargv[i++]=strtok(NULL," "));
118 //如果是cd命令,不需要创建子进程,让shell自己执行对应的命令,本质就是执行系统接口
119 //像这种不需要让我们的子进程来执行,而是让shell自己执行的命令 --- 内建/内置命令
120 if(myargv[0] != NULL && strcmp(myargv[0],"cd") == 0)
121 {
122 if(myargv[1] != NULL)
123 {
124 chdir(myargv[1]);
125 continue;
126 }
127 }
128
129 if(myargv[0] != NULL && strcmp(myargv[0],"echo") == 0)
130 {
131 if(strcmp(myargv[1],"$?") == 0)
132 {
133 printf("%d,%d\n",lastCode,lastsig);
134 }
135 else
136 {
137 printf("%s\n",myargv[1]);
138 }
139 continue;
140 }
141
142 //测试分割是否成功
143 #ifdef DEBUG
144 for(int i=0;myargv[i];++i)
145 {
146 printf("myargv[%d]:%s\n",i,myargv[i]);
147 }
148 #endif
149
150 //创建子进程
151 pid_t id = fork();
152 assert(id != -1);
153 if(id == 0)
154 {
155
156 switch(redirType)
157 {
158 case DEFAULT_REDIR:
159 //什么都不做
160 break;
161 case INPUT_REDIR:
162 {
163 int fd=open("log.txt",O_RDONLY);
164 if(fd <0)
165 {
166 perror("open");
167 return 1;
168 }
169 //重定向文件已经打开了
170 dup2(fd,0);
171 }
172 break;
173 case OUTPUT_REDIR:
174 case APPEND_REDIR:
175 {
176 umask(0);
177 int flags=O_WRONLY|O_CREAT;
178 if(redirType == OUTPUT_REDIR) flags|=O_TRUNC;
179 else flags|=O_APPEND;
180 int fd=open("log.txt",flags,0666);
181 if(fd < 0)
182 {
183 perror("open");
184 return 1;
185 }
186 dup2(fd,1);
187 }
188 break;
189 default:
190 printf("bug?\n");
191 break;
192
193 }
194 //子进程
195 execvp(myargv[0],myargv);
196 exit(1);
197 }
198 //父进程
199 int status=0;
W>200 pid_t ret= waitpid(id,NULL,0);
201 assert(ret>0);
202 lastCode=(status>>8)&0xFF;
203 lastsig=status&0x7F;
204 }
205 return 0;
206 }