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();
}

目前功能比较基本,会不断补充;感谢支持!! !

本文临近尾声;相信读到此处的你,肯定对它有了不一样的理解,或多或少,都是一种收获;如遇到问题的地方也欢迎大家积极留言,真诚感谢!

 

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

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

相关文章

Language Translation with TorchText

前言&#xff1a; 利用torchtext类来处理一个著名的数据集&#xff0c;包含了一些英文和德文句子。利用该数据处理sequence-to-sequence模型&#xff0c;通过注意力机制&#xff0c;可以将德语翻译成英语。Torchtext&#xff1a;它是 PyTorch 生态系统中的一个库&#xff0c;主…

【Redis篇】 List 列表

在 Redis 中&#xff0c;List 是一种非常常见的数据类型&#xff0c;用于表示一个有序的字符串集合。与传统的链表结构类似&#xff0c;Redis 的 List 支持在两端进行高效的插入和删除操作&#xff0c;因此非常适合实现队列&#xff08;Queue&#xff09;和栈&#xff08;Stack…

11.爬虫

前言&#xff1a; 正则表达式的作用&#xff1a; 作用一&#xff1a;校验字符串是否满足规则 作用二&#xff1a;在一段文本中查找满足要求的内容 一.Pattern类和Matcher类&#xff1a; 1.Pattern类&#xff1a;表示正则表达式 a.因此获取Pattern对象就相当于获取正则表达式…

【Linux篇】权限管理 - 用户与组权限详解

一. 什么是权限&#xff1f; 首先权限是限制人的。人 真实的人 身份角色 权限 角色 事物属性 二. 认识人–用户 Linux下的用户分为超级用户和普通用户 root :超级管理员&#xff0c;几乎不受权限的约束普通用户 :受权限的约束超级用户的命令提示符是#&#xff0c;普通用…

【RDMA】RDMA read和write编程实例(verbs API)

WRITE|READ编程&#xff08;RDMA read and write with IB verbs&#xff09; &#xff08;本文讲解的示例代码在&#xff1a;RDMA read and write with IB verbs | The Geek in the Corner&#xff09; 将 RDMA 与verbs一起使用非常简单&#xff1a;首先注册内存块&#xff0c…

UE5 C++ 不规则按钮识别,复选框不规则识别 UPIrregularWidgets

插件名称&#xff1a;UPIrregularWidgets 插件包含以下功能 你可以点击任何图片&#xff0c;而不仅限于矩形图片。 UPButton、UPCheckbox 基于原始的 Button、Checkbox 扩展。 复选框增加了不规则图像识别功能&#xff0c;复选框增加了悬停事件。 欢迎来到我的博客 记录学习过…

洛谷P2670扫雷游戏(Java)

三.P2670 [NOIP2015 普及组] 扫雷游戏 题目背景 NOIP2015 普及组 T2 题目描述 扫雷游戏是一款十分经典的单机小游戏。在 n 行 m列的雷区中有一些格子含有地雷&#xff08;称之为地雷格&#xff09;&#xff0c;其他格子不含地雷&#xff08;称之为非地雷格&#xff09;。玩…

如何加强游戏安全,防止定制外挂影响游戏公平性

在现如今的游戏环境中&#xff0c;外挂始终是一个困扰玩家和开发者的问题。尤其是定制挂&#xff08;Customized Cheats&#xff09;&#xff0c;它不仅复杂且隐蔽&#xff0c;更能针对性地绕过传统的反作弊系统&#xff0c;对游戏安全带来极大威胁。定制挂通常是根据玩家的需求…

概率论相关知识随记

作为基础知识的补充&#xff0c;随学随记&#xff0c;方便以后查阅。 概率论相关知识随记 期望&#xff08;Expectation&#xff09;期望的定义离散型随机变量的期望示例&#xff1a;掷骰子的期望 连续型随机变量的期望示例&#xff1a;均匀分布的期望 期望的性质线性性质期望的…

DICOM MPPS详细介绍

文章目录 前言一、常规检查业务流程二、MPPS的作用三、MPPS的原理1、MPPS与MWL2、MPPS服务过程 四、MPPS的实现步骤1、创建实例2、传递状态 五、总结 前言 医院中现有的DICOM MWL(Modality Worklist)已开始逐渐得到应用&#xff0c;借助它可以实现病人信息的自动录入&#xff0…

Secured Finance 推出 TVL 激励计划以及基于 FIL 的稳定币

Secured Finance 是新一代 DeFi 2.0 协议&#xff0c;其正在推出基于 FIL 的稳定币、固定收益市场以及具有吸引力的 TVL 激励计划&#xff0c;以助力 Filecoin 构建更强大的去中心化金融生态体系&#xff0c;并为 2025 年初 Secured Finance 协议代币的推出铺平道路。Secure Fi…

FPGA Xilinx维特比译码器实现卷积码译码

FPGA Xilinx维特比译码器实现卷积码译码 文章目录 FPGA Xilinx维特比译码器实现卷积码译码1 Xilinx维特比译码器实现2 完整代码3 仿真结果 MATLAB &#xff08;n,k,m&#xff09;卷积码原理及仿真代码&#xff08;你值得拥有&#xff09;_matlab仿真后代码-CSDN博客 MATLAB 仿真…

Linux 权限管理:用户分类、权限解读与常见问题剖析

&#x1f31f; 快来参与讨论&#x1f4ac;&#xff0c;点赞&#x1f44d;、收藏⭐、分享&#x1f4e4;&#xff0c;共创活力社区。&#x1f31f; &#x1f6a9;用通俗易懂且不失专业性的文字&#xff0c;讲解计算机领域那些看似枯燥的知识点&#x1f6a9; 目录 &#x1f4af;L…

rabbitmq 安装延时队列插件rabbitmq_delayer_message_exchange(linux centOS 7)

1.插件版本 插件地址&#xff1a;Community Plugins | RabbitMQ rabbitmq插件需要对应的版本&#xff0c;根据插件地址找到插件 rabbitmq_delayer_message_exchange 点击Releases 因为我rabbitmq客户端显示的版本是&#xff1a; 所以我选择插件版本是&#xff1a; 下载 .ez文…

遗传算法与深度学习实战(26)——编码卷积神经网络架构

遗传算法与深度学习实战&#xff08;26&#xff09;——编码卷积神经网络架构 0. 前言1. EvoCNN 原理1.1 工作原理1.2 基因编码 2. 编码卷积神经网络架构小结系列链接 0. 前言 我们已经学习了如何构建卷积神经网络 (Convolutional Neural Network, CNN)&#xff0c;在本节中&a…

数学建模之熵权法

熵权法 概述 **熵权法(Entropy Weight Method,EWM)**是一种客观赋权的方法&#xff0c;原理&#xff1a;指标的变异程度越小&#xff0c;所包含的信息量也越小&#xff0c;其对应的权值应该越低&#xff08;例如&#xff0c;如果对于所有样本而言&#xff0c;某项指标的值都相…

同道猎聘Q3营收降利润增,AI或成估值重塑关键词

2024年&#xff0c;经济向好的趋势没有改变&#xff0c;挑战却仍然存在。企业纷纷进行结构性变革优化或业务方向调整。这一点反映到人才市场&#xff0c;绝大多数企业对招聘扩张持保守态度&#xff0c;降本增效的主题仍在延续。 作为人才市场水温变化的“温度计”&#xff0c;…

46 基于单片机的烧水壶系统设计

目录 一、主要功能 二、硬件资源 三、程序编程 四、实现现象 一、主要功能 基于STC89C52RC单片机&#xff0c;采用四个按键&#xff0c;通过DS18B20检测温度&#xff0c;开机显示实时温度 第一个按键为切换功能按键&#xff0c;按下后&#xff0c;可以设置烧水温度的大小&…

推荐学习笔记:矩阵补充和矩阵分解

参考&#xff1a; 召回 fun-rec/docs/ch02/ch2.1/ch2.1.1/mf.md at master datawhalechina/fun-rec GitHub 业务 隐语义模型与矩阵分解 协同过滤算法的特点&#xff1a; 协同过滤算法的特点就是完全没有利用到物品本身或者是用户自身的属性&#xff0c; 仅仅利用了用户与…

【机器学习】—Transformers的扩展应用:从NLP到多领域突破

好久不见&#xff01;喜欢就关注吧~ 云边有个稻草人-CSDN博客 目录 引言 一、Transformer架构解析 &#xff08;一&#xff09;、核心组件 &#xff08;二&#xff09;、架构图 二、领域扩展&#xff1a;从NLP到更多场景 1. 自然语言处理&#xff08;NLP&#xff09; 2…