二十. 内建shell的实现

路径解析转换

路径有绝对路径和相对路径之分,在平常使用linux的过程中,我们在输入命令和参数的时候,往往使用的都是相对路径。相对路径是基于当前的工作路径的出的。

当前工作路径 + 相对路径 = 绝对路径

比如说当前工作路径为 /home/work 此时输入 ls file,那么file所在的绝对路径就是 /home/work/file 了。

因为这个kernel比较简单,如果路径以根目录/ 开头,就会被认为是绝对路径,其他的路径都会被认为是相对路径,会将其转换成绝对路径来使用。

以下是路径转换函数

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
/* 将路径old_abs_path中的..和.转换为实际路径后存入new_abs_path */
static void wash_path(char *old_abs_path, char *new_abs_path)
{
assert(old_abs_path[0] == '/');
char name[MAX_FILE_NAME_LEN] = {0};
char *sub_path = old_abs_path;
sub_path = path_parse(sub_path, name);
if (name[0] == 0)
{
// 若只键入了"/",直接将"/"存入new_abs_path后返回
new_abs_path[0] = '/';
new_abs_path[1] = 0;
return;
}
new_abs_path[0] = 0; // 避免传给new_abs_path的缓冲区不干净
strcat(new_abs_path, "/");
while (name[0])
{
/* 如果是上一级目录“..” */
if (!strcmp("..", name))
{
char *slash_ptr = strrchr(new_abs_path, '/');
/*如果未到new_abs_path中的顶层目录,就将最右边的'/'替换为0,
这样便去除了new_abs_path中最后一层路径,相当于到了上一级目录 */
if (slash_ptr != new_abs_path)
{ // 如new_abs_path为“/a/b”,".."之后则变为“/a”
*slash_ptr = 0;
}
else
{ // 如new_abs_path为"/a",".."之后则变为"/"
/* 若new_abs_path中只有1个'/',即表示已经到了顶层目录,就将下一个字符置为结束符0. */
*(slash_ptr + 1) = 0;
}
}
else if (strcmp(".", name))
{ // 如果路径不是‘.’,就将name拼接到new_abs_path
if (strcmp(new_abs_path, "/"))
{ // 如果new_abs_path不是"/",就拼接一个"/",此处的判断是为了避免路径开头变成这样"//"
strcat(new_abs_path, "/");
}
strcat(new_abs_path, name);
} // 若name为当前目录".",无须处理new_abs_path

/* 继续遍历下一层路径 */
memset(name, 0, MAX_FILE_NAME_LEN);
if (sub_path)
{
sub_path = path_parse(sub_path, name);
}
}
}

/* 将path处理成不含..和.的绝对路径,存储在final_path */
void make_clear_abs_path(char *path, char *final_path)
{
char abs_path[MAX_PATH_LEN] = {0};
/* 先判断是否输入的是绝对路径 */
if (path[0] != '/')
{
// 若输入的不是绝对路径,就拼接成绝对路径
memset(abs_path, 0, MAX_PATH_LEN);
if (getcwd(abs_path, MAX_PATH_LEN) != NULL)
{
if (!((abs_path[0] == '/') && (abs_path[1] == 0)))
{ // 若abs_path表示的当前目录不是根目录/
strcat(abs_path, "/");
}
}
}
strcat(abs_path, path);
wash_path(abs_path, final_path);
}

路径转换完成之后接下来就来实现几个常用的shell。

ls

ls的命令支持以下形式 ls -l, ls -h, ls filename,它们的功能自不必多说,函数的前半部分对其参数进行解析,后半部分根据参数对应的功能构造好路径,将对应的信息输出出来。

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
/* ls命令的内建函数 */
void buildin_ls(uint32_t argc, char **argv)
{
char *pathname = NULL;
struct stat file_stat;
memset(&file_stat, 0, sizeof(struct stat));
bool long_info = false;
uint32_t arg_path_nr = 0;
uint32_t arg_idx = 1; // 跨过argv[0],argv[0]是字符串“ls”
while (arg_idx < argc)
{
if (argv[arg_idx][0] == '-')
{ // 如果是选项,单词的首字符是-
if (!strcmp("-l", argv[arg_idx]))
{ // 如果是参数-l
long_info = true;
}
else if (!strcmp("-h", argv[arg_idx]))
{ // 参数-h
printf("usage: -l list all infomation about the file.\n-h for help\nlist all files in the current dirctory if no option\n");
return;
}
else
{ // 只支持-h -l两个选项
printf("ls: invalid option %s\nTry `ls -h' for more information.\n", argv[arg_idx]);
return;
}
}
else
{ // ls的路径参数
if (arg_path_nr == 0)
{
pathname = argv[arg_idx];
arg_path_nr = 1;
}
else
{
printf("ls: only support one path\n");
return;
}
}
arg_idx++;
}

if (pathname == NULL)
{ // 若只输入了ls 或 ls -l,没有输入操作路径,默认以当前路径的绝对路径为参数.
if (NULL != getcwd(final_path, MAX_PATH_LEN))
{
pathname = final_path;
}
else
{
printf("ls: getcwd for default path failed\n");
return;
}
}
else
{
make_clear_abs_path(pathname, final_path);
pathname = final_path;
}

if (stat(pathname, &file_stat) == -1)
{
printf("ls: cannot access %s: No such file or directory\n", pathname);
return;
}
if (file_stat.st_filetype == FT_DIRECTORY)
{
struct dir *dir = opendir(pathname);
struct dir_entry *dir_e = NULL;
char sub_pathname[MAX_PATH_LEN] = {0};
uint32_t pathname_len = strlen(pathname);
uint32_t last_char_idx = pathname_len - 1;
memcpy(sub_pathname, pathname, pathname_len);
if (sub_pathname[last_char_idx] != '/')
{
sub_pathname[pathname_len] = '/';
pathname_len++;
}
rewinddir(dir);
if (long_info)
{
char ftype;
printf("total: %d\n", file_stat.st_size);
while ((dir_e = readdir(dir)))
{
ftype = 'd';
if (dir_e->f_type == FT_REGULAR)
{
ftype = '-';
}
sub_pathname[pathname_len] = 0;
strcat(sub_pathname, dir_e->filename);
memset(&file_stat, 0, sizeof(struct stat));
if (stat(sub_pathname, &file_stat) == -1)
{
printf("ls: cannot access %s: No such file or directory\n", dir_e->filename);
return;
}
printf("%c %d %d %s\n", ftype, dir_e->i_no, file_stat.st_size, dir_e->filename);
}
}
else
{
while ((dir_e = readdir(dir)))
{
printf("%s ", dir_e->filename);
}
printf("\n");
}
closedir(dir);
}
else
{
if (long_info)
{
printf("- %d %d %s\n", file_stat.st_ino, file_stat.st_size, pathname);
}
else
{
printf("%s\n", pathname);
}
}
}

cd

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
/* cd命令的内建函数 */
char *buildin_cd(uint32_t argc, char **argv)
{
if (argc > 2)
{
printf("cd: only support 1 argument!\n");
return NULL;
}

/* 若是只键入cd而无参数,直接返回到根目录. */
if (argc == 1)
{
final_path[0] = '/';
final_path[1] = 0;
}
else
{
make_clear_abs_path(argv[1], final_path);
}

if (chdir(final_path) == -1)
{
printf("cd: no such directory %s\n", final_path);
return NULL;
}
return final_path;
}

mkdir

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
/* mkdir命令内建函数 */
int32_t buildin_mkdir(uint32_t argc, char **argv)
{
int32_t ret = -1;
if (argc != 2)
{
printf("mkdir: only support 1 argument!\n");
}
else
{
make_clear_abs_path(argv[1], final_path);
/* 若创建的不是根目录 */
if (strcmp("/", final_path))
{
if (mkdir(final_path) == 0)
{
ret = 0;
}
else
{
printf("mkdir: create directory %s failed.\n", argv[1]);
}
}
}
return ret;
}

rmdir

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
/* rmdir命令内建函数 */
int32_t buildin_rmdir(uint32_t argc, char **argv)
{
int32_t ret = -1;
if (argc != 2)
{
printf("rmdir: only support 1 argument!\n");
}
else
{
make_clear_abs_path(argv[1], final_path);
/* 若删除的不是根目录 */
if (strcmp("/", final_path))
{
if (rmdir(final_path) == 0)
{
ret = 0;
}
else
{
printf("rmdir: remove %s failed.\n", argv[1]);
}
}
}
return ret;
}

rm

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
/* rm命令内建函数 */
int32_t buildin_rm(uint32_t argc, char **argv)
{
int32_t ret = -1;
if (argc != 2)
{
printf("rm: only support 1 argument!\n");
}
else
{
make_clear_abs_path(argv[1], final_path);
/* 若删除的不是根目录 */
if (strcmp("/", final_path))
{
if (unlink(final_path) == 0)
{
ret = 0;
}
else
{
printf("rm: delete %s failed.\n", argv[1]);
}
}
}
return ret;
}

ps

1
2
3
4
5
6
7
8
9
10
/* ps命令内建函数 */
void buildin_ps(uint32_t argc, char **argv UNUSED)
{
if (argc != 1)
{
printf("ps: no argument support!\n");
return;
}
ps();
}

clear

1
2
3
4
5
6
7
8
9
10
/* clear命令内建函数 */
void buildin_clear(uint32_t argc, char **argv UNUSED)
{
if (argc != 1)
{
printf("clear: no argument support!\n");
return;
}
clear();
}

pwd

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
/* pwd命令的内建函数 */
void buildin_pwd(uint32_t argc, char **argv UNUSED)
{
if (argc != 1)
{
printf("pwd: no argument support!\n");
return;
}
else
{
if (NULL != getcwd(final_path, MAX_PATH_LEN))
{
printf("%s\n", final_path);
}
else
{
printf("pwd: get current work directory failed.\n");
}
}
}

上面的很多对文件和目录操作的命令都是在文件系统中实现的,这里只是对其进一步的封装。

接下来就要为这些命令提供统一的调用接口,也就是shell拉。

shell

shell在执行的时候肯定要对用户的输入进行解析,根据解析结果找到对应的处理程序,然后调用执行,首先上命令解析函数

该函数主要是对命令进行拆分,根据其拆分标志,一般是空格,将一个个分割好的参数存入argv中。

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
/* 分析字符串cmd_str中以token为分隔符的单词,将各单词的指针存入argv数组 */
static int32_t cmd_parse(char *cmd_str, char **argv, char token)
{
assert(cmd_str != NULL);
int32_t arg_idx = 0;
while (arg_idx < MAX_ARG_NR)
{
argv[arg_idx] = NULL;
arg_idx++;
}
char *next = cmd_str;
int32_t argc = 0;
/* 外层循环处理整个命令行 */
while (*next)
{
/* 去除命令字或参数之间的空格 */
while (*next == token)
{
next++;
}
/* 处理最后一个参数后接空格的情况,如"ls dir2 " */
if (*next == 0)
{
break;
}
argv[argc] = next;

/* 内层循环处理命令行中的每个命令字及参数 */
while (*next && *next != token)
{ // 在字符串结束前找单词分隔符
next++;
}

/* 如果未结束(是token字符),使tocken变成0 */
if (*next)
{
*next++ = 0; // 将token字符替换为字符串结束符0,做为一个单词的结束,并将字符指针next指向下一个字符
}

/* 避免argv数组访问越界,参数过多则返回0 */
if (argc > MAX_ARG_NR)
{
return -1;
}
argc++;
}
return argc;
}

接下来就上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
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
void my_shell(void)
{
cwd_cache[0] = '/';
while (1)
{
print_prompt();
memset(final_path, 0, MAX_PATH_LEN);
memset(cmd_line, 0, MAX_PATH_LEN);
readline(cmd_line, MAX_PATH_LEN);
if (cmd_line[0] == 0)
{
// 若只键入了一个回车
continue;
}
argc = -1;
argc = cmd_parse(cmd_line, argv, ' ');
if (argc == -1)
{
printf("num of arguments exceed %d\n", MAX_ARG_NR);
continue;
}
if (!strcmp("ls", argv[0]))
{
buildin_ls(argc, argv);
}
else if (!strcmp("cd", argv[0]))
{
if (buildin_cd(argc, argv) != NULL)
{
memset(cwd_cache, 0, MAX_PATH_LEN);
strcpy(cwd_cache, final_path);
}
}
else if (!strcmp("pwd", argv[0]))
{
buildin_pwd(argc, argv);
}
else if (!strcmp("ps", argv[0]))
{
buildin_ps(argc, argv);
}
else if (!strcmp("clear", argv[0]))
{
buildin_clear(argc, argv);
}
else if (!strcmp("mkdir", argv[0]))
{
buildin_mkdir(argc, argv);
}
else if (!strcmp("rmdir", argv[0]))
{
buildin_rmdir(argc, argv);
}
else if (!strcmp("rm", argv[0]))
{
buildin_rm(argc, argv);
}
else
{
printf("external command\n");
}
}
}

在上一节中说了,执行这个shell的是init进程fork出的子进程,所以要将这个myshell函数放入init进程中,当fork返回值为0时,就执行myshell

1
2
3
4
5
6
7
8
9
10
11
12
13
void init()
{
pid_t pid = fork();

if(fork)
{
// ...
}
else (fork == 0)
{
myshell();
}
}

这就是init进程目前所做的工作,它是在内核主线程创建之前就已经创建好了的进程。

接下来就运行一下,享受一下成果。

mark

可以看到确实能进行一定程度上的交互了,但是目前的命令都是内部命令,写死在程序中的,这样想新增命令时非常不方便,所以后面要实现加载用户程序,让shell支持外部命令。