代码见github
引言
为了实现更加强大的shell,我们接着上一次实现的简单shell程序,继续给我们的shell程序添加功能。 前文提到,shell程序的主要功能有:
- 执行程序
- 可编程
- 管理输入输出
这一次我们就来实现保存变量,简单的控制流等脚本语言的功能。
为了让shell更好用,我们还将添加一些命令行解析等功能,使得shell能够同时实现多个命令(命令用分号分开)。之前的shell在运行其他命令时,如果终端按下Ctrl + C会对前台由该终端控制的进程(包括父子进程)传递SIGINT信号,shell进程和运行中的子进程都会终止,因此我们需要忽略父进程的SIGINT信号,而不能忽略子进程的SIGINT信号(fork函数产生的进程会继承父进程的信号屏蔽)。
命令行解析
命令行解析可以采用动态分配内存也可以直接分配固定大小内存在栈中,但是脚本有可能执行一系列命令,为了避免栈溢出,我们采用动态分配内存的方法保存每一条命令。
程序越来越大后,为了便于管理,方便编译,我们新建文件splitline.c包含有关命令行解析的函数,并将一些功能,比如设置信号捕捉函数,执行命令等功能封装成函数,这样主程序就能写成非常简洁的形式:
#include<string.h>
#include<sys/wait.h>
#include<signal.h>
#include"my_shell.h"
#define PROMPT ">"
int main(){
char *cmdline,*prompt,**arglist;
int result;
void set_sig();
prompt = PROMPT;
set_sig();
while((cmdline = next_cmd(prompt,stdin)) != NULL){
if((arglist = splitline(cmdline)) != NULL){
result = execute(arglist);
}
free(cmdline);
}
}
void set_sig(){
signal(SIGINT,SIG_IGN);
}
和命令行解析相关的函数为next_cmd函数和splitline函数,这两个函数都放在splitline.c中,前者从标准输入中读取命令,后者将next_cmd函数得到的命令行分割为字符串数组。
next_cmd函数
next_cmd函数利用 getc函数从标准输入中读取每个字符,然后给字符串分配动态内存,定义宏is_delimiter判断命令参数是否结束(默认为’\n’和’;’),emalloc和erealloc函数分别代表包含错误处理的malloc和realloc库函数。
char *next_cmd(char *prompt,FILE *fp){
/* purpose:read next command line from fp
returns:dynamically allocated string holding command line
errors:NULL at EOF
notes: allocates space in BUFSIZ chunks.
*/
char *buf;
int bufspace = 0;
int pos = 0;
int c;
printf("%s",prompt);
while((c = getc(fp)) != EOF){
if (pos + 1 >= bufspace){
if(bufspace == 0)
buf = emalloc(BUFSIZ);
else
buf = erealloc(buf,bufspace + BUFSIZ);
bufspace +=BUFSIZ;
}
if(is_delimiter(c))
break;
buf[pos++] = c;
}
if(c == EOF && pos == 0)
return NULL;
buf[pos] = '\0';
return buf;
}
spiltline函数
splitline函数将命令行分割为命令行参数的字符串数组。需要注意的是strtok函数会改变line字符串,每次调用返回一个字符串的地址(char*)。字符串数组的元素保存了命令行参数字符串(地址),这里的每个字符串不需要再进行动态分配内存了,故主函数中只需要调用一次free函数。
char **splitline(char *line){
char **args = emalloc(sizeof(char*) * MAXARGS);
int index = 0;
args[index++] = strtok(line," ");
while((args[index++] = strtok(NULL," ")) != NULL);
return args;
}
到这里我们就完成了命令行解析的功能了,现在的shell可以一次执行多条命令,如下所示:
wutao@wutao-XXXX:~/git/shell/my_shell$ ./sh
>ls -l;date;echo end
total 40
-rw-rw-r-- 1 wutao wutao 477 3月 12 15:28 execute.c
-rw-rw-r-- 1 wutao wutao 558 3月 12 15:46 my_shell.c
-rw-rw-r-- 1 wutao wutao 301 3月 12 15:12 my_shell.h
-rw-rw-r-- 1 wutao wutao 152 3月 12 17:04 README.md
-rwxrwxr-x 1 wutao wutao 13696 3月 12 17:28 sh
-rw-rw-r-- 1 wutao wutao 1745 3月 12 15:59 splitline.c
-rw-rw-r-- 1 wutao wutao 188 3月 12 14:52 test.c
2017年 03月 12日 星期日 17:29:11 CST
end
>>>
实现编程控制流
if
…
then语句是如何运行的
shell中的if语句和其他编程语言不同之处在于,if中的条件是一条命令,而Unix系统中的命令以exit(0)表示成功,也就是说if条件中的命令正常执行完毕exit(0)退出,则执行then代码段命令,否则执行else代码段命令,如果if后接着多个命令则以最后一个命令的exit值作为这个语句的条件值。
所以我们可以总结if的工作流程:
- shell运行if之后的命令
- shell检查命令的exit值
- 如果值为0则执行then代码段
- 否则执行else代码段
- 关键词fi代表if段代码结束
if控制流的实现
由于添加了if语句,我们在执行命令时不能直接运行execute函数,而是要添加对控制语句(if,then,else,fi)的判断来执行命令。我们可以新建一个controlflow.c文件保存和控制流有关的函数,利用两个静态枚举变量,一个表示当前程序读取到命令的状态,包括自然状态,希望遇到一个then语句,处于then代码块,处于else代码块。另一个变量保存if条件命令exit状态:
enum states {NEUTRAL,WANT_THEN,THEN_BLOCK,ELSE_BLOCK};
enum results {SUCCESS,FAIL};
static int if_state = NEUTRAL;
static int if_result = SUCCESS;
这里就要通过一些控制流函数来帮助我们存储程序的状态了,我们可以把之前主函数中简单执行execute函数改写为一个更复杂的process函数,process函数判断命令是否为控制命令,如果是则处理一些状态变量,否则执行execute函数。process函数的代码如下:
int rv = 0;
if(arglist[0] == NULL)
return rv;
if(is_control_command(arglist[0]))
rv = do_control_command(arglist);
else if(ok_to_execute())
rv = execute(arglist);
return rv;
}
process函数首先判断命令是否是控制流命令:
int is_control_command(char *cmd){
return (strcmp(cmd,"if")==0||strcmp(cmd,"then")==0||strcmp(cmd,"fi")==0||strcmp(cmd,"else")==0);
}
如果是控制流命令则处理控制流的状态变量:
int do_control_command(char **args){
char *cmd = args[0];
int rv = -1;
if(strcmp(cmd,"if")==0){
if(if_state != NEUTRAL)
perror("if_state error");
else{
last_stat = process(args + 1);//执行if后的命令
if_result = (last_stat == 0 ? SUCCESS:FAIL);
if_state = WANT_THEN;
rv = 0;
}
}
else if(strcmp(cmd,"then") == 0){
if(if_state != WANT_THEN)
perror("if_state error");
if_state = THEN_BLOCK;
if(ok_to_execute())
process(args + 1);
rv = 0;
}
else if(strcmp(cmd,"else") == 0){
if(if_state != THEN_BLOCK)
perror("if_state error");
if_state = ELSE_BLOCK;
if(ok_to_execute())
process(args + 1);
rv = 0;
}
else if(strcmp(cmd,"fi") == 0){
if(if_state!=ELSE_BLOCK)
perror("if_state error");
else{
if_state = NEUTRAL;
rv = 0;
}
}
return rv;
}
ok_to_execute则判断当前命令是否可以执行,如果处于THEN_BLOCK且if_result为FAIL或处于ELSE_BLOCK且if_result为SUCCESS则拒绝执行当前命令,返回rv=0;否则可以执行命令。
int ok_to_execute(){
int rv = 1;
if(if_state == WANT_THEN){
perror("if_state error");
rv = 0;
}
else if(if_state == THEN_BLOCK && if_result == FAIL)
rv = 0;
else if(if_state == ELSE_BLOCK && if_result == SUCCESS)
rv = 0;
return rv;
}
最后我们只需要把主程序的execute函数改为process函数就行了。
让我们来试一试我们的if控制流:
wutao@wutao-XXXX:~/git/shell/my_shell2$ cc -o sh my_shell.c splitline.c process.c controlflow.c
wutao@wutao-XXXX:~/git/shell/my_shell2$ ./sh
>if echo hello;then echo inthen;else echo inelse;fi;
hello
inthen
>>>>>if diff my_shell.c my_shell.h;then echo inthen;else echo inelse;fi;
...#省略了diff的输出,diff命令只对两个内容一样的文件exit(0)
inelse
程序还存在一些问题,因为每执行一条语句会会产生一个提示符”>”,因此执行多个命令后就产生了多个提示符。
小结
这一篇文章里我们实现了命令行解析和if控制流的内容,但是仍然存在很多不足,比如if语句无法嵌套,无法输出重定向等等。将来我们会一步一步完善我们的shell。