Linux命令行解释器的模拟实现
欢迎拜访:羑悻的小杀马特.-CSDN博客
本篇主题:Linux命令行解释器
制作日期:2024.12.04
隶属专栏:linux之旅
本篇简介:
主线带你用ubuntu版系统步步分析实现基础版本的shell;比如支持重定向操作,内建命令:cd,echo,alias等;
其中本篇应用主要相关知识点:环境变量,文件基础IO,进程控制,程序替换等;
本篇编写语言:C/C++混编。
目录
编辑
本篇简介:
一·整体框架:
二·初始化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;//干掉\nif(strlen(p)==0) return false;else return true;
}
这里用到了fgets:
也就是:从流中获得字符最多是size个到串s中;读取成功返回这个s;否则返回NULL。
五·重定向判断:
这里我们封装的是redirect函数来完成;简单说就是让它检查我们输入的cl中是否有> < >>等重定向标识符;然后根据左右分别是命令,文件等给它分离开了;并给对应的文件重定向(dup2一下):
预处理:首先利用标识来枚举一下重定向状态:输出,输入,还是追加:
下面就说一下细节处理:
这里值得关注的是:我们从数组末尾开始找
标识符的;这样然后利用覆盖0的操作来完成前方命令的截断:判断顺序:< > >>;其次就是它可能文件前面存在空格;故我们再构建一个去除空格函数。
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这个指针将会随之变化最后分割结束为nullwhile(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前记录的;故是先前的pwdgetpwd();}
效果展示:
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 $PATHif(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=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;
}
效果展示:
当我们退出后重新进入:
发现没了;符合我们的预期。
7.4 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这个指针将会随之变化最后分割结束为nullwhile(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[1024];
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每次调完新的目录开始就使用它不仅能改变了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==std::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;//干掉\nif(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这个指针将会随之变化最后分割结束为nullwhile(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前记录的;故是先前的pwdgetpwd();}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 $PATHif(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};//把命令参数输入到clwhile(!gtcmdline(cl,sizeof(cl))){}redirect(cl);//把命令参数这个串拆解到argv里:cmdparse(cl);//判断是否是内建命令由bash自己完成(这里模拟的是main自己执行)if(checkinkeycmd()) {// lastpwd();continue;} execute();}//销毁表所开辟的空间destroy();
}
目前功能比较基本,会不断补充;感谢支持!! !
本文临近尾声;相信读到此处的你,肯定对它有了不一样的理解,或多或少,都是一种收获;如遇到问题的地方也欢迎大家积极留言,真诚感谢!
相关文章:
Linux命令行解释器的模拟实现
欢迎拜访:羑悻的小杀马特.-CSDN博客 本篇主题:Linux命令行解释器 制作日期:2024.12.04 隶属专栏:linux之旅 本篇简介: 主线带你用ubuntu版系统步步分析实现基础版本的shell;比如支持重定向操作࿰…...
模块化设计割草机器人系统研究与实现(系统架构师论文)
摘要: 割草机器人项目旨在实现自主草坪修剪,以提高园艺工作的效率。本文以具体项目为背景,介绍系统架构师在项目开发过程中的架构设计策略、关键技术应用、问题解决方案和优化措施。首先,本文分析割草机器人项目的总体架构需求&a…...
javascript(前端)作为客户端端通过grpc与cpp(服务端)交互
参考文章 https://blog.csdn.net/pathfinder1987/article/details/129188540 https://blog.csdn.net/qq_45634989/article/details/128151766 前言 临时让我写前端, 一些配置不太懂, 可能文章有多余的步骤但是好歹能跑起来吧 你需要提前准备 公司有自带的这些, 但是版本大都…...
股市复盘笔记
复盘是股市投资中非常重要的一个环节,它指的是投资者在股市收盘后,对当天的市场走势、个股表现以及自己的交易行为进行回顾和总结,以便更好地指导未来的投资决策。以下是对复盘的详细解释: 一、复盘的目的 总结市场走势ÿ…...
【SARL】单智能体强化学习(Single-Agent Reinforcement Learning)《纲要》
📢本篇文章是博主强化学习(RL)领域学习时,用于个人学习、研究或者欣赏使用,并基于博主对相关等领域的一些理解而记录的学习摘录和笔记,若有不当和侵权之处,指出后将会立即改正,还望谅…...
Linux 权限管理:用户分类、权限解读与常见问题剖析
🌟 快来参与讨论💬,点赞👍、收藏⭐、分享📤,共创活力社区。🌟 🚩用通俗易懂且不失专业性的文字,讲解计算机领域那些看似枯燥的知识点🚩 目录 💯L…...
Windows通过指令查看已安装的驱动
Windows通过指令查看已安装的驱动 在 Windows 操作系统中,有几种命令可以用来查看已安装的驱动程序。以下是常见的几种方法: 1. 使用 pnputil 查看已安装驱动程序 pnputil 是一个 Windows 内置工具,可以列出所有已安装的驱动程序包。 命令…...
Springboot(五十一)SpringBoot3整合Sentinel-nacos持久化策略
上文中我记录了在Springboot项目中链接sentinel-dashboard使用限流规则的全过程。 但是呢,有一个小小的问题,我重启了一下我本地的sentinel-dashboard服务,然后,我之前创建的所有的流控规则都没了…… 这……好像有点不合理啊,咱就不能找地儿存储一下?你这一重启就没了,…...
Scala的正则表达式
package hfdobject Test35_3 {def main(args: Array[String]): Unit {println("a\tb")//定义一个规则 正则表达式//1. .表示除了换行之外的其他的任意单个字符//2. \d等于[0-9] 匹配一个数字//3. \D除了\d之外的其他的任意字符,表示非数字//4. \w等价于[…...
cuda12.1版本的pytorch环境安装记录,并添加到jupyter和pycharm中
文章目录 前置准备使用anaconda prompt创建虚拟环境创建虚拟环境激活pytorch虚拟环境把pytorch下载到本地使用pip把安装包安装到pytorch环境中进入python环境检验是否安装成功将环境添加到jupyter在pycharm中使用该环境: 前置准备 安装anaconda,我的版本…...
牛客linux
1、 统计文件的行数 # 方法 1 wc -l ./nowcoder.txt | awk {print $1} # 方法 2 ,awk 可以打印所有行的行号, 或者只打印最后一行 awk {print NR} ./nowcoder.txt |tail -n 1 awk END{print NR} ./nowcoder.txt # 方法 3 grep -c 、-n等等 grep -c "" ./…...
通过HTML Canvas 在图片上绘制文字
目录 前言 一、HTML Canvas 简介 二、准备工作 三、绘制图片 四、绘制文字 五、完整代码 效果演示: 前言 HTML canvas 为我们提供了无限的创意可能性。今天,我们就来探索一下如何通过 HTML canvas 将图片和文字绘制到图片上,创造出独特…...
【C#】ListBox中找到多个image中的其中一个并重置赋值以便清理占用内存
1.ListBox中定义多个image 定义ListBox前台代码及Image控件的赋值 <ListBox Background"{DynamicResource BackgroundBrush}" ItemsSource"{Binding ElementNameDRFinish,PathImages}" Style"{x:Null}" Name"ImageList"ItemConta…...
Apache Commons工具类库使用整理
文章目录 Apache Commons工具类库分类- commons-lang3字符串工具:StringUtils日期工具:DateUtils数值工具:NumberUtils对象工具:ObjectUtils数组工具:ArrayUtils异常工具:ExceptionUtils枚举工具࿱…...
npm 设置镜像
要在npm中设置镜像,你可以使用npm config命令。以下是设置npm镜像的步骤: 临时使用淘宝镜像: npm --registry https://registry.npmmirror.com install package-name 永久设置镜像: npm config set registry https://registry…...
数据结构自测5
第6章 树和二叉树 自测卷解答 一、下面是有关二叉树的叙述,请判断正误(每小题1分,共10分) ( √ )1. 若二叉树用二叉链表作存贮结构,则在n个结点的二叉树链表中只有n—1个非空指针域。 ÿ…...
【后端面试总结】缓存策略选择
一般来说我们常见的缓存策略有三种,他们各自的优劣势和实现逻辑分别如下 Cache Aside(旁路缓存) 特点: 灵活性高:应用程序直接与缓存和数据库交互,具有高度的灵活性,可以根据业务需求自定义缓…...
40分钟学 Go 语言高并发:RPC服务开发实战
RPC服务开发实战 一、RPC服务基础概览 开发阶段关键点重要程度考虑因素接口设计API定义、协议选择、版本控制⭐⭐⭐⭐⭐可扩展性、兼容性服务实现业务逻辑、并发处理、资源管理⭐⭐⭐⭐⭐性能、可靠性错误处理异常捕获、错误码、故障恢复⭐⭐⭐⭐稳定性、可维护性性能测试负载…...
Linux 无界面模式下使用 selenium
文章目录 前言什么是无界面模式?具体步骤安装谷歌浏览器查看安装的谷歌浏览器的版本下载对应版本驱动并安装Python 测试代码 总结个人简介 前言 在 Linux 服务器上运行自动化测试或网页爬虫时,常常需要使用 Selenium 来驱动浏览器进行操作。然而&#x…...
算法第一弹-----双指针
目录 1.移动零 2.复写零 3.快乐数 4.盛水最多的容器 5.有效三角形的个数 6.查找总价值为目标值的两个商品 7.三数之和 8.四数之和 双指针通常是指在解决问题时,同时使用两个指针(变量,常用来指向数组、链表等数据结构中的元素位置&am…...
学习如何解决“区间划分”问题(一般方法论+实例应用讲解)
文章目录 解决“区间划分”问题的一般方法论方法论:解决区间划分问题的四步法1. 问题分析与建模2. 动态规划状态的定义3. 状态转移方程4. 初始条件与边界 方法论应用:最小和最大石子合并得分问题描述步骤 1:问题分析与建模步骤 2:…...
消息中间件-Kafka2-3.9.0源码构建
消息中间件-Kafka2-3.9.0源码构建 1、软件环境 JDK Version 1.8Scala Version 2.12.0Kafka-3.9.0 源码包 下载地址:https://downloads.apache.org/kafka/3.9.0/kafka-3.9.0-src.tgzGradle Version > 8.8Apache Zookeeper 3.7.0 2、源码编译 打开源码根目录修改…...
达梦归档文件名与实例对应关系
默认的,达梦归档文件名比较难以看懂,且多实例下不好区分 靠它就行 select upper(to_char((select DB_MAGIC), xxxxxxxxxx)) mag_id; 这样就对上号了。...
STL算法之sort
STL所提供的各式各样算法中,sort()是最复杂最庞大的一个。这个算法接受两个RandomAccessIterators(随机存取迭代器),然后将区间内的所有元素以渐增方式由小到大重新排列。还有一个版本则是允许用户指定一个仿函数代替operator<作为排序标准。STL的所有…...
elementui table滚动分页加载
文章目录 概要 简化的实现示例: 小结 概要 在使用 Element UI 的 Table 组件时,如果需要实现滚动分页加载的功能,可以通过监听 Table 的滚动事件来动态加载更多数据。 简化的实现示例: <template><el-table ref"…...
【MySQL 进阶之路】索引的使用
5.索引的使用规则 在数据库管理系统(DBMS)中,索引是提高查询效率的关键机制之一。MySQL索引优化是指通过设计、调整和选择合适的索引策略,以提高数据库的查询性能和降低资源消耗。以下是一些关键的索引使用规则: 1. …...
FPGA中所有tile介绍
FPGA中包含的tile类型,以xinlinx 7k为例,可以通过f4pga项目中的原语文件夹查看,主要包含以下这些: 以下是您提到的 Xilinx 7 系列 FPGA 中各种模块的含义及用途: 1. BRAM (Block RAM) BRAM 是 FPGA 中的块存储资源&…...
理解 Python PIL库中的 convert(‘RGB‘) 方法:为何及如何将图像转换为RGB模式
理解 Python PIL库中的 convert(RGB) 方法:为何及如何将图像转换为RGB模式 在图像处理中,保持图像数据的一致性和可操作性是至关重要的。Python的Pillow库(继承自PIL, Python Imaging Library)提供了强大的工具和方法来处理图像&…...
LVS默认的工作模式支持哪些负载均衡算法?
LVS默认的工作模式支持哪些负载均衡算法? LVS(Linux Virtual Server)默认支持多种负载均衡算法,这些算法在不同的场景下具有各自的优势。以下是 LVS 默认支持的负载均衡算法及其特点: 1. 轮询调度(Round Robin Sched…...
C/C++中的调用约定
在C/C编程中,调用约定(calling conventions)是一组指定如何调用函数的规则。主要在你调用代码之外的函数(例如OS API,操作系统应用程序接口)或OS调用你(如WinMain的情况)时起作用。如果编译器不知道正确的调用约定,那么你很可能会遇到非常奇怪…...
RAG评估指南:从检索到生成,全面解析LLM性能评估方法
前言 这一节我们将从时间线出发对RAG的评估方式进行对比,这些评估方式不仅限于RAG流程之中,其中基于LLM的评估方式更加适用于各行各业。 RAG常用评估方式 上一节我们讲了如何用ROUGE 这个方法评估摘要的相似度,由于篇幅限制,没…...
极兔速递开放平台快递物流查询API对接流程
目录 极兔速递开放平台快递物流查询API对接流程API简介物流查询API 对接流程1. 注册用户2. 申请成为开发者3. 企业认证4. 联调测试5. 发布上线 签名机制详解1. 提交方式2. 签名规则3. 字段类型与解析约定 物流轨迹服务极兔快递单号查询的其他方案总结 极兔速递开放平台快递物流…...
FFmpeg:强大的音视频处理工具指南
FFmpeg:强大的音视频处理工具指南 1. FFmpeg简介2. 核心特性2.1 基础功能2.2 支持的格式和编解码器 3. 主要组件3.1 命令行工具3.2 开发库 4. 最新发展5. 安装指南5.1 Windows系统安装5.1.1 直接下载可执行文件5.1.2 使用包管理器安装 5.2 Linux系统安装5.2.1 Ubunt…...
项目集成篇:springboot集成redistemple实现自定义缓存,并且可以设置过期时间
在Spring Boot中集成Redis并使用RedisTemplate实现自定义缓存功能,同时能够设置缓存项的过期时间,可以通过以下步骤来完成。我们将创建一个服务层方法,该方法将使用RedisTemplate直接与Redis交互,并为每个缓存项设置特定的过期时间…...
ClickHouse守护进程
背景描述 维护CK过程中,有时候会有CK OOM,并且CK自己没有自动拉起的情况出现;那么这个时候就需要守护进程,最初我不说了Supervisor来做守护进程,但是当我手动kill的时候发现并没有自动拉起。 解决方案 于是乎自己写…...
【Vivado】xdc约束文件编写
随手记录一下项目中学到的约束文件编写技巧。 时序约束 创建生成时钟 参考链接: Vivado Design Suite Tcl Command Reference Guide (UG835) Vivado Design Suite User Guide: Using Constraints (UG903) 通过Clocking Wizard IP创建的时钟(MMCM或…...
Nginx静态资源配置
基本配置原则 明确资源目录:为不同类型的静态资源指定不同的路径,这样可以避免路径冲突,并且便于管理。正确设置文件权限:确保 Nginx 具有读取静态资源的权限。缓存优化:为静态资源设置缓存头(如 expires&…...
365天深度学习训练营-第P7周:马铃薯病害识别(VGG-16复现)
文为「365天深度学习训练营」内部文章 参考本文所写记录性文章,请在文章开头带上「👉声明」 🍺 要求: 自己搭建VGG-16网络框架【达成√】调用官方的VGG-16网络框架【达成√】如何查看模型的参数量以及相关指标【达成√】 &#…...
docker学习笔记(三)--容器数据卷
文章目录 一、数据卷的介绍二、简单用法--直接指定挂载路径三、具名挂载与匿名挂载具名挂载匿名挂载 一、数据卷的介绍 docker将应用和环境打包成一个镜像,形成一个容器运行。那么容器产生的数据,如果不通过docker commit命令提交生成新的镜像ÿ…...
联通光猫DT741-csf 完全po解 改桥接
1.管理员密码破解,把光猫的loid pppoe用户名密码,各个连接vlan id记下来 打开链接 http://192.168.1.1/hidden_version_switch.html version选择Default Version,点击submit,光猫默认重启。重启后ip地址变为192.168.1.1 并且dhcp…...
Java Web 2 JS Vue快速入门
一 JS快速入门 1.什么是JavaScript? 页面交互: 页面交互是指用户与网页之间的互动过程。例如,当用户点击一个按钮,网页会做出相应的反应,如弹出一个对话框、加载新的内容或者改变页面的样式等;当用户在表…...
【数据结构】动态规划-基础篇
针对动态规划问题,我总结了以下5步: 确定dp数组以及下标的含义; 递推公式; dp数组如何初始化; 遍历顺序; 打印dp数组(用来debug); 以上5步适用于任何动态规划问题&#x…...
从watch、watchEffect、useEffect原理到vue、react响应原理
正文 1.核心原理 Vue中的watch、watchEffect是基于Vue的响应式系统(Proxy),依赖于ref或reactive数据的变化。React中的useEffect基于状态驱动的重新渲染机制,通过依赖数组 [dependency],手动声明需要追踪的状态或属性…...
Cursor+Devbox AI开发快速入门
1. 前言 今天无意间了解到 Cursor 和 Devbox 两大开发神器,初步尝试以后发现确实能够大幅度提升开发效率,特此想要整理成博客以供大家快速入门. 简单理解 Cursor 就是一款结合AI大模型的代码编辑器,你可以将自己的思路告诉AI,剩下的目录结构的搭建以及项目代码的实现均由AI帮…...
SpringBoot+MyBatis整合ClickHouse实践
整合Spring Boot、MyBatis和ClickHouse可以让你使用Java开发的应用程序高效地与ClickHouse数据库进行交互。以下是一个基本的步骤指南,帮助你完成这个整合过程: 1. 添加依赖 首先,在你的pom.xml文件中添加必要的Maven依赖。你需要引入Sprin…...
在数据库设计中同步冗余字段的思考与实践
目录 前言1. 冗余字段设计的背景与场景1.1 场景描述1.2 冗余字段的必要性 2. 冗余字段设计的优点2.1 提高查询效率2.2 简化应用逻辑 3. 冗余字段设计的缺点与挑战3.1 数据不一致问题3.2 更新开销增加3.3 数据冗余占用存储空间 4. 如何同步更新冗余字段4.1 手动更新方式4.2 使用…...
MacOS安装sshfs挂载远程电脑硬盘到本地
文章目录 sshfs简介sshfs安装下载安装macFUSE安装sshfs sshfs使用注意事项 sshfs简介 SSHFS(SSH Filesystem)是一种基于FUSE(用户空间文件系统)的文件系统,它允许你通过SSH协议挂载远程文件系统。使用SSHFS࿰…...
6.824/6.5840(2024)环境配置wsl2+vscode
本文是经过笔者实践得出的最速の环境配置 首先,安装wsl2和vscode 具体步骤参见Mit6.s081环境配置踩坑之旅WSL2VScode_mit6s081-CSDN博客 接下来开始为Ubuntu(笔者使用的版本依然是20.04)配置go的相关环境 1、更新Ubuntu的软件包 sudo apt-get install build-es…...
查询产品所涉及的表有(product、product_admin_mapping)
文章目录 1、ProductController2、AdminCommonService3、ProductApiService4、ProductCommonService5、ProductSqlService1. 完整SQL分析可选部分(条件筛选): 2. 涉及的表3. 总结4. 功能概述 查询指定管理员下所有产品所涉及的表?…...
C# 冒泡的算法
C# 冒泡的算法 public void BubbleSort(int[] arr) {int temp;for (int j 0; j < arr.Length - 2; j){for (int i 0; i < arr.Length - 2; i){if (arr[i] > arr[i 1]){temp arr[i 1];arr[i 1] arr[i];arr[i] temp;}}} }使用方法 int[] array new int[] { 5,…...