该文档是关于CS 537项目3——Shell的说明,主要内容包括项目的截止日期、迟交政策、测试相关信息、协作要求、在Linux实验室机器上完成项目、提交方式(包括代码文件和README.md文件)以及对使用在线资源的要求等。此外,还详细介绍了该项目的具体内容,即构建一个简单的Unix Shell,包括基本Shell的功能、管道、环境变量和Shell变量、路径、历史记录以及内置命令等方面的规范和要求。
管理信息
- 截止日期:2月20日,晚上11:59
- 项目最多可延迟3天提交,但每延迟一天将被扣10个百分点。
- 延迟天数:
- 如果您需要在项目上额外的时间,每个人将有2个个人项目的延迟天数和3个小组项目的延迟天数(本学期总共5个延迟天数)。截止日期后,我们将为按时评分复制提交目录的副本。
- 要使用延迟天数或延迟提交作业,您将提交文件以及一个额外的文件,该文件仅包含一个数字,表示您的作业延迟的天数(例如1、2、3)。每连续一天,我们将复制任何包含这些slipdays.txt文件的目录。当您提交最终提交时,此文件必须存在,否则我们将不知道对您的代码进行评分。
- 我们将跟踪您从一个项目到另一个项目的延迟天数和延迟提交情况,并在您用完延迟天数后开始扣除百分比。
- 在用完延迟天数后,如果延迟1天提交,您最多可以获得90%的分数;延迟2天,最多可以获得80%的分数;延迟3天,最多可以获得70%的分数,但对于任何单个作业,如果没有例外情况,我们将在第三天后不接受提交。这意味着如果您在单个作业上使用了两个个人延迟天数,您只能再额外延迟一天提交,总共延迟3天,扣除10%。
- 任何例外情况都需要向教师请求。
- 示例slipdays.txt
1
- 测试将在
~cs537 - 1/tests/P3
中提供。该目录中有一个README.md
文件,其中包含运行测试的说明。测试部分完成,您的项目可能会根据其他测试进行评分,但是这些测试旨在尽可能完整地涵盖我们希望您处理的解析和边界情况。 - 问题:我们将使用Piazza回答所有问题。
- 协作:作业必须由您自己完成。复制代码(来自他人)被视为作弊。阅读此内容以获取更多关于哪些是可以的,哪些是不可以的信息。 请帮助我们所有人度过一个愉快的学期,不要这样做。
- 这个项目将在Linux实验室机器上完成, 因此您可以更多地了解在典型的基于UNIX的平台(Linux)上用C编程。您的解决方案将在这些机器上进行测试。
- 提交方式:将
wsh.c
和您可能编写的任何头文件复制到您的提交目录,~cs537 - 1/handin/<登录名>/P3/
。请不要编写多个.c
文件。您还应该创建一个README.md
文件来描述您的实现。该文件应包括您的姓名、您的cs登录名、您的wisc ID和电子邮件,以及您的实现状态。如果一切正常,就这么说。如果有您知道不起作用的东西,请告诉我们。 - 如果适用,一个名为resources.txt的描述使用的在线资源的文档。欢迎您使用在线资源来帮助您完成作业。我们不建议您使用大型语言模型,如ChatGPT。对于本课程,我们已经看到这些工具给出的示例或解释接近但不完全正确,如果学生本身不知道正确答案,会让他们更加困惑。请注意,当您向教学人员寻求帮助时,我们不会协助您使用这些LLM,并且我们希望您能够向教学人员介绍您的代码和逻辑。在线资源(例如堆栈溢出)和生成工具正在改变包括计算机科学和教育在内的许多行业。但是,如果您使用在线资源,您需要提交一份文档,描述您对这些资源的使用。在该文档中指出您的解决方案中有多少是完全由您自己完成的,有多少是使用这些工具完成的。请具体说明,指出使用的资源以及您与这些资源的交互方式。不给予外部资源信用是一种剽窃形式。在您的代码中使用该资源的地方对其进行注释是一个好习惯。您不会因为使用LLM或阅读帖子而受到惩罚,但您不应该在在线论坛上发布关于本课程项目的帖子。您的大部分代码也应该由您自己努力编写,并且您应该能够解释您提交的所有代码。
Unix Shell
在这个项目中,您将构建一个简单的Unix Shell。Shell是命令行界面的核心,因此是Unix/C编程环境的核心。熟练掌握Shell的使用对于在这个领域变得精通是必要的;了解Shell本身是如何构建的是这个项目的重点。 这个作业有三个具体目标:
-
进一步熟悉Linux编程环境。
-
学习进程是如何创建、销毁和管理的。
-
接触Shell中必要的功能。
概述
在这个作业中,您将实现一个命令行解释器(CLI),或者更常见的叫法,一个Shell。Shell应该以这种基本方式运行:当您输入一个命令(响应其提示)时,Shell创建一个子进程来执行您输入的命令,然后在完成后等待更多用户输入时打印一个新的提示。这将重复进行,直到用户输入exit
。您的可执行文件的名称应该是wsh
。 Shell可以在没有参数或单个参数的情况下调用;其他任何情况都是错误的。这是没有参数的方式:
prompt>./wsh
wsh>
此时,wsh
正在运行,并准备接受命令。尽情输入吧! 上面的模式称为交互模式,允许用户直接输入命令。Shell还支持批处理模式,它从批处理文件中读取输入并从其中执行命令。以下是如何使用名为script.wsh
的批处理文件运行Shell:
prompt>./wsh script.wsh
批处理模式和交互模式的一个区别是:在交互模式中,会打印一个提示(wsh>
)。在批处理模式中,不应打印提示。 您应该构建您的Shell,使其为每个新命令创建一个进程(内置命令除外,如下所述)。您的基本Shell应该能够解析命令并运行与该命令对应的程序。例如,如果用户输入ls - la / tmp
,您的Shell应该运行程序/ bin / ls
,并带有给定的参数- la
和/ tmp
(Shell如何知道运行/ bin / ls
?这是一个称为Shell 路径的东西;下面会有更多介绍)。
结构
基本Shell
Shell非常简单(概念上):它在一个while循环中运行,反复请求输入以告诉它要执行什么命令。然后它执行该命令。这个循环会无限期地继续,直到用户输入内置命令exit
,此时它会退出。就是这样! 对于读取输入行,您应该使用getline()
。这允许您轻松获取任意长的输入行。通常,Shell将在交互模式下运行,用户输入一个命令(一次一个),Shell对其进行操作。然而,您的Shell也将支持批处理模式,在这种模式下,Shell会得到一个命令输入文件;在这种情况下,Shell不应从stdin
读取用户输入,而应从该文件中获取要执行的命令。 在任何一种模式下,如果您到达文件结束标记(EOF),您应该调用exit(0)
并优雅地退出。EOF可以通过按Ctrl - D
生成。 要将输入行解析为组成部分,您可能想要使用strsep()
。仔细阅读手册页以获取更多详细信息。 要执行命令,请查看fork()
、exec()
和wait()/waitpid()
。查看这些函数的手册页,并阅读相关的书籍章节以获得简要概述。 您会注意到exec
系列中有多种命令;对于这个项目,您必须使用execvp
。您不应该使用system()
库函数调用来运行命令。请记住,如果execvp()
成功,它将不会返回;如果它返回,说明存在错误(例如,命令不存在)。最具挑战性的部分是正确指定参数。
管道
管道是基本的UNIX概念之一,wsh
支持它。它允许将多个简单程序组合在一起,以创建具有复杂行为的命令。这非常强大,以至于它在Shell脚本中基本无处不在,因为您可以用很少的代码表达非常复杂的行为。管道,用|
表示,将左侧程序的标准输出重定向到右侧程序的输入。我们建议使用dup2
来重定向子进程的标准输出。
<program1> <arglist1> | <program2> <arglist2> |... | <programN> <arglistN>
下面的示例展示了一个命令,它压缩文件f.txt
,解压缩它并打印解压缩文件的最后10行。
cat f.txt | gzip - c | gunzip - c | tail - n 10
这个管道流中的进程都应该具有相同的进程组ID。您可以在这里阅读更多关于进程和进程组ID的信息。
我们的Shell不应该在管道中处理内置命令(如下所述)。也就是说,在管道中,每个命令都被视为用户程序,并通过execvp执行
环境变量和Shell变量
每个Linux进程都有其一组环境变量。这些变量存储在environ
外部变量中。您应该使用man environ
来了解更多关于此的信息。 关于环境的一些重要事项如下: 1. 当调用fork
时,子进程会获得environ
变量的副本。 2. 当使用exec
系列的系统调用时,新进程将根据使用的确切系统调用获得environ
变量作为其环境或用户指定的环境。请参阅man 3 exec
。 3. 有一些函数,如getenv
和setenv
,允许您查看和更改当前进程的环境。请参阅man environ
以获取更多详细信息。 Shell变量与环境变量不同。它们只能在Shell命令中使用,仅在Shell的当前会话中有效,并且不会被Shell创建的任何子进程继承。 我们使用内置的local
命令来定义和设置一个Shell变量:
local MYSHELLVARNAME = somevalue
然后可以在命令中使用这个变量,如下所示:
cd $MYSHELLVARNAME
这将转换为
cd somevalue
在我们的Shell实现中,不存在的变量应该被替换为空字符串。对空字符串的赋值将清除该变量。 环境变量可以通过使用内置的export
命令进行添加或修改,如下所示:
export MYENVVARNAME = somevalue
执行此命令后,MYENVVARNAME
变量将存在于Shell生成的任何子进程的环境中。
变量替换:每当$
符号用于标记的开头时,它总是后跟一个变量名。当Shell解释命令时,变量值应直接替换为它们的名称。在我们的Shell中,标记总是由空格分隔,并且变量名和值保证每个都是一个单独的标记。例如,对于命令mv $ab $cd,
,您需要替换变量ab
和cd
。如果一个变量同时作为环境变量和Shell变量存在,则环境变量优先。请注意,对于一个标记被视为变量名,该标记必须以$
符号开头,这意味着在命令如cd ab$ef
中,标记ab$ef
是一个正常标记,它不是一个变量名,我们的Shell不得尝试替换$ef
。 您可以在处理变量赋值时假设以下情况: – 每行最多有一个变量赋值。 – 包含变量赋值的行不会包括管道或任何其他命令。 – 变量的整个值将在同一行上,跟随=
运算符。不会有多行值;您不需要担心值周围的引号。 – 变量名和值不会包含空格或=
字符。 – 您应该能够赋值的变量数量没有限制。 – 不会有递归变量定义的形式,如local anothervar = $previousvar
显示变量:env
实用程序程序(不是Shell内置的)可用于打印环境变量。对于本地变量,我们在Shell中使用一个名为vars
的内置命令。Vars将以<var>=<value>
的格式打印所有本地变量及其值,每行一个变量。变量应按插入顺序打印,最近创建的变量最后打印。对现有变量的更新将在变量列表中就地修改它们,而不会在列表中移动它们。这里是一个示例:
wsh> local a = b
wsh> local c = d
wsh> vars
a = b
c = d
wsh>
路径
在我们上面的示例中,用户输入ls
,但Shell知道执行程序/ bin / ls
。您的Shell如何知道这一点? 原来有一个PATH环境变量来描述搜索可执行文件的目录集;构成路径的目录集有时称为Shell的搜索路径。路径变量包含当用户输入命令时要搜索的所有目录的列表,按顺序排列。您可以通过在默认Shell(例如bash
)中运行echo $PATH
来找出您的路径。
重要:请注意,Shell本身并不实现ls
或其他命令(除了内置命令)。它所做的只是在路径指定的目录之一中找到这些可执行文件,并创建一个新进程来运行它们。 在exec
系列的命令中,某些命令将允许程序员指定搜索可执行文件的目录,而其他命令将使用PATH
环境变量的值。我们鼓励您使用后一种类型。 注意:大多数Shell允许您使用绝对路径或相对路径指定二进制文件而不使用搜索路径。例如,用户可以输入绝对路径/ bin / ls
并执行ls
二进制文件而不需要搜索路径。用户也可以指定一个相对路径,该路径以当前工作目录开头并直接指定可执行文件,例如./main
。在这个项目中,您不必担心这些功能。
历史记录
您的Shell还将跟踪用户执行的最后五个命令。使用内置命令history
来显示历史记录列表,如下所示。如果连续多次执行相同的命令,它应该只存储在历史记录列表中一次。最近的命令是编号1。内置命令不应存储在历史记录中。
wsh> history
1) man sleep
2) man exec
3) rm - rf a
4) mkdir a
5) ls
默认情况下,历史记录应存储最多五个命令。历史记录的长度应可配置,使用history set <n>
,其中n
是一个整数。如果历史记录中的命令少于其容量,只需打印存储的命令(不要为空插槽打印空行)。如果使用history set
缩小较大的历史记录,请删除不再适合历史记录的命令。 要从历史记录中执行命令,请使用history <n>
,其中n
是历史记录中的第n个命令。例如,在上面的示例中运行history 1
应该再次执行man sleep
。以这种方式执行的历史记录列表中的命令不应记录在历史记录中。这意味着连续运行history n
应该重复运行相同的命令。 如果history被调用的整数大于历史记录的容量,或者如果history被调用的数字没有对应的命令,它将什么也不做,并且Shell应该打印下一个提示。
内置命令
每当您的Shell接受一个命令时,它应该检查该命令是否是内置命令。内置命令应该直接在您的Shell中执行,而无需分叉和执行新的进程。例如,要实现exit
内置命令,您只需在您的wsh源代码中调用exit(0);
,然后它将退出Shell。 以下是wsh
的内置命令列表:
-
exit
:当用户输入exit时,您的Shell应该简单地以0作为参数调用exit
系统调用。向exit
传递任何参数都是错误的。 -
cd
:cd
总是接受一个参数(0个或多于1个参数都应被视为错误)。要更改目录,请使用用户提供的参数调用chdir()
系统调用;如果chdir
失败,也是一个错误。 -
export
:用作export VAR=<value>
来创建或将变量VAR
分配为环境变量。 -
local
:用作local VAR=<value>
来创建或将变量VAR
分配为Shell变量。 -
vars
:在“环境变量和Shell变量”部分前面描述过。 -
history
:在历史记录部分前面描述过。
其他提示
请记住,在担心所有错误条件和最终情况之前,先使Shell的基本功能正常工作。例如,首先使单个命令运行(可能首先是一个没有参数的命令,如ls
)。 接下来,添加内置命令。然后,尝试处理管道和终端控制。每一项都需要在解析方面多做一些努力,但每一项都不应该太难实现。建议您将解析和执行的过程分开 –