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


目录

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


前言

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

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

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

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

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

在这里插入图片描述

  1. 我们观察一下bash命令行,它的格式是[wzx@VM-12-3-centos lesson20]$ 这种形式,所以我们自定义实现的bash也应该类似于这种形式,通常普通用户使用$,root用户使用#,但是这里我们为了和小编使用的普通用户的$进行区分,所以我们使用#作为最后一个字符,即 [用户@主机名 路径]# 这种形式
  2. 那么我们就需要获取当前的用户名,主机名,以及当前所在的工作路径,对于这些信息我们都可以使用putenv进行获取这些环境变量信息,其中用户名在环境变量中有USER,主机名在环境变量中也有HOSTNAME,当前所在的工作路径在环境变量中也有PWD进行获取(注意:后面讲到内建命令cd的时候,小编会对当前工作路径的获取方式进行修改,这里使用环境变量PWD进行获取便于理解)
    在这里插入图片描述
#include <stdio.h>
#include <stdlib.h>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;
}

运行结果如下
在这里插入图片描述

  1. 我们可以看到当前我们的程序也可以打印出[用户@主机名 路径]# 这种形式了,但是注意观察,bash命令行解释器打印出[用户@主机名 路径]$之后并没有进行换行,而是等待我们进行输入,所以小编将我们添加的换行\n去掉
  2. 这个等待其实就是阻塞式等待键盘设备就绪,即等待用户输入,原理其实很简单一个sacnf就可以让我们也是实现这样的功能,那么我们将用户的输入使用字符数组commandline存储起来便于我们后续的字符串分隔,那么对于这个字符数组的大小通常是1024,并且我们也喜欢使用宏LINE_SIZE进行定义这个1024,便于进行修改
  3. 我们并不喜欢直接将诸如格式之类的直接定义在实现代码中,我们通常使用一个宏FORMATE定义在开头便于我们进行修改
#include <stdio.h>
#include <stdlib.h>#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;
}

运行结果如下
在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

  1. 但是对于第三个参数呢?FILE* stream就是一个流对象,即FILE*就是我们曾经打开的文件,流,对我们来说很陌生,但是这里小编要介绍三个标准流中的stdin,它是一个文件的流对象,在我们的c语言程序启动的时候,编译器就会默认帮我们打开一个读取输入文件,我们的输入就会输入到这个文件中,流对象stdin就是对这个输入的文件进行管理和读取写入等一系列操作的入口,所以我们可以使用stdin作为fgets的第三个参数
#include <stdio.h>
#include <stdlib.h>#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;
}

运行结果如下
在这里插入图片描述

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

在这里插入图片描述

  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 <stdio.h>
#include <stdlib.h>
#include <string.h>#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\0commandline[strlen(commandline) - 1] = '\0';printf("echo: %s\n", commandline);}return 0;
}

运行结果如下
在这里插入图片描述

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

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

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

在这里插入图片描述

  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 <stdio.h>
#include <stdlib.h>
#include <string.h>#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\0cline[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;
}

运行结果如下
在这里插入图片描述

  1. 我们希望将解析命令行放入splitstring这个函数中,会使用到字符数组commandline以及字符串指针数组用来存储分割后的字符串的起始地址,所以我们将其进行传参,并且根据字符串分隔的次数,返回argc即命令加选项的个数,如果在函数外接收的argc的个数为0,说明此时用户仅仅按下回车换行,即没有有效输入,我们应该continue重新与用户进行交互
#include <stdio.h>
#include <stdlib.h>
#include <string.h>#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\0cline[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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#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\0cline[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 > 0int 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;
}

运行结果如下
在这里插入图片描述

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

四、内建命令

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

在这里插入图片描述
在这里插入图片描述

  1. 那么首先我们要实现的内建命令是cd命令,cd命令的作用就是修改当前工作路径,如何修改呢?其实操作系统提供了一个系统调用chdir用于修改当前的工作路径,传入路径即可进行修改当前进程的工作路径,同时由于与用户进行交互的字符串,[用户@主机名 路径]# 中有对当前工作路径的打印,并且这个工作路径是从使用getenv从环境变量PWD中获取的,所以我们还要对这个环境变量进行更新,所以我们可以使用sprintf对调用获取的getpwd获取的字符串(字符串其实是首字符的地址,有了地址就可以对字符串进行写入)进行格式化写入路径即可
  2. 我们期望将内建命令的放在buildcommand这个函数,由于需要对命令行参数中的命令以及选项进行获取和相应的判断,所以传参命令行参数argv,同时还需要命令行参数的个数argc判断内建命令的命令以及选项个数,因为诸如cd命令,它的执行只能是cd 后面跟路径,所以命令加选项的个数只能是两个,我们需要对其进行判断,所以需要传参argc命令行参数的个数
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#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\0cline[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 > 0int 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;
}

运行结果如下
在这里插入图片描述

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

在这里插入图片描述

  1. 所以我们需要对getpwd获取当前工作路径的方式进行修改,不使用getenv从环境变量PWD中获取当前的工作路径,而是采用系统调用getcwd获取当前进程的工作路径,同时我们使用一个字符数组pwd将这个路径进行存储,字符数组pwd的大小使用宏LINE_SIZE进行定义,那么getpwd这个函数的就不设置返回值了,而是直接对pwd字符数组进行写入即可,由于pwd字符数组是一个全局变量,所以写入的结果我们在任何函数都可以进行获取这个路径
  2. 同时我们还应使用sprintf对当前进程的环境变量中的PWD对应的工作路径进行修改为getcwd对应的当前的工作路径
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#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\0cline[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 > 0int 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;
}

运行结果如下
在这里插入图片描述
使用getcwd获取绝对路径之后,对于命令行中,[用户@主机名 路径]# 中有对当前工作路径的打印就是当前进程的绝对路径了

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

运行结果如下
在这里插入图片描述
在这里插入图片描述

  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. 所以经过上面的分析,我们还应该维护给当前进程维护自己的环境变量表,这里小编就简单的维护可以存储一个字符串自己的环境变量表进行演示了,感兴趣的读者友友可以自己尝试编写一个二维的环境变量表,可以存储多个字符串,也就可以维护多个环境变量

在这里插入图片描述

  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 <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#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\0cline[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 > 0int 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;
}

运行结果如下
在这里插入图片描述
此时当前进程就可以使用export添加并且使用echo &查看环境变量了

  1. 仔细观察一下下面ls的运行,bash命令行解释器有进行配色,而小编编写的程序去运行的ls命令却没有配色,这时候我们在ls的命令后面添加- -color选项就可以使我们的ls命令带上配色,达到与bash命令行解释器一样的效果
    在这里插入图片描述
  2. 那么我们同样需要对ls命令进行特殊处理一下,这个特殊处理小编就放在内建命令的模块进行处理,那么就在命令行参数中添加一个选项- -color即可完成
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#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\0cline[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 > 0int 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;
}

运行结果如下
在这里插入图片描述

五、源代码

makefile

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

myshell.c

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>#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\0cline[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 > 0int 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;
}

总结

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.pswp.cn/pingmian/93254.shtml
繁体地址,请注明出处:http://hk.pswp.cn/pingmian/93254.shtml
英文地址,请注明出处:http://en.pswp.cn/pingmian/93254.shtml

如若内容造成侵权/违法违规/事实不符,请联系英文站点网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【Python】Python爬虫学习路线

文章目录Python爬虫学习路线&#xff1a;从入门到实战的全景指南一、地基&#xff1a;Python核心基础1. 基础语法与数据结构2. 面向对象编程&#xff08;OOP&#xff09;3. 正则表达式&#xff08;Regex&#xff09;4. 模块与包管理二、工具链&#xff1a;Python爬虫核心库1. 网…

VUE+SPRINGBOOT从0-1打造前后端-前后台系统-用户管理

在现代Web应用开发中&#xff0c;前后端分离架构已经成为主流模式。本文将通过一个完整的用户管理系统案例&#xff0c;详细介绍如何使用Vue.js Element UI构建前端界面&#xff0c;结合Spring Boot实现后端服务&#xff0c;实现前后端分离开发。该系统包含用户信息的增删改查…

基于uni-app+vue3实现的微信小程序地图范围限制与单点标记功能实现指南

一、功能概述本文将分步骤讲解如何使用uni-app框架在微信小程序中实现以下功能&#xff1a;显示基础地图绘制特定区域范围&#xff08;以郑州市为例&#xff09;实现点击地图添加标记点限制标记点只能在指定区域内添加显示选中位置的坐标信息二、分步骤实现步骤1&#xff1a;搭…

C# 反射和特性(关于应用特性的更多内容)

关于应用特性的更多内容 至此&#xff0c;我们演示了特性的简单使用&#xff0c;都是为方法应用单个特性。本节将讲述特性的其他使 用方式。 多个特性 可以为单个结构应用多个特性。 多个特性可以使用下面任何一种格式列出。 独立的特性片段一个接一个。通常&#xff0c;它们彼…

【iOS】KVC原理及自定义

目录 前言 KVC定义及API KVC的使用 基本类型 集合类型 访问非对象类型——结构体 集合操作符 层层嵌套 KVC底层原理 设值过程 取值过程 自定义KVC setter方法 getter方法 KVC异常小技巧 自动转换类型 设置空值 未定义的key 前言 在平时的开发中我们经常用到K…

完整设计 之 智能合约系统:主题约定、代理协议和智能合约 (临时命名)----PromptPilot (助手)答问之2

摘要&#xff08;CSDN的AI助手生成的&#xff09;智能合约系统架构设计摘要本设计构建了一个多层次智能合约系统&#xff0c;包含150字以内的核心架构&#xff1a;三级架构体系&#xff1a;元级&#xff08;序分&#xff09;&#xff1a;MetaModel合约定义系统核心原则模型级&a…

Java基础 8.16

1.final关键字基本介绍final中文意思&#xff1a;最后的&#xff0c;最终的final可以修饰类、属性、方法和局部变量在某些情况下&#xff0c;程序员可能有以下需求&#xff0c;就会使用到final当不希望类被继承时,可以用final修饰当不希望父类的某个方法被子类覆盖/重写(overri…

YOLOv8目标检测网络结构理论

目录 YOLOv8的网络结构图&#xff1a; Backbone 卷积块&#xff08;Conv Block&#xff09; Conv2d层 BatchNorm2d层 SiLU激活函数 瓶颈块(Bottleneck Block) C2f 模块结构 Neck SPPF(空间金字塔池化快速) PAN - FPN Head 结构1.卷积层和激活函数: 2.预测层(Predi…

docker部署hadoop集群

Docker部署hadoop集群下载资源构建镜像启动容器搭建集群配置ssh免密节点职责安排修改配置文件启动集群测试上传下载执行wordcount程序补充配置历史服务器日志聚集单节点启动Java客户端使用HDFSMapReduce下载资源 java华为镜像下载地址&#xff1a;Index of java-local/jdk (hu…

常用的T-SQL命令

文章目录1. 数据库操作2. 表操作3. 数据插入、更新、删除4. 数据查询5. 存储过程6. 事务处理7、如何使用T-SQL在表中设置主键和外键&#xff1f;1. 设置主键&#xff08;PRIMARY KEY&#xff09;方法1&#xff1a;创建表时定义主键方法2&#xff1a;通过ALTER TABLE添加主键2. …

C++面试题及详细答案100道( 31-40 )

《前后端面试题》专栏集合了前后端各个知识模块的面试题&#xff0c;包括html&#xff0c;javascript&#xff0c;css&#xff0c;vue&#xff0c;react&#xff0c;java&#xff0c;Openlayers&#xff0c;leaflet&#xff0c;cesium&#xff0c;mapboxGL&#xff0c;threejs&…

给纯小白的 Python 操作 Excel 笔记

&#x1f9f0; 1. 先装工具电脑键盘按 Win R&#xff0c;输入 cmd&#xff0c;回车&#xff0c;把下面一行粘进去回车&#xff0c;等它跑完。 bashpip install openpyxl——————————————————&#x1f6e0;️ 2. 打开一个空白的 Excel 打开 Jupyter Notebook…

HTML 常用属性介绍

目录 HTML 属性 HTML 属性速查表 一、通用属性&#xff08;所有元素适用&#xff09; 二、链接与引用相关属性 三、表单与输入控件属性 四、媒体与多媒体属性 五、事件属性&#xff08;常用 JavaScript 事件&#xff09; 六、其他常用属性 核心通用属性 id 属性 cla…

HTML5练习代码集:学习与实践核心特性

本文还有配套的精品资源&#xff0c;点击获取 简介&#xff1a;HTML5作为新一代网页标准&#xff0c;对Web开发提供了更丰富的功能和工具。本练习代码集专门针对HTML5的核心特性&#xff0c;包括语义化标签、离线存储、多媒体支持、图形绘制等&#xff0c;以及CSS3的3D效果和…

【RH134知识点问答题】第 10 章:控制启动过程

目录 1. 请简要说明 RHEL9 的启动过程。 2. 系统重启和关机的命令分别是什么? 3. Systemd target 是什么&#xff1f; 4. 重置丢失的 root 密码需要哪些步骤&#xff1f; 5. 如何让系统日志在重启后持久保留 1. 请简要说明 RHEL9 的启动过程。 答&#xff1a;①开机自检…

Apollo10.0学习之固态雷达与IMU的外参标定

固态雷达&#xff08;如Livox、禾赛等非旋转式激光雷达&#xff09;与IMU&#xff08;惯性测量单元&#xff09;的外参标定&#xff08;Extrinsic Calibration&#xff09;是自动驾驶、机器人定位&#xff08;如LIO-SAM、FAST-LIO&#xff09;的关键步骤。1. 标定原理 外参标定…

HTML5实现古典音乐网站源码模板1

文章目录 1.设计来源1.1 网站首页1.2 古典音乐界面1.3 著名人物界面1.4 古典乐器界面1.5 历史起源界面 2.效果和源码2.1 动态效果2.2 源代码 源码下载万套模板&#xff0c;程序开发&#xff0c;在线开发&#xff0c;在线沟通 作者&#xff1a;xcLeigh 文章地址&#xff1a;http…

40 C++ STL模板库9-容器2-vector

C STL模板库9-容器2-vector 文章目录C STL模板库9-容器2-vector一、基础概念1. 类型成员&#xff08;Type Members&#xff09;2. 模板参数二、构造函数1. 语法2. 示例三、元素访问1. 函数说明2. 示例代码四、容量操作1. 函数说明2. 关键点说明3. 关键操作解析4. 操作示例五、修…

GPT-5系列文章2——新功能、测试与性能基准全解析

引言 2025年8月&#xff0c;OpenAI正式发布了其新一代旗舰模型GPT-5。与业界此前期待的AGI(人工通用智能)突破不同&#xff0c;GPT-5更像是OpenAI对现有技术的一次深度整合与用户体验优化。本文将全面解析GPT-5的新特性、实际测试表现以及官方发布的基准数据&#xff0c;帮助开…

利用cursor+MCP实现浏览器自动化释放双手

小伙伴们&#xff0c;我们今天利用cursorMCP实现浏览器自动化&#xff0c;释放双手&#xff0c;工作效率嘎嘎提升&#xff01;前期准备&#xff1a;安装node.js网址&#xff1a;https://nodejs.org/zh-cn下载下来安装即可。 下载browser-tools-mcp扩展程序&#xff1a;下载扩展…