盐山建网站,cms+wordpress模板,宽带费用多少钱一年,模板网站建设哪家好接受每一个人的批评#xff0c;可是保留你自己的判断。 ——莎士比亚 一段时间的没有更新是由于最近开学期间比较的忙#xff0c;同时也是由于刚开学的几门课才学习的时候有点迷糊#xff0c;需要在学校课堂上花的时间更多了#xff0c;所以才没有更新的#xff0c;求放过…接受每一个人的批评可是保留你自己的判断。 ——莎士比亚 一段时间的没有更新是由于最近开学期间比较的忙同时也是由于刚开学的几门课才学习的时候有点迷糊需要在学校课堂上花的时间更多了所以才没有更新的求放过。 简单shell的实现 1、shell介绍2、shell实现概括3、shell实现困难4、shell实现具体方式4、1、main函数4、2、MakeCommandLineAndPrint函数4、3、GetUserCommand函数4、4、SplitCommand函数4、5、CheckBuildin函数4、6、ExecuteCommand函数 5、总结 1、shell介绍
对于什么是shell问题来说这是个好问题但是其实如果你看过我之前的文章的话应该能准确的理解什么是shell如果想要看之前怎么介绍的话就会到之前文章里看一看。这里的话就简单讲一下吧shell简单点来说就是一个你的老板的一个秘书这里的老板也能够看作是内核你想要让你的老板有什么行为的话你的报告换句话说就是你得将你的命令行代码给到你的老板的秘书也就是shell会通过shell来帮助你去找到老板但是并不是直接就能够找到并且让他去执行给到老板前秘书也会自己考虑一下这个命令行的方式有没有什么不妥的地方如果有的话也就不会直接麻烦操作系统这样的话既保证了内核的安全性也保证了运行时候的效率这里的效率提升就是因为能够秘书在接收到几次一样的请求之后能够不再去进行判断直接否定。
2、shell实现概括
对于shell实现来说每一次的命令行输入都会对应着有着一段的运行结果。那对于这种方式来说可以看作是一个在一个父进程的情况下一个子进程在不断的执行不同的命令或者换句话说是在不断的替换进程(其中的环境变量是从父进程传下来的)。 所以我们可以用进程替换的思想去实现一个shell进程(这里的这种进程要一直进行这样才能够实现执行多次的命令行。 由于我们每次输入的命令行指令都是会被bash读到然后寻找指定的命令行中提到的程序然后执行相关的选项。就像这篇文章讲的那样我们的程序中能够读取到我们输入的东西所以为什么我们不能够利用这点来实现每次的命令行输入将对应到进程替换成我们需要的进程运行结束之后再退出来。 按照这样的方法的话我就能够奠定了我们实现shell主要实现方向。
3、shell实现困难
1、对于shell来说不仅仅是读取到我们输入到的命令行是什么我们还需要在执行之前每次都会有一段的前置的信息这一段的前置消息就是分别对应着用户名主机名以及当前目录所以第一个目标就是要解决基本信息的获取以及显示。 2、除此之外我们还需要将读取到的命令行参数存放在数组之中所以我们需要根据每一次的用户的命令字符串切分为不同的字符串数组其中的要求就是依据空格为分界符号。 3、拆分后分别的放在一个字符串数组之中。然后进行进程替换这里的进程替换选择的函数是execvp这个在之前的文章中讲述过具体的使用方法不知道的可以回顾一下这个进程替换的系统调用函数能够解决我们的问题。 4、当然如果我们知道内建命令那么我们还需要额外的去实现内建命令构建的操作。
4、shell实现具体方式
4、1、main函数
首先构建一个main函数。 包含一下最主要的函数最主要的需要实现的功能。 为了方便后续的使用我们把512定义为一个SIZE简单的认为这是一个大小的限制(就类似数组大小的限制)。
#define SIZE 512
int main()
{int quit 0;while(!quit){// 1. 我们需要自己输出一个命令行MakeCommandLineAndPrint();// 2. 获取用户命令字符串char usercommand[SIZE];int n GetUserCommand(usercommand, sizeof(usercommand));if(n 0) return 1;// 3. 命令行字符串分割. SplitCommand(usercommand, sizeof(usercommand));// 4. 检测命令是否是内建命令n CheckBuildin();if(n) continue;// 5. 执行命令ExecuteCommand();}return 0;
}4、2、MakeCommandLineAndPrint函数
让每一个命令行都打印出自己的相关的信息。这个函数也不需要传参因为所有需要得到的都已经存在于环境变量中了。所以为了能够打印相关的信息就要去读取。所以我们就需要去编写相关函数去编写读取的方法。 首先第一步是构建一个框架。
void MakeCommandLineAndPrint()
{char line[SIZE];const char *uswenameGetUserName();const char *hostnameGetHostName();const char *cwdGetCwd(); SkipPath(cwd);snprintf(line,sizeof(line),[%s%s %s] ,username,hostname,strlen(cwd)1 ? /:cwd1);printf(%s,line);fflush(stdout);
}其次就是去实现每一个函数的具体意义。 首先我们来看SkipPath 为什么这里会有一个SkipPath呢难道说每次得到的还不是我们正常使用的cwd吗那当然不是能够直接使用的啊。所以对于这个函数来说就是为了处理一开始得到的不是我们最终想要的结果。如果不知道原本是什么的话其实简单说一下也就是从家目录到当前目录的所有的路径都在环境变量的cwd中。所以我们才需要进行额外的处理。为了能够不用多余的函数来增加我们shell的时间复杂度并且为了能够不传指针就能够实现对于变量的改写我们需要使用到宏。因为宏是一个能够在编译的时候就能在原本的位置中展开这也就不会造成重新开栈重新消耗空间考虑形参和实参的关系。
#define SkipPath(p) do {p(strlen(p)-1); while(*p!/)p--;} while(0)这里单独的写出来do{}while来包含主要的程序主要的作用是为了防止出现优先级错误的情况。 其中的几个得到环境变量相关信息的函数本质上都是一样的。大概看看应该能够看懂。
const char *GetUserName()
{const char *name getenv(USER);if(name NULL) return None;return name;
}
const char *GetHostName()
{const char *hostname getenv(HOSTNAME);if(hostname NULL) return None;return hostname;
}
const char *GetCwd()
{const char *cwd getenv(PWD);if(cwd NULL) return None;return cwd;
}这样的话就能够实现我们编写的shell的第一步了。
4、3、GetUserCommand函数
这个函数的话是要读取用户输入的字符串当然用户在输入的时候是有空格的所以对于该函数需要注意的是这里不能够是直接使用scanf函数而是要找到一个能够按照行来拿到字符串的函数。这样的话才能够保证不会因为存在空格反而不能读到正确的结果。 所以这个函数是什么呢有没有比较好的一个接口呢我的建议是选择一个char *fgets(char *s,int size,FILE *stream)如果能够 正确返回那么返回s的起始位置的地址。如果返回错误就返回NULL。建议使用这个文件流相关的知识那是因为之后的文章中马上就要讲解有关于文件流的知识。其中的size指的是s的大小。并且输入的话存放在的位置是在s中。其中有一个不注意就会忘记的一点是我们每次输入的时候按回车才能实现fgets真正的读完所以说如果我们不干涉的话在最后会有一个多余的回车。所以我们需要进行改写将函数内部传入命令行之后进行sizeof结尾置零操作。 对于这个函数传参的设计的话应该是需要传入两个。 第一个参数是我们在main函数创建的一个专门存放命令行内容的usercommand数组这是因为这个数组在读完数据之后还需要进行之后的操作就比如说分割操作。 第二个参数就是我们用来得到这个字符串所占据的内存大小因为在fgets函数使用的时候需要用到。 这样的话注意点以及一些传参的设计都已经搞定了下面就是真正的代码的实现。
#define ZERO \0
int GetUserCommand(char command[], size_t n)
{char *s fgets(command, n, stdin);if(s NULL) return -1;command[strlen(command)-1] ZERO;return strlen(command);
}4、4、SplitCommand函数
对于分割命令行参数的函数来说我们需要像之前那样定义一个宏函数来帮助我们实现不用传参的操作吗其实宏函数确实能够实现但是对于学习阶段来说我们其实可以想一下之前在介绍C语言中的字符串函数的时候有一个函数其实能够刚好符合我们的要求。strtok函数能够根据特定的字符来找到字符串中每一个字符的位置如果只执行一次的话找到的就是第一个要求的字符如果接着执行的话就会在第一个基础上往后找。根据函数的这个属性的话我们就能够利用这个函数从前往后的一次寻找空格来自动帮我们分开字符串。当然找到了符合条件的情况下就会返回从左到右的第一个子串后续的会返回第一个结尾之后的第二个位置的子串。如果找不到符合条件的话就返回NULL。 为什么就是需要我们去实现一个字符串分割为多个呢那是因为无论未来我们是用什么样子的系统调用的程序替换都需要我们命令行输入的一个一个打散的而不是整个一起的方式去读取。 其中的NUM是用来默认设置一个命令行参数的个数的通常情况下来说一个指令后面加上的选项不会超过NUM默认的32个的如果超过的话可以自行修改NUM让其能够存放在gArgv[]之中
#define NUM 32
#define SEP
char *gArgv[NUM];
void SplitCommand(char command[], size_t n)
{(void)n;// ls -a -l -n - ls -a -l -ngArgv[0] strtok(command, SEP);int index 1;while((gArgv[index] strtok(NULL, SEP))); // done, 故意写成,表示先赋值在判断. 分割之后strtok会返回NULL刚好让gArgv最后一个元素是NULL, 并且while判断结束
}这里定义的SEP我们需要找到的目标的位置是空格但是这里非常容易错那是因为strtok函数中的第二个参数是字符串而不是字符。
4、5、CheckBuildin函数
内建命令的特点就是不需要考虑当前环境或者是默认的配置的条件在什么地方shell都能够运行出来相对于的结果。 对于现在的我来说我只认识两个内建命令。分别是cd命令echo $?命令。这两个我在之前讲环境变量的时候讲述过了其特点。所以要想这两个命令的与众不同肯定是在函数结构上的与众不同。就比如之前的一些命令的话会存在于bin目录之下但是内建命令可能就直接存在程序之中这样的话不会受到环境的因素也能够实现相对应的指令。 所以根据内建命令的特点我写了一个检查内建命令的函数如果满足条件的话就会直接运行不会先替换进程然后执行这样就能够避免环境改变造成无法执行相关功能的问题。 函数的返回值设置为int类型这样做的话能够判断是否用户输入的为内建命令如果是内建命令的话就会执行完也就不会再去执行下一个的ExecuteCommand函数。避免了重复执行的错误。
char cwd[SIZE*2];
int lastcode 0;
const char *GetHome()
{const char *home getenv(HOME);if(home NULL) return /;return home;
}
void Cd()
{const char *path gArgv[1];if(path NULL) path GetHome();//如果是空的话会在直接返回家目录// path 一定存在chdir(path);// 刷新环境变量char temp[SIZE*2];getcwd(temp, sizeof(temp));snprintf(cwd, sizeof(cwd), PWD%s, temp);putenv(cwd); // OK
}int CheckBuildin()
{int yes 0;const char *enter_cmd gArgv[0];if(strcmp(enter_cmd, cd) 0){yes 1;Cd();}else if(strcmp(enter_cmd, echo) 0 strcmp(gArgv[1], $?) 0){yes 1;printf(%d\n, lastcode);lastcode 0;}return yes;
}这里容易错的地方就是环境变量也是需要更新的不能说我们进行了好几次的cd或者其他命令之后环境变量因为没有进行更新从而错误。 这样的话能够实现简单的内建命令。那我们该怎么去执行内建命令之外的命令呢当然是使用进程替换
4、6、ExecuteCommand函数
进程替换那就是说在该函数中需要使用到fork()函数并且还需要判断使用哪一个系统调用函数来确定传参条件。考虑之后还是使用execvp函数。下面是实现的代码。
void ExecuteCommand()
{pid_t id fork();if(id 0) Die();else if(id 0){// childexecvp(gArgv[0], gArgv);exit(errno);}else{// fahterint status 0;pid_t rid waitpid(id, status, 0);if(rid 0){lastcode WEXITSTATUS(status);if(lastcode ! 0) printf(%s:%s:%d\n, gArgv[0], strerror(lastcode), lastcode);}}
}5、总结
这样的话就简单的把一个shell的指令完全的自我实现了其中当然也会有很多的不足的地方但是基本上的内容都已经实现。希望读者能够在本篇文章的基础之上学到更多理解过多的关于shell编程的快乐也希望能够通过该篇文章给自己的学习路上添砖加瓦做到更好。