> 技术文档 > 【linux】自定义shell——bash命令行解释器小程序_bash解释器

【linux】自定义shell——bash命令行解释器小程序_bash解释器


小编个人主页详情<—请点击
小编个人gitee代码仓库<—请点击
linux系列专栏<—请点击
倘若命中无此运,孤身亦可登昆仑,送给屏幕面前的读者朋友们和小编自己!
在这里插入图片描述


目录

    • 前言
    • 一、交互问题,获取命令行
    • 二、字串的分隔问题,解析命令
    • 三、普通命令的执行
    • 四、内建命令
    • 五、源代码
      • makefile
      • myshell.c
    • 六、给shell程序添加重定向
    • 总结

前言

【linux】linux进程控制(三)(进程程序替换,exec系列函数)——书接上文 详情请点击<——
本文由小编为大家介绍——【linux】自定义shell——bash命令行解释器小程序

本文会基于进程控制中的进程创建,进程终止,进程等待,进程替换的知识去模拟实现bash命令行解释器小程序,建议对于进程控制的知识不熟悉的读者友友,可以点击后方蓝字进行学习后再来阅读本文

  1. 进程创建,进程终止详情请点击<——
  2. 进程等待详情请点击<——
  3. 进程替换详情请点击<——

shell是一个外壳程序,shell是操作系统层面命令行解释器,在linux中的命令行解释器是bash(shell的范围更大,bash仅限于linux),shell/bash的本质也就是一个进程,执行指令的时候,也就是通过创建子进程执行的,所以当我们登录的时候,系统就是要为我们启动一个shell进程,所以小编可以通过编写一个程序,在命令行解释器中启动,这样就基于命令行解释器的基础上运行起来我们自主实现的shell了

一、交互问题,获取命令行

【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 我们观察一下bash命令行,它的格式是[wzx@VM-12-3-centos lesson20]$ 这种形式,所以我们自定义实现的bash也应该类似于这种形式,通常普通用户使用$,root用户使用#,但是这里我们为了和小编使用的普通用户的$进行区分,所以我们使用#作为最后一个字符,即 [用户@主机名 路径]# 这种形式
  2. 那么我们就需要获取当前的用户名,主机名,以及当前所在的工作路径,对于这些信息我们都可以使用putenv进行获取这些环境变量信息,其中用户名在环境变量中有USER,主机名在环境变量中也有HOSTNAME,当前所在的工作路径在环境变量中也有PWD进行获取(注意:后面讲到内建命令cd的时候,小编会对当前工作路径的获取方式进行修改,这里使用环境变量PWD进行获取便于理解)
    【linux】自定义shell——bash命令行解释器小程序_bash解释器
#include #include char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}int main(){ printf(\"[%s@%s %s]# \\n\", getusrname(), gethostname(), getpwd()); return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 我们可以看到当前我们的程序也可以打印出[用户@主机名 路径]# 这种形式了,但是注意观察,bash命令行解释器打印出[用户@主机名 路径]$之后并没有进行换行,而是等待我们进行输入,所以小编将我们添加的换行\\n去掉
  2. 这个等待其实就是阻塞式等待键盘设备就绪,即等待用户输入,原理其实很简单一个sacnf就可以让我们也是实现这样的功能,那么我们将用户的输入使用字符数组commandline存储起来便于我们后续的字符串分隔,那么对于这个字符数组的大小通常是1024,并且我们也喜欢使用宏LINE_SIZE进行定义这个1024,便于进行修改
  3. 我们并不喜欢直接将诸如格式之类的直接定义在实现代码中,我们通常使用一个宏FORMATE定义在开头便于我们进行修改
#include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024char commandline[LINE_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}int main(){ printf(FORMATE, getusrname(), gethostname(), getpwd()); scanf(\"%s\", commandline); printf(\"echo: %s\\n\", commandline);//打印测试 return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 此时我们第一次输入lllllllllllllllll,scanf可以正常读取命令行中用户输入的内容,并且测试打印也无误
  2. 可是我们知道指令通常都是带选项即空格的,那么当我们进行第二次输入ls -a -l的时候,此时进行打印字符数组commandline中的内容的时候,却只有一个ls,对于后面的 -a -l没有进行读取,这是因为scanf会默认遇到空格或者换行就结束读取,此时ls和-a中间我们使用了空格进行分隔,所以scanf读取到这个空格就停止了,所以字符数组commandline中就只会有ls
  1. 那么接下来进行读取我们应该一次读取一行,即遇到空格不结束读取,遇到换行才结束读取,很多读者友友心中第一反应应该就是getline这个函数了吧,使用getline可以一次获取一行,并且遇到空格不结束,遇到换行才结束,符合我们的需求

【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 但是今天小编教大家一个新的玩法同样也可以实现我们的需求,那么就是fgets,它的作用是从文件的流中读取内容,并将这个内容输入到字符数组中,fgets需要传入一个字符数组,字符数组的大小,流,前两个参数我们都可以轻松搞定

【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 但是对于第三个参数呢?FILE* stream就是一个流对象,即FILE*就是我们曾经打开的文件,流,对我们来说很陌生,但是这里小编要介绍三个标准流中的stdin,它是一个文件的流对象,在我们的c语言程序启动的时候,编译器就会默认帮我们打开一个读取输入文件,我们的输入就会输入到这个文件中,流对象stdin就是对这个输入的文件进行管理和读取写入等一系列操作的入口,所以我们可以使用stdin作为fgets的第三个参数
#include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024char commandline[LINE_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}int main(){ printf(FORMATE, getusrname(), gethostname(), getpwd()); fgets(commandline, sizeof(commandline), stdin); printf(\"echo: %s\\n\", commandline); return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 我们可以看到这样小编输入的ls -a -l就都被fgets读取写入到字符数组commandline中了,那么下面我们对比一下下面小编使用scanf的上一次的运行结果

【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 我们可以看到相比较一下,使用fgets获取的字符串多了一个换行,明明我们打印即printf(“echo: %s\\n”, commandline)只有一个\\n换行,可是这里却多了一个换行,即这里有两个换行,那么这个换行究竟是如何来的呢?
  2. 读者友友仔细思考一下,我们在bash命令行中输入完成指令后,例如ls -a -l输入完成后,是不是都要按一下回车换行,bash命令行才结束读取执行命令,由于这个回车换行也是一个字符,同样也被写入到了stdin这个流中了,所以fgets就会一并将回车换行也进行读取,所以这里打印的时候才会多出一个换行来
  1. 所以我们还应该将这里给特殊处理一下,将字符数组commandline中的换行调整为\\0,同时其实bash命令行解释器是一个进程,当我们启动xShell并且登录的时候,bash命令行解释器这个进程就启动了,我们在命令行上输入,按下回车,bash命令行解释器创建子进程给我们完成任务,我们接着就是输入下一个指令,下一个指令……,仔细思考一下,bash命令行解释器我们如何退出,是不是要按下右上角的❌才可以进行退出,类似的,诸如微信,qq,网易云等也要按下右上角的❌才可以退出,这些软件同样是程序,同样的都要以进程的方式进行运行,如果我们不按右上角的❌这些进程就会一直运行,除非电脑没电,所以这些进程一旦启动,都是以死循环的方式进行运行,只有我们按下右上角的❌才会终止进程,同样的bash命令行解释器也是一个死循环的进程,所以我们模拟bash命令行解释器的程序整体应该也是死循环
#include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024char commandline[LINE_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}int main(){ while(1) { printf(FORMATE, getusrname(), gethostname(), getpwd()); fgets(commandline, sizeof(commandline), stdin); //ls -a -l\\n\\0 commandline[strlen(commandline) - 1] = \'\\0\'; printf(\"echo: %s\\n\", commandline); } return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 如上,多出的那一个换行就被小编去掉了,打印无误,并且也进行了死循环式的等待
  2. 当我们想要退出的时候,按下ctrl+c即可退出
  1. 所以第一个模块我们就完成了,所以我们去掉用于打印字符数组内容的打印代码,由于这个模块是我们的程序和用户进行交互的区域模块,所以我们将其放在interact函数中
  2. 并且希望使用传参的方式进行调用interact()获取用户的输入,写入到字符数组commandline中,所以字符数组commandline就应该进行传参,由于fgets要使用字符数组的大小,而字符数组的大小我们又无法在interact函数内求出,所以也应该interact函数外求出进行传参
#include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024char commandline[LINE_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}void interact(char* cline, int size){ printf(FORMATE, getusrname(), gethostname(), getpwd()); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\'; printf(\"%s\\n\", cline);}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); } return 0;}

二、字串的分隔问题,解析命令行

  1. 上一个模块,我们可以将用户的输入写入到一个字符数组commandline中了,那么接下来我们就要解析一下用户的输入,如果用户的输入带选项的指令,那么选项和指令之间,选项和选项之间都是以空格为分隔符,例如ls -a -l,所以我们应该按照空格为分隔符进行分隔用户的输入,即字符数组commandline

【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 那么我们就可以使用c语言的stoke,进行分隔字符数组commandline,strtok的第一个参数传入需要分隔的字符串,第二个参数输入需要分隔的字符的合集,对于这个字符的合集我们使用宏DELIM定义一下,便于进行修改,strtok的第一个参数第一次需要传入传入进行分隔的字符串,它会在需要分隔的字符的合集中匹配字符,当遇到匹配的字符之后,它就会将匹配分隔的字符对应字符串的位置设置为\\0,并且移动到下一个位置,停止,等待第二次调用,所以这个strtok需要进行第一次的预处理,剩下的分隔strtok的第一个参数传入NULL,它就会将所有匹配分隔字符的字符串位置全部设置为\\0了,当移动到最后,没有字符串可以进行分隔的时候,它会返回NULL,我们可以利用这个NULL退出循环
  2. 我们需要获取分隔的字符串,并且将这个分隔的字符串放到一个字符串数组argv中,初始化argv的大小的时候,我们使用宏ARGV_SIZE定义一下大小为32,因为一个命令就算选项在这么多,一行中带的选项一般不会超过32个
  3. 同样的,我们可以根据分隔的次数-1,去统计argc的次数,即命令加选项的个数
#include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}void interact(char* cline, int size){ printf(FORMATE, getusrname(), gethostname(), getpwd()); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\';}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); //解析命令行 int i = 0; argv[i++] = strtok(commandline, DELIM); while(argv[i++] = strtok(NULL, DELIM)); int argc = i - 1; if(argv[0] == NULL) argc = 0;//当用户没有输入的时候,argc为0,应该特殊处理一下 if(argc == 0) continue;//当argc为0的时候,用户没有输入,这时候应该重新与用户交互 for(int j = 0; argv[j]; j++)//打印命令行参数进行测试 { printf(\"argv[%d]: %s\\n\", j, argv[j]); } printf(\"argc: %d\\n\", argc); } return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 我们希望将解析命令行放入splitstring这个函数中,会使用到字符数组commandline以及字符串指针数组用来存储分割后的字符串的起始地址,所以我们将其进行传参,并且根据字符串分隔的次数,返回argc即命令加选项的个数,如果在函数外接收的argc的个数为0,说明此时用户仅仅按下回车换行,即没有有效输入,我们应该continue重新与用户进行交互
#include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}void interact(char* cline, int size){ printf(FORMATE, getusrname(), gethostname(), getpwd()); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\';}int splitstring(char* cline, char* _argv[]){ int i = 0; _argv[i++] = strtok(cline, DELIM); while(_argv[i++] = strtok(NULL, DELIM)); if(_argv[0] == NULL) { return 0; } return i - 1;}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); //解析命令行 int argc = splitstring(commandline, argv); if(argc == 0) { continue; } } return 0;}

三、普通命令的执行

  1. 我们上两个模块我们已经可以接收用户输入,将用户输入的字符串解析出来,那么接下来就是根据解析出来的命令和选项去执行命令了,对于普通命令,是由bash创建子进程,子进程去执行普通命令,由于我们有命令就是argv[0],但是我们没有路径,我们有命令行参数argv,子进程进行程序替换execvp即可
  2. 接下来就是父进程使用waitpid等待指定的子进程即可,获取子进程的退出码即可
  3. 同样的,我们希望将普通命令的执行放在normalexcute函数中执行,
#include #include #include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}void interact(char* cline, int size){ printf(FORMATE, getusrname(), gethostname(), getpwd()); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\';}int splitstring(char* cline, char* _argv[]){ int i = 0; _argv[i++] = strtok(cline, DELIM); while(_argv[i++] = strtok(NULL, DELIM)); if(_argv[0] == NULL) { return 0; } return i - 1;}void normalexcute(char* _argv[]){ int id = fork(); if(id < 0) { perror(\"fork\"); return; } else if(id == 0) { execvp(_argv[0], _argv); exit(EXIT_CODE); } else { //id > 0 int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { lastcode = WEXITSTATUS(status); } }}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); //解析命令行 int argc = splitstring(commandline, argv); if(argc == 0) { continue; } //普通命令的执行 normalexcute(argv); } return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 其中小编模拟实现的bash命令行解释器就可以初步运行起来执行普通命令了,诸如ls,pwd,whoami等普通命令都可以执行
  2. 但是当我们使用cd命令的时候,即cd …想要退回上级目录发现,退回失败,并且当前进程的路径仍然没有变化,这就很令人困惑,我不是fork子进程,子进程进行程序替换执行这个命令了吗?为什么当前进程的路径没有发生变化?
  3. 恰恰如此,正是由于是子进程执行的这个cd命令,所以变化的是子进程的当前工作路径,和当前的进程,也就是父进程的工作路径的无关,所以我们应该让父进程执行这个cd命令,这样当前进程的路径才能切换,这种不创建子进程执行,而是由父进程亲自执行的命令我们称为内建命令,请读者友友继续阅读,由小编进行讲解我们的程序如何让父进程亲自执行内建命令

四、内建命令

  1. 其实内建命令很简单,即使用 if 语句进行判断即可,既然普通命令都可以通过可执行程序的方式列举出来,那么内建命令同样也可以使用 if 语句逐个判断出来,在bash命令行解释器中,常见的内建命令有40多个,这里小编模拟实现3个内建命令供大家理解学习bash命令行解释器
  2. 注意这个内建命令的判断以及执行的位置应该是在普通命令执行之前进行判断,因为内建命令我们不期望让子进程来执行,所以也应该使用一个变量ret进行判断,执行内建命令就执行普通命令,不执行内建命令就执行普通命令

【linux】自定义shell——bash命令行解释器小程序_bash解释器
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 那么首先我们要实现的内建命令是cd命令,cd命令的作用就是修改当前工作路径,如何修改呢?其实操作系统提供了一个系统调用chdir用于修改当前的工作路径,传入路径即可进行修改当前进程的工作路径,同时由于与用户进行交互的字符串,[用户@主机名 路径]# 中有对当前工作路径的打印,并且这个工作路径是从使用getenv从环境变量PWD中获取的,所以我们还要对这个环境变量进行更新,所以我们可以使用sprintf对调用获取的getpwd获取的字符串(字符串其实是首字符的地址,有了地址就可以对字符串进行写入)进行格式化写入路径即可
  2. 我们期望将内建命令的放在buildcommand这个函数,由于需要对命令行参数中的命令以及选项进行获取和相应的判断,所以传参命令行参数argv,同时还需要命令行参数的个数argc判断内建命令的命令以及选项个数,因为诸如cd命令,它的执行只能是cd 后面跟路径,所以命令加选项的个数只能是两个,我们需要对其进行判断,所以需要传参argc命令行参数的个数
#include #include #include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}char* getpwd(){ return getenv(\"PWD\");}void interact(char* cline, int size){ printf(FORMATE, getusrname(), gethostname(), getpwd()); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\';}int splitstring(char* cline, char* _argv[]){ int i = 0; _argv[i++] = strtok(cline, DELIM); while(_argv[i++] = strtok(NULL, DELIM)); if(_argv[0] == NULL) { return 0; } return i - 1;}void normalexcute(char* _argv[]){ int id = fork(); if(id < 0) { perror(\"fork\"); return; } else if(id == 0) { execvp(_argv[0], _argv); exit(EXIT_CODE); } else { //id > 0 int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { lastcode = WEXITSTATUS(status); } }}int buildcommand(int _argc, char* _argv[]){ if(_argc == 2 && strcmp(_argv[0], \"cd\") == 0) { chdir(_argv[1]); sprintf(getpwd(), \"%s\", _argv[1]); return 0; } return 1;}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); //解析命令行 int argc = splitstring(commandline, argv); if(argc == 0) { continue; } //内建命令的执行 int ret = buildcommand(argc, argv); //普通命令的执行 if(ret) { normalexcute(argv); } } return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 经过我们的 if 判断之后果然我们的路径发生了改变,但是对于[用户@主机名 路径]# 中有对当前工作路径的打印,却成为了…这并不是我们期望看到的,因为如果我们使用cd …改变当前路径,这个…是相对路径,而不是绝对路径,所以我们使用argv[1]进行访问,会将…写入到环境变量PWD中,我们期望每时每刻getpwd获取的当前的工作路径是绝对路径,所以就不能使用环境变量进行获取

【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 所以我们需要对getpwd获取当前工作路径的方式进行修改,不使用getenv从环境变量PWD中获取当前的工作路径,而是采用系统调用getcwd获取当前进程的工作路径,同时我们使用一个字符数组pwd将这个路径进行存储,字符数组pwd的大小使用宏LINE_SIZE进行定义,那么getpwd这个函数的就不设置返回值了,而是直接对pwd字符数组进行写入即可,由于pwd字符数组是一个全局变量,所以写入的结果我们在任何函数都可以进行获取这个路径
  2. 同时我们还应使用sprintf对当前进程的环境变量中的PWD对应的工作路径进行修改为getcwd对应的当前的工作路径
#include #include #include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char pwd[LINE_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}void getpwd(){ getcwd(pwd, sizeof(pwd));}void interact(char* cline, int size){ getpwd(); printf(FORMATE, getusrname(), gethostname(), pwd); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\';}int splitstring(char* cline, char* _argv[]){ int i = 0; _argv[i++] = strtok(cline, DELIM); while(_argv[i++] = strtok(NULL, DELIM)); if(_argv[0] == NULL) { return 0; } return i - 1;}void normalexcute(char* _argv[]){ int id = fork(); if(id < 0) { perror(\"fork\"); return; } else if(id == 0) { execvp(_argv[0], _argv); exit(EXIT_CODE); } else { //id > 0 int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { lastcode = WEXITSTATUS(status); } }}int buildcommand(int _argc, char* _argv[]){ if(_argc == 2 && strcmp(_argv[0], \"cd\") == 0) { chdir(_argv[1]); getpwd(); sprintf(getenv(\"PWD\"), \"%s\", pwd); return 0; } return 1;}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); //解析命令行 int argc = splitstring(commandline, argv); if(argc == 0) { continue; } //内建命令的执行 int ret = buildcommand(argc, argv); //普通命令的执行 if(ret) { normalexcute(argv); } } return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器
使用getcwd获取绝对路径之后,对于命令行中,[用户@主机名 路径]# 中有对当前工作路径的打印就是当前进程的绝对路径了

  1. 那么我们接下来尝试测试export,export可以添加环境变量,那么我们看一下我们的程序是否可以进行添加当前进程的环境变量

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器
【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 无法添加环境变量,因为export也是内建命令,如果不使用 if 语句进行判断,那么上述是fork子进程,让子进程进程程序替换执行export,将MYVALUE添加到子进程的环境变量中
  2. 奇怪,明明是子进程执行了export,那么按道理来讲,我们使用env去查看,由于env也没有经过 if 语句判断,所以env实际上查看的是子进程的环境变量,那么此时子进程由于export添加环境变量MYVALUE之后,应该可以查看到这个MYVALUE,但是这里子进程的环境变量却没有这个MYVALUE,这又是为什么呢?
  3. 其实环境变量的本质是字符串指针数组,真正的环境变量是在内核空间中,是由bash进程启动的时候,从操作系统的环境变量的配置文件 .bash_profile 中将环境变量字符串读取拷贝到内核空间的,进程中的环境变量是字符串指针数组,里面存储着指向环境变量字符串的一个个的指针
  4. 我们读取用户的输入,并且进行分隔,将其放到char* argv[]字符串指针数组中,以上面为例argv[0]就存储着export,argv[1]就存储着MYVALUE=1111111111111111111111,当进程执行export的时候,实际上是在环境变量表中找到一个空闲的位置,将要添加的环境变量对应的字符串的指针放到进程的环境变量表中,此时环境变量中就存储着argv[1]对应的字符串的地址,但是由于我们的程序要不断的与用户进行交互,所以argv[1]的内容会进行不断的替换,但是在环境变量表中还存储着argv[1]对应的字符串的地址,所以在环境变量中就找不到原来MYVALUE=1111111111111111111111了,取而代之的是新的内容,由于小编使用env进行查看,所以argv[1]位置处就为NULL,所以env查看环境变量的内容就无法查看到MYVALUE=1111111111111111111111了,MYVALUE=1111111111111111111111位置处的内容被替换成NULL
  1. 所以经过上面的分析,我们还应该维护给当前进程维护自己的环境变量表,这里小编就简单的维护可以存储一个字符串自己的环境变量表进行演示了,感兴趣的读者友友可以自己尝试编写一个二维的环境变量表,可以存储多个字符串,也就可以维护多个环境变量

【linux】自定义shell——bash命令行解释器小程序_bash解释器

  1. 同时小编将export也是用 if 语句进行判断,当argc对一个的命令行参数的个数为两个(export的作用就是导入环境变量,所以命令是export,选项是要添加的环境变量,所以argc对应的命令行参数的个数必须为两个)并且是export的时候,我们就将argv[1]的内容写入到我们维护的自己的环境变量中,并且将我们自己的环境变量使用putenv放到进程的环境变量中
  2. 但是我们现在还无法查看父进程的环境变量,如果使用env,那么查看的是子进程的环境变量,这里小编换一种方式使用echo $环境变量的方式去查看父进程的环境变量,此时面临着同样的境遇,如果不采用 if 语句进行判断,那么此时也就是采用的fork创建子进程,子进程进行程序替换去执行echo $环境变量,那么查看到的仍然为子进程的环境变量,所以这里的echo仍然为内建命令,所以我们需要使用 if 语句对echo进行判断,当为echo $?的时候,将上一个进程的退出码打印出来,当为echo $环境变量的时候,此时使用getenv获取环境变量进行打印即可,但是这里需要注意不可以直接使用getenv(argv[1])进行获取,因为此时argv[1]中的字符串指针指向的内容实际上是 $环境变量,所以应该跳过$这个字符,即getenv(argv[1] + 1)进行获取环境变量进行打印,当上述这两种情况都不是,那么就直接打印argv[1]的字符串内容,因为echo的作用就是打印字符串内容
  3. 所以当我们编写好内建命令export以及echo之后,我们就可以在当前的父进程中添加并且查看环境变量了
#include #include #include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char pwd[LINE_SIZE];char myenv[LINE_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}void getpwd(){ getcwd(pwd, sizeof(pwd));}void interact(char* cline, int size){ getpwd(); printf(FORMATE, getusrname(), gethostname(), pwd); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\';}int splitstring(char* cline, char* _argv[]){ int i = 0; _argv[i++] = strtok(cline, DELIM); while(_argv[i++] = strtok(NULL, DELIM)); if(_argv[0] == NULL) { return 0; } return i - 1;}void normalexcute(char* _argv[]){ int id = fork(); if(id < 0) { perror(\"fork\"); return; } else if(id == 0) { execvp(_argv[0], _argv); exit(EXIT_CODE); } else { //id > 0 int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { lastcode = WEXITSTATUS(status); } }}int buildcommand(int _argc, char* _argv[]){ if(_argc == 2 && strcmp(_argv[0], \"cd\") == 0) { chdir(_argv[1]); getpwd(); sprintf(getenv(\"PWD\"), \"%s\", pwd); return 0; } else if(_argc == 2 && strcmp(_argv[0], \"export\") == 0) { sprintf(myenv, \"%s\", _argv[1]); putenv(myenv); return 0; } else if(_argc == 2 && strcmp(_argv[0], \"echo\") == 0) { if(strcmp(_argv[1], \"$?\") == 0) { printf(\"%d\\n\", lastcode); lastcode = 0; } else if(*_argv[1] == \'$\') { char* ret = getenv(_argv[1] + 1); if(ret) { printf(\"%s\\n\", ret); } } else { printf(\"%s\\n\", _argv[1]); } return 0; } return 1;}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); //解析命令行 int argc = splitstring(commandline, argv); if(argc == 0) { continue; } //内建命令的执行 int ret = buildcommand(argc, argv); //普通命令的执行 if(ret) { normalexcute(argv); } } return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器
此时当前进程就可以使用export添加并且使用echo &查看环境变量了

  1. 仔细观察一下下面ls的运行,bash命令行解释器有进行配色,而小编编写的程序去运行的ls命令却没有配色,这时候我们在ls的命令后面添加- -color选项就可以使我们的ls命令带上配色,达到与bash命令行解释器一样的效果
    【linux】自定义shell——bash命令行解释器小程序_bash解释器
  2. 那么我们同样需要对ls命令进行特殊处理一下,这个特殊处理小编就放在内建命令的模块进行处理,那么就在命令行参数中添加一个选项- -color即可完成
#include #include #include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char pwd[LINE_SIZE];char myenv[LINE_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}void getpwd(){ getcwd(pwd, sizeof(pwd));}void interact(char* cline, int size){ getpwd(); printf(FORMATE, getusrname(), gethostname(), pwd); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\';}int splitstring(char* cline, char* _argv[]){ int i = 0; _argv[i++] = strtok(cline, DELIM); while(_argv[i++] = strtok(NULL, DELIM)); if(_argv[0] == NULL) { return 0; } return i - 1;}void normalexcute(char* _argv[]){ int id = fork(); if(id < 0) { perror(\"fork\"); return; } else if(id == 0) { execvp(_argv[0], _argv); exit(EXIT_CODE); } else { //id > 0 int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { lastcode = WEXITSTATUS(status); } }}int buildcommand(int _argc, char* _argv[]){ if(_argc == 2 && strcmp(_argv[0], \"cd\") == 0) { chdir(_argv[1]); getpwd(); sprintf(getenv(\"PWD\"), \"%s\", pwd); return 0; } else if(_argc == 2 && strcmp(_argv[0], \"export\") == 0) { sprintf(myenv, \"%s\", _argv[1]); putenv(myenv); return 0; } else if(_argc == 2 && strcmp(_argv[0], \"echo\") == 0) { if(strcmp(_argv[1], \"$?\") == 0) { printf(\"%d\\n\", lastcode); lastcode = 0; } else if(*_argv[1] == \'$\') { char* ret = getenv(_argv[1] + 1); if(ret) { printf(\"%s\\n\", ret); } } else { printf(\"%s\\n\", _argv[1]); } return 0; } if(strcmp(_argv[0], \"ls\") == 0) { _argv[_argc++] = \"--color\"; _argv[_argc] = NULL; } return 1;}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); //解析命令行 int argc = splitstring(commandline, argv); if(argc == 0) { continue; } //内建命令的执行 int ret = buildcommand(argc, argv); //普通命令的执行 if(ret) { normalexcute(argv); } } return 0;}

运行结果如下
【linux】自定义shell——bash命令行解释器小程序_bash解释器

五、源代码

makefile

myshell:myshell.cgcc $^ -o $@ -std=c99.PHONY:cleanclean:rm -f myshell

myshell.c

#include #include #include #include #include #define FORMATE \"[%s@%s %s]# \"#define LINE_SIZE 1024#define DELIM \" \"#define ARGC_SIZE 32#define EXIT_CODE 11int lastcode = 0;char commandline[LINE_SIZE];char* argv[ARGC_SIZE];char pwd[LINE_SIZE];char myenv[LINE_SIZE];char* getusrname(){ return getenv(\"USER\");}char* gethostname(){ return getenv(\"HOSTNAME\");}void getpwd(){ getcwd(pwd, sizeof(pwd));}void interact(char* cline, int size){ getpwd(); printf(FORMATE, getusrname(), gethostname(), pwd); fgets(cline, size, stdin); //ls -a -l\\n\\0 cline[strlen(cline) - 1] = \'\\0\';}int splitstring(char* cline, char* _argv[]){ int i = 0; _argv[i++] = strtok(cline, DELIM); while(_argv[i++] = strtok(NULL, DELIM)); if(_argv[0] == NULL) { return 0; } return i - 1;}void normalexcute(char* _argv[]){ int id = fork(); if(id < 0) { perror(\"fork\"); return; } else if(id == 0) { execvp(_argv[0], _argv); exit(EXIT_CODE); } else { //id > 0 int status = 0; int ret = waitpid(id, &status, 0); if(ret == id) { lastcode = WEXITSTATUS(status); } }}int buildcommand(int _argc, char* _argv[]){ if(_argc == 2 && strcmp(_argv[0], \"cd\") == 0) { chdir(_argv[1]); getpwd(); sprintf(getenv(\"PWD\"), \"%s\", pwd); return 0; } else if(_argc == 2 && strcmp(_argv[0], \"export\") == 0) { sprintf(myenv, \"%s\", _argv[1]); putenv(myenv); return 0; } else if(_argc == 2 && strcmp(_argv[0], \"echo\") == 0) { if(strcmp(_argv[1], \"$?\") == 0) { printf(\"%d\\n\", lastcode); lastcode = 0; } else if(*_argv[1] == \'$\') { char* ret = getenv(_argv[1] + 1); if(ret) { printf(\"%s\\n\", ret); } } else { printf(\"%s\\n\", _argv[1]); } return 0; } if(strcmp(_argv[0], \"ls\") == 0) { _argv[_argc++] = \"--color\"; _argv[_argc] = NULL; } return 1;}int main(){ while(1) { //交互 interact(commandline, sizeof(commandline)); //解析命令行 int argc = splitstring(commandline, argv); if(argc == 0) { continue; } //内建命令的执行 int ret = buildcommand(argc, argv); //普通命令的执行 if(ret) { normalexcute(argv); } } return 0;}

六、给shell程序添加重定向

由于需要使用到一定的知识铺垫,所以小编结合了后面讲解的文件重定向的知识,进一步讲解了如何给shell程序添加重定向,感兴趣的读者友友可以点击后方蓝字链接学习详情请点击<——


总结

以上就是今天的博客内容啦,希望对读者朋友们有帮助
水滴石穿,坚持就是胜利,读者朋友们可以点个关注
点赞收藏加关注,找到小编不迷路!