UNIX Shell programming

一个简单的shell编写

在本项目中,我们编写了一个简单的壳程序,它只有执行命令,执行历史命令,使用管道,输出输入流定向等基本功能。但是它的编写却并不简单,我们现在来剖析一下。

一.总目标

用户会输入一个字符串,我们的目标就是解析这个字符串(以空格划分)为命令参数,存到一个二维char类型的数组args中,其中args[0]是命令,其余是参数。
之后我们把它丢到 execvp函数里,这个函数长这样:

1
execvp(args[0],args);

然后我们的操作系统就会自动调用这个接口来搞事情了。
但是在这之中我们还需要处理其他特殊情况:比如有没有管道符号|,命令是否是!!,有没有重定向?有没有允许并发的&&符号?

二.分步实现

1.输入

首先我们要解决输入的解码问题。我们自己写了一个decode函数解码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
void decode(char *command, char **args) {
for (int i = 0; i <= MAX_LINE / 2 + 1; i++)
args[i] = NULL;
int cutIndex = 0, order_index = 0, inner_index = 0;
char ch = command[cutIndex];
while (ch != '\n' && ch != '\0' && ch != EOF) {
free(args[order_index]);
args[order_index] = (char *) malloc(sizeof(char) * MAX_LINE);
memset(args[order_index], '\0', sizeof(args[order_index]));
//for each block
inner_index = 0;
while (ch != ' ' && ch != '\n' && ch != '\0' && ch != EOF) {
args[order_index][inner_index] = ch;
++cutIndex;
++inner_index;
ch = command[cutIndex];
}
//写入'\0'
args[order_index][inner_index] = '\0';//我就是个智障,这个地方花了至少4h
//now comes the kongge
if (ch == ' ') {
inner_index = 0;
++order_index;
while (ch == ' ') {
++cutIndex;
ch = command[cutIndex];
}
}
}
}

实现很繁,但是并不难,用malloc分配空间后**一定要注意在后面补’\0’**,血的教训。你会注意到C其实很蠢,什么基本功能都要自己实现,试想如果你有python的split()函数……

2.历史记录查询

当用户输入!!时,我们要从历史的记录中寻找,如果历史没有,那么报错。history就是一个存历史命令的二维数组。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void add_to_hist(char *command, char *history[]) {
for (int i = 0; i < HIST_SIZE; i++) {
if (history[i] == NULL) {//找到一个空的
history[i] = (char *) malloc(sizeof(char) * MAX_LINE);
strcpy(history[i], command);
return;
}
}
free(history[0]);
history[0] = NULL;
for (int i = 1; i < HIST_SIZE; i++) {
history[i - 1] = history[i];
}
history[HIST_SIZE] = command;
return;
}

还是想吐槽C,连个队列都没有,我自己又懒得打一个,只能这样傻乎乎的实现了。

3.管道实现

关键来了,注意看
如何处理管道问题呢?我们会先写一个函数找出管道符号|的位置,在我这叫get_pipe_pos(),这个函数如果没有管道符号时会返回-1。这个时候的处理流程是:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
pid_t pid;
pid = fork();
if(pid==0)//child process
{
should_run=0;
output_redirect(args);
input_redirect(args);
execvp(args[0],args);

}
else//parent process
{
if(!will_wait)
{
wait(NULL);
}
will_wait=0;
}

}

直接开到子进程里面去,同时父进程看是不是要根据&&来执行等待。引用恐龙书的话,就是:

II. Executing Command in a Child Process

The first task is to modify the main() function in Figure 3.36 so that a child process is forked and executes the command specified by the user. This will require parsing what the user has entered into separate tokens and storing the tokens in an array of character strings (args in Figure 3.36). For example, if the user enters the command ps -ael at the osh> prompt, the values stored in the args array are:
args[0] = “ps”
args[1] = “-ael”
args[2] = NULL
This args array will be passed to the execvp() function, which has the
following prototype:

execvp(char *command, char *params[])

Here, command represents the command to be performed and params stores the
parameters to this command. For this project, the execvp() function should
be invoked as execvp(args[0], args). Be sure to check whether the user included & to determine whether or not the parent process is to wait for the child to exit.

有管道呢?

我们的策略是再开一个子进程让他们在管道中通信,其中平凡实现略,比较值得一提的是dup2()函数,它将现有的文件描述符复制到另一个文件描述符。这个函数在处理重定向的时候也会用到,官方解释:

This child will also create another child process (which will execute less) and will establish a pipe between itself and the child process it creates. Implementing pipe functionality will also require using the dup2() function .

代码比较长,一并附于文末。

4.数据流重定向

其实这个也用到了dup2()函数,其实就是把输出的对象改到了一个文件中,并且把输入的对象改为了一个文件而已:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//这是输入流定向
char filename[10];
strcpy(filename,args[i+1]);//之前得到具体参数的位置并存到filename
int fd = open(filename,O_RDWR|O_NOCTTY|O_NDELAY);//Linux系统的打开管道其实就是读文件
free(args[i]);//释放'<'的那一块
args[i]=NULL;
free(args[i+1]);
args[i+1]=NULL;
dup2(fd,STDIN_FILENO);//将文件作为输入
char *order_from_file;
order_from_file=(char*)malloc(sizeof(char)*MAX_LINE);
fgets(order_from_file,MAX_LINE,stdin);//输入到这个字符串再解码
decode(order_from_file,args+i);

free(order_from_file);
close(fd);

再来看看输出流定向:

1
2
3
4
5
6
7
8
9
10
char filename[10];
strcpy(filename,args[i+1]);//获取文件名

int fd = open(filename,O_RDWR|O_NOCTTY|O_NDELAY);
dup2(fd,STDOUT_FILENO);//跟之前的STDIN不同,这里作为输出了。
close(fd);
free(args[i]);
args[i]=NULL;
free(args[i+1]);
args[i+1]=NULL;//一定要清理空间啊

三.完整代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <sys/types.h>

#define MAX_LINE 80
#define HIST_SIZE 10

void decode(char *command, char **args) {
for (int i = 0; i <= MAX_LINE / 2 + 1; i++)
args[i] = NULL;
int cutIndex = 0, order_index = 0, inner_index = 0;
char ch = command[cutIndex];
while (ch != '\n' && ch != '\0' && ch != EOF) {
free(args[order_index]);
args[order_index] = (char *) malloc(sizeof(char) * MAX_LINE);
memset(args[order_index], '\0', sizeof(args[order_index]));
//for each block
inner_index = 0;
while (ch != ' ' && ch != '\n' && ch != '\0' && ch != EOF) {
args[order_index][inner_index] = ch;
++cutIndex;
++inner_index;
ch = command[cutIndex];
}
//写入'\0'
args[order_index][inner_index] = '\0';//我就是个智障,这个地方花了至少4h
//now comes the kongge
if (ch == ' ') {
inner_index = 0;
++order_index;
while (ch == ' ') {
++cutIndex;
ch = command[cutIndex];
}
}
}
}

void add_to_hist(char *command, char *history[]) {
for (int i = 0; i < HIST_SIZE; i++) {
if (history[i] == NULL) {//找到一个空的
history[i] = (char *) malloc(sizeof(char) * MAX_LINE);
strcpy(history[i], command);
return;
}
}
free(history[0]);
history[0] = NULL;
for (int i = 1; i < HIST_SIZE; i++) {
history[i - 1] = history[i];
}
history[HIST_SIZE] = command;
return;
}

char *find_history(char **history) {
for (int i = HIST_SIZE - 1; i >= 0; --i) {
if (history[i] != NULL)
return history[i];
}
return NULL;
}

int get_pipe_pos(char **args) {
for (int i = 0; args[i] != NULL; i++) {
if (!strcmp(args[i], "|"))//找到一个管道
{
return i;
}
}
//没找到
return -1;
}
void output_redirect(char**args)
{
for(int i=0;i<MAX_LINE/2+1&&args[i]!=NULL;i++)
{
if(!strcmp(args[i],">"))//>exists
{
char filename[10];
strcpy(filename,args[i+1]);

int fd = open(filename,O_RDWR|O_NOCTTY|O_NDELAY);
dup2(fd,STDOUT_FILENO);
close(fd);
free(args[i]);
args[i]=NULL;
free(args[i+1]);
args[i+1]=NULL;
}
}
}
void input_redirect(char **args)
{
for(int i=0;i<MAX_LINE/2+1&&args[i]!=NULL;i++)
{
if(!strcmp(args[i],"<"))
{
char filename[10];
strcpy(filename,args[i+1]);

int fd = open(filename,O_RDWR|O_NOCTTY|O_NDELAY);
free(args[i]);
args[i]=NULL;
free(args[i+1]);
args[i+1]=NULL;
dup2(fd,STDIN_FILENO);
char *order_from_file;
order_from_file=(char*)malloc(sizeof(char)*MAX_LINE);
fgets(order_from_file,MAX_LINE,stdin);
decode(order_from_file,args+i);

free(order_from_file);
close(fd);
}

}
}
int will_wait = 0;

int main(void) {
char *args[MAX_LINE / 2 + 1]; /* command line (of 80) has max of 40 arguments */
char *history[HIST_SIZE];
for (int i = 0; i < HIST_SIZE; i++)history[i] = NULL;

int should_run = 1;

while (should_run) {
will_wait = 0;
printf("@_@>");
fflush(stdout);
//read the order as a string
char *cutorder;
cutorder = (char *) malloc(sizeof(char) * MAX_LINE);
fgets(cutorder, MAX_LINE, stdin);
//处理!!的情况
if (cutorder[0] == '!' && cutorder[1] == '!')//!!
{
free(cutorder);
cutorder = find_history(history);
if (cutorder == NULL) {
printf("No command in history! What the hell are you doing?\n ");
continue;
}
} else //not !!
add_to_hist(cutorder, history);
//decode the order
decode(cutorder, args);
free(cutorder);
/**
* After reading user input, the steps are:
* (1) fork a child process
* (2) the child process will invoke execvp()
* (3) if command includes &, parent and child will run concurrently
*/
//dealing with exit
if (args[0] && !strcmp(args[0], "exit")) {
should_run = 0;
continue;
}
//now we have a complete args
for (int i = 0; args[i] != NULL; i++) {
if (strcmp(args[i], "&&") == 0 && args[i + 1] == NULL)//with && at the end
{
will_wait = 1;
free(args[i]);
args[i] = NULL;
}
}
//dealing with pipes
//管道的一般用法是,进程在使用fork函数创建子进程前先创建一个管道,该管道用于在父子进程间通信,然后创建子进程,
//之后父进程关闭管道的读端,子进程关闭管道的写端。父进程负责向管道写数据而子进程负责读数据。当然父进程可以关闭管道的写端而子进程关闭管道的读端。
int pipe_pos= get_pipe_pos(args);
if(pipe_pos!=-1)//has pipes
{
pid_t pid;
pid=fork();
if(pid==0)//child process_1
{
int fd[2];//管道端口
pid_t subpid;
//create pipe
if(pipe(fd)==-1)//fail to create pipe
{
fprintf(stderr,"HaHaHa your pipe failed");
return 1;
}
subpid=fork();

if(subpid>0)//that is child process_1
{
for(int i=pipe_pos;args[i]!=NULL&&i<MAX_LINE/2+1;i++)
{
free(args[i]);
args[i]=NULL;
}//the params after pipe_pos is useless
close(fd[0]);
dup2(fd[1],STDOUT_FILENO);
execvp(args[0],args);
}
else if(subpid==0)//child of childprocess1
{
int num_param=0;
for(int i=pipe_pos;i+1<MAX_LINE/2+1&&args[i+1]!=NULL;i++)
{
num_param++;
strcpy(args[i-pipe_pos],args[i+1]);
}
for(int i=num_param;i<MAX_LINE/2+1&&args[i]!=NULL;i++)
{
free(args[i]);
args[i]=NULL;
}
close(fd[1]);
dup2(fd[0],STDIN_FILENO);
execvp(args[0],args);
}

}
else//big father process
{
wait(NULL);
}
}
else
{
pid_t pid;
pid = fork();
if(pid==0)//child process
{
should_run=0;
output_redirect(args);
input_redirect(args);
execvp(args[0],args);

}
else//parent process
{
if(!will_wait)
{
wait(NULL);
}
will_wait=0;
}

}
}
return 0;
}



注:这个project还有一个简单的模块编写,就不记载在这里了。