如何用 C 语言编写一个 Shell

如何用 C 语言编写一个 Shell

Shell 具体指的是什么?

Shell,或者说 Unix shell,本质上是一个命令行解释器。它是一个计算机程序,允许用户通过命令行与操作系统交互。Shell 有两种界面形式:命令行界面 (CLI) 和图形用户界面 (GUI),例如 Windows 中的文件浏览器。常见的 CLI 有 shzshbash 等,其中,sh 是第一个流行的 shell,bash 是 Linux 系统自带的 shell,而 zsh 则是最受欢迎的 shell 之一。

当你登录到一个类 Unix 系统时,shell 会自动在这个会话中运行。你也可以输入以下命令查看当前 shell 和系统内置的 shell 列表:

1
2
3
4
5
6
7
8
9
10
vagrant@ubuntu-xenial:~$ echo $SHELL
/bin/bash
vagrant@ubuntu-xenial:~$ cat /etc/shells
# /etc/shells: valid login shells
/bin/sh
/bin/dash
/bin/bash
/bin/rbash
/usr/bin/tmux
/usr/bin/screen

Shell 的生命周期

作为一个程序,shell 也有自己的生命周期:

  • 初始化: 读取与该 shell 配套的配置文件,例如.zshrc.condarc 等。
  • 解释: 循环读取命令行并执行命令。
  • 终止: 检测到异常或者接收到终止命令,释放资源然后退出。

如何编写一个 Shell?

实现难点

Shell 并不负责命令行的真正执行,而是通过 execvp 系统调用来执行命令。根据手册,execvp 是一个更加友好的 exec 方法,其中 v 表示支持一组可变参数,p 表示无需提供目标程序的完整路径。

手册描述截图

虽然 shell 像是一个搬运工,但在执行某些特殊命令时,仍然需要单独处理。例如 cd 命令,它允许你更改当前目录。然而,“当前目录” 是一个进程的属性,如果让子程序执行,shell 的 “当前目录” 不会发生变化。同理,还有 exit 命令。

代码细节

初步框架:

1
2
3
4
5
6
int main() {
ZionSH *zionSh = new ZionSH();
zionSh->initialize();
int terminate = zionSh->interpret();
return terminate;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
void ZionSH::initialize() {
std::cout << "Zion Shell initializing..." << std::endl;
}

int ZionSH::interpret() {
char *line;
char **args;
int status;
do {
std::cout << "ZionSh>";
line = read_line();
args = split_line(line);
status = execute(args);
// 释放存储空间
free(line);
free(args);
} while (status);
}

在执行 exec 时,它会用目标程序替换当前程序,除非发生错误。因此,我们需要用 fork 函数生成子程序,并通过得到的进程 ID 判断是子进程还是父进程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int ZionSH::execute(char **args) {
int status;
pid_t pid = fork();
if (pid == 0) {
if (execvp(args[0], args) == -1) {
fprintf(stderr, "Fail to execute the input command.\n");
return EXIT_FAILURE;
}
} else if (pid < 0) {
fprintf(stderr, "Fail to fork.\n");
return EXIT_FAILURE;
} else {
do {
waitpid(pid, &status, WUNTRACED);
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
}
return 0;
}

主进程需要等待子进程运行结束后再退出,这里用到的 waitpidWIFEXITEDWIFSIGNALED 可以通过手册查询。

需要参考的指令如下:

代码实现

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
#include "../headers/zionSh.h"

int cd(char **args);
int help(char **args);
int exit(char **args);

int (*sp_func[]) (char **) = {
&cd,
&help,
&exit
};

std::string sp_func_name[] = {
"cd",
"help",
"exit"
};

void ZionSH::initialize() {
std::cout << "Zion Shell initializing..." << std::endl;
}

int ZionSH::interpret() {
char *line;
char **args;
do {
std::cout << "ZionSh>";
line = read_line();
args = split_line(line);
int status = -1;
for (int i = 0; i < num_sp_func(); i++) {
if (sp_func_name[i] == args[0]) {
status = (*sp_func[i])(args);
if (status == 0) {
return EXIT_SUCCESS;
}
}
}
if (status == -1) {
status = execute(args);
if (status == EXIT_FAILURE) {
return EXIT_FAILURE;
}
}
// 释放存储空间
free(line);
free(args);
} while (true);
}

char *ZionSH::read_line() {
int curr = 0;
int buffSize = BUFF_SIZE;
char *line = static_cast<char *>(malloc(buffSize * sizeof(char)));
int c;

if (!line) {
fprintf(stderr, "allocation error!");
exit(EXIT_FAILURE);
}

while (true) {
c = getchar();
if (c == EOF || c == '\n') {
line[curr] = '\0';
return line;
}
line[curr] = (char) c;
curr++;
if (curr >= buffSize) {
buffSize += BUFF_SIZE;
void *newBuffer = realloc(line, buffSize);
if (!newBuffer) {
fprintf(stderr, "reallocation error!");
exit(EXIT_FAILURE);
}
line = static_cast<char *>(newBuffer);
}
}
}

char **ZionSH::split_line(char *line) {
char **args = static_cast<char **>(malloc(ARG_SIZE * sizeof(char *)));
if (!args) {
fprintf(stderr, "Fail to allocate.\n");
exit(EXIT_FAILURE);
}
char *token;
int num_args = 0;
while (true) {
token = strtok(line, TOK_DELIM);
if (token == nullptr) break;
args[num_args] = token;
num_args++;
if (num_args % ARG_SIZE == 0) {
char **ex_args = static_cast<char **>(realloc(args, (num_args + ARG_SIZE) * sizeof(char *)));
args = ex_args;
if (!args) {
fprintf(stderr, "Fail to reallocate.\n");
exit(EXIT_FAILURE);
}
}
}
args[num_args] = nullptr;
return args;
}

int ZionSH::execute(char **args) {
int status;
pid_t pid = fork();
if (pid == 0) {
if (execvp(args[0], args) == -1) {
fprintf(stderr, "Fail to execute the input command.\n");
return EXIT_FAILURE;
}
} else if (pid < 0) {
fprintf(stderr, "Fail to fork.\n");
return EXIT_FAILURE;
} else {
do {
waitpid(pid, &status, WUNTRACED);
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
}
return 0;
}

int cd(char **args) {
if (args[1] == nullptr) {
fprintf(stderr, "No path for cd\n");
} else if (chdir(args[1]) == -1) {
fprintf(stderr, "Error change dir: %s\n", strerror(errno));
}
return 1;
}

int help(char **args) {
std::cout << "help statements.";
return 1;
}

int exit(char **args) {
return EXIT_SUCCESS;
}

int ZionSH::num_sp_func() {
return sizeof sp_func_name / sizeof(std::string);
}

一个基本的 shell 程序框架就实现好了,它能够初始化、读取和解释命令行输入,并通过 forkexecvp 系统调用来执行命令。此外,它还包含了几个特殊命令的处理,如 cdexit

作者

zion h4

发布于

2024-07-14

更新于

2024-09-08

许可协议

评论