操作系统leb3实验报告

实验名称:实验3:实现Linux命令解释器

实验目的

  • 1、利用Linux命令与C语言完成自己的命令解释器
  • 2、熟悉命令执行流程

实验内容

本次实验的内容由下面几部分组成:

  1. 分析且运行myshell。
  2. 扩充myshell功能,使其支持以下内部命令:
    1. cd <目录>——更改当前的工作目录到另一个<目录>。如果<目录>未指定,输出当前工作目录。如果<目录>不存在,应当有错误信息提示。
    2. echo <内容>——显示echo后的内容且换行。
    3. help——简短概要地输出myshell的使用方法和基本功能
    4. jobs一输出 myshell当前的一系列子进程,必须提供子进程的命名和PID号。
  3. 添加重定向和管道功能。

实验环境

  1. VMware
  2. Linux

实验作业

一、主要流程

命令的分析执行过程包括:初始化环境,打印提示符,获取用户输入命令,解析命令,寻找命令文件和执行命令,如图
![image.png](https://img-blog.csdnimg.cn/img_convert/3be20126759d00b29dce5fb7ddb94f1e.png#clientId=ua1a01eaa-170f-4&from=paste&height=554&id=u55ea3973&margin=[object Object]&name=image.png&originHeight=738&originWidth=722&originalType=binary&ratio=1&size=73532&status=done&style=none&taskId=u07e65d79-3c17-4574-8732-a03f599e5c7&width=542)

二、myshell的程序框架

程序的主执行框架为

for (;;)
{
	1显示提示符
	2读入一行命令
	3判断此命令是否为“exit”,若是则退出
	4分析并执行这行命令
}

主程序myshell.c(初始)

#include<stdio.h>
#include<string.h>
#include<limits.h>
#include<unistd.h>
#include<sys/types.h>

#define PROMPT_STRING "[myshell]$"
#define QUIT_STRING "exit\n"

static char inbuf[MAX_CANON];
char * g_ptr;
char * g_lim;
extern void yylex();

int main (void)
{
	for(;;)
	{
		if(fputs(PROMPT_STRING,stdout)==EOF)//输出[myshell]$
			continue;
		if(fgets(inbuf,MAX_CANON,stdin)==NULL)//接收输入命令
			continue;
		if(strcmp(inbuf,QUIT_STRING)==0)//比较是不是exit/n
			break;
		g_ptr=inbuf;//命令
		g_lim=inbuf+strlen(inbuf);//命令加命令长度
		yylex();//由lex创建的扫描程序的入口点yylex()。调用yylex()启动或者重新开始扫描。如果lex动作执行讲数值传递给调用的程序return,那么对yylex()的下次调用就从它的停止地方继续。 规则段中的所有代码都被拷贝到yylex()。以空白开始的行被假定是用户代码。"%%"后的代码直接放置在接近扫描程序的开始处,在第一条执行的语句之前。 
	}
	return 0;
}

值得一提的是yylex()是由lex创建的扫描程序的入口点yyle。
调用yylex()启动或者重新开始扫描。如果lex动作执行讲数值传递给调用的程序return,那么对yylex()的下次调用就从它的停止地方继续。 规则段中的所有代码都被拷贝到yylex()。以空白开始的行被假定是用户代码。"%%"后的代码直接放置在接近扫描程序的开始处,在第一条执行的语句之前。
我们采用专业的语法分析工具 flex,此工具使用非常方便,也易于对语法进行扩展和修改。虽然flex包含许多内容,但这里我们只涉及其中很少的部分,读者通过简单的学习即可掌握。上文用到的yylex()函数由flex根据一个输入文件 parse.lex 生成。此文件由三部分组成,下面分别进行介绍。

parse.lex(初始)

parse.lex文件的第一部分如下,是C代码,包括后面要用的头文件、数据变量和函数原型。
//定义段
%{
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>

//input related从主函数中所得参数,即命令
extern char* g_ptr;
extern char* g_lim;

#undef YY_INPUT
#define YY_INPUT(b,r,ms)(r=my_yyinput(b,ms))//告诉flex如何使用输入信息
static int my_yyinput(char* buf,int max);

//cmd-arguments related依赖函数
#define MAX_ARG_CNT 256
static char* g_argv[MAX_ARG_CNT];
static int g_argc=0;

static void add_arg(const char* arg);
static void reset_args();

//cmd-handlers执行句柄
static void exec_simple_cmd();

//在主函数中,命令行的输入信息被放在了全局变量g_ptr和g_lim中,这里进一步告诉flex如何使用该输入信息,通过宏YY_INPUT和函数my_yyinput来具体实现。

%}

//规则段,parse.lex文件的第二部分描述了一组模式匹配规则,这是语法分析的核心。


%%
[^ \t\n]+  {add_arg(yytext);}//匹配\t\n,如果不存在,就加入到参数数组中
\n  {exec_simple_cmd(); reset_args();}//匹配\n,如果存在就执行
.     ;//忽略其他

//用户代码段,parse.lex文件的第三部分是前面用到的所有函数的定义。
%%

static void add_arg(const char* arg)//添加参数数组
{
	char *t;
	if((t=malloc(strlen(arg)+1))==NULL)
	{
		perror("Failed to allocate memory");
		return ;
	}
	strcpy(t,arg);
	g_argv[g_argc]=t;
	g_argc++;
	g_argv[g_argc]=0;
}

static void reset_args()//清空参数数组
{
	int i;
	for(i=0;i<g_argc;i++)
	{
		free(g_argv[i]);
		g_argv[i]=0;
	}
	g_argc=0;
}

static void exec_simple_cmd()//建立子进程执行
{
	pid_t childpid;
	int status;
	if((childpid=fork())==-1)
	{
		perror("Failed to fork child");
		return ;
	}
	if(childpid==0)
	{
		execvp(g_argv[0],g_argv);//
		perror("Failed to execute command");
		exit(1);
	}
	waitpid(childpid,&status,0);//回收子进程
}

static int my_yyinput(char* buf,int max)
{
	int n;
	n=g_lim-g_ptr;
	if(n>max)
	n=max;
	if(n>0)
	{
		memcpy(buf,g_ptr,n);
		g_ptr+=n;
	}

	return n;
}


规则段的扫描:如果扫描到一个不含空白字符和换行符的字符串,则将其加入到参数数组中去函数add_arg(yytext)的功能是将该字符串yytext放到参数数组g_argv中去,详细代码见下文的第三部。第3行的意思是,如果扫描到换行符,则将参数数组中的所有参数构成的序列当作一个简单命令去执行,然后清空参数数组。第4行的意思是,忽略其他所有字符(在这里,因为不匹配前两项规则的字符只剩下空白字符,所以实际忽略的是所有空白字符)。

Makefile

CC=gcc
CFLAGS=-g -Wall
LEXSRC=parse.lex
SRC=myshell.c lex.yy.c
mysh:
	flex $(LEXSRC)
	$(CC) $(CFLAGS) -o  myshell $(SRC) -lfl

函数exec_simple_cmd()执行时,所有的参数(包括命令名)都已按序放在了数组g_argvl中,此函数的功能就是创建一个子进程,来运行这个命令,并传递相应的参数,然后等待该子进程结束。
在函数 exec_simple_cmd()的第5行,调用函数fork()创建了一个子进程,随后子进程在作了第9行的条件判断后会执行第10行的函数调用execvp(),此调用将会执行参数数组中的命令并传递参数。如果上述调用成功,那么第11行将没有机会执行;否则子进程将在第11行报错后于第12行结束。父进程则在第9行作了条件判断后转而执行第14行,以等待子进程结束,然后返回。

三、知识补充

lex

Lex 是一种生成扫描器的工具。扫描器是一种识别文本中的词汇模式的程序。 这些词汇模式(或者常规表达式)在一种特殊的句子结构中定义,这个我们一会儿就要讨论。
一种匹配的常规表达式可能会包含相关的动作。这一动作可能还包括返回一个标记。 当 Lex 接收到文件或文本形式的输入时,它试图将文本与常规表达式进行匹配。 它一次读入一个输入字符,直到找到一个匹配的模式。 如果能够找到一个匹配的模式,Lex 就执行相关的动作(可能包括返回一个标记)。 另一方面,如果没有可以匹配的常规表达式,将会停止进一步的处理,Lex 将显示一个错误消息。
Lex 和 C 是强耦合的。一个 .lex 文件(Lex 文件具有 .lex 的扩展名)通过 lex 公用程序来传递,并生成 C 的输出文件。这些文件被编译为词法分析器的可执行版本。
Lex 的常规表达式
常规表达式是一种使用元语言的模式描述。表达式由符号组成。符号一般是字符和数字,但是 Lex 中还有一些具有特殊含义的其他标记。 下面两个表格定义了 Lex 中使用的一些标记并给出了几个典型的例子。
常规表达式举例

字符含义
A-Z, 0-9, a-z构成了部分模式的字符和数字。
.匹配任意字符,除了 \n。
-用来指定范围。例如:A-Z 指从 A 到 Z 之间的所有字符。
[ ]一个字符集合。匹配括号内的 任意 字符。如果第一个字符是 ^ 那么它表示否定模式。例如: [abC] 匹配 a, b, 和 C中的任何一个。
*****匹配 _0个_或者多个上述的模式。
+匹配 _1个_或者多个上述模式。
?匹配 _0个或1个_上述模式。
$作为模式的最后一个字符匹配一行的结尾。
{ }指出一个模式可能出现的次数。 例如: A{1,3} 表示 A 可能出现1次或3次。
**用来转义元字符。同样用来覆盖字符在此表中定义的特殊意义,只取字符的本意。
^否定。
****
"<一些符号>"字符的字面含义。元字符具有。
/向前匹配。如果在匹配的模版中的“/”后跟有后续表达式,只匹配模版中“/”前 面的部分。如:如果输入 A01,那么在模版 A0/1 中的 A0 是匹配的。
( )将一系列常规表达式分组。

Lex 中的标记声明类似 C 中的变量名。每个标记都有一个相关的表达式。 (下表中给出了标记和表达式的例子。) 使用这个表中的例子,我们就可以编一个字数统计的程序了。 我们的第一个任务就是说明如何声明标记。

常规表达式含义
joke[rs]匹配 jokes 或 joker。
A{1,2}shis+匹配 AAshis, Ashis, AAshi, Ashi。
(A[b-e])+匹配在 A 出现位置后跟随的从 b 到 e 的所有字符中的 0 个或 1个。
标记相关表达式含义
数字(number)([0-9])+1个或多个数字
字符(chars)[A-Za-z]任意字符
空格(blank)" "一个空格
字(word)(chars)+1个或多个 chars
变量(variable)(字符)+(数字)(字符)(数字)*

Lex 编程可以分为三步:

  1. 以 Lex 可以理解的格式指定模式相关的动作。
  2. 在这一文件上运行 Lex,生成扫描器的 C 代码。
  3. 编译和链接 C 代码,生成可执行的扫描器。

注意: 如果扫描器是用 Yacc 开发的解析器的一部分,只需要进行第一步和第二步。 关于这一特殊问题的帮助请阅读 Yacc和 将 Lex 和 Yacc 结合起来部分。
现在让我们来看一看 Lex 可以理解的程序格式。一个 Lex 程序分为三个段:第一段是 C 和 Lex 的全局声明,第二段包括模式(C 代码),第三段是补充的 C 函数。 例如, 第三段中一般都有 main() 函数。这些段以%%来分界。 那么,回到字数统计的 Lex 程序,让我们看一下程序不同段的构成。
C 和 Lex 的全局声明
这一段中我们可以增加 C 变量声明。这里我们将为字数统计程序声明一个整型变量,来保存程序统计出来的字数。 我们还将进行 Lex 的标记声明。
字数统计程序的声明

 %{
         
int wordCount = 0;
 %}
         chars [A-za-z\_\‘\.\"]
         numbers ([0-9])+
         delim [" "\n\t]
         whitespace {delim}+
         words {chars}+
         %%

两个百分号标记指出了 Lex 程序中这一段的结束和三段中第二段的开始。
Lex 的模式匹配规则
让我们看一下 Lex 描述我们所要匹配的标记的规则。(我们将使用 C 来定义标记匹配后的动作。) 继续看我们的字数统计程序,下面是标记匹配的规则。

字数统计程序中的 Lex 规则

    {words} { wordCount++; /*
         increase the word count by one*/ }
         {whitespace} { /* do
         nothing*/ }
         {numbers} { /* one may
         want to add some processing here*/ }
         %%
 

C 代码
Lex 编程的第三段,也就是最后一段覆盖了 C 的函数声明(有时是主函数)。注意这一段必须包括 yywrap() 函数。 Lex 有一套可供使用的函数和变量。 其中之一就是 yywrap。 一般来说,yywrap() 的定义如下例。我们将在 高级 Lex 中探讨这一问题

void main()         
{         
    yylex(); /* start the analysis*/         
    printf(" No of words:%d\n", wordCount);         
}         
int yywrap()         
{         
    return 1;         
} 

高级 Lex
Lex 有几个函数和变量提供了不同的信息,可以用来编译实现复杂函数的程序。 下表中列出了一些变量和函数,以及它们的使用。 详尽的列表请参考 Lex 或 Flex 手册(见后文的 资源)。
Lex 变量

yyinFILE* 类型。 它指向 lexer 正在解析的当前文件。
yyoutFILE* 类型。 它指向记录 lexer 输出的位置。 缺省情况下,yyin 和 yyout 都指向标准输入和输出。
yytext匹配模式的文本存储在这一变量中(char*)。
yyleng给出匹配模式的长度。
yylineno提供当前的行数信息。 (lexer不一定支持。)

Lex 函数

yylex()这一函数开始分析。 它由 Lex 自动生成。
yywrap()这一函数在文件(或输入)的末尾调用。 如果函数的返回值是1,就停止解析。 因此它可以用来解析多个文件。 代码可以写在第三段,这就能够解析多个文件。 方法是使用 yyin 文件指针(见上表)指向不同的文件,直到所有的文件都被解析。 最后,yywrap() 可以返回 1 来表示解析的结束。
yyless(int n)这一函数可以用来送回除了前?n? 个字符外的所有读出标记。
yymore()这一函数告诉 Lexer 将下一个标记附加到当前标记后。

Makefile

Makefile是组织代码编译的一种简单办法。make工具和makefile是比较复杂且强大的,本教程甚至还没有触及到make工具的皮毛,但是作为入门指南,它可以帮助你快速又轻松地为中小型项目创建自己的Makefile。
一个简单的例子
首先我们需要准备三个文件。这三个文件分别代表主程序,工具函数的实现和声明。

//hellomake.c
#include<hellomake.h>
int main()
{
    // call a func in another file.
    myPrintHelloMake();
    return 0;
}

//hellofunc.c
#include<stdio.h>
#include<hellomake.h>

void myPrintHelloMake()
{
    printf("Hello, makefiles!\n");
    return;
}

//hellomake.h
void myPrintHelloMake();

有了这三个文件,我们可以用下面的指令来编译
gcc -o hellomake hellomake.c hellofunc.c -I.
这条指令编译两个c文件,并且生成可执行文件hellomake
-I.参数告诉工gcc从当前目录寻找头文件hellomake.h。如果没有makefile的话,我们在测试/修改/调试代码的时候一般可以在terminal上用上下键切换编译指令,这样就不用每编译一次都要敲一次编译指令了。
但是不幸的是,这种方法有两个缺点:1, 如果你不小心丢掉了编译指令(比如不小心关掉了terminal)或者换了一个电脑,你就得重新敲一遍编译指令;2,就算你只修改了某一个c文件,你也必须把所有的源文件全部重新编译一次,这个是非常耗时间的,也是完全没有必要的。
这两个问题都可以通过写一个makefile来解决。
最简单的Makefile文件长这样:

hellomake: hellomake.c hellofun.c
    gcc -o hellomake hellomake,c hellofun.c -I.

把这两行写入到一个名为Makefile或makefile的文件中,然后在terminal输入make,系统就会按照makefile文件中定义的规则编译你的代码。注意,make命令不带参数的话,会默认执行makefile文件中的第一条规则。此外,通过将命令依赖的文件列表放在:之后的第一行,make就知道如果其中的任何文件发生更改,就需要执行hellomake规则。这样就马上解决了第一个问题,不需要在terminal中上下翻找最后一个编译命令了。但是这样写,make仍然不能有效的只编译最新的更改。
有一点需要特别注意的是,在gcc命令之前有一个tab字符。事实上,在任何指令之前,都必须加一个tab字符,这是make的要求,不加tab, make会不开心的:)
更进一步,让makefile更加高效一点。

CC=gcc
CFLAG=-I.
hellomake:hellomake.o hellofunc.o
    $(CC) -o hellomake hellomake.o hellofunc.o


现在,我们在makefile中定义了一些常量,CC和CFLAG,这些常量告诉make如何编译hellomake.c和hellofunc.c文件。CC宏定义了要使用哪个编译器,CFLAG是一些编译标志。通过将目标文件hellomake.o和hellofunc.o放在依赖列表中,make就知道首先需要编译c文件得到目标文件,然后链接得到可执行文件hellomake。对于大多数小型项目来说,这种makefile已经足够了。
但是上个版本的makefile还忽略了一点:对头文件的依赖。如果你修改了hellomake.h文件,然后重新执行make,这时候即使需要重新编译c文件,make也不会重编的。为了解决这个问题,我们得告诉make,c文件依赖哪些h文件。

CC=gcc
CFLAG=-I.
DEPS = hellomake.h

%.o:%.c $(DEPS)
    $(CC) -c -o $@ $<  $(CFLAGS)
hellomake: hellomake.o hellofunc.o
    $(CC) -o hellomake hellomake.o hellofunc.o


在这个版本中,我们新加了一个常量DEPS,这是c文件依赖的头文件集合。然后,我们又定义了一个适用于所有*.o文件的规则,这个规则说明.o文件依赖于同名的.c文件和DEPS中包含的头文件。为了产生.o文件,make需要使用CC常量定义的编译器编译.c文件。-c标志表示产生目标文件, -o $@表示将输出文件命名为:左边的文件名, < 表 示 依 赖 列 表 中 的 第 一 个 项 。 最 后 使 用 特 殊 的 宏 <表示依赖列表中的第一个项。 最后使用特殊的宏 <使@ 做 最 后 一 次 简 化 , 让 编 译 规 则 更 加 通 用 。 ^做最后一次简化,让编译规则更加通用。 @ $^分别表示:左边和右边。在下面的例子中,所有的头文件都应该作为DEPS宏的一部分,所有的目标文件*.o都应该作为OBJ宏的一部分。

CC=gcc
CFLAGS=-I.
DEPS = hellomake.h
OBJ = hellomake.o hellofunc.o

%.o:%.c $(DEPS)
    $(CC) -c -o $@ $<  $(CFLAGS)
hellomake: $(OBJ)
    $(CC) -o $@ $^ $(CFLAGS)


如果我们想把头文件,源文件和其他库文件分别放在不同的文件夹,那么makefile该怎么写呢?另外,我们可以隐藏那些烦人的中间文件(目标文件)吗?当然可以!
下面的makefile定义了include文件夹,lib文件夹路径,并且把目标文件放到src文件夹的子文件夹obj里面。同时,还定义了一个宏,用于包含任何你想要包含的库,比如math库-lm。这个makefile文件应该位于src目录,注意,这个makefile 还包含了一个规则用于清理source和obj文件夹,只需要输入make clean即可。.PHONY规则可以让make不去改动任何名为clean的文件(如果有的话)。

IDIR = ../include
CC=gcc
CFLAGS=-I$(IDIR)

ODIR=obj
LDIR=../lib

LIBS=-lm

_DEPS=hellomake.h
DEPS=$(pathsubst %,$(IDIR)/%,$(_DEPS))

_OBJ= hellomake.o hellofunc.o
OBJ = $(pathsubst %,$(ODIR)/%,$(_OBJ))

$(ODIR)/%.o:%.c $(DEPS)
    $(CC -c -o $@ $< $(CFLAGS
hellomake:$(OBJS)
    $(CC) -o $@ $^ $(CFLAGS) $(LIBS)
.PHONY:clean
clean:
    rm -f $(ODIR)/*.o *~ core $(INCDIR)/*~


现在,我们有了一个非常不错的makefile,你可以对其进行简单的修改来管理中小型软件项目了。你也可以将多个规则写到一个makefile里面,甚至可以在一个规则中调用其他规则。\

execvp

int execvp(const char* file, const char* argv[]);

  • (1) 第一个参数是要运行的文件,会在环境变量PATH中查找file,并执行.
  • (2) 第二个参数,是一个参数列表,如同在shell中调用程序一样,参数列表为0,1,2,3……因此,wensen.sh 作为第0个参数,需要重复一遍.
  • (3) argv列表最后一个必须是 NULL.
  • (4) 失败会返回-1, 成功无返回值,但是,失败会在当前进程运行,执行成功后,直接结束当前进程,可以在子进程中运行.

dup

![](https://img-blog.csdnimg.cn/img_convert/a1455d2adb6a6ef5eaf70ddec19abbf5.png#from=url&id=fT4NJ&margin=[object Object]&originHeight=533&originWidth=707&originalType=binary&ratio=1&status=done&style=none)

#include <unistd.h> 
int dup(int oldfd); 
int dup2(int oldfd, int newfd);

当调用dup函数时,内核在进程中创建一个新的文件描述符,此描述符是当前可用文件描述符的最小数值,这个文件描述符指向oldfd所拥有的文件表项。
dup2和dup的区别就是可以用newfd参数指定新描述符的数值,如果newfd已经打开,则先将其关闭。如果newfd等于oldfd,则dup2返回newfd, 而不关闭它。dup2函数返回的新文件描述符同样与参数oldfd共享同一文件表项。

实验结果

  1. 扩充myshell功能,使其支持以下内部命令:
    1. cd <目录>——更改当前的工作目录到另一个<目录>。如果<目录>未指定,输出当前工作目录。如果<目录>不存在,应当有错误信息提示。

利用chdir()这个系统调用,将第一个参数值作为目录值,跳转至相应目录。

  1. echo <内容>——显示echo后的内容且换行。

  1. help——简短概要地输出myshell的使用方法和基本功能

  1. jobs一输出 myshell当前的一系列子进程,必须提供子进程的命名和PID号。

首先扫描整个参数类表,判断指令,不同指令进入不同分支,完成相应的功能。

  1. 添加重定向和管道功能。

因为利用正则表达式来确定函数调用,为了方便,所以把这三个功能放在了一个reDirect()函数里面,然后在这个函数中根据不同的cmd_flag进行不同的功能执行。
当cmd_flag == SP_PIPE:
此时执行管道功能,由于用for循环执行子进程不方便,当然可以调用子函数,不过这里直接就只在主程序中调用了两个子程序来执行一个管道的两条命令,如果要实现多条管道,因为在数据结构上已经得到了支持,所以只需要多一些条件判断语句即可。管道执行的过程如下:
1) 初始化管道的读、写端。
2) 用系统调用pipe()创建个管道。
3)调用一个子进程来执行’|’前面的命令,调用dup2()函数将该命令的标准输出端连接到管道的输入端,然后关闭管道的输入端,执行该命令,再调用另外一个子进程来执行’|’后面的命令,将该命令的标准输入端和管道的输出端连接,然后再关闭管道输出端,执行命令。
当cmd_flag == SP_IN:
输入重定向就是将该命令的标准输入重定向到文件中去,先用acess()判断文件是否存在,如果存在继续运行。用open系统调用以只读方式打开命令中指出的文件,然后用dup2用刚刚打开的文件标识符来替换掉标准输入,这样就实现了重定向,最后用调用close关闭文件,不让它继续被用。
接下来的操作同外部命令的处理一样,利用fork()调用子进程,然后通过execvp()执行此命令,waitpid等待子进程结束发信号给父进程。
当cmd_flag == SP_OUT:
输出重定向就是把改变命令的输出对象。先用open系统调用打开命令中指出的文件,第二个参数是O_CREAT|O_RDWR|O_TRUNC,即若文件不存在就创建一个,然后覆盖掉文件中以前存在的内容,将命令的输出重新写入。和上面差不多,直接通过调用dup2()用刚刚打开的文件替换掉标准输出,这样就实现了重定向,最后用调用close关闭文件,不让它继续被用。
接下来的操作同外部命令的处理一样,利用fork()调用子进程,然后通过execvp()执行此命令,waitpid等待子进程结束发信号给父进程
运行代码:myshell.c

#include<stdio.h>
#include<string.h>
#include<limits.h>
#include<unistd.h>
#include<sys/types.h>

#define PROMPT_STRING "[myshell]$"
#define QUIT_STRING "exit\n"

static char inbuf[MAX_CANON];
char * g_ptr;
char * g_lim;
extern void yylex();

static void konge(char*pStr)
{
	char *pTmp = pStr;
	while (*pStr)
	{
		if (*pStr != ' ')
		{
			*pTmp++ = *pStr;
		}
		++pStr;
	}
	*pTmp = '\0';
}




int main (int agrc,char** agrv)
{
	for(;;)
	{
		if(fputs(PROMPT_STRING,stdout)==EOF)
			continue;
		if(fgets(inbuf,MAX_CANON,stdin)==NULL)
			continue;
		if(strcmp(inbuf,QUIT_STRING)==0)
			break;
		g_ptr=inbuf;
		g_lim=inbuf+strlen(inbuf);
		
		if(inbuf[0]=='c'&&inbuf[1]=='d')
		{
			konge(inbuf);// qu chu kong ge
			int i=strlen(inbuf)-1;
			char path[100]={0};
			strncpy(path,inbuf+2,i-2);//du qu dizhi
			printf("%s\n",path);
			chdir(path);//zhi xing 
		
		}
		else if(strcmp(inbuf,"help\n")==0)
		{
			printf("\twelcome use myshell\n\tthis is help:\n\t\tls\tcha kan dang qian mu lu\n\t\tcd\tgen gai mu lu\n");
			printf("\t\tjobs\tjing cheng xing xi\n");
		}
		else
		{
			yylex();
		}
	}
	return 0;
}

parse.lex

%{
#include<errno.h>
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include <fcntl.h>
#include<sys/types.h>
#include<sys/wait.h>

//input related
extern char* g_ptr;
extern char* g_lim;

char *str1="jobs";
static char *str2 = ">";
static char *str3 = "|";
int loc_len = 0;
int pipe_len = 0;
#undef YY_INPUT
#define YY_INPUT(b,r,ms)(r=my_yyinput(b,ms))
static int my_yyinput(char* buf,int max);

//cmd-arguments related
#define MAX_ARG_CNT 256
static char* g_argv[MAX_ARG_CNT];
static int g_argc=0;
static char* g_argv1[MAX_ARG_CNT];
static int g_argc1 = 0;

static void add_arg(const char* arg);
static void reset_args();

//cmd-handlers
static void exec_simple_cmd();
static void redirect();


%}

%%
[^ \t\n]+  {add_arg(yytext);}
\n  {exec_simple_cmd(); reset_args();}
.     ;
%%

static void add_arg(const char* arg)
{
	char *t;
	if((t=malloc(strlen(arg)+1))==NULL)
	{
		perror("Failed to allocate memory");
		return ;
	}
	strcpy(t,arg);
	if(!strcmp(t,str2))
	{
		loc_len = g_argc;
	}
	if(!strcmp(t,str3))
	{
		pipe_len= g_argc;
	}
	if (pipe_len)
	{
		if(strcmp(t,str3))
		{
		g_argv1[g_argc1] = t;
		g_argc1++;
		g_argv1[g_argc1] = 0;
		}
	}
	else
	{
		g_argv[g_argc]=t;
		g_argc++;
		g_argv[g_argc]=0;
	}
}

static void reset_args()
{
	int i;
	for(i=0;i<g_argc;i++)
	{
		free(g_argv[i]);
		g_argv[i]=0;
	}
	g_argc=0;
	for (i = 0; i < g_argc1; i++)
   	{
		free(g_argv1[i]);
		g_argv1[i] = 0;
	}
	g_argc1 = 0;
	loc_len = 0;
	pipe_len = 0;
	
}

static void exec_simple_cmd()
{
	pid_t childpid,childpid1;
	int status,status1;
	if((childpid=fork())==-1)
	{
		perror("Failed to fork child");
		return ;
	}
	if(childpid==0)
	{
		if(!strcmp(g_argv[0],str1))
		{
			char str[100];
			sprintf(str,"ps -o 'pid ppid cmd'|awk '{if($2==%d){print $0}}'",getpid());
 			system(str);
		}
		else if(loc_len != 0)
		{
			redirect();
		}
		else if(pipe_len!=0)
		{
			int fd[2];
			pipe(fd);
			if ((childpid1 = fork()) == -1)
			{
				perror("fork error"); exit(1);
			}
			if (childpid1 == 0)
			{
				close(fd[1]);
				dup2(fd[0], 0);
				execvp(g_argv1[0], g_argv1);
				exit(1);
			}
			else
			{
				close(fd[0]);
				dup2(fd[1], 1);
				execvp(g_argv[0], g_argv);
				exit(1);
			}
			waitpid(childpid1, &status1, 0);
		}
		else
		{
			execvp(g_argv[0],g_argv);
			perror("Failed to execute command");
			exit(1);
		}
	}
	waitpid(childpid,&status,0);
}

static int my_yyinput(char* buf,int max)
{
	int n;
	n=g_lim-g_ptr;
	if(n>max)
	n=max;
	if(n>0)
	{
		memcpy(buf,g_ptr,n);
		g_ptr+=n;
	}

	return n;
}

static void redirect()
{
	int fd_file = open(g_argv[g_argc - 1], O_RDWR|O_CREAT, 0666);
	//备份输出描述符
	int fd_backup = dup(STDOUT_FILENO);
	/* 重定向标准输出到文件 */
	dup2(fd_file, STDOUT_FILENO);
	close(fd_file);/* 重定向后可以关闭*/
	g_argv[loc_len] = 0;
	execvp(g_argv[0], g_argv);
	dup2(fd_backup, STDOUT_FILENO);
	close(fd_backup);/* 重定向后可以关闭*/

}

运行图:
![image.png](https://img-blog.csdnimg.cn/img_convert/0eef25c7c3b53e5fd6ce96ea6a3dc7ea.png#clientId=ua1a01eaa-170f-4&from=paste&height=297&id=ua64b0c43&margin=[object Object]&name=image.png&originHeight=396&originWidth=641&originalType=binary&ratio=1&size=108592&status=done&style=none&taskId=u715c8ceb-0ce7-406a-ad1d-0363b3aa7c1&width=481)

实验总结

此次课程设计其实在一开始编写时候觉得有点困难,因为对LINUX不是非常了解,但后来上网查找资料学习后,学会调用外部的函数达到与系统相同功能即可,这样一来很多问题都可以解决了,许多功能直接调用外部的函数即可实现所需要的功能。花的时间最多的是在Copy函数的编写,出现段错误等问题。最后还是一一解决了。
本次课程设计中,可谓收获颇大,首先从一开始对Linux一无所知,到做完课设后有了一定程度的了解,如果没有这次课设,我想我很难有机会去进一步熟悉Linux这个操作系统以及在Linux下的编程。
通过这次课程设计,我不仅对LINUX加深了了解,也强化了C语言编程的能力,这些学到的东西却可以使我受益终生。除了知识技术上的东西,我更锻炼了自己的快速学习能力和编程实战能力;我学会了如何快速有效地从网络获取自己需要的信息。同时这次设计给我们很大收获,使我们对操作系统的基本知识和基本命令有了进一步的提高,能让我们更早的适应社会。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值