OS-挑战性任务

info 以下为shell任务的试题
SHELL 挑战性任务
任务要求
本任务需要你基于MOS LAB6中的Shell进行增强,完成任务内容一节中的要求,提交实现代码以及实现报告并通过自动化评测,即可拿到该挑战性任务对应的分数。代码与报告提交通道将会在后续分别发布。
任务内容
支持相对路径
MOS 中现有的文件系统操作并不支持相对路径,对于一切路径都从根目录开始查找,因此在 shell 命令中也需要用绝对路径指代文件,这为命令的描述带来了不便。你需要为每个进程维护工作目录这一状态,实现相关内建指令,并为其他与路径相关的指令提供路径支持。
你可能需要了解以下术语:
- 工作目录:进程当前所在的目录,用于解析相对路径。
- 绝对路径:以
/
开头的路径,从根目录开始。 - 相对路径:不以
/
开头的路径,相对于当前工作目录解析,可能包含以下特殊符号。 - 特殊符号:
.
表示当前路径,..
表示上一级路径
你需要实现以下功能:
内建指令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 |
注意事项
父进程能够将工作路径传递给子进程。
环境变量管理
MOS 中的Shell目前并不支持环境变量,你需要在shell中增加对环境变量的支持。
规定环境变量在命令中以$
开头,名称与C语言变量命名要求,且长度不超过16,环境变量的值长度同样不超过16。环境变量可分为局部变量与非局部变量,仅非局部变量可传入子Shell中,并且只有非只读变量可被修改。
你需要实现以下内容:
支持 内建指令
declare [-xr] [NAME [=VALUE]]
,其中:-x
表示变量NAME
为环境变量,否则为局部变量。- 环境变量对子 shell 可见,也就是说在 shell 中输入
sh.b
启动一个子 shell 后,可以读取并修改NAME
的值,即支持环境变量的继承。 - 局部变量对子 shell 不可见,也就是说在 shell 中输入
sh.b
启动一个子 shell 后,没有该局部变量。
- 环境变量对子 shell 可见,也就是说在 shell 中输入
-r
表示将变量NAME
设为只读。只读变量不能被declare
重新赋值或被unset
删除。- 如果变量
NAME
不存在,需要创建该环境变量;如果变量NAME
存在,将该变量赋值为VALUE
。 - 其中
VALUE
为可选参数,缺省时将该变量赋值为空字符串即可。 - 如果没有
[-xr]
及[NAME [=VALUE]]
部分,即只输入declare
,则输出当前 shell 的所有变量,包括局部变量和环境变量。
支持内建指令
unset NAME
命令,若变量NAME
不是只读变量,则删除变量NAME
。支持在命令中展开变量的值,如使用
echo.b $NAME
打印变量NAME
的值。
注意事项
当执行
declare
指令时,需要以<var>=<val>
(<var>
为环境变量名,<val>
为对应的值)输出当前Shell中的所有环境变量,评测不会对输出顺序进行测试。子Shell对环境变量的修改不会影响父Shell,且上述指令不能正确执行时返回非零值。
输入指令优化
指令自由输入
现有的 shell 不支持在输入命令时移动光标。你需要实现:键入命令时,可以使用 <Left>
和 <Right>
移动光标位置,并可以在当前光标位置进行字符的增加与删除。要求每次在不同位置键入后,可以完整回显修改后的命令,并且键入回车后可以正常运行修改后的命令。
不带 .b
后缀指令
你需要实现不带 .b
后缀的指令,但仍需兼容带有 .b
后缀的指令,如 ls
与 ls.b
都应能够正确列出当前目录下的文件。
快捷键
你需要在Shell中实现以下快捷键:
快捷键 | 行为 |
---|---|
left-arrow | 光标尝试向左移动,如果可以移动则移动 |
right-arrow | 光标尝试向右移动,如果可以移动则移动 |
backspace | 删除光标左侧 1 个字符并将光标向左移动 1 列;若已在行首则无动作 |
Ctrl-E | 光标跳至最后 |
Ctrl-A | 光标跳至最前 |
Ctrl-K | 删除从当前光标处到最后的文本 |
Ctrl-U | 删除从最开始到光标前的文本 |
Ctrl-W | 向左删除最近一个 word:先越过空白(如果有),再删除连续非空白字符 |
!!! 上述行为与Bash行为一致,如有模糊之处可直接参考Bash。
历史指令
你需要实现 shell 中保存历史指令的功能,可以通过 <Up>
和 <Down>
选择所保存的指令并执行。你需要将历史指令保存到根目录的 .mos_history
文件中(一条指令一行),为了评测的方便,我们设定 $HISTFILESIZE=20
(bash 中默认为 500),即在 .mos_history
中至多保存最近的 20 条指令。你还需要支持通过 history
命令输出 .mos_history
文件中的内容。
注:在 bash 中,
history
为 shell built-in command,我们规定需要将history
实现为 built-in command。
你需要将当前执行的指令先存入 .mos_history
中,例如:
1 | echo `ls | cat`echo meow # comment |
当历史指令为空时,依次执行上述四条指令后,后两条指令会分别输出
1 | echo `ls | cat` |
与
1 | echo `ls | cat` |
使用 <Up>
能够切换到上一条指令(如果上一条指令存在),使用 <Down>
能够切换到下一条指令(如果下一条指令存在)。能够选择的指令范围为:用户当前输入的指令与 .mos_history
文件中保存的所有指令。例如在执行了上述四条指令后,用户输入了 echo
,此时 <Up>
应该将指令切换至 history | cat
,再次进行三次 <Up>
后切换至 echo \
ls | cat`,此时再次
<Up>应保留在该指令(因为已经不存在上一条指令);再进行四次
<Down>后将切换回
echo,此时再次
<Down>` 应保留在该指令(因为不存在下一条指令)。
注意事项
历史指令输入时光标的处理参考Bash,当切换到新的指令时,光标会自动恢复到输入最末端。
实现注释功能
你需要使用 #
实现注释功能,例如 ls | cat # this is a comment meow
,ls | cat
会被正确执行,而后面的注释则会被抛弃。
实现反引号
你需要使用反引号实现指令替换。你需要将反引号内指令执行的所有标准输出代替原有指令中的反引号内容。例如:
1 | echo `ls | cat | cat | cat` |
实现一行多指令
你需要支持使用;
将指令隔开,并按顺序执行,比如:
1 | ls;ls;ls;ls |
指令条件执行
你需要实现 Linux shell 中的 &&
与 ||
。对于 command1 && command2
,command2
被执行当且仅当 command1
返回 0;对于 command1 || command2
,command2
被执行当且仅当 command1
返回非 0 值。
注: 评测中保证不出现括号。并且需要注意的是,在 bash 中
&&
与||
的优先级相同,按照从左到右的顺序求值。例如
cmd1 || cmd2 && cmd3
,若cmd1
返回 0,则cmd1
执行后cmd2
不会被执行,cmd3
会被执行;若cmd1
返回非 0 且cmd2
返回非 0,则cmd3
将不会被执行。提示:你可能需要修改 MOS 中对用户进程
exit
的实现,使其能够返回值。
更多指令
你需要实现 touch
,mkdir
,rm
指令以及内建指令exit
,只需要考虑如下情形:
touch
:touch <file>
:创建空文件file
,若文件存在则放弃创建,正常退出无输出。若创建文件的父目录不存在则输出touch: cannot touch '<file>': No such file or directory
。例如touch nonexistent/dir/a.txt
时应输出touch: cannot touch 'nonexistent/dir/a.txt': 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,否则返回非零值即可。
追加重定向
你需要实现 shell 中 >>
追加重定向的功能,例如:
1 | ls >> file1 |
最后文件 file1
中将会有两次 ls
指令的输出。
实现提示
Shell是一种命令解释器,对输入指令进行解析并执行。现有MOS实现的Shell实现的较为简陋,如果在其基础上尝试实现上述内容可能复杂度较高,可以参考sh,bash等工业界shell实现原理进行重新实现,以下是一个可行的实现方案:
其中:
- 输入指令层
- 输入缓冲管理:负责原始命令的读取,支持行编辑、历史记录等功能。
- 屏幕输出管理:负责屏幕输出内容刷新。
- 词法分析
- 将读入的原始指令划分为最小语法单元Token,并识别特殊标识符(重定向,管道,注释,标识符等)
- 语法分析
- 根据Token序列构建语法树AST并识别命令结构
- AST节点执行
- 遍历AST,根据节点类型在当前进程处执行并更新状态
语法分析可以使用递归下降法进行解析,EBNF文法如下:
1 | # 指令输入 |
AST节点指令的执行参考Shell Command Language。
测试
评测通过make clean && MOS_PROFILE=release make all && make run
进行测试。
提交代码时请确保代码可以执行上述指令。
相对路径+新增指令
1 | pwd |
输出为:
1 | / |
环境变量+注释
1 | /mkdir.b -p dir1/dir2 |
输出应为:
1 | /dir1/dir2 |
指令输入优化+历史指令
1 | # 由于需要展示输入快捷键,这里仅作说明 |
输出为:
1 | /dir1 |
追加重定向+条件执行+反引号
1 | /mkdir -p /dir1/dir2 |
输出应为:
1 | /dir1 |
info 以下为笔者的实现
输入指令优化
不带.b
后缀指令
就是简单的字符串处理,如果末尾没有.b
,那么加上.b
1 | // lab6-ch |
注释功能
也是字符串处理,将#
后面的内容全部丢弃
1 | // lab6-ch |
快捷键
为了实现快捷键,我们需要对终端的显示逻辑进行一定修改。首先利用display
函数刷新并重绘当前命令。之后使用readline
函数,在while
循环中不断读取用户的键盘输入行为,根据用户的键盘行为设置指针或缓冲区内容,并将处理后的缓冲区使用display
函数打印出来。
1 | if (c == '\x1b') { // ESC - potential arrow key |
如果读取到的是上箭头或下箭头,则读取历史内容缓冲区,并将指定历史内容复制到缓冲区中打印出来。
历史指令
在每个shell初始化阶段需要调用load_mosh_history
函数来初始化历史命令缓冲区并新建.mos_history
文件。通过快捷键获得历史指令的操作在上文已经提到,history
内建指令的实现在下文会提到。
1 | void load_mosh_history() { |
反引号
在runcmd
中,parsecmd
前,调用substitute_backticks
函数,解析并执行反引号内的指令。
substitute_backticks
的核心处理逻辑如下:
一旦找到反引号对,它会提取其中的命令,之后
fork
一个子进程来执行这个命令,并利用pipe
捕获该命令的标准输出。父进程等待子进程执行完毕,然后从管道读取子进程的输出结果。它会移除输出结果末尾的一个换行符,然后将处理后的结果拼接到一个临时的缓冲区中。在拼接前,函数会先把反引号之前的部分字符串复制到临时缓冲区。
函数重复上述过程,直到处理完原字符串中所有的反引号。最后,用构建好的临时缓冲区内容覆盖掉原始的
command_string
,从而完成替换。
指令条件执行
指令条件执行的实现依赖于读取各指令的返回值。关于这部分内容可参看exit
指令的实现部分。
指令条件执行的核心逻辑位于parsecmd
函数中:
当解析器遇到
&&
时,我们会fork()
一个子进程去执行&&
左侧的command1
。父进程则会调用ipc_recv()
等待,直到子进程执行完毕并返回一个退出状态码。如果状态码为0
,那么就会继续解析并执行&&
右侧的command2
。如果状态码非0
,它会跳过command2
。当解析到
||
时,同样会fork()
子进程执行||
左侧的command1
,父进程等待结果。逻辑与&&
相反。只有当父进程收到的状态码为非0
(代表command1
执行失败)时,它才会继续解析并执行||
右侧的command2
。如果状态码为0
,则跳过command2
。
内建指令
几乎所有的内建指令的第一步都是要通过系统调用syscall_get_cwd
获取当前工作目录
cd
获得工作目录后,调用normalize_path
函数,将用户输入的包含.
,..
的相对路径与工作目录结合,获得绝对路径。之后使用check_if_directory
函数,检查该路径是一个目录而非文件,如果检查通过,那么使用syscall_set_cwd
更新工作目录
我将每个进程的工作目录存储在env
块中,syscall_get_cwd
和syscall_set_cwd
系统调用本质就是寻找该进程控制块,并更新或获得其env_cwd
域。
pwd
直接通过syscall_get_cwd
获取当前进程的工作目录即可
declare
与unset
为了存储用户定义的环境变量,我设计了一个EnvVar
结构体来存储相关信息
1 | struct EnvVar { |
之后设置一个专门的内存地址,并分配一页空间来存储当前进程的EnvVar
结构体
1 |
对于declare
指令处理的核心逻辑如下:
- 解析以
-
为开头的参数,根据参数设置只读或全局的flag
位 - 如果没有解析到参数,那么遍历当前环境变量并打印
- 如果解析到参数,那么按照
NAME=VALUE
的格式解析变量名和值,然后检查变量是否存在,如果存在检查是否只读,若只读则报错,否则重新赋值;如果变量不存在则寻找一个可用空间创建新的EnvVar
结构体,并按照之前设置的flag
位对相应标志进行置位
unset
指令的实现较为简单,只需要将通过find_variable
函数找到变量对应的结构体,然后将其所有域置0即可
history
实现非常简单,直接读取历史指令缓冲区即可
exit
为了能够将指令的实现结果回传给父进程,我修改了exit
函数,并新设置一个inner_exit
函数,用来返回内建指令的返回值。他们的实现逻辑是相近的,都是利用syscall_ipc_try_send
系统调用像父进程发送返回值。
1 | void exit(void) { |
相应的,父进程需要使用exit_status = ipc_recv(NULL, 0, 0);
读取子进程的返回值。
对于exit
指令来说,子进程向父进程发送特定返回值EXIT
,当父进程读取到该返回值时,立即退出。
更多指令
首先需要注意的是,由于25年的shell实现了cd
,导致工作目录会发生变化,因此所有非内建指令都需要根据工作目录和相对目录拼接出一个绝对目录,然后根据这个绝目录进行操作
mkdir
main
函数负责解析命令行参数,侦测 -p
选项并设定一个全局标志。
核心的 mkdir
函数根据此标志有两种行为:
- 标准模式:创建单个目录。若目录已存在或其父目录不存在,就会报错。
-p
模式:递归创建路径中所有不存在的父目录。若目标目录已存在,则不会报错。
另外需要对文件系统的serve_open
函数进行一定修改,使其支持创建文件夹:
1 | if ((rq->req_omode & O_MKDIR) && (r = file_create(rq->req_path, &f)) < 0 && |
touch
这个指令的实现非常简单,就是直接调用open
函数,如果返回值为错误码,那么报错并直接返回该错误码
rm
- 遍历命令行参数,查找
-r
或-rf
。 - 如果找到
-r
,设置递归标志flag_r
。如果找到-rf
,同时设置递归标志flag_r
和强制标志flag_f
。 - 对每个非选项的参数(即文件或目录路径),调用
rm
函数。
rm
函数的逻辑为
- 尝试打开路径,如果路径不存在:检查是否设置了强制标志
flag_f
。如果设置了,则忽略该错误并返回;否则,报错“文件或目录不存在”。 - 如果路径存在,获取其状态信息。如果目标是目录(
st.st_isdir
为真),但未设置递归标志flag_r
,则报错“是一个目录”。 - 如果通过以上所有检查,则调用
remove(path)
来删除该文件或目录。
- Title: OS-挑战性任务
- Author: OWPETER
- Created at : 2025-06-29 17:15:21
- Updated at : 2025-08-09 17:29:10
- Link: https://owpeter.github.io/2025/06/29/OS/OS-挑战性任务/
- License: This work is licensed under CC BY-NC-SA 4.0.