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的问题 第二区别与参数表的传递有关。(不细说了)
最后一个区别与向新程序传递环境表有关。
通常,一个进程允许将其环境传播给其子进程,但也有时有这种情况,进程想要为子进程制定某一个确定的环境,比如初始化一个新登录的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;
- }
休息一下,下一章见~