Preface
上一篇我们实现了一个最简单的shell,并且这个shell只是去执行了bash的指令,那么我们如果要去实现所有的命令需要怎么做呢?比如ls。
首先,我们就应该想到解析参数,因为只要解析了参数我们就能调用exec函数去执行命令了。
一般来讲,
int mian(argc,**argv)
这是最常见的传入命令行参数的方式,那么问题来了,argv是怎么样从string解析出来的呢?需要考虑很多鲁棒性的问题,去空格,取命令等等。下面我们就先来实现怎么取解析输入命令吧。
解析输入命令
这里要好好利用strtok这个函数,可以很方便的切分 char[] 类型的字符串。
我从 stackoverflow 的回答里找到很多巧妙的办法 传送门
我认为用下面这种方法最简洁并易于理解。
enum { kMaxArgs = 64 }; int argc = 0; char *argv[kMaxArgs]; // 解析命令成 (argc,**argv) int parse_para(char commandLine[]) { char *p2; p2 = strtok(commandLine," "); while (p2 && argc < kMaxArgs-1) { printf("%s\n",p2); argv[argc++] = p2; p2 = strtok(0," "); } argv[argc] = 0; }
其实个人更喜欢 c++ 的做法
#include <vector> #include <string> #include <sstream> std::string cmd = "mycommand arg1 arg2"; std::istringstream ss(cmd); std::string arg; std::list<std::string> ls; std::vector<char*> v; while (ss >> arg) { ls.push_back(arg); v.push_back(const_cast<char*>(ls.back().c_str())); } v.push_back(0); // need terminating null pointer execv(v[0],&v[0]);
不管哪种方式,这样我们每次输入的string就可以转化成argc和**argv了(全局变量)
接下来,介绍一个函数 ---> getopt
man 3 getopt 可以获得一个例子
getopt() The following trivial example program uses getopt() to handle two program options: -n,with no associated value; and -t val,which expects an associated value. #include <unistd.h> #include <stdlib.h> #include <stdio.h> int main(int argc,char *argv[]) { int flags,opt; int nsecs,tfnd; nsecs = 0; tfnd = 0; flags = 0; while ((opt = getopt(argc,argv,"nt:")) != -1) { switch (opt) { case 'n': flags = 1; break; case 't': nsecs = atoi(optarg); tfnd = 1; break; default: /* '?' */ fprintf(stderr,"Usage: %s [-t nsecs] [-n] name\n",argv[0]); exit(EXIT_FAILURE); } } printf("flags=%d; tfnd=%d; nsecs=%d; optind=%d\n",flags,tfnd,nsecs,optind); if (optind >= argc) { fprintf(stderr,"Expected argument after options\n"); exit(EXIT_FAILURE); } printf("name argument = %s\n",argv[optind]); /* Other code omitted */ exit(EXIT_SUCCESS); }
ok~到此,我们可以解析参数了,那么下一步就是要执行命令,在这里,不得不去介绍Unix的exec函数族,8.10 函数exec详细讲解了。
执行命令
8.3节曾提到用fork函数创建新的子进程后,子进程往往要调用一种exec函数以执行另一个程序。当进程调用一种exec函数时,该进程执行的程序完全替代为新程序。 因为调用exec并不创建新进程,所以前后的进程ID并未改变。exec只是用磁盘上的一个新程序替代了当前进程的正文段,数据段,堆段和栈段。 一共有7个不同的exec函数。 #include <unistd.h> int execl(const char *pathname,const char *arg0,... /* (char *)0 */); int execv(const char *pathname,char *const argv[]); int execle(const char *pathname,... /* (char *)0,char *const envp[] */); int execve(const char *pathname,char *const argv[],char *const envp[]); int execlp(const char *filename,... /* (char *)0 */); int execvp(cosnt char *filename,char *const argv[]); int fexecve(int fd,char *const envp[]); 7个函数的返回值:若出错则返回-1,若成功则没有返回值
在APUE中,解释好长的一段,主要集中了三种不同的区别:
-
第一个区别是前4个函数取路径名作为参数,后两个函数取文件名作为参数,最后一个取文件描述符作为参数。
如果filename中包含/,则就将其视为路径名。
否则就按照PATH环境变量,在它所指定的各目录中搜寻可执行文件。
PATH变量包含了一张目录表(成为路径前缀): PATH=/bin:/usr/bin:/usr/local/bin:.
如果 execlp或者execvp使用路径前缀中的一个找到了一个可执行文件,但是该文件不是由连接编译器产生的可执行文件,则就认为该文件是一个shell脚本,试着用/bin/sh去调用它。
fexecve函数参数是文件描述符,这个很重要,因为是文件描述符,所以就可以无竞争地执行该文件。否则,拥有特权的恶意用户可以去篡改该程序。(这里我的理解),具体是一个TOCTTOU的问题3.3节 TOCTTOU: 基本思想:如果有两个基于文件的函数调用,其中第二个调用依赖于第一个调用的结果,那么程序就是脆弱的。 因为两个调用并不是原子操作,在两个函数调用之间文件可能改变了,这样也就造成了第一个调用的结果不再有效。 文件系统命名空间中的TOCTTOU错误通常处理的就是那些颠覆文件系统权限的小把戏,这些小把戏通过骗取特权程序降低特权文件的权限控制或者让特权文件打开一个安全漏洞等方式进行。
第二区别与参数表的传递有关。(不细说了)
最后一个区别与向新程序传递环境表有关。
通常,一个进程允许将其环境传播给其子进程,但也有时有这种情况,进程想要为子进程制定某一个确定的环境,比如初始化一个新登录的shell时,login程序通常会创建一个之定义少数几个变量的特殊环境,而在我们登录时,可以通过shell启动文件,将其他变量加到环境中去。
其实还有更加详细的分析,但是我也不提太多了,因为我们的目标是星辰大海,不可因小失多。其实我一直认为学习这种大部头的方法就是,你先找定一个方向,比如我要实现一个Jas-shell(我自己取的名 :)),然后利用这本书的知识不断去完善我的shell,在这其中,我不能面面俱到,细致入微,但求大刀阔斧,直指前方。当未来我实现了,刚好也大概过了一遍这本书,我会回头慢慢咀嚼细节,然后update我的作品。
不小心废话了一下,哈哈,半桶水叮当响,各位看客一笑了之~
好了,下面我贴出一个实例,就是在我们第一章实现的基本shell上改的,至于里面用到的imitate_ls的实现,我放到下一章讲~
其中的 /home/jasperyang/CLionProjects/Jas-shell/imitate_ls 是我实现的ls没代码贴出来,大家耐心等我下一章~或者你们可以自己实现。
// // Created by jasperyang on 17-6-6. // #include "apue.h" #include <sys/wait.h> #include "myerr.h" static void sig_int(int); /* our signal-catching function */ static int parse_para(char commandLine[]); enum { kMaxArgs = 64 }; int argc=0; //命令行参数个数 char *argv[kMaxArgs]; //命令行参数 int main(void) { char buf[MAXLINE]; /* from apue.h */ pid_t pid; int status; if(signal(SIGINT,sig_int)==SIG_ERR) err_sys("signal error"); printf("%% "); /* print prompt (printf requires %% to print %) */ while(fgets(buf,MAXLINE,stdin) != NULL) { if(buf[strlen(buf) -1] == '\n'){ buf[strlen(buf)-1]=0; /* replace newline with null */ } if((pid = fork()) < 0) { err_sys("fork error"); } else if (pid == 0){ /* child */ argc = 0; parse_para(buf); printf("%s\n",argv[0]); if(!strcmp(argv[0],"ls")) { if (execv("/home/jasperyang/CLionProjects/Jas-shell/imitate_ls",argv) < 0) { printf("execv error: %s\n",strerror(errno)); exit(-1); } } else { err_ret("couldn't execute: %s",buf); } exit(127); } /* parent */ if((pid = waitpid(pid,&status,0)) < 0) err_sys("waitpid error"); printf("%% "); } exit(0); } //中断信号 void sig_int(int signo) { printf("interrupt\n%% "); } // 解析命令成 (argc,**argv) int parse_para(char commandLine[]) { char *p2; p2 = strtok(commandLine," "); } argv[argc] = 0; }
休息一下,下一章见~