实验思路
一行多命令
实现效果
用 ;
分开同一行内的两条命令,表示依次 执行前后两条命令。;
左右的命令都可以为空。
实现
fork 一个子进程执行已解析出的命令,父进程等待子进程执行结束后再继续解析。
1 2 3 4 5 6 7 8 9 10 11 case ';' : r = fork(); if (r == 0 ) { return argc; } else { wait(r); *rightpipe = 0 ; return parsecmd(argv, rightpipe); } break ;
后台任务
实现效果
用 &
分开同一行内的两条命令,表示同时 执行前后两条命令。&
左侧的命令应被置于后台执行,Shell 只等待 &
右侧的命令执行完毕,然后继续执行后续语句,此时用户可以输入新的命令,并且可能同时观察到后台任务的输出。&
左侧的命令不能为空。
实现
与上一个类似,只是父进程不需要等待子进程直接继续解析即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 case '&' : if (!argc) { debugf("sh: grammar error around &\n" ); exit (); } r = fork(); if (r == 0 ) { background = 1 ; return argc; } else { *rightpipe = 0 ; return parsecmd(argv, rightpipe); } break ;
引号支持
实现效果
将引号(包括单引号和双引号)内的内容看作单个字符串,对于紧挨着引号的内容将其连接视作整体,引号内输入回车也视作普通字符,直到引号匹配。
实现
对于标识符和单词等的解析在 _gettoken
,读到一个有效引号(不在引号内的引号)后删除这一字符并更新当前状态(是否在引号内),当在引号内时忽略空格和预留字符的作用。(通过修改输入逻辑保证命令的引号一定匹配)
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 int quat = 0 ;*p1 = s; while (*s && (quat || !strchr (WHITESPACE SYMBOLS, *s))) { if (strchr ("\'\"" , *s)) { if (!quat) { quat = *s; char *t = s; while (*t) { *t = *(t + 1 ); t++; } } else { if (quat == *s) { quat = 0 ; char *t = s; while (*t) { *t = *(t + 1 ); t++; } } else { s++; } } } else { s++; } } *p2 = s; return 'w' ;
键入命令时任意位置的修改
实现效果
可以使用左右键移动光标位置,并在该位置进行删除和增添字符,对于修改后的命令及时回显,并且输入回车后可以正常运行修改后的命令。删除键以及左右键到达边界时,保持原样并发出警报。
实现
主要思路是增加变量 len 记录输入命令的总长度,变量 p 为当前读取字符的位置,变量 left 为左边界的位置,变量 quat 为当前引号类型,字符分为程序内部字符数组以及控制台显示字符,p 相当于光标位置,输入字符后保证程序和控制台的一致性即可。
修改 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 len = 0 ; quat = 0 ; left = 0 ; for (p = 0 ; p < n; p++) { char c; debugf("\x1b[s" ); read_char(fd, &c); if (c == '\r' || c == '\n' ) { if (!quat) { buf[len] = '\0' ; if (iscons(fd) && len) { save_cmd(buf, record); } else if (!len) { record->tot_history--; } return ; } else { p = len; buf[len++] = '\n' ; buf[len] = '\0' ; left = len; debugf("> " ); } } else if (c == 0x1b ) { read_control(fd, buf, record); } else if (c == 0x7f || c == '\b' ) { read_backspace(fd, buf); } else { read_normal(fd, buf, c); } }
以正常字符为例,先将 p 之后的字符后移,然后更新字符长度并赋值,如果是控制台输入则需要相应的进行回显。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 void read_normal (int fd, char *buf, char c) { for (int i = len; i > p; i--) { buf[i] = buf[i - 1 ]; } len++; buf[p] = c; buf[len] = '\0' ; if (strchr ("\'\"" , c)) { if (!quat) { quat = c; } else if (quat == c) { quat = 0 ; } } if (iscons(fd)) { debugf("\x1b[s\x1b[D\x1b[K" ); debugf("%s\x1b[u" , &buf[p]); } }
程序名称 .b
的省略
实现效果
允许命令省略 .b
后缀,并在当前命令没有该后缀时加上后缀继续尝试打开。
实现
在 spawn
函数中第一个参数 prog
就是程序名,首先会根据 prog
打开文件,如果此时没找到文件并且没有后缀 .b
,那么可以尝试添加 .b
后缀之后再打开看是否可行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 if ((fd = open(prog, O_RDONLY)) < 0 ) { int len = strlen (prog); if (fd == -E_NOT_FOUND && !(prog[len - 2 ] == '.' && prog[len - 1 ] == 'b' )) { char buf[4096 ]; int i = 0 ; while (prog[i] != '\0' ) { buf[i] = prog[i]; i++; } buf[i++] = '.' ; buf[i++] = 'b' ; buf[i++] = '\0' ; fd = open(buf, O_RDONLY); } if (fd < 0 ) { return fd; } }
tree
实现效果
tree [-adfs] [-L n] [path…]
-a,显示隐藏目录和文件(给 ls.b 也加入了该参数)
-d,只显示目录
-f,显示完整的路径名
-s,显示文件或目录的大小
-L n,限制显示层级
实现
参考 ls.b
,记录当前目录路径以及输出前缀,读取目录中的文件控制块,对于文件加上前缀输出即可,对于目录输出后需要继续递归处理。
mkdir & touch & 重定向 >
实现效果
mkdir [-pv] dir…
-p,目录已存在不报错,且按需创建父目录
-v,为每一个已创建的目录打印信息
touch [-c] file…
-c,不创建任何文件
当重定向 >
目标路径不存在时,自动创建并写入。
实现
修改 serv_open
通过文件打开模式 O_MKDIR
(创建目录),O_CREAT
(创建文件),O_EXCL
(文件存在时报错) 以及 O_TRUNC
(清空文件) 进行操作,如果打开文件不存在并且需要创建文件或者创建目录,那么通过 file_create
进行文件的创建再赋上文件类型即可,如果文件存在并且有 O_EXCL
那么返回文件存在错误,如果有 O_TRUNC
使用 file_truncate
将文件内容清除,如果没有读写需求使用 file_close
关闭文件。
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 void serve_open (u_int envid, struct Fsreq_open *rq) { struct File *f ; struct Filefd *ff ; int r; struct Open *o ; if ((r = open_alloc(&o)) < 0 ) { ipc_send(envid, r, 0 , 0 ); return ; } r = file_open(rq->req_path, &f); int o_mode = rq->req_omode; if (r == -E_NOT_FOUND && ((o_mode & O_CREAT) || (o_mode & O_MKDIR))) { if ((r = file_create(rq->req_path, &f)) < 0 ) { ipc_send(envid, r, 0 , 0 ); return ; } f->f_type = (o_mode & O_CREAT) ? FTYPE_REG : FTYPE_DIR; } else if (r >= 0 && (o_mode & O_EXCL)) { ipc_send(envid, -E_FILE_EXISTS, 0 , 0 ); return ; } else if (r < 0 ) { ipc_send(envid, r, 0 , 0 ); return ; } o->o_file = f; ff = (struct Filefd *)o->o_ff; ff->f_file = *f; ff->f_fileid = o->o_fileid; o->o_mode = rq->req_omode; ff->f_fd.fd_omode = o->o_mode; ff->f_fd.fd_dev_id = devfile.dev_id; if (o_mode & O_TRUNC) { file_truncate(f, 0 ); } if (!(o_mode & O_ACCMODE)) { file_close(f); } ipc_send(envid, 0 , o->o_ff, PTE_D | PTE_LIBRARY); }
对于 mkdir
使用 open(path, O_MKDIR | O_EXCL)
,对于 touch
使用 open(file_path, O_CREAT)
,对于重定向使用 open(t, O_CREAT | O_WRONLY)
。
历史命令
实现效果
能够通过上下键进行历史命令的切换,能够继续编辑按下回车后能正确执行。对于新输入的命令切换后能暂时保存,可以切换回来继续编辑。对于边界情况保持原样并且发出警报。输入命令后将命令写入 .history
文件中。
history [-c] [n]
-c,清空历史文件
n,输出最近 n 条指令(默认全部输出)
实现
在 shell 初始时使用 open(“.history”, O_TRUNC | O_CREAT)
创建或者清空历史命令文件。
命令的切换
为了方便在 sh.c
中使用结构体变量储存命令相关参数,并且该变量需要对 fork 的子进程共享。
1 2 3 4 5 struct history { int tot_history; int now_history; int end[1022 ]; }
实现一系列函数完成相关功能。读取到上下键之后通过 get_pre_cmd
或者 get_next_cmd
函数进行命令的切换,内部都是以 read_cmd
为核心进行操作的。当一条命令完整读入后对其进行储存。当输入部分命令后进行切换可以对该命令进行临时储存,之后可以切换回该命令继续操作。命令切换后需要对这一行进行清除,并重新输出显示新的命令。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 if (up) { debugf("\x1b[u" ); buf[len] = '\0' ; clear(buf); get_pre_cmd(buf, record); print_buf(buf); } void read_cmd (int fd, char *buf, int offset, int size) ;int write_cmd (int fd, char *buf, history *record) ;void get_pre_cmd (char *buf, history *record) ;void get_next_cmd (char *buf, history *record) ;void save_cmd (char *buf, history *record) ;
history.b
输出
将整个文件内容读入,记录有效的 \n
的数量以及位置 pos[]
确定命令的情况,接着根据 pos
输出即可。
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 void print (int n) { int r; int fd; if ((fd = open(".history" , O_RDONLY)) < 0 ) { debugf("open file .history error: %d\n" , fd); exit (); } if ((r = read(fd, buf, sizeof (buf))) < 0 ) { debugf("read file .history error: %d\n" , r); exit (); } tot = 0 ; pos[0 ] = -1 ; int quat = 0 ; for (int i = 0 ; i < r; i++) { if (buf[i] == '\'' || buf[i] == '\"' ) { if (!quat) { quat = buf[i]; } else if (buf[i] == quat) { quat = 0 ; } } if (buf[i] == '\n' && !quat) { pos[++tot] = i; } } if (!tot) { debugf("no history\n" ); exit (); } for (int i = n > tot ? 1 : tot - n + 1 ; i <= tot; i++) { int start = pos[i - 1 ] + 1 ; int end = pos[i]; buf[end] = '\0' ; debugf("%-4d %s\n" , i, &buf[start]); } }
shell 环境变量
实现效果
子 shell 能够继承环境变量并对其做出修改;局部变量对子 shell 不可见。
delcare [-xr] [NAME [=VALUE]]
-x,设置环境变量,否则为局部变量
-r,变量只读,不能被重新赋值或者删除
NAME
不存在则创建,否则重新赋值(如果可以的话)。VALUE
缺省为空字符串。只输入 declare
则输出所有变量,包括环境变量和局部变量。
unset NAME
如果 NAME
不是只读变量,则删除变量(环境变量和局部变量都删除)。
支持在命令中展开变量的值,优先使用环境变量,没有对应的变量则为空字符串。
实现
declare & unset
将一对变量用结构体储存
1 2 3 4 5 6 7 8 9 #define ENV_VAR_FREE 0 #define ENV_VAR_RDONLY 1 #define ENV_VAR_RDWR 2 struct Env_var { char name[20 ]; char value[20 ]; int mode; };
在 sh.c
中记录两个变量的数组,分别为环境变量和局部变量,在初始时通过 syscall_mem_map
各自修改 perm
使得环境变量能够在子 shell 中共享,局部变量可以在 fork 出的子进程中共享。
1 2 3 Env_var global_env_var[MAXENVVARS] __attribute__((aligned(BY2PG))); Env_var local_env_var[MAXENVVARS] __attribute__((aligned(BY2PG)));
unset
和 declare
属于内置命令,不通过加载程序执行,所以需要修改 runcmd
的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (strcmp (argv[0 ], "unset" ) == 0 ) { unset_main(argc, argv, global_env_var, local_env_var); } else if (strcmp (argv[0 ], "declare" ) == 0 ) { declare_main(argc, argv, global_env_var, local_env_var); } else { int child = spawn(argv[0 ], argv); close_all(); if (child >= 0 ) { wait(child); } else { debugf("spawn %s: %d\n" , argv[0 ], child); } if (rightpipe) { wait(rightpipe); } if (argc > 1 && (strcmp (argv[0 ], "history" ) == 0 || strcmp (argv[0 ], "history.b" ) == 0 ) && strcmp (argv[1 ], "-c" )) { record.tot_history = record.now_history = 0 ; } }
主要实现通过以下函数的调用
1 2 3 4 5 6 Env_var *find_env_var (Env_var *env_var_array, char *name) ; Env_var *get_free_env_var (Env_var *env_var_array) ; int set_env_var (Env_var *env_var_array, char *name, char *value, int mode) ; int unset_env_var (Env_var *env_var_array, char *name) ; void print_env_var (Env_var *env_var_array, char *name) ;
变量展开
增加预留字符 $
,将其后面的字符串在变量数组中进行匹配,并指向相应的 VALUE
。
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 (); } Env_var *var = find_env_var(global_env_var, t); if (var == NULL ) { var = find_env_var(local_env_var, t); if (var == NULL ) { t = "" ; } else { t = var->value; } } else { t = var->value; } argv[argc++] = t; break ;
测试程序
一行多命令
主要验证是否顺序执行。
1 ls;mkdir test;ls;mkdir test/a;tree test
后台任务
可以看到两个任务交错输出,并且回到 shell 后后台任务仍在运行。
引号支持
检测输入是否正确要求引号匹配以及对引号内回车的处理;引号连接功能的检测;是否将引号内容作为单个字符串
1 2 3 4 5 6 echo abc"def';& gh" ijkecho abc'deg";& fg' ijktouch 'ab c' tree
键入命令时任意位置的修改
随机输入命令并进行修改,使得及时回显且能够正确运行,同时检测左右移动以及删除键的边界,具体过程可现场演示。
实现程序名称中 .b
的省略
在其他测试命令中可以体现,可以交错使用包含和不包含 .b 的命令。
tree
1 2 3 4 5 6 7 tree tree -a mkdir -p a/b/ctree -d tree -fs tree -L 2 tree a a/b
mkdir
1 2 3 4 5 6 7 8 9 mkdir test1ls mkdir -v test2ls mkdir test2mkdir test3/test4ls mkdir -pv test3/test4tree
touch
1 2 3 4 touch test1 test2ls touch -c test4ls
重定向
1 2 3 4 ls cat script > test ls cat test
历史命令
1 2 3 4 5 6 7 8 9 ls echo hello osmkdir test history history 3history 10cat .history history -ccat .history
shell 环境变量
检测能否正常设置(包括缺省);检测可见性是否正确;检测能否重新赋值和删除;检测只读变量;检测是否能展开变量
1 2 3 4 5 6 7 declare hello =world;declare -x os;declare sh declare declare -x os =best;declare unset os;declare declare -r os =best;declare os;unset os;declare echo os is $os
遇到的问题和解决方案
“后台”
因为 shell 等待输入调用的 cons_read
内部会不断调用系统调用 syscall_cgetc
,而系统调用内部又是忙等待,所以会导致后台任务在此时其实并不会被调用运行,只有在其它命令输入后并执行的过程中才会调用它,要想真正实现后台运行就需要去掉 sys_cgetc
中的忙等待,直接返回 scancharc()
才能真正实现“后台”执行。
1 2 3 4 int sys_cgetc () { return scancharc(); }
控制符
关于上下左右键的读取,它们并不是由单个 ASCII
码构成的,而是 CSI
(control sequence introducer) 控制序列构成,具体可以使用 man console_codes
查看详细文档。下面列出一些用到的控制符
控制码
效果
\x1b[nA
向上移动n行
\x1b[nB
向下移动n行
\x1b[nC
向右移动n列
\x1b[nD
向左移动n列
\x1b[3~
Del键
\x1b[nP
删除光标右边n个字符,剩下部分左移,光标不动
\x1b[s
保存光标位置
\x1b[u
回到光标保存位置
\x1b[K
删除光标到行尾的内容
\x1b[nm
颜色等设置
.b
的增加
由于参数是连续放置的,所以直接在 prog
后添加后缀的话会影响到第一个参数,将其变成 b
,所以需要重新开个数组添加后缀。
tree —— 前缀
经过观察可以发现,tree 命令最后一项和其他项的前缀是有区别的,例如
最后一项的前面的符号是 └──
,而其他项前面的符号是 ├──
最后一项如果是目录的话,对于其子项前缀加上空格即可,而其他项如果是目录,前缀需要加 “| ”。
为了实现最后一项的判断,需要先找到一个合法的文件控制块,然后循环读取合法的文件控制块,并对前一个进行输出,最后进行最后一项的输出。
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 void print (struct File f, int end, char *path, char *pre, int dep) ;struct File f1 , f2 ;do { if ((r = readn(fd, &f1, sizeof (f1))) < 0 ) { debugf("read file error: %d" , r); exit (); } if (!r) { break ; } } while (!check(f1)); while ((r = readn(fd, &f2, sizeof (f2))) == sizeof (f2)) { if (check(f2)) { print(f1, 0 , path, pre, dep); f1 = f2; } } if (r < 0 ) { debugf("read file error: %d" , r); exit (); } if (check(f1)) { print(f1, 1 , path, pre, dep); }
共享
一开始环境变量和局部变量映射的 perm 分别是 PTE_D | PTE_LIBRARY 和 PTE_D,但这样会出现局部变量设置不成功的情况,这是因为每次执行命令是使用 fork 出的子进程操作,由于写时复制机制使得内容修改对父进程没有影响,所以增加一种 perm —— PTE_FORK
使得只在 fork 的父子进程之间共享,在 duppage
时加入对这种 perm 的判断,本地环境变量的 perm 设为 PTE_D | PTE_FORK。
经过测试设置环境变量后执行其他程序命令会影响环境变量内容,如下图所示
我猜测因为环境变量会进行映射,但是其他程序相应的位置有自己的变量进行修改或者初始化,就使得 shell 进程的环境变量进行了错误的修改。解决方案就是再加一种 perm —— PTE_SH
,只在装载 sh.b
程序的时候共享页面,在 spwan
进行映射时加上判断,环境变量的 perm 设为 PTE_D | PTE_FORK | PTE_SH。
除此之外,如果直接定义两个数组由于在映射的时候是页面为单位,两个数组的边界的 perm 就会出现问题,需要使用 __atrribute__((aligned(BY2PG)))
使其以页面开头为起始地址分配空间,对于 record
同理。