学习一个极简的C语言shell brenns10/lsh,这个库的亮点不是复杂,而是极简,用不到两百行的代码就完成了一个极简的 shell。

源码学习

lsh 这个 shell 的主体是一个 loop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void lsh_loop(void) {
while (1) {
printf("> ");

char *line = lsh_read_line();
char **args = lsh_split_line(line);
int status = lsh_execute(args);

free(line);
free(args);

if (!status) {
break;
}
}
}

int main(int argc, char **argv) {
lsh_loop();
return EXIT_SUCCESS;
}

shell 在每次交互时都依次执行:

  • lsh_read_line 读取输入
  • lsh_split_line 分割输入
  • lsh_execute 执行命令

注意这里在正常情况会返回 1 继续 loop,只有 exit 命令会返回 0 退出 loop。

前两步处理的逻辑都很简单——字符串的处理,只是在 C 语言中需要分配缓冲区等显得复杂。

1
2
3
4
5
6
7
8
9
10
11
12
13
char *lsh_read_line(void) {
char *line = NULL;
size_t bufsize = 0; // have getline allocate a buffer for us
if (getline(&line, &bufsize, stdin) == -1) {
if (feof(stdin)) {
exit(EXIT_SUCCESS); // We received an EOF
} else {
perror("lsh: getline\n");
exit(EXIT_FAILURE);
}
}
return line;
}
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
char **lsh_split_line(char *line) {
int bufsize = 64;
int position = 0;
char **tokens = malloc(bufsize * sizeof(char *));

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

char *token = strtok(line, " \t\r\n\a");
while (token != NULL) {
tokens[position++] = token;
if (position >= bufsize) {
bufsize += 64;

char **tokens_backup = tokens;
tokens = realloc(tokens, bufsize * sizeof(char *));
if (!tokens) {
free(tokens_backup);
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
}

token = strtok(NULL, " \t\r\n\a");
}
tokens[position] = NULL;
return tokens;
}

第三步执行命令才是 shell 的核心

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 lsh_execute(char **args) {
// An empty command was entered.
if (args[0] == NULL) {
return 1;
}

// builtin commands
for (unsigned i = 0; i < sizeof(builtin_str) / sizeof(char *); i++) {
if (strcmp(args[0], builtin_str[i]) == 0) {
return (*builtin_func[i])(args);
}
}

// external commands
pid_t pid = fork();
if (pid == 0) { // Child process
if (execvp(args[0], args) == -1) {
perror("lsh");
}
exit(EXIT_FAILURE);
} else if (pid < 0) { // Error forking
perror("lsh");
} else { // Parent process
int status;
do {
waitpid(pid, &status, WUNTRACED);
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
}

return 1;
}

这里的逻辑很清楚,分成了三步:

  • 处理空输入
  • 处理内置命令
  • 处理外部命令

内置命令部分,lsh 实现了三个内置命令:cd,help,exit。对应的处理直接调用传参即可

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
int lsh_cd(char **args);
int lsh_help(char **args);
int lsh_exit(char **args);

char *builtin_str[] = {"cd", "help", "exit"};

int (*builtin_func[])(char **) = {&lsh_cd, &lsh_help, &lsh_exit};

int lsh_cd(char **args) {
if (args[1] == NULL) {
fprintf(stderr, "lsh: expected argument to \"cd\"\n");
} else {
if (chdir(args[1]) != 0) {
perror("lsh");
}
}
return 1;
}

int lsh_help(char **args) {
printf("LSH (Based on Stephen Brennan's LSH)\n");
printf("Type program names and arguments, and hit enter.\n");
printf("The following are built in:\n");

for (unsigned i = 0; i < sizeof(builtin_str) / sizeof(char *); i++) {
printf(" %s\n", builtin_str[i]);
}

printf("Use the man command for information on other programs.\n");
return 1;
}

int lsh_exit(char **args) { return 0; }

外部命令的处理逻辑最复杂,也是 shell 最核心的部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// external commands
pid_t pid = fork();
if (pid == 0) { // Child process
if (execvp(args[0], args) == -1) {
perror("lsh");
}
exit(EXIT_FAILURE);
} else if (pid < 0) { // Error forking
perror("lsh");
} else { // Parent process
int status;
do {
waitpid(pid, &status, WUNTRACED);
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
}

这里 shell 先 fork() 复制出一个子进程,然后父进程和子进程靠 fork() 的返回值来判断“我是谁”,进入不同分支:

  • 子进程用 execvp() 把自己替换成目标程序;
  • 父进程保留为 shell,并用 waitpid() 等这个程序结束。

源码

源码主要基于 brenns10/lsh,但是进行了一些简化,包括移除注释等。

lsh.c
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int lsh_cd(char **args);
int lsh_help(char **args);
int lsh_exit(char **args);

char *builtin_str[] = {"cd", "help", "exit"};

int (*builtin_func[])(char **) = {&lsh_cd, &lsh_help, &lsh_exit};

int lsh_cd(char **args) {
if (args[1] == NULL) {
fprintf(stderr, "lsh: expected argument to \"cd\"\n");
} else {
if (chdir(args[1]) != 0) {
perror("lsh");
}
}
return 1;
}

int lsh_help(char **args) {
printf("LSH (Based on Stephen Brennan's LSH)\n");
printf("Type program names and arguments, and hit enter.\n");
printf("The following are built in:\n");

for (unsigned i = 0; i < sizeof(builtin_str) / sizeof(char *); i++) {
printf(" %s\n", builtin_str[i]);
}

printf("Use the man command for information on other programs.\n");
return 1;
}

int lsh_exit(char **args) { return 0; }

int lsh_execute(char **args) {
// An empty command was entered.
if (args[0] == NULL) {
return 1;
}

// builtin commands
for (unsigned i = 0; i < sizeof(builtin_str) / sizeof(char *); i++) {
if (strcmp(args[0], builtin_str[i]) == 0) {
return (*builtin_func[i])(args);
}
}

// external commands
pid_t pid = fork();
if (pid == 0) { // Child process
if (execvp(args[0], args) == -1) {
perror("lsh");
}
exit(EXIT_FAILURE);
} else if (pid < 0) { // Error forking
perror("lsh");
} else { // Parent process
int status;
do {
waitpid(pid, &status, WUNTRACED);
} while (!WIFEXITED(status) && !WIFSIGNALED(status));
}

return 1;
}

char *lsh_read_line(void) {
char *line = NULL;
size_t bufsize = 0; // have getline allocate a buffer for us
if (getline(&line, &bufsize, stdin) == -1) {
if (feof(stdin)) {
exit(EXIT_SUCCESS); // We received an EOF
} else {
perror("lsh: getline\n");
exit(EXIT_FAILURE);
}
}
return line;
}

char **lsh_split_line(char *line) {
int bufsize = 64;
int position = 0;
char **tokens = malloc(bufsize * sizeof(char *));

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

char *token = strtok(line, " \t\r\n\a");
while (token != NULL) {
tokens[position++] = token;
if (position >= bufsize) {
bufsize += 64;

char **tokens_backup = tokens;
tokens = realloc(tokens, bufsize * sizeof(char *));
if (!tokens) {
free(tokens_backup);
fprintf(stderr, "lsh: allocation error\n");
exit(EXIT_FAILURE);
}
}

token = strtok(NULL, " \t\r\n\a");
}
tokens[position] = NULL;
return tokens;
}

void lsh_loop(void) {
while (1) {
printf("> ");

char *line = lsh_read_line();
char **args = lsh_split_line(line);
int status = lsh_execute(args);

free(line);
free(args);

if (!status) {
break;
}
}
}

int main(int argc, char **argv) {
lsh_loop();
return EXIT_SUCCESS;
}