Linux命令行解释器的模拟实现过程
目录
- 一、整体框架:
- 二、初始化myshell的环境变量表和命令行参数表:
- 三、命令行提示行的打印:
- 四、获取命令参数:
- 五、重定向判断:
- 六、语义分析:
- 七、 内建命令判断:
- 7.1 cd:
- 7.1.1 cd :
- 7.1.2 cd -:
- 7.1.3 cd / :
- 7.1.4 cd ~:
- 7.1.5 cd +dirname:
- 7.2 echo:
- 7.3 export:
- 7.4 alias:
- 八、子进程执行操作:
- 九、myshell代码汇总:
一、整体框架:
首先我们把这个myshell大致进行框架展示出:
我们首先创建数组cl保存要输入的字符串;而只要读取失败就要一直读取故我们在获取,命令行输入的时候利用了while循环;其次就是如果是内建命令;我们就要直接父进程执行完;无需execute再让子进程执行了。
先说一下想法:这里可执行程序,把它当成真正shell的bash;大部分命令都是通过调用子进程来程序替换完成;有些命令是内建的,故需要自己完成;而首先这个程序会继承原本bash的那张环境变量表;这里我们模拟实现一下真正的bash的那两张表:也就是说我们用数组,通过拷贝原bash的表,改变environ指针来维护我们的数组(也就是我们自己的可执行程序要调用的那张环境变量表) :这里补充一点:对于环境变量如果我们env命令:它是通过environ指针来进行查找打印的;局部打印就不一定了。 后面我们具体实现的时候会有所体现,之后我们道来。
然后下面就是一步步对这些拆开的函数进行实现了。
二、初始化myshell的环境变量表和命令行参数表:
这里我们自己开了两个数组来模拟这两张表;也就是拷贝父bash的那两种表拷贝过来(简单模拟一下)这俩张表的内容就可以作为我们后面程序替换执行命令要传递的参数等。
void initenv(){ memset(env, 0, sizeof(env)); envs= 0; for(int i=0;environ[i];i++){ env[i]=(char*)malloc(strlen(environ[i])+1);//这里模拟的也可以不开空间,直接栈上 strcpy(env[i], environ[i]); envs++; } env[envs] = NULL; // for(int i = 0; env[i]; i++) // { // putenv(env[i]); // } environ = env;//用自己生成的env表 }
这里我们的命令行参数表暂时不需要填充,但是需要把环境变量表由bash那里拷贝过来;并改变了environ指针指向,也就是说等我们执行env操作的时候它就会打印我们的这个env数组了;比如后序我们用putenv等命令的话,它就会通过environ指针对我们的这个数组进行一些增加/覆盖环境变量的操作了。
三、命令行提示行的打印:
我们让它格式输出这样的格式:
#define FT "my simulate shell:%s@%s %s# "//snprintf的format最大值
首先我们对比一下真正的命令解释器:
我们此刻需要替换掉%s的就是通过环境变量找到USER,HOSTHOME ,PWD了:
const char* getuser(){ const char*u=getenv("USER"); return u==NULL?"NONE":u; } const char* gethostname(){ const char*h=getenv("HOSTNAME"); return h==NULL?"NONE":h; } const char* getpwd(){ const char*p=getenv("PWD"); return p==NULL?"NONE":p; }
但是这样我们会发现因为我们维护的这张环境变量表,未添加其他修改功能,这里需要我们手动修改;这样pwd就不会变了(当换目录的时候):因此我们手动维护一下:
下面是我们要添加的全局变量(因为导入environ维护的二维数组应该是地址;故给它整成全局):
//这里获得环境变量和其他上面不同;因为当我们通过chdir改变当前目录的时候它在环境变量中的记录(真正的bash实现了)而我们没有实现,因此我们 //可以通过getcwd每次调完新的目录开始就使用它不仅能改变了env的pwd也就是新的位置;还能打印命令行提示符的时候变化 const char *getpwd(){ char *pwd = getcwd(cwd, sizeof(cwd)); if(pwd != NULL) { snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd); putenv(cwdenv);//put进去的是环境变量的地址也就是一个指针指向的是那段量,因此指针要么是全局要么在堆上 } return pwd==NULL?"NONE":pwd; }
普及一下用到的getcwd:
参数:放入的数组;最大字节数;成功返回这段pwd失败就是NULL。
这里我们再做一个小优化;也就是把路径变短一下就乘当下目录:
std::string convert(const char *pwd){ std::string s=pwd; auto pos=s.rfind('/'); if(pos==std::string::npos)return "error"; if(pos==s.size()-1)return "/"; else return s.substr(pos+1); }
因此我们从末尾给它分割了一下:最后调用它返回的string对象的c_str接口就好;
这里顺便说一下;因为后面很多都用到这一点:就是经常操作的时候把char串变成string然后调用它的c_str()为了方便;以及后面很多要注意作用域:因此考虑了全局设计。
再下面就是命令行打印了:
void ptcmdprompt(){ char p[CS]={0}; snprintf(p,sizeof(p),FT, getuser(),gethostname(),convert(getpwd()).c_str() ); printf("%s",p); //无\n及时刷新标准输出缓冲区 fflush(stdout); }
下面展示下效果:
四、获取命令参数:
这里逻辑比较简单就是把我们输入的字符读入然后把后面的\n去掉:
bool gtcmdline(char *p,int size){ char *j=fgets(p,size,stdin); // printf("参数是:%d",size); if(j==NULL)return false; //printf("%s\n",p); p[strlen(p)-1]=0;//干掉\n if(strlen(p)==0) return false; else return true; }
这里用到了fgets:
也就是:从流中获得字符最多是size个到串s中;读取成功返回这个s;否则返回NULL。
五、重定向判断:
这里我们封装的是redirect函数来完成;简单说就是让它检查我们输入的cl中是否有> < >>等重定向标识符;然后根据左右分别是命令,文件等给它分离开了;并给对应的文件重定向(dup2一下):
预处理:首先利用标识来枚举一下重定向状态:输出,输入,还是追加:
下面就说一下细节处理:
这里值得关注的是:我们从数组末尾开始找
标识符的;这样然后利用覆盖0的操作来完成前方命令的截断:判断顺序:< > >>;其次就是它可能文件前ULqlo面存在空格;故我们再构建一个去除空格函数。
void eliminatesp(char cl[],int &fg){ while(isspace(cl[fg])) fg++; } void redirect(char cl[]){ status=NOPUT_RE; int start=0; int end=strlen(cl)-1; while(start<end){ if(cl[end]=='<'){ cl[end++] = 0; eliminatesp(cl, end); status = INPUT_RE; filename = cl+end; break; } else if(cl[end]=='>'){ if(cl[end-1]=='>'){ status=APPPUT_RE; cl[end-1]=0; } else{ status=OUTPUT_RE; cl[end]=0; } end++; eliminatesp(cl,end); filename = cl+end; break; } else{ end--; } } }
下面我们把获得了重定向左边的命令和右边的文件下面就是利用dup2完成重定向操作了:
这里由于不是内建命令;故我们还是放在子进程来执行:
if(status==INPUT_RE){ int fd=open(filename.c_str(),O_RDONLY); if(fd < 0) exit(1); dup2(fd,0); close(fd); } else if(status==OUTPUT_RE){ int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) exit(1); dup2(fd,1); close(fd); } else if(status==APPPUT_RE){ int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd < 0) exit(1); dup2(fd,1); close(fd); } else { }//NOPUT_RE无重定向操作
故我们只需当分开始fork后对我们标志的进行判断即可;但是当子进程完成重定向执行完退出后我们又要对status这个状态给它重置一下。
六、语义分析:
简单来说就是利用我们的strtok函数完成对空格的分割;然后把它填入到我们自己创建的argv数组中;注:这里最后也要补上NULL;注意好边界处理:
bool cmdparse(char *c){ argc=0; std::string cc=c; // 浅拷贝: // if(_alias[cc]!="") c = &((_alias[cc])[0]); // 借助string的深拷贝赋值完成对hash内数据的深拷贝: if(_alias[cc]!=""){ hash_cp =_alias[cc]; c=&(hash_cp[0]); } argv[argc++]=strtok(c,sp);//此处c这个指针将会随之变化最后分割结束为null while(argv[argc++]=strtok(NULL,sp)){} argc--; return true; }
这里我们先暂时忽略对alias重命名的内建命令的设计(后面会谈到) 。
七、 内建命令判断:
下面我们把整体框架展示一下:
当我们在main函数主体内分析是不是内建命令;如果是内建命令那么就直接由main这个进程执行完然后直接开始下一层循环,就不往下走了;否则就走我们的execute函数。
//内建命令:和execute执行是分开的 bool checkinkeycmd() { std::string cmd = argv[0]; if(cmd == "cd") { // printf("cd进入!\n"); Cd(); return true; } else if(cmd == "echo") { Echo(); return true; } else if(cmd == "export") { // } else if(cmd == "alias"&&argc>=2) { // } return false; }
下面我们分四个部分来对相关内建命令进行单独处理:
7.1 cd:
这里cd其实就是change directory;它完成的操作其实就是帮我们改变目录;但是我们另外让它把我们对应的环境变量表也给改变;其实就要操作我们所维护的那个env数组了。
下面我们就不对应把cd 的相关都实现一遍;大概实现常用的这几个:
注意:这里我们为了可以实现cd -:也就是会定义好变量保存上一次访问的目录;方便回去;故当每次chdir都会保存一下;并改变env表中的pwd
这里我们用的是string;也就是利用了它可以被const类型的字符串初始化;也可以通过c_str完成对应转换。
7.1.1 cd :
这里单纯的cd也就是只有命令无参数此时argc=1;故直接跳到家目录:
const char *gethome() { const char *home = getenv("HOME"); return home == NULL ? "" : home; }
保存原先目录位置然后改变再覆盖env对应pwd即可:
if(argc == 1) //直接返回到家目录,但是此时没有更改env的pwd,故我们后面调用getpwd()完成更改env标记 借助string完成了否则对返回const多次覆盖保存较为麻烦 { std::string home = gethome(); if(home.empty()) return false; std::string tmp=getpwd(); lastpwd=tmp; chdir(home.c_str()); getpwd(); return true; }
下面我们保存第二个参数:
std::string where = argv[1];
效果展示:
7.1.2 cd -:
if(where=="-") //上一个工作目录 { // std::cout<<lastpwd<<std::endl; chdir(lastpwd.c_str());//这里的lastpwd是我们在新切换目录更改env前记录的;故是先前的pwd getpwd(); }
效果展示:
7.1.3 cd / :
else if(where=="/"){ std::string tmp=getpwd(); lastpwd=tmp; chdir("/"); getpwd(); }
效果展示:
7.1.4 cd ~:
这里分为普通用户还是root:普通用户是家目录而root就是登机目录了:
if(!strcmp(getuser(),"root")){ std::string tmp=getpwd(); lastpwd=tmp; chdir("/root"); getpwd(); } else { std::string home = gethome(); std::string tmp=getpwd(); lastpwd=tmp; chdir(home.c_str()); getpwd(); }
效果展示:
7.1.5 cd +dirname:
else { std::string tmp=getpwd(); lastpwd=tmp; // std::cout<<lastpwd<<std::endl; chdir(where.c_str()); getpwd(); }
演示效果:
7.2 echo:
这里我们分为 echo $?;echo $+环境变量:
全局变量lastcode保存上次的子进程退出码;方便下一次打印:
对echo $?我们规定只要走了子进程就会返回1;比如内建命令等就返回0。
void Echo(){ //echo $? echo $PATH if(argc==2){ std::string func=argv[1]; if(func=="$?"){ std::cout << lastcode << std::endl; lastcode = 0; } else if(func[0]=='$'){ std::string envname = func.substr(1); const char * envvalue= getenv(envname.c_str()); if(envvalue) std::cout << envvalue << std::endl; } else { std::cout << func << std::endl; } } }
演示效果:
7.3 export:
这里我们只需在我们维护的env数组多开一个空间然后把我们要导入的串记录一下完成深拷贝(注意最后一个置空):
void Export(){ env_str=arghttp://www.devze.comv[1]; env[envs]=(char*)calloc(strlen(argv[1])+1,1); for(int i=0;i<env_str.size();i++)env[envs][i]=env_str[i]; envs++; env[envs]=NULL; }
效果展示:
当我们退出后重新进入:
发现没了;符合我们的预期。
7.4 alias:
首先我们先认识一下linux中的alias也就是起别名:
查看别名表:
起别名(alias 别名=原名):
使用效果:
删除别名(unalias+别名):
这里比如我们的ll就是alias起别名来的;下面我们就来模拟实现一下(简单版本的alias)。
这里用到了映射,故我们采用了哈希表;
全局变量:
cur,pre是分别是别名和原名 ;
hash_cp是命令行分析过程的对hash表内取得值的一个深拷贝;反之strtok函数破坏了;导致再次使用这个别名就会出现找原名时候被破坏的结果。
封装Alias函数:
void Alias(){ std::string sec=argv[1]; auto pos=sec.find('='); cur=sec.substr(0,pos); pre=sec.substr(pos+1,std::string::npos); for(int i=2;i<argc;i++){ pre+=" "; pre+=argv[i]; } _alias[cur]=pre; }
对语义分析部分修改:
argc=0; std::string cc=c; // 浅拷贝: // if(_alias[cc]!="") c = &((_alias[cc])[0]); // 借助string的深拷贝赋值完成对hash内数据的深拷贝: if(_alias[cc]!=""){ hash_cp =_alias[cc]; c=&(hash_cp[0]); } argv[argc++]=strtok(c,sp);//此处c这个指针将会随之变化最后分割结束为null while(argv[argc++]=strtok(NULL,sp)){} argc--; return true;
效果展示:
八、子进程执行操作:
main函数的进程fork后让子进程得到相关指令和参数并用exec系列函数进行程序替换(这里选用的execvp):
然后父进程阻塞等待回收资源和相关信息:
int execute(){ pid_t pid=fork(); if(pid==0){ //printf("argv[0]: %s\n", argv[0]); if(status==INPUT_RE){ int fd=open(filename.c_str(),O_RDONLY); if(fd < 0) exit(1); dup2(fd,0); close(fd); } else if(status==OUTPUT_RE){ int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) exit(1); dup2(fd,1); close(fd); } else if(status==APPPUT_RE){ int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd < 0) exit(1); dup2(fd,1); close(fd); } else { }//NOPUT_RE无重定向操作 execvp(argv[0],argv); exit(1); } int status=0; pid_t id=waitpid(pid,&status,0); if(id > 0) { lastcode = WEXITSTATUS(status);//对于这里规定当execute的子进程执行完就返回1;内建命令或者其他都是返回0 } return 0; }
九、myshell代码汇总:
#include <IOStream> #include <cstdio> #include <cstring> #include <cstdlib> #include <unistd.h> #include <sys/types.h> #include <sys/wait.h> #include<unordered_map> #include <sys/types.h> #include <sys/stat.h> #include <fcntl.h> extern char**environ; #define CS 1024//命令行提示符最大值 #define FT "my simulate shell:%s@%s %s# "//snprintf的format最大值 #define sp " "//space #define MC 128//命令行参数最大值 //这里模拟了bash的两张表,而不是main直接继承下(改变environ指针)而是重新布置了一下数组,让environ指针指向我们所布置的数组。 // 1. 命令行参数表 #define MAXARGC 128 char *argv[MAXARGC]; int argc = 0; // 2. 环境变量表 #define MAX_ENVS 100 char *env[MAX_ENVS]; int envs = 0; char cwd[102编程客栈4]; char cwdenv[1029]; //char *lastpwd=(char*)calloc(1024,1); std::string lastpwd; int lastcode=0; //char export_env[1024]; std::string env_str; //对alias的适用: std::unordered_map<std::string,std::string>_alias; std::string cur,pre; std::string hash_cp; //重定向: std::string filename; #define NOPUT_RE 0 #define INPUT_RE 1 #define OUTPUT_RE 2 #define APPPUT_RE 3 int status;//重定向方式 void initenv(){ memset(env, 0, sizeof(env)); envs= 0; for(int i=0;environ[i];i++){ env[i]=(char*)malloc(strlen(environ[i])+1);//这里模拟的也可以不开空间,直接栈上 strcpy(env[i], environ[i]); envs++; } env[envs] = NULL; // for(int i = 0; env[i]; i++) // { // putenv(env[i]); // } environ = env;//用自己生成的env表 } //获取一些环境变量: const char* getuser(){ const char*u=getenv("USER"); return u==NULL?"NONE":u; } const char* gethostname(){ const char*h=getenv("HOSTNAME"); return h==NULL?"NONE":h; } //const char* getpwd(){ // const char*p=getenv("PWD"); // return p==NULL?"NONE":p; //} const char *gethome() { const char *home = getenv("HOME"); return home == NULL ? "" : home; } //这里获得环境变量和其他上面不同;因为当我们通过chdir改变当前目录的时候它在环境变量中的记录(真正的bash实现了)而我们没有实现,因此我们 //可以通过getcwd每次调完新的目录ULqlo开始就使用它不仅能改变了env的pwd也就是新的位置;还能打印命令行提示符的时候变化 const char *getpwd(){ char *pwd = getcwd(cwd, sizeof(cwd)); if(pwd != NULL) { snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd); putenv(cwdenv);//put进去的是环境变量的地址也就是一个指针指向的是那段量,因此指针要么是全局要么在堆上 } return pwd==NULL?"NONE":pwd; } //const char *getpwd(){ // // 调用了这个,在Getpwd中 // return Getpwd(); //} //打印命令行提示符:把pwd的最后一个名称得到 std::string convert(const char *pwd){ std::string s=pwd; auto pos=s.rfind('/'); if(pos==stdhttp://www.devze.com::string::npos)return "error"; if(pos==s.size()-1)return "/"; else return s.substr(pos+1); } void ptcmdprompt(){ char p[CS]={0}; snprintf(p,sizeof(p),FT, getuser(),gethostname(),convert(getpwd()).c_str() ); printf("%s",p); //无\n及时刷新标准输出缓冲区 fflush(stdout); } //获得命令行参数: bool gtcmdline(char *p,int size){ char *j=fgets(p,size,stdin); // printf("参数是:%d",size); if(j==NULL)return false; //printf("%s\n",p); p[strlen(p)-1]=0;//干掉\n if(strlen(p)==0) return false; else return true; } //命令行解释:把输入的命令行参数分出来方便后序传给要调用的main的argv bool cmdparse(char *c){ // printf("%s\n",c); argc=0; std::string cc=c; // std::cout<<"内容:"<<_alias[cc]<<std::endl; // 浅拷贝: // if(_alias[cc]!="") c = &((_alias[cc])[0]); // 借助string的深拷贝赋值完成对hash内数据的深拷贝: if(_alias[cc]!=""){ hash_cp =_alias[cc]; c=&(hash_cp[0]); } //printf("%s\n",c); argv[argc++]=strtok(c,sp);//此处c这个指针将会随之变化最后分割结束为null while(argv[argc++]=strtok(NULL,sp)){} argc--; //printf("%s\n%s\n",argv[0],argv[1]); // printf("%s%s\n",argv[0],argv[1]); return true; } //void lastpwd(){ // // printf("11111111111111111"); // // printf(" %s%d\n ",argv[0],argc); // if(!strcmp(argv[0],"cd")&&argc==2){ // // printf("执行\n"); // std::string s("LASTPWD"); // s+="="; // s+=argv[1]; // //std::cout<<s<<std::endl; // // char p[s.size()+1]={0}; // char* p = (char*)calloc(s.size() + 1, 1); // for(int i=0;i<s.size();i++) p[i]=s[i]; // printf("huanbian:%s\n",p); // environ[envs]=(char*)calloc(strlen(p)+1,1); // putenv(p); // } //} bool Cd(){ if(argc == 1) //直接返回到家目录,但是此时没有更改env的pwd,故我们后面调用getpwd()完成更改env标记 借助string完成了否则对返回const多次覆盖保存较为麻烦 { std::string home = gethome(); if(home.empty()) return false; std::string tmp=getpwd(); lastpwd=tmp; chdir(home.c_str()); getpwd(); return true; } else{ std::string where = argv[1]; // printf("%s\n",argv[1]); // cd - / cd ~ if(where=="-") //上一个工作目录 { // std::cout<<lastpwd<<std::endl; chdir(lastpwd.c_str());//这里的lastpwd是我们在新切换目录更改env前记录的;故是先前的pwd getpwd(); } else if(where=="/"){ std::string tmp=getpwd(); lastpwd=tmp; chdir("/"); getpwd(); } else if(where=="~")//家目录 { if(!strcmp(getuser(),"root")){ std::string tmp=getpwd(); lastpwd=tmp; chdir("~"); getpwd(); } else { std::string home = gethome(); std::string tmp=getpwd(); lastpwd=tmp; chdir(home.c_str()); getpwd(); } } // else if(where==".."){} 上级目录 else { std::string tmp=getpwd(); lastpwd=tmp; // std::cout<<lastpwd<<std::endl; chdir(where.c_str()); getpwd(); } return true; } } void Echo(){ //echo $? echo $PATH if(argc==2){ std::string func=argv[1]; if(func=="$?"){ std::cout << lastcode << std::endl; lastcode = 0; } else if(func[0]=='$'){ std::string envname = func.substr(1); const char * envvalue= getenv(envname.c_str()); if(envvalue) std::cout << envvalue << std::endl; } else { std::cout << func << std::endl; } } } void Export(){ env_str=argv[1]; env[envs]=(char*)calloc(strlen(argv[1])+1,1); for(int i=0;i<env_str.size();i++)env[envs][i]=env_str[i]; envs++; env[envs]=NULL; } void Alias(){ std::string sec=argv[1]; auto pos=sec.find('='); cur=sec.substr(0,pos); pre=sec.substr(pos+1,std::string::npos); for(int i=2;i<argc;i++){ pre+=" "; pre+=argv[i]; } _alias[cur]=pre; } //分子进程执行: int execute(){ pid_t pid=fork(); if(pid==0){ //printf("argv[0]: %s\n", argv[0]); if(status==INPUT_RE){ int fd=open(filename.c_str(),O_RDONLY); if(fd < 0) exit(1); dup2(fd,0); close(fd); } else if(status==OUTPUT_RE){ int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_TRUNC, 0666); if(fd < 0) exit(1); dup2(fd,1); close(fd); } else if(status==APPPUT_RE){ int fd=open(filename.c_str(),O_CREAT | O_WRONLY | O_APPEND, 0666); if(fd < 0) exit(1); dup2(fd,1); close(fd); } else { }//NOPUT_RE无重定向操作 execvp(argv[0],argv); exit(1); } int status=0; pid_t id=waitpid(pid,&status,0); if(id > 0) { lastcode = WEXITSTATUS(status);//对于这里规定当execute的子进程执行完就返回1;内建命令或者其他都是返回0 } return 0; } //内建命令:和execute执行是分开的 bool checkinkeycmd() { std::string cmd = argv[0]; if(cmd == "cd") { // printf("cd进入!\n"); Cd(); return true; } else if(cmd == "echo") { Echo(); return true; } else if(cmd == "export") { Export(); } else if(cmd == "alias"&&argc>=2) { Alias(); } return false; } void eliminatesp(char cl[],int &fg){ while(isspace(cl[fg])) fg++; } void redirect(char cl[]){ status=NOPUT_RE; int start=0; int end=strlen(cl)-1; while(start<end){ if(cl[end]=='<'){ cl[end++] = 0; eliminatesp(cl, end); status = INPUT_RE; filename = cl+end; break; } else if(cl[end]=='>'){ if(cl[end-1]=='>'){ status=APPPUT_RE; cl[end-1]=0; } else{ status=OUTPUT_RE; cl[end]=0; } end++; eliminatesp(cl,end); filename = cl+end; break; } else{ end--; } } } void destroy(){ for(int i=0;env[i];i++){ free(env[i]); } } int main() { //自己的环境变量和命令行参数表的初始化: initenv(); while(1) { //命令提示行打印: ptcmdprompt(); char cl[CS]={0}; //把命令参数输入到cl while(!gtcmdline(cl,sizeof(cl))){} redirect(cl); //把命令参数这个串拆解到argv里: cmdparse(cl); //判断是否是内建命令由bash自己完成(这里模拟的是main自己执行) if(checkinkeycmd()) { // lastpwd(); continue; } execute(); } //销毁表所开辟的空间 destroy(); }
目前功能比较基本,会不断补充;感谢支持!! !
以上就是Linux命令行解释器的模拟实现过程的详细内容,更多关于Linux命令行解释器的资料请关注编程客栈(www.devze.com)其它相关文章!
精彩评论