1.程序替换
在学习完一些列的进程替换接口之后我们大概就能知道,我们的环境变量以及命令行参数是如何传递给子进程的,这些参数是我们在调用进程替换时就传给了子进程的数据。
那么如果我们自己要实现一个简单的命令行解释器,我们是不是首先就需要对命令行的参数进行解析? 命令行参数的解析我们首先需要获取一行字符串,然后以空格为间隔将字符串拆分为程序名和选项。
#include<stdio.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
char stringArg[1024]; //获取命令行输入
char* argv[32]; //拆解后的命令行参数数组
int main()
{
stringArg[1023]=0;
//printf("%s\n",getenv("HOST"));
while(1)
{
memset(stringArg,0,1024); //全部初始化\0 ,便于计算长度
memset(argv,0,32*sizeof(char*));//全部初始化为NULL,命令行数组的结尾必须是NULL
//打印一行提示信息 用户名@主机名 当前路径$
printf("[%s@ %s %s]$ ",getenv("USER"),getenv("HOSTNAME"),getenv("PWD")); //不换行
fflush(stdout); //将提示信息打印出来
//获取字符串 要读空格,不能用 scanf ,使用fgets
fgets(stringArg,1023,stdin);
stringArg[strlen(stringArg)-1]=0; //将最后的 \n 换成 \0
if(strlen(stringArg)==0)
{
//什么也没输入
continue;
}
//拆解字符串 strtok
int i=0;
argv[i++]=strtok(stringArg," ");
while(argv[i++]=strtok(NULL," ")); //最后一次当字符串结束再使用strtok,会返回NULL,刚好作为循环结束条件
#ifdef _DEBUG_ARGV//测试切割功能
char**p=argv;
while(*p)
printf("%s ",*p++);
printf("\n");
#endif
}
return 0;
}
切割字符串的工作可以直接使用 strtok 函数来完成。
完成切割字符串的工作之后,我们就可以将该命令行数组用于程序替换的参数了,这是最基础的shell。
//程序替换
pid_t id=fork();
if(id<0)
{
perror("fork failed");
exit(1);
}
if(id==0)
{
//程序替换
execvp(argv[0],argv);
//如果替换失败则会执行下面的代码
perror("execvp failed");
exit(1);
}
//父进程等待回收僵尸
int status=0;
waitpid(id,&status,0);
if(WIFEXITED(status))//正常退出
{
if(WEXITSTATUS(status))//退出码不为0
{
printf("运行成功,退出码为 :%d\n", WEXITSTATUS(status));
}
}
else//异常终止
{
printf("%s\n",strerror(errno));
}
2.进程工作目录
在上面的代码实现下还有很多的小问题,比如我们使用cd命令的时候并不能切换目录,这是为什么呢?
要理解这个问题,首先我们需要知道一个概念:进程的工作目录
当我们启动一个进程时,我们打开进程目录 /proc,找到我们的进程,然后进到我们进程的目录中,我们能够看到两个特殊的东西
exe就是我们的二进制可执行程序的位置,这个我们很好理解,而cwd是一串路径,这就是进程的工作目录,当我们启动一个进程时,一般是我们在哪个路径下启动的这个进程,工作路径就是当前所在路径。我们可以在其他路径下降这个程序跑起来观察一下。
当我们在上一级目录下将该程序跑起来,进程的工作目录就变成了执行运行命令时所在的路径,这就是进程的工作路径。
那么进程工作路径可以修改吗?
在命令行中,我们的cd命令就是修改进程工作路径的,也就是我们的shell的工作路径,所以我们经常能够通过cd命令进入不同的路径,我们的环境变量PWD其实严格上来说就是当前进程工作路径。 而在程序中,我们可以使用 chdir 来更改工作目录,
到了这里,我们就能知道为什么我们的shell的cd命令不起作用了 ,因为我们是通过创建子进程然后进行程序替换执行的cd命令,那么修改的就是子进程的工作目录,而子进程工作目录被修改时,发生写时拷贝,不会影响父进程。而我们pwd查的是父进程的工作目录,所以我们的cd命令其实不应该让子进程执行,而是由我们的父进程自己执行,因为cd命令要修改的是我们当前进程也就是父进程的工作目录。
像这种不需要创建子进程来执行的,而是让shell自己执行的命令,叫做内建命令或者内置命令
比如我们前面提到的 echo 命令也是一个内建命令,这就是他为什么能够显示本地变量的原因,因为他不是通过创建子进程来执行的,而是shell自己执行。
如何模拟实现cd命令呢? 其实也很简单,在创建子进程之前判断一下 命令的第一个字串是否为 cd,如果是 cd ,我们就直接使用chdir来更换工作目录。同时我们还要判断一下是否切换成功。
//内建命令
if(strcmp("cd",argv[0])==0) //cd
{
const char*changepath=argv[1];
int ret=chdir(changepath);
if(ret==-1)
{
printf("%s\n",strerror(errno));
}
continue;
}
而如果是echo目录,我们也是直接在当前目录下执行,而是直接在父进程打印。但是echo还有一个问题,就是空格我们输入的内容,也需要打印,所以我们需要在切割第一个字串串之后判断是否为echo,如果为echo,后续就先不切割了,然后再判断是否有 $ 符号,也就是在stringArg[5]是否为$ ,如果为$ ,则需要去找我们的变量列表中的变量来打印。但是我们这里只支持环境变量就够了,其他的实现起来太复杂。
3.重定向
重定向的符号无非就是 > >> < ,而且不会出现在第一个子串上,所以只需要在将第一个基础命令切割出来之后,判断一下是否有重定向的符。判断完之后将重定向符号变为空格,以便切割。
同时,重定向只能在子进程中去替换 fd ,也就是在程序替换之前替换,防止父进程也被替换了
//判断是否有重定向
int j=0;
for(j=0;j<strlen(stringArg);++j)
{
if(stringArg[j]=='<')
{
//输入重定向
MODE=CHANGEIN;
stringArg[j]='\0';
filename=&stringArg[j+1];
break;
}
else if(stringArg[j]=='>')
{
stringArg[j]='\0';
if(stringArg[j+1]=='>')
{
j++;
MODE=CHANGEAPPEND;
stringArg[j]='\0';
filename=&stringArg[j+1];
break;
}
else
{
MODE=CHANGEOUT;
filename=&stringArg[j+1];
break;
}
}
}
if(id==0)
{
//先检查是否有重定向
if(MODE==CHANGEOUT)
{
int fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
dup2(fd,1);
}
if(MODE==CHANGEIN)
{
int fd=open(filename,O_RDONLY);
dup2(fd,0);
}
if(MODE==CHANGEAPPEND)
{
int fd = open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);
dup2(fd,1);
}
//程序替换
execvp(argv[0],argv);
//如果替换失败则会执行下面的代码
perror("execvp failed");
exit(1);
}
为什么要把重定向符号设置为 \0 呢?因为我们不想要在切割字串的时候还将后面的文件名也切割进去,我们默认这些符号后面就是目标的文件名了。
完整代码
#include<stdio.h>
2 #include<assert.h>
3 #include<string.h>
4 #include<stdlib.h>
5 #include<unistd.h>
6 #include<sys/types.h>
7 #include<sys/wait.h>
8 #include<errno.h>
9 #include<sys/stat.h>
10 #include<fcntl.h>
11
12
13 char stringArg[1024]; //获取命令行输入
14 char* argv[32]; //拆解后的命令行参数数组
15 #define CHANGEOUT 1 //输出重定向
16 #define CHANGEIN 2 //输入重定向
17 #define CHANGEAPPEND 4 //追加重定向
18 char*filename; //重定向的目标文件
19 int MODE; //记录是否重定向
20
21
22 int main()
23 {
24 stringArg[1023]=0;
25 //printf("%s\n",getenv("HOST"));
26
27 while(1)
28 {
29 //重置errno
30 errno=0;
31 MODE =0 ;
32
33 //获取字符串提取命令行
34
35 memset(stringArg,0,1024); //全部初始化\0 ,便于计算长度
36 memset(argv,0,32*sizeof(char*));
37 //打印一行提示信息 用户名@主机名 当前路径$
38 printf("[%s@ %s %s]$ ",getenv("USER"),getenv("HOSTNAME"),getenv("PWD")); //不换行
39 fflush(stdout); //将提示信息打印出来
40 //获取字符串 要读空格,不能用 scanf ,使用fgets
41 fgets(stringArg,1023,stdin);
42 stringArg[strlen(stringArg)-1]=0; //将最后的 \n 换成 \0
43 if(strlen(stringArg)==0)
44 {
45 //什么也没输入
46 continue;
47 }
48
49 //判断是否有重定向
50 int j=0;
for(j=0;j<strlen(stringArg);++j)
52 {
53 if(stringArg[j]=='<')
54 {
55 //输入重定向
56 MODE=CHANGEIN;
57 stringArg[j]='\0';
58 filename=&stringArg[j+1];
59 break;
60 }
61 else if(stringArg[j]=='>')
62 {
63 stringArg[j]='\0';
64 if(stringArg[j+1]=='>')
65 {
66 j++;
67 MODE=CHANGEAPPEND;
68 stringArg[j]='\0';
69 filename=&stringArg[j+1];
70 break;
71 }
72 else
73 {
74 MODE=CHANGEOUT;
75 filename=&stringArg[j+1];
76 break;
77 }
78 }
79 }
80
81 //拆解字符串 strtok
82 int i=0;
83 argv[i++]=strtok(stringArg," ");
84 if(strcmp("echo",argv[0])==0&&MODE==0) //echo
85 {
86 //检查$
87 if(stringArg[5]=='$')
88 {
89 //只考虑环境变量
90 printf("%s\n",getenv(&stringArg[6]));
91 }
92 else
93 printf("%s\n",&stringArg[5]);
94 continue;
95 }
W> 96 while(argv[i++]=strtok(NULL," ")); //最后一次当字符串结束再使用strtok,会返回NULL,刚好作为结束条件以及命令行参数数组的结尾NULL
97
98
99 //内建命令
100 if(strcmp("cd",argv[0])==0) //cd
{
102 const char*changepath=argv[1];
103 int ret=chdir(changepath);
104 if(ret==-1)
105 {
106 printf("%s\n",strerror(errno));
107 }
108 continue;
109
110 }
111
112 //程序替换
113 else
114 {
115 pid_t id=fork();
116
117 if(id<0)
118 {
119 perror("fork failed");
120 exit(1);
121 }
122 if(id==0)
123 {
124 //先检查是否有重定向
125 if(MODE==CHANGEOUT)
126 {
127 int fd=open(filename,O_WRONLY|O_CREAT|O_TRUNC,0666);
128 dup2(fd,1);
129 }
130 if(MODE==CHANGEIN)
131 {
132 int fd=open(filename,O_RDONLY);
133 dup2(fd,0);
134 }
135 if(MODE==CHANGEAPPEND)
136 {
137 int fd = open(filename,O_WRONLY|O_CREAT|O_APPEND,0666);
138 dup2(fd,1);
139 }
140 //程序替换
141 execvp(argv[0],argv);
142 //如果替换失败则会执行下面的代码
143 perror("execvp failed");
144 exit(1);
145 }
146
147 //父进程等待回收僵尸
148 int status=0;
149 waitpid(id,&status,0);
150 if(WIFEXITED(status))//正常退出
{
152 if(WEXITSTATUS(status))//退出码不为0
153 {
154 printf("运行成功,退出码为 :%d\n", WEXITSTATUS(status));
155 }
156 }
157 else//异常终止
158 {
159 printf("%s\n",strerror(errno));
160 }
161 }
162
163
164 #ifdef _DEBUG_ARGV//测试切割功能
165 char**p=argv;
166 while(*p)
167 printf("%s ",*p++);
168 printf("\n");
169 #endif
170 }
171
172
173 return 0;
174 }