Linux下具有基本功能的shell的具体代码实现(详细)

注册 Vultr VPS 送你10美金 免费玩4个月

在前几个月对Linux的学习过程中,一直在与shell进行交互,感觉shell充满了神秘感。偶然看到一篇文章讲解了shell的实现,感觉也不是很难的样子,于是自己也开始开发自己的minishell,顺便也巩固了前一段时间学习的linux系统编程的知识。

先来展示一下我的这个minishell实现的功能:


1. 支持ls,touch,wc 等外部命令
2. 支持输入输出重定向符

3. 支持管道命令

4 .支持后台作业

5. 支持cd,jobs,kill,exit等内部命令(自己还写了一个about 命令 ^ _ ^)

6. 支持对ctrl+c 和ctrl +z 信号的处理


接下来我们按照编写的步骤一一来分析:

(一)命令的解析

输入命令的解析在本程序中占到了很大的比重,虽然像这种解析普通命令的程序(正则表达式太难了。。)的解释器难度不大,但是健壮性和全面性还是需要周全考虑的。
这里采用了分段解析,先除去起始空格,制表符等,并以此和一些‘|’,‘<’为分割界限来解析命令至COMMAND结构体。直接看代码吧,注释很详细!

/*
 * 解析命令
 * 成功返回解析到的命令个数,失败返回-1
 */
int parse_command(void)
{
	/* cat < test.txt | grep -n public > test2.txt & */
	if (check("/n"))
		return 0;

	/* 判断是否内部命令并执行它 */
	if (builtin())
		return 0;


	/* 1、解析第一条简单命令 */
       
	get_command(0);
	/* 2、判定是否有输入重定向符 */
	if (check("<"))
		getname(infile);
	/* 3、判定是否有管道 */
	int i;
	for (i=1; i<PIPELINE; ++i)
	{
		if (check("|"))
			get_command(i);
		else
			break;
	}
	/* 4、判定是否有输出重定向符 */
	if (check(">"))
	{
		if (check(">"))
			append = 1;
		getname(outfile);
	}
	/* 5、判定是否后台作业 */
	if (check("&"))
		backgnd = 1;
	/* 6、判定命令结束‘/n’*/
	if (check("/n"))
	{
		cmd_count = i;
		return cmd_count;
	}
	else
	{
		fprintf(stderr, "Command line syntax error/n");
		return -1;
	}
}

/*
 * 解析简单命令至cmd[i]
 * 提取cmdline中的命令参数到avline数组中,
 * 并且将COMMAND结构中的args[]中的每个指针指向这些字符串
 */
void get_command(int i)
{
	/*   cat < test.txt | grep -n public > test2.txt & */

	int j = 0;
	int inword;
	while (*lineptr != '/0')
	{
		/* 去除空格 */
		while (*lineptr == ' ' || *lineptr == '/t')
			lineptr++;

		/* 将第i条命令第j个参数指向avptr */
		cmd[i].args[j] = avptr;
		/* 提取参数 */
		while (*lineptr != '/0'
			&& *lineptr != ' '
			&& *lineptr != '/t'
			&& *lineptr != '>'
			&& *lineptr != '<'
			&& *lineptr != '|'
			&& *lineptr != '&'
			&& *lineptr != '/n')
		{
				/* 参数提取至avptr指针所向的数组avline */
				*avptr++ = *lineptr++;
				inword = 1;
		}
		*avptr++ = '/0';
		switch (*lineptr)
		{
		case ' ':
		case '/t':
			inword = 0;
			j++;
			break;
		case '<':
		case '>':
		case '|':
		case '&':
		case '/n':
			if (inword == 0)
				cmd[i].args[j] = NULL;
			return;
		default: /* for '/0' */
			return;
		}
	}
}

/*
 * 将lineptr中的字符串与str进行匹配
 * 成功返回1,lineptr移过所匹配的字符串
 * 失败返回0,lineptr保持不变
 */
int check(const char *str)
{
	char *p;
	while (*lineptr == ' ' || *lineptr == '/t')
		lineptr++;

	p = lineptr;
	while (*str != '/0' && *str == *p)
	{
		str++;
		p++;
	}

	if (*str == '/0')
	{
		lineptr = p;	/* lineptr移过所匹配的字符串 */
		return 1;
	}

	/* lineptr保持不变 */
	return 0;
}

void getname(char *name)
{
	while (*lineptr == ' ' || *lineptr == '/t')
		lineptr++;

	while (*lineptr != '/0'
			&& *lineptr != ' '
			&& *lineptr != '/t'
			&& *lineptr != '>'
			&& *lineptr != '<'
			&& *lineptr != '|'
			&& *lineptr != '&'
			&& *lineptr != '/n')
	{
			*name++ = *lineptr++;
	}
	*name = '/0';
}

(二)命令的执行和实现

  1、程序框架:

   在对命令的解析完毕后,我们先考虑两个大的方向,即是外部命令还是内部命令?

   外部命令的话,我们只需要fork一个子进程,用execvp()来执行就可以了;对于内部命令则需要自己去实现。

   提出两个问题:第一个,为什么要使用execvp() ?第二个,为什么要fork一个子进程来实现,直接while循环不可以吗?

  解答:

 (1)我们之所以使用execvp(),是因为函数的原型是 int execvp(const char *file ,char * const argv []); 第一个参数是命令文件名,第二个是参数,执行命令非 常的方便。

(2)一旦执行execvp(),当前进程就会被execvp的进程所替代,执行完后就会结束程序,所以while循环是不可以的,必须要fork一个子进程来执行。

while(1) {                  /* repeat forever */
  type_prompt();             /* display prompt on the screen */
  read_command(command,parameters);    /* read input from terminal */
  if(fork()!=0) {               /* fork off child process */
    /* Parent code */
    waitpid(-1,&status,0);         /* wait for child to exit */
   } else {
    /* Child code */
    execvp(command,parameters);    /* execute command */
  }

}

利用这个框架,外部命令(可执行文件)的功能基本实现(vi ,top ,ps等均可使用)。


2、输入输出重定向

当分析出来有输入输出重定向的符号时,我们要使用dup()函数来实现。函数详解请参考 我的博客

对于输入的句法分析结果,我们使用一个结构体来保存:

typedef struct command
{
	char *args[MAXARG+1];	/* 解析出的命令参数列表 */
	int infd;
	int outfd;
} COMMAND;

基本流程:

	/* 子进程 */
		if (cmd[i].infd != 0)
		{
			close(0);
			dup(cmd[i].infd);
		}
		if (cmd[i].outfd != 1)
		{
			close(1);
			dup(cmd[i].outfd);
		}
                 
          
		int j;
		for (j=3; j<OPEN_MAX; ++j)
			close(j);

其中cmd[i].infd和cmd[i].outfd是解析出来的重定向位置的全局变量。


3、管道命令

 管道命令是使用pipe()函数实现的。关于管道的详解请参考 我的博客

假如我们有  a | b | c 这样一个形式的命令,那么是需要创建两条管道的,依次类推。

	int i;
	int fd;
	int fds[2];
	for (i=0; i<cmd_count; ++i)
	{
		/* 如果不是最后一条命令,则需要创建管道 */
		if (i<cmd_count-1)
		{
			pipe(fds);
			cmd[i].outfd = fds[1];
			cmd[i+1].infd = fds[0];
		}
                
		forkexec(i);

		if ((fd = cmd[i].infd) != 0)
			close(fd);

		if ((fd = cmd[i].outfd) != 1)
			close(fd);
	}

	if (backgnd == 0)
	{
		/* 前台作业,需要等待管道中最后一个命令退出 */
		while (wait(NULL) != lastpid)
			;
	}


4.后台作业和信号处理

判断后台,我们只需要解析命令看是否存在 “&”,若存在则backgnd = 1,不再对后台进程进行wait。为了避免僵尸进程,我们可是选择使用signal()处理SIGCHLD,将其忽略,同时忽略SIGINT和SIGQUIT信号(后台不响应ctrl+c,ctrl+z)。但是注意backgnd=0的时候要将这两个信号再设置成默认处理,否则前台也不能响应信号了。


5.内部命令

1、 cd命令的实现 
cd命令的实现主要依赖于系统调用chdir()。我们通过将第一个参数传入chdir就可以进行一次成功的cd调用。通过判断chdir()不同的返回值可以判断出更改目录成功与否,并能输出错误原因。

void do_cd(void)
{
    get_command(0);
    int fd;
    fd=open(*(cmd[0].args),O_RDONLY);
    fchdir(fd);
    close(fd);
}

 

2、 jobs命令的实现 
jobs命令我们维护一个链表,每次当有一个后台进程运行的时候,都要向这个链表中添加一个数据。并当子进程结束的时候会向父进程发送SIGCHLD信号,父进程也就是Shell要处理这个信号,并且将后台进程链表中相应的进程进行处理,也就是将其移除。

	/* 父进程 */
		if (backgnd == 1)
        {
           /*添加入jobs的链表*/
               NODE *p=(NODE*)malloc(sizeof(NODE));
               p->npid=pid;
               printf("%s",cmd[0].args[0]);
               strcpy(p->backcn,cmd[0].args[0]);
               // printf("%s",p->backcn);

               NODE* tmp=head->next;
               head->next=p;
               p->next=tmp;
        }


 3、 exit命令的实现 
exit命令分两部分实现。第一,当词法分析到exit的时候直接调用系统调用exit()就可以了。第二,退出之前要判断一下后台进程链表中是否还有未执行完的任务,如果有未执行完的任务,要提示用户,等待用户选择。

void do_exit(void)
{
    int Pgnum=0;
    NODE* tmp=head->next;
    while(tmp!=NULL)
    {
        Pgnum++;
        tmp=tmp->next;
    }
    if(Pgnum!=0)
    {
       printf("There are programs in the background,are you sure to exit?y/N/n");
       char c= getchar();
       if(c=='N')
           return ;
       else
           goto loop;
    }
   loop:
	printf("exit/n");
	exit(EXIT_SUCCESS);
}

4、 kill命令的实现 
kill命令的实现是通过信号来实现的,我们使用kill -9 +pid来强制结束后台进程,用kill系统调用向相应的进程发送SIGQUIT信号来使进程强制退出。

void do_kill(void)
{
    get_command(0);
    int num=atoi(cmd[0].args[1]);
    signal(SIGQUIT,SIG_DFL);
    kill(num,SIGQUIT);
    signal(SIGQUIT,SIG_IGN);
    NODE *bng=head->next;
    NODE *pre=head;
    while(bng!=NULL)
    {
        if(bng->npid==num)
        {
            NODE* nxt=bng->next;
            pre->next=nxt;
            break;
        }
        pre=bng;
        bng=bng->next;
    }
}

到这里,本程序的功能已经基本实现,效果还算不错。  

注:本程序的具体源码托管至 Github   ,欢迎大家关注!

然而依然存在一些不足之处:

1.因为时间和测试不足的关系,肯定存在着bug

2.没能支持正则表达式等复杂的命令解析

3.不能执行shell脚本。

4.没有实现上下键查看历史命令的功能。

总的来说,自己收获很大,也希望可以帮助到大家!


版权声明:本文为博主原创文章,未经博主允许不得转载。

注册 Vultr VPS 送你10美金 免费玩4个月