自己写一个shell

正好最近看了csapp,然后正好也借着这个机会补一补C语言,并且加深下自己的理解,于是开始动手自己写一个shell。

总体架构

总体架构其实非常非常简单,就是一个死循环,不停处理用户的输入,除非用户退出。所以在bash中,整体架构是这样子的:

  • 初始化阶段。shell也会读取自己的配置文件,然后进行初始化。
  • 解释阶段。shell会从标准输入、文件等地方获取命令并且执行。
  • 终止阶段。shell执行关闭命令,并且释放内存,终止。

首先肯定从最简单的入手,那么只需要中间的解释阶段即可。

循环的实现

再次进行具体化,在循环里不停读取用户的输入,然后解析用户的输入,将其解析成对应的参数,最后执行就可以了,由此可以抽象出三个函数:

  • csh_read_line():获取用户的输入,其实有对应的c语言的封装可以非常容易获取用户输入的一行,不过这里打算还是老老实实写。
  • csh_split_line(line):将用户的输入解析成对应的args的数组。
  • csh_execute(args):真正执行用户的命令。

获取用户输入

由于不确定用户输入的一行到底有多长,所以我们需要使用malloc来动态分配内存。并且还需要注意,因为读到文件末尾之后会返回一个EOF,所以需要用int来接受(这点我不理解)。

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
char *csh_read_line()
{
// 默认buf大小
int bufsize = BUFSIZE;
// 游标位置
int position = 0;
// 开辟空间
char *buffer = malloc(sizeof(char) * bufsize);
int c;

if (!buffer)
{
fprintf(stderr, "csh: allocation error\n");
exit(EXIT_FAILURE);
}

while (1)
{
// 读取一个字符
c = getchar();

// 如果c是EOF,就是用户输入了ctrl+d,那么就需要把最后一个置位成'\0'
if (c == EOF || c == '\n')
{
buffer[position] = '\0';
return buffer;
}
else
{
buffer[position] = c;
}
position++;

// 如果到了最后,那么就需要扩容,这里简单就每一次扩容1024字节
if (position >= bufsize)
{
bufsize += BUFSIZE;
buffer = realloc(buffer, bufsize);
if (!buffer)
{
fprintf(stderr, "csh: allocation error\n");
exit(EXIT_FAILURE);
}
}
}
}

其实上面的这么多,用标准库的一个函数就可以解决了。

解析用户的输入

这一步为了简化,就是直接利用中间的空格作为分隔符来进行分割,而且这个简单的shell并不支持引号,所以如果是echo 'a b'这条命令,那么在正常的shell里面是会输出a b的,但是在我们的shell里,会输出'ab'

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
char **csh_split_line(char *line)
{
int bufsize = BUFSIZE;
int position = 0;
// 参数数组
char **tokens = malloc(bufsize * sizeof(char *));

// 单个参数
char *token;

if (!tokens)
{
fprintf(stderr, "csh: allocation error\n");
exit(EXIT_FAILURE);
}

token = strtok(line, DELIM);
while (token != NULL)
{
tokens[position] = token;
position++;

if (position >= bufsize)
{
bufsize += BUFSIZE;
tokens = realloc(tokens, bufsize * sizeof(char *));
if (!tokens)
{
fprintf(stderr, "csh: allocation error\n");
exit(EXIT_FAILURE);
}
}

token = strtok(NULL, DELIM);
}
// 最后一个为NULL
tokens[position] = NULL;
return tokens;
}

其实核心就是strtok这个函数,就是利用这个获取到参数的。

执行用户的命令

这一部分反而是最简单的,因为它只需要利用fork创建一个子进程,然后利用exec对应的函数进行处理就可以了。

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
int csh_launch(char **args)
{
pid_t pid, wpid;
int status;

pid = fork();
if (pid == 0)
{
// 子进程
if (execvp(args[0], args) == -1)
{
perror("csh");
}
exit(EXIT_FAILURE);
}
else if (pid < 0)
{
// 错误处理
perror("csh");
}
else
{
// 父进程
do
{
wpid = waitpid(pid, &status, WUNTRACED);
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
}

return 1;
}

OK,到这里一个最简单的shell就写完了。

CSAPP Lab