Challenge-Shell 实验报告

实现不带 .b 后缀指令

int spawn(char *prog, char **argv) 中对 prog 特判,检查是否为 .b 结尾,如果不是则增加后缀。

1
2
3
4
5
6
7
8
char prog_with_dotb[1024];
strcpy(prog_with_dotb, prog);
int len = strlen(prog_with_dotb);
if (!(prog_with_dotb[len-1]=='b' && prog_with_dotb[len-2]=='.')) {
prog_with_dotb[len]='.';
prog_with_dotb[len+1]='b';
prog_with_dotb[len+2]=0;
}

实现指令条件执行

获取进程返回值

void libmain(int argc, char **argv) 中获取 main 的返回值并通过 ipc 发送给父进程(runcmd 函数进程),然后再通过 ipc 从父进程发送给 shell 主进程(sh.c 的 main 函数进程),从而获取单条命令的返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void libmain(int argc, char **argv) {
// set env to point at our env structure in envs[].
env = &envs[ENVX(syscall_getenvid())];
// call user main routine
int res = main(argc, argv);
ipc_send(env->env_parent_id, res, 0, 0);
// exit gracefully
exit();
}

int runcmd(char *s) {
// codes
int child = spawn(argv[0], argv);
if (child >= 0) {
res = ipc_recv(0, 0, 0);
wait(child);
}
// codes
}

实现条件控制

首先在读入命令的时候进行预处理,将 ||&& 用特殊字符标记。

1
2
3
4
5
for (int i=0;buf[i];i++)
if (buf[i]=='|' && buf[i+1]=='|')
(buf[i]=1), buf[i+1]=' ';
else if (buf[i] == '&' && buf[i+1]=='&')
(buf[i]=2), buf[i+1]=' ';

然后扫描字符串,显然字符串被每个特殊字符分隔成了多个命令,单独执行并获取返回值,然后决定是否执行下一条命令即可。注意末尾的最后一条命令。

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
for (int i=0, j=0;i<=len;i++) {
if (buf[i]==1 || buf[i]==2 || i==len || buf[i]==3) {
// 取出单个指令
char command[1024];
int k=0;
for (;j<i;j++)
command[k++]=buf[j];
command[k]=0;
j++;
// 执行并获取返回值
if ((r = fork()) < 0) {
user_panic("fork: %d", r);
}
if (r == 0) {
ipc_send(env->env_parent_id, runcmd(command), 0, 0);
exit();
} else {
res = ipc_recv(0, 0, 0);
wait(r);
}
// 条件控制
if (res == 0) {
if (buf[i] == 1) {
while (buf[i] != 2 && buf[i])
i++;
if (i==len) {
break;
} j=i+1;
}
} else if (buf[i] == 2) {
break;
}
}
}

实现更多指令

touch 指令

仿照 sh.c 的 parsecmd 函数中对于 > 的处理即可。

sh.c 中的代码如下,注意课程组的原始实现是错误的,需要在 open 函数(第 6 行)的参数里加上 O_TRUNC。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
case '>': {
if (gettoken(0, &t) != 'w') {
debugf("syntax error: > not followed by word\n");
exit();
}
if ((fd = open(t, O_WRONLY | O_CREAT | O_TRUNC)) < 0) {
debugf("err when open %s for write: %d\n", t, fd);
exit();
}
if ((r = dup(fd, 1)) < 0) {
debugf("err when dup %d: %d\n", fd, r);
exit();
}
if ((r = close(fd)) < 0) {
debugf("err when close %d: %d\n", fd, r);
exit();
}
break;
}

那么 touch.c 就很简单了。

  1. 首先通过简单的字符串处理找到文件所在的目录

    1
    2
    3
    4
    5
    for (;i>=0;i--)
    if (path[i]=='/')
    break;
    for (int j=0;j<=i;j++)
    dir[j]=path[j];
  2. 用 Stat 结构体检查目录状态,注意判断目录是否为 dir 类型

    1
    2
    3
    4
    5
    6
    7
    8
    struct Stat st;
    if ((r = stat(dir, &st)) < 0) {
    printf("touch: cannot touch '%s': No such file or directory\n", path);
    return;
    } else if (!st.st_isdir) {
    printf("touch: cannot touch '%s': No such file or directory\n", path);
    return;
    }
  3. 然后检查文件是否存在

    1
    2
    3
    if ((r = stat(path, &st)) == 0) {
    return;
    }
  4. 如果一切正常,那么新建文件,复制 sh.c 的代码即可。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    if ((fd = open(path, O_WRONLY | O_CREAT)) < 0) {
    debugf("err when open %s for write: %d\n", path, fd);
    return;
    }
    if ((r = dup(fd, 1)) < 0) {
    debugf("err when dup %d: %d\n", fd, r);
    return;
    }
    if ((r = close(fd)) < 0) {
    debugf("err when close %d: %d\n", fd, r);
    return;
    }

    mkdir 指令

参数的处理仿照 ls.c 即可:

1
2
3
4
5
6
7
8
ARGBEGIN {
default:
usage();
case 'p':
flag[(u_char)ARGC()]++;
break;
}
ARGEND

对于父目录和目录状态的检查同 touch 指令,不再赘述。注意检查是否有 -p 选项,如果有则递归创建。

1
2
3
4
5
6
7
8
9
10
r = stat(dir, &st);
if (r != 0 && !flag['p']) {
printf("mkdir: cannot create directory '%s': No such file or directory\n", path);
return;
} else if (r == 0) {
// codes
} else {
mkdir(dir);
mkdir(path);
}

创建目录的方法和创建文件的方法相同,加入参数 O_MKDIR 即可。

1
2
3
4
if ((fd = open(path, O_WRONLY | O_CREAT | O_MKDIR | O_TRUNC)) < 0) {
debugf("err when open %s for write: %d\n", path, fd);
return;
}

rm 指令

参数的处理仿照 ls.c。路径的检查和上文一样。最后调用 remove 函数即可。

1
2
3
4
5
6
7
8
9
10
if ((r = stat(path, &st)) != 0) {
if (!flag['f'])
printf("rm: cannot remove '%s': No such file or directory", path);
return;
}
if (st.st_isdir && !flag['r']) {
printf("rm: cannot remove '%s': Is a directory", path);
return;
}
remove(path);

实现反引号

仿照管道符号 |,用管道获取输出内容并复制到字符串里,然后作为参数回传即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
case '`': {
int p[2];
if ((r = pipe(p)) < 0) {
// error
}
if ((*rightpipe = fork()) < 0) {
// error
}
if (*rightpipe != 0) {
// open pipe
// read outputs
for (int i=0;i<8192;i++)
read(0, echo_buf+i, 1);
// save as argument
argv[argc++]=echo_buf;
break;
}
else {
// open pipe
runcmd(t);
return 0;
}
break;
}

实现注释功能

在 sh.c 的 SYMBOLS 里加入 # 号,并在 parsecmd 里进行对应的处理即可。

1
2
3
case '#': {
return argc;
}

实现历史指令

打印历史指令

用一个数组保存所有指令,在 runcmd 里特判 history 指令即可。

1
2
3
4
5
6
7
8
9
if (strcmp(argv[0], "history") == 0) {
for (int i = cnt_commands >= 20 ? cnt_commands - 20 : 0; i < cnt_commands; i++)
printf ("%s\n", commands[i]);
close_all();
if (rightpipe) {
wait(rightpipe);
exit();
} return 0;
}

实现光标移动

在 readline 函数里对读取的字符进行即时判断即可。详见下面这段代码。就是个前端工作

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
if (' ' <= c && c <= '~') {
len++;
for (int j = len; j > i; j--)
buf[j] = buf[j-1];
buf[i] = c;
// flush
} else if (c == 0x7f || c == '\b') {
if (i == 0) {
printf ("\b ");
} else {
printf ("\b");
len--;
i--;
for (int j = i; j < len; j++) {
buf[j] = buf[j+1];
printf ("%c", buf[j]);
} printf (" \b");
for (int j = i; j < len; j++)
printf ("\b");
} i--;
} else if (c == 27) { // <------------------ HERE !
for (int _ = 0; _ < 2; _++)
if ((r = read(0, &c, 1)) != 1) {
// check error
}
switch (c) {
case 'A': { // up
printf("\033[B");
if (now_id == 0) {
; // do nothing
} else {
for (; i < len; i++)
printf (" ");
for (int i = 0; i < len; i++)
printf ("\b \b");
buf[len] = 0;
strcpy(commands[now_id], buf);
now_id--;
strcpy(buf, commands[now_id]);
for (i = 0; i < len; i++)
printf ("%c", buf[i]);
} i = len - 1;
break;
} case 'B': { // down
for (; i < len; i++)
printf (" ");
for (int i = 0; i < len; i++)
printf ("\b \b");
if (now_id == cnt_commands) {
; // do nothing
} else {
now_id++;
strcpy(buf, commands[now_id]);
for (i = 0; i < len; i++)
printf ("%c", buf[i]);
} i = len - 1;
break;
} case 'C': { // right
if (i == len) {
i--;
printf ("\b");
} break;
} case 'D': { // left
if (i == 0) {
i--;
printf (" ");
} else i-=2;
break;
}
}
}

实现一行多指令

同条件控制,在读入命令的时候进行预处理,以 ; 为间隔分隔各个指令分别执行即可。

实现追加重定向

在 _gettoken 函数里对 >>> 进行特判,如果是 >> 则返回 ~

在 parsecmd 里进行对应的处理:打开(新建)文件,不要清空文件(即 O_TRUNC 参数)把光标移动到文件末尾,其余和 > 的处理一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
case '~': {
if (gettoken(0, &t) != 'w') {
// error
}
if ((fd = open(t, O_WRONLY | O_CREAT)) < 0) {
// error
}
// move cursor to end of the file
struct Stat st;
stat(t,&st);
seek(fd, st.st_size);
if ((r = dup(fd, 1)) < 0) {
debugf("err when dup %d: %d\n", fd, r);
exit();
}
if ((r = close(fd)) < 0) {
debugf("err when close %d: %d\n", fd, r);
exit();
}
break;
}

实现引号支持

在 _gettoken 函数里对双引号特判,将双引号之间的内容作为一个字段(即 w)回传即可。

1
2
3
4
5
6
7
8
if (*s == '"') {
s++;
*p1 = s;
while (*s++ != '"');
*(s-1)=0;
*p2 = s;
return 'w';
}

实现前后台任务管理

与历史指令的实现相同。在 runcmd 里特判输入即可。

jobs 指令需要获取对应进程状态,可以通过判断 env_status 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
char *get_status(int envid) {
struct Env *e = envs + ENVX(envid);
if ((e->env_status == ENV_FREE) || e->env_id != envid) {
return DONE;
} else {
return RUNNING;
}
}

if (strcmp(argv[0], "jobs") == 0) {
for (int i=1;i<=cnt_bg;i++) {
printf("[%d] %-10s 0x%08x %s\n", i, get_status(ids[i]), ids[i], cmds[i]);
} close_all();
if (rightpipe) {
wait(rightpipe);
exit();
} return 0;
}

fg 指令的效果和 wait 相同。

1
2
3
4
5
6
7
8
9
if (strcmp(argv[0], "fg") == 0) {
if (jobid == 0 || jobid > cnt_bg) {
// error
} else if (get_status(ids[jobid]) != RUNNING) {
// error
} wait(ids[jobid]);
close_all();
return 0;
}

其中 kill 指令需要魔改 syscall_env_destroy 以便让非父进程也可以杀掉对应的进程。具体来说是在 sys_env_destroy 中关闭 envid2env 的 checkperm 选项。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
int sys_env_destroy(u_int envid) {
struct Env *e;
try(envid2env(envid, &e, 0)); // DO NOT CHECK!
printk("[%08x] destroying %08x\n", curenv->env_id, e->env_id);
env_destroy(e);
return 0;
}

if (strcmp(argv[0], "kill") == 0) {
if (jobid == 0 || jobid > cnt_bg) {
// error
} else if (get_status(ids[jobid]) != RUNNING) {
// error
}
syscall_env_destroy(ids[jobid]); // FORCE!
close_all();
return 0;
}