SHELL挑战性任务实现报告

任务要求

基于lab6实现的shell进行增强。

任务内容

支持相对路径

为每个进程维护工作目录这一状态(明示在PCB中加东西),实现cdpwd等内建指令,并为其他与路径相关的指令提供路径支持。

工作目录:进程当前所在目录,来解析相对路径。

绝对路径:以/开头路径,始于根目录。

相对路径:不以/开头路径,相对于当前工作目录解析,可能含有.(当前路径)和..(上一级路径)特殊符号(要特殊处理)。

其中,cd的具体要求:

输入 行为 输出 返回值
cd 切换工作目录到 / 0
cd <abspath> 若绝对路径 <abspath> 存在且为目录,切换到该目录 0
cd <relpath> 根据当前工作路径,对相对路径<relpath>,若存在且为目录,切换到该目录 0
cd <noexist_path> 路径不存在 cd: The directory '原始输入' does not exist\n 1
cd <filepath> 路径存在但不是目录 cd: '原始输入' is not a directory\n 1
cd <arg1>..<argn> 输入多于 1 个参数 Too many args for cd command\n 1
cd . 解析为当前目录 /dir1/dir2,无变化 0
cd .. 解析为 /dir1,若存在且为目录,切换到该目录 0

pwd的具体要求:

输入 行为 输出 返回值
pwd 输出当前工作目录 /dir1/dir2\n 0
pwd arg1 ... argn 参数数量错误 pwd: expected 0 arguments; got n\n 2

注意:父进程能将工作路径传递给子进程

环境变量管理

规定在命令中以$开头,名称同C语言,长度不超过16

分为局部变量非局部变量两种,仅有后者可以传入子Shell中,且只有非只读变量可被修改。

具体实现要求如下:

  1. 支持内建命令declare [-xr] [NAME [=VALUE]]

    • -x指变量为非局部变量,否则为局部变量。前者可被子进程(子shell)继承,并且可以读取并修改;后者不能被子shell继承。
    • -r表示变量只读不可修改,即不能被declare重赋值或被unset删除。
    • 若变量不存在则创建,否则,重新赋值。
    • VALUE为可选参数,缺省时将变量赋为空字符串。
    • 若只输入了declare则输出当前shell到的所有变量,包括局部和非局部,以<var>=<val>的形式输出。
  2. 支持内建指令unset NAME命令,若变量不为只读变量,则删除其。

  3. 支持在命令中展开变量的值,如用echo.b $NAME打印变量的值。

  4. 注意:子shell继承的非局部变量被其修改后并不影响父shell,且declare不能正常执行时要返回非零值

输入指令优化

指令自由输入

需实现:键入命令时,可使用左箭头和右箭头来移动光标,并可在当前光标位置进行字符增减。要求每次在不同位置键入后都可完整回显修改后的命令,且键入回车后可以正常运行修改后的命令

不带.b后缀指令

需实现不带.b后缀指令且兼容带有.b后缀指令。

快捷键

快捷键 行为
left-arrow 光标尝试向左移动,如果可以移动则移动
right-arrow 光标尝试向右移动,如果可以移动则移动
backspace 删除光标左侧 1 个字符并将光标向左移动 1 列;若已在行首则无动作
Ctrl-E 光标跳至最后
Ctrl-A 光标跳至最前
Ctrl-K 删除从当前光标处到最后的文本
Ctrl-U 删除从最开始到光标前的文本
Ctrl-W 向左删除最近一个 word:先越过空白(如果有),再删除连续非空白字符

历史指令

实现保存历史指令的功能,并且可以通过上下箭头选择所保存的指令并执行,需将历史指令保存在/.mos_history文件中(一条指令一行),且最多只保存最近的20条指令,并且需要支持history指令输出其内容。

注意此处需要将history实现为内建指令,且写入/.mos_history的时机应该在指令输入完成后,执行之前。

使用上下箭头切换指令时,可选指令范围为:用户输入指令与/.mos_history中保存的所有指令。

注意:此处切换到新指令时光标应该自动恢复到输入最末端。

实现注释功能

使用#实现注释功能,将抛弃#之后的内容。

实现反引号

需将反引号内指令执行的所有标准输出代替原有指令中的反引号内容。

实现一行多指令

支持使用;将指令隔开按序执行。

指令条件执行

实现&&||,对于 command1 && command2command2 被执行当且仅当 command1 返回 0;对于 command1 || command2command2 被执行当且仅当 command1 返回非 0 值。

注意这两者的优先级相同,若多个同时出现则从左到右依次执行。

提示:可能需要修改MOS中对用户进程exit的实现,使其能返回值。

更多指令

  • touch <file>:创建空文件,若文件存在则放弃创建,正常退出无输出。 若创建文件的父目录不存在则输出 touch: cannot touch '<file>': No such file or directory

  • mkdir:

    • mkdir <dir>:若目录已存在则输出 mkdir: cannot create directory '<dir>': File exists,若创建目录的父目录不存在则输出 mkdir: cannot create directory '<dir>': No such file or directory,否则正常创建目录。
    • mkdir -p <dir>:当使用 -p 选项时忽略错误,若目录已存在则直接退出,若创建目录的父目录不存在则递归创建目录
  • rm:

    • rm <file>:若文件存在则删除 <file>,否则输出 rm: cannot remove '<file>': No such file or directory
    • rm <dir>:命令行输出: rm: cannot remove '<dir>': Is a directory
    • rm -r <dir>|<file>:若文件或文件夹存在则删除,否则输出 rm: cannot remove '<dir>|<file>': No such file or directory
    • rm -rf <dir>|<file>:如果对应文件或文件夹存在则删除,否则直接退出。
  • (内建指令)exit:退出当前shell

rm、mkdir、touch指令,成功执行则返回0,否则返回非零值。

追加重定向

实现>>

具体修改部分和一些重要的实现细节

大体流程

由于我并不想通过构建AST语法树,所以我是采用直接对命令字符串进行处理的方式来解析相应的指令并运行的。
在原有框架的基础上,总体流程是sh.c程序从main主函数进入(相当于启动shell),打印好提示词后,尝试将/.mos_history文件中的内容载入到内存,此后做好一些模式的判定后,循环读取输入的命令行,将其中非空且不与前一条指令重复的指令暂存到内存并写入.mos_history文件,并对命令行进行诸如裁剪前后导空格展开可展环境变量展开反引号并执行去除注释内容等操作。
最后若该命令行非空且不以注释开头,则运行该命令行,其中可能包括由于;分割的多组指令,按顺序运行分割后的每组指令,每组指令中可能包含了条件指令和管道嵌套指令等。

新建工具函数

为了便于后续的操作,并且将OO课程中的“封装”思想应用,在lib/string.c中添加了如下函数的实现,对应也在include/string.h中添加了相应的声明:

1
2
3
4
5
6
char *strncpy(char *dst, const char *src, size_t n);
char *strcat(char *dst, const char * src);
char *strtok(char *str, const char *delimiters);
char *strstr(const char *haystack, const char *needle);
int resolve_path(const char *original_path, const char *current_directory,
char *resolved_path_buffer);

其中前四个函数与C语言函数库中对应的函数的功能和实现大致一致,而最后一个函数承担了将传入的待解析路径original_path(绝对路径不做处理,相对路径在处理了...特殊符号后同当前工作目录current_directory拼接)经过处理后放入解析后的resolved_path_buffer中,以供后续其他函数的使用,而由于相对路径的出现,在一些需要对路径进行操作的函数中需要考虑相对路径,也就是先用该函数来处理一下。

一些重要细节

支持相对路径

通过在PCB中设置工作路径并提供获得和修改工作路径的系统调用来为内建指令cdpwd提供使用便利,对于相对路径的处理,我是在所有用到路径的地方,例如:file.c中的openremove函数等,都使用了resolve_path函数解析成绝对路径来实现。
需要注意的是,工作路径是需要在fork的时候由父进程传递给子进程,也就是在env.c中的env_alloc函数中将能获取到的父进程工作路径传给子进程。

环境变量管理

我的处理是在PCB中增加存储环境变量的数组和相应的总个数,其中对每个环境变量单独设置一个结构体变量存储其相应的属性,如变量名、变量值、是否可传递给子进程、是否只读等。同样需要在env.c中的env_alloc函数进行初始化,即全置零,并且需要在kern/syscall_all.c中的sys_exofork函数中进行父子进程之间的环境变量继承,对于不可传递的变量则无需传递给子进程。
同样的,declareunset等内建指令,也是通过系统调用来进行处理。
declare在没有参数时会同通过syscall_all_args系统调用将当前进程拥有的所有环境变量(局部和非局部)的名和值写入传入的指针中,直接输出即可;并会在有参数时对于有等号(即同时传入名和值)和无等号(只传入名而无值)进行不同的处理(通过syscall_set_args系统调用前者传入相应值后者传入空串)。
unset则是会通过调用syscall_unset_args的系统调用来解除当前进程中可能的环境变量。

输入指令优化

不带.b后缀指令

这个只需要在通过spawn函数运行需要烧录的指令时,按顺序运行指令和加上.b后缀的指令(就算是原来就带有.b后缀的也没事,因为只要这两者一个成功就会停止运行,因为前者要是没成功说明本身就未被定义,那么加上.b后缀也仍未被定义)。

指令自由输入和快捷键

由于这个的实现类似,其实左右移动和快捷键表现在终端就是输入一串字符序列,而我们要做的就是在读取命令行时,通过循环读入的方式不断判断读入的字符可能表示的操作,并在判断出相应操作后进行处理。
这里给出一张快捷键和左右移动对应字符序列表格(摘自zzy的博客):

指令 输入
up-arrow 0x1b[A
down-arrow 0x1b[B
left-arrow 0x1b[D
right-arrow 0x1b[C
backspace \b或者0x7f
Ctrl-E 0x5
Ctrl-A 0x1
Ctrl-K 0xb
Ctrl-U 0x15
Ctrl-W 0x17

大致思路就是修改read_line函数的逻辑,首先设置光标位置index用来表示光标在命令行缓冲区中的索引,用len来代表当前命令行的总长度,避免出现超出命令行长度的情况出现。这两个参数也是用来回显的重要参数,回显函数redisplay_line通过打印\r$ 将光标移动到行首,打印\033[K来清除光标后到行尾的内容,并打印修改后的内容,通过打印\033[%dG来移动光标位置,注意传入%d的参数应该是index + 3,因为一开始有\r$ 三个字符占位了。

上述快捷指令通过修改lenindex并调用回显函数,即可实现相应的快捷操作,这里需要注意的一点就是使用上下键时,由于需要和历史指令进行交互,因此需要尤其注意内存区的指令会因为切换的修改而被修改,除非它最终被执行,也就是当你对某条历史指令进行修改后切换走再切换回来时是需要留存上次修改后的结果的,而对于在某条历史指令上进行修改后并执行是不会保留到内存区的;同时切换后的index即光标位置应该定位在新命令行末尾,相应的len也需要进行改变

退出循环的时机是输入了\n\r的换行符,或者长度超过了最大命令行长度。

历史指令

大致思路是维护一个20*最大命令行长度的二维数组history_lines、一个当前输入的指令区存放使用上下键切换后的当前输入current_cmd、总共的历史指令数量history_lines_num和当前浏览的历史指令在二维数组中的索引history_current_index(对于当前输入,其值为-1,其余则为对应数组索引)。

其中尤其需要注意history_lines的维护,因为仅保有最近使用的20条指令,因此,需要格外注意溢出情况的发生。

搭配着上下键的使用,需要注意当浏览到最老指令时,上键将失灵;同样对于当前输入指令,下键也将失灵。

根据指导书给定的样例:

1
2
3
4
5
6
7
8
9
10
11
12
13
$ echo `ls | cat`
...
$ echo meow # comment
meow
$ history
1 echo `ls | cat`
2 echo meow # comment
3 history
$ history | cat
1 echo `ls | cat`
2 echo meow # comment
3 history
4 history | cat

发现指令写入/.mos_history的时机应该是指令输入完毕(即键入换行符)之后,到指令执行之前,这点是需要尤其关注的。

注释功能

大致思路是在解析命令行时,在遇到#时将其置为\0,即可在之后对其进行处理时忽略掉原来#之后的内容,进而实现了注释的效果。

实现反引号

大致思路是在解析命令行阶段,提取出命令行中潜在的反引号中的内容,单独开一个子进程来运行该内容后将运行结果的内容替换掉原来的内容即可,可以新开一个字符数组,循环寻找第一个反引号,第一个反引号之前的内容一字不差的写入新数组,提取反引号内的内容,若未找到第二个则把包括之前第一个反引号在内到末尾的所有内容写入新数组;反之,则将获取到的输出结果填入新数组中。

实现一行多指令

大致思路是在运行命令行阶段,根据;分割命令行成不同组的命令行,按顺序依次运行即可。

指令条件执行

大致思路是在需要在将命令行分完组后,对于每个单独的命令组再进行对&&||的识别,分割成不同的命令token,并根据运算规则,依次进行命令token的运行,

//摘自指导书

对于 command1 && command2command2 被执行当且仅当 command1 返回 0;对于 command1 || command2command2 被执行当且仅当 command1 返回非 0 值。

对于样例提到的运算优先级相同的情况,我的方法是采用“跳过法”,首先用一个变量last_status存储当前运行到的命令token之前所有 命令token的执行结果,若当前操作是&&last_status为0或者||last_status不为0时才可进行当前命令token的运行,否则就应跳过下一个命令进而执行下下个命令token

大致代码如下(发现昨天提交上去的代码貌似还是有点问题,啸修改后如下):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//i==0代表一开始什么操作符都没有的时候需要直接运行
if (i == 0 || (ops[i - 1] == '&' && last_status == 0)
|| (ops[i - 1] == '|' && last_status != 0)) {
last_status = run_single_cmd(seg[i]);
}

// 如果是 &&,上一条失败就跳过下一条指令
else if (i > 0 && ops[i - 1] == '&' && last_status != 0) {
i++;
}

// 如果是 ||,上一条成功就跳过下一条指令
else if (i > 0 && ops[i - 1] == '|' && last_status == 0) {
i++;
}

更多指令(touchmkdirrm)

touch

大致实现思路就是先判断用法使用是否正确,再检查文件是否存在,尤其注意不需要重复创建存在的文件,因此,若存在则直接返回即可,否则通过创建模式打开指定的路径便会创建相对应的文件,最后再通过close(fd)关闭相应文件即可。

mkdir

大致思路就是判断完参数输入要求后,由于此处有选项-p的加入,因此还需要去多进行一步的判断,任何非-p的选项输入都要认定为非法输入(因为只要求实现这个),大致流程就是先通过路径的Stat结构体来判断目录文件是否存在,对于已存在一模一样的目录或者创建目录的父目录不存在,未加-p选项的报错;反之加了-p选项,对已存在的情况正常退出,对父目录不存在的则递归创建目录。

递归的思路大致是从根目录开始逐级分解传入的目录路径,对于不存在的目录进行创建,这里采用的是使用O_MKDIR模式打开文件,相应的需要在fs/serv.c文件的serve_open函数中实现对于O_MKDIR模式的判断与处理,其实也就是同创建普通文件一样通过file_creat创建一个新文件,将其设置为目录文件的属性即可。(目录就是文件的含金量还在升高)

rm

大致思路是在判断完命令行参数数量后,循环解析命令行选项-r(递归删除)和-f(强制删除),获取完相应路径后进行相应的删除操作,尤其注意有无选项以及不同选项下的输出反馈的不同,无选项时只能删除存在的文件,否则会报错;存在-r选项时递归删除存在的文件或文件夹,否则报错;存在-rf时递归删除存在文件或文件夹,否则正常退出。

其中需要注意递归删除的大致流程:

  1. 打开目录: 使用 open 函数打开目录,获取文件描述符 fd。若打开失败,则返回 -1。
  2. 读取目录内容: 使用循环和 readn 函数读取目录中的所有文件和子目录的信息,存储在 struct File 结构体 f 中。
  3. 构建完整路径: 对于每个文件或子目录,构建其完整路径 full_path
  4. 判断类型: 使用 stat 函数获取文件或子目录的类型,存储在 struct Stat 结构体 st 中。
  5. 递归删除
    • 如果 st.st_isdir 为真,表示当前项是一个子目录,则递归调用 remove_directory_recursive 函数删除该子目录。
    • 否则,表示当前项是一个文件,使用 remove 函数删除该文件。
  6. 关闭目录: 关闭文件描述符 fd
  7. 删除空目录: 使用 remove 函数删除已经为空的目录。

以上这三种命令都是外部命令,而非内建命令,也就是是需要烧录到生成镜像中才可使用的,因此还需要在new.mk中加入如下代码来实现烧录:

1
2
3
4
5
6
7
INITAPPS += 

USERLIB +=

USERAPPS += rm.b \
mkdir.b \
touch.b

重定向

由于原有环境并未提供重定向模式,因此我自行设计了一个判断是否为追加模式的变量append_mode在解析参数token时,若遇到>>则将其置为1表示是重定向,在之后的操作中只需要在对文件操作前判断出是重定向模式后将文件读写指针移动到文件末尾即可(可通过Stat结构体获取文件大小再通过seek函数移动文件读写指针),实现重定向模式。

心得体会

这次的挑战性任务繁复杂多,但只要抽丝剥茧,厘清其中脉络,还是可以较好的完成的。收获颇多!