Bash Shell

Shell

对Linux不是太陌生的读者都应该对Shell有一定的了解,就是这个程序在我们登陆后自动执行,打印出一个$符号,然后等待我们输入命令。Linux下最常用的Shell应用程序是Bash,绝大部分Linux发行版默认安装的都是它。下面我们也来亲手编写一个Shell程序,这个Shell远远不如Bash复杂,但也能满足我们一般的使用,下面,我们就开始。

首先,给这个Shell取一个名字,不妨就叫做Mini Shell。

Linux系统的命令分为内部命令和外部命令两种,内部命令由Shell程序实现,如cd、echo等,Linux的内部命令数量有限,而且绝大部分都很少用到。而每一个Linux外部命令都是一个单独的应用程序,我们非常熟悉的ls、cp等绝大多数命令都是外部命令,这些命令都以可执行文件的形式存在,绝大部分放在目录/bin和/sbin中。这样一来,我们编程的难度就可以大大下降了,我们只需要实现很有限的内部命令,对于其它的输入,统统当作应用程序来执行即可。

为了简单明了起见,Mini Shell只实现了2个内部命令: 
1、cd 用于切换目录,和我们熟悉的命令cd类似,除了没有那么多的附加功能。 
2、quit 用于退出Mini Shell。

下面是程序清单:

1: 	/* mshell.c */
2: 	#include <sys/types.h>
1: 	#include <unistd.h>
3: 	#include <sys/wait.h>
4: 	#include <string.h>
5: 	#include <errno.h>
6: 	#include <stdio.h>
7: 
9: 	void do_cd(char *argv[]);
10: 	void execute_new(char *argv[]);
11:
12:	main()
13:	{
14:		char *cmd=(void *)malloc(256*sizeof(char));
15:		char *cmd_arg[10];
16:		int cmdlen,i,j,tag;
17:	
18:		do{
19:			/* 初始化cmd */
20:			for(i=0;i<255;i++) cmd[i]='\0';
21:	
22:			printf("-=Mini Shell=-*| ");
23:			fgets(cmd,256,stdin);
24:	
25:			cmdlen=strlen(cmd);
26:			cmdlen--;
27:			cmd[cmdlen]='\0';
28:	
29:			/* 把命令行分解为指针数组cmd_arg */
30:			for(i=0;i<10;i++) cmd_arg[i]=NULL;
31:			i=0; j=0; tag=0;
32:			while(i<cmdlen && j<10){
33:				if(cmd[i]==' '){
34:					cmd[i]='\0';
35:					tag=0;
36:				}else{
37:					if(tag==0)
38:						cmd_arg[j++]=cmd+i;
39:					tag=1;
40:				}
41:				i++;
42:			}
43:			
44:			/* 如果参数超过10个,就打印错误,并忽略当前输入 */
45:			if(j>=10 && i<cmdlen){
46:				printf("TOO MANY ARGUMENTS\n");
47:				continue;
48:			}
49:			
50:			/* 命令quit:退出Mini Shell */
51:			if(strcmp(cmd_arg[0],"quit")==0)
52:				break;
53:	
54:			/* 命令cd */
55:			if(strcmp(cmd_arg[0],"cd")==0){
56:				do_cd(cmd_arg);
57:				continue;
58:			}
59:			
60:			/* 外部命令或应用程序 */
61:			execute_new(cmd_arg);
62:		}while(1);
63:	}
64:	
65:	/* 实现cd的功能 */
66:	void do_cd(char *argv[])
67:	{
68:		if(argv[1]!=NULL){
69:			if(chdir(argv[1])<0)
70:				switch(errno){
71:				case ENOENT:
72:					printf("DIRECTORY NOT FOUND\n");
73:					break;
74:				case ENOTDIR:
75:					printf("NOT A DIRECTORY NAME\n");
76:					break;
77:				case EACCES:
78:					printf("YOU DO NOT HAVE RIGHT TO ACCESS\n");
79:					break;
80:				default:
81:					printf("SOME ERROR HAPPENED IN CHDIR\n");
82:				}
83:		}
84:	
85:	}
86:	
87:	/* 执行外部命令或应用程序 */
88:	void execute_new(char *argv[])
89:	{
90:		pid_t pid;
91:	
92:		pid=fork();
93:		if(pid<0){
94:			printf("SOME ERROR HAPPENED IN FORK\n");
95:			exit(2);
96:		}else if(pid==0){
97:			if(execvp(argv[0],argv)<0)
98:				switch(errno){
99:				case ENOENT:
100:					printf("COMMAND OR FILENAME NOT FOUND\n");
101:					break;
102:				case EACCES:
103:					printf("YOU DO NOT HAVE RIGHT TO ACCESS\n");
104:					break;
105:	                        default:
106:	                                printf("SOME ERROR HAPPENED IN EXEC\n");
107:				}
108:			exit(3);
109:		}else 
110:			wait(NULL);
111:	}

这个程序稍稍有点长,我们来对它作一下详细的解释:

函数main:

14行:定义字符串cmd,用于接收用户输入的命令行。 
15行:定义指针数组cmd_arg,它的形式和作用都与我们熟悉的char *argv[]一样。

从以上2个定义可以看出Mini Shell对命令输入的2个限制:首先,用户输入的命令行必须在255个字符之内(除去字符串结束标志'\0');其次,命令行的参数个数不得超过10个(包括命令本身在内)。

18行:进入一个do-while循环,这个循环是本程序的主体部分,基本思想是"等待输入命令--处理已输入命令--等待输入命令"。

22行:打印输入提示信息。在Mini Shell中,你可以随意定自己喜欢的命令输入提示信息,本程序中使用了"-=Mini Shell=-*| ",是不是有点像一个CS高手?如果不喜欢,你可以用任意的字符替换它。

23行:接收用户输入。

25-27行:fgets接受输入时,会将输入字符串时末尾的换行符("\n")一起接受,这是我们不需要的,所以要把它去掉。本程序中简单的用字符串结束标志'\0'覆盖了字符串cmd的最后一个字符来实现这个目的。

30行:初始化指针数组cmd_arg。

32-42行:对输入进行分析,将cmd中参数间的空格用'\0'填充,并把各参数的起始地址分别赋与cmd_arg数组。这样就把cmd分解成了cmd_arg,但分解后的各命令参数仍然使用着cmd的内存空间,所以在命令执行结束前不宜对cmd另外赋值。

45行:如果还未分析到输入字符串的末尾(i<cmdlen),而分析出的参数已经达到或超过了10个(j>=10),就认为输入的命令行超出了10个参数的限制,打印错误并重新接收命令。

51-52行:内部命令quit:字符串cmd_arg[0]就是命令本身,如果命令是quit,则退出循环,也就等于退出该程序。

55-58行:内部命令cd:调用函数do_cd()完成cd命令的动作。

61行:对于其它的外部命令和应用程序,调用函数execute_new()执行。

函数do_cd:

68行:仅仅考虑紧跟在命令后面的参数argv[1],而不再考虑其它的参数。如果这个参数存在,就把它作为要转换的目录。

69行:调用系统调用chdir切换当前目录,参见附录1。

70-82行:对chdir可能出现的错误进行处理。

函数execute_new:

92行:调用系统调用fork产生新的子进程。

93行:如果返回负值,说明fork调用出现错误。

96行:如果返回0,说明当前进程是子进程。

97行:调用execvp执行新的应用程序,并检测调用是否出错(返回负值)。这里使用execvp的原因是它可以自动在各默认目录里寻找目标应用程序的位置,而不必我们自己编程实现。

98-107行:对execvp可能出现的错误进程处理。

108行:如果execvp的执行出现错误,子进程在这里终止。表面上看起来,这个exit是接着97行的错误判断的下一行语句,而非if语句的一部分,似乎无论调用execvp成功与否都会接着执行exit。但事实上,如果execvp调用成功的话,这个进程将会被新的程序代码填充,因而根本不可能执行到这一行。反之,如果执行到了这一行,说明前面的execvp调用一定出现了错误。这样的效果和exit被包含在if语句中的效果是完全一样的。

109行:如果fork返回其它值,说明当前进程是父进程。

110行:调用系统调用wait。wait在这里有两个作用:

  1. 使父进程在此暂停,等待子进程执行完毕。这样,就可以等子进程的所有信息全部输出完毕后才打印命令提示符,等待下一条命令的输入,从而避免了命令提示符和应用程序输出混杂在一起的现象。
  2. 收集子进程退出后留下的僵尸进程。可能有读者一直对这个问题存有疑问--"我们编程生成的子进程由我们自己设计的父进程负责收集,但我们手动执行的这个父进程由谁收集呢?"现在大家应该明白了,我们从命令行执行的所有进程最后都是由shell收集的。

关于Mini Shell的编译和运行,这里就不再敷述了,有兴趣的读者可以自行动手实验,或者对这个程序进行改进,使之更接近甚至超过我们正使用的Bash。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值