学习完基本指令、开发环境、进程的概念和创建进程等内容,我们就可以写自己的shell了
文章目录
目录
前言
我们用的shell和指令都是一些进程所构建起来的就,当我们学习完这些东西以后,我们也可以写一个自己的shell了。
一、myshell构思
当我们打开xshell连接当终端以后我们会看到如图内容
1、左右方括号,一个是用户名,@标识符,远端的机器,当前工作目录还有$符,这么一个字符串,那么我们就可用c语言和一些指令来获取这些字符串然后打印出来。
2、光标卡在那里等待我们输入指令。那么我们可以定义一个字符串用来存储我们要输入的指令。
3、拿到指令应给怎么做?
4、遇到特殊指令应给怎么特殊处理。
二、前缀字符串的打印
所用的宏和全局变量,头文件
cpp
#include<iostream>
#include<unistd.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<sys/wait.h>
using namespace std;
#define LINE_SIZE 1024
#define LEFT "["
#define RIGHT "]"
#define RED "$"
#define ARGV_SIZE 32
#define BLEAK " \t"
int lastcode=0;
int argc=0;
char* argv[ARGV_SIZE];
char s[LINE_SIZE];
char buf[LINE_SIZE];
char commandline[LINE_SIZE];
我们通过指令env查看到的环境变量中有这些所要信息。
这些都是我们需要获取到的信息,然后打印
我们学习过getenv这个库函数,我们man 3 getenv查看参数和返回值。
可以看到这是用来获取环境变量的函数,其中参数为需要获取的变量的字符串名;返回值为char*
也就是说我们返回的也是一个字符串,这个字符串也就是环境变量
实例:
获取终端:
cpp
char * gethostname()
{
return getenv("HOSTNAME");
}
获取用户名:
cpp
char* getusername()
{
return getenv("LOGNAME");
//return getcwd(buf,sizeof(buf));
}
现在已经获取到了两个我们需要打印的字符串了,还有一个当前工作路径,我们也可以这样来获取,但是当我们后面工作路径改变了,环境变量表不会改变,我们需要修改环境变量表,所以我们想先把pwd给保存起来,所以我们用getcwd,这个也是用来获取当前路径的
这个函数的作用是将路径保存到buf这个数组当中。
实例
cpp
void getpwd()
{
getcwd(buf,sizeof(buf));
}
现在我们需要打印的前缀字符串基本想要的东西就都出来了,接下来就是打印出来。
而我们的pwd不需要那么长,只需要当前的文件夹名,所以我们截取一下
cpp
char* Strsuffix(char sr[])
{
//取buf的最后
size_t len=strlen(buf)-1;
while(len)
{
if(buf[len-1]!='/')len--;
else break;
}
//char s[LINE_SIZE];
//char* s[LINE_SIZE];
strncpy(sr,&buf[len],len);
return sr;
}
cpp
#define LINE_SIZE 1024
#define LEFT "["
#define RIGHT "]"
void interact(char line[],int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT RED" ",getusername(),gethostname(),Strsuffix(s));
//cout<<LEFT<<*getusername()<<"@"<<*gethostname()<<" "<<buf<<RIGHT;
//获取输入指令
char* ss = fgets(line,size,stdin);//获取指令
assert(ss);
(void)ss;
line[strlen(line)-1]='\0';//加入终止符
//cout<<line;
}
这就是前缀的打印
编译运行一下
一模一样
三、获取命令行和分割命令
当我们打印完前缀时就要立马让光标停下来,也就是在进程的状态中所提到的进程阻塞。
我们需要一个fgets这个在c语言时所用的函数。以上面的前缀打印中已经写入了。
分割命令
在我们学习环境变量的时候,我们当时就提了argv,argc 和env这三个main函数中的参数。
argv是用来记录我们记录我们指令的个数,argc用来保存指令名,env为父进程继承下来的环境变量。
回归myshell,既然我们获取到了命令的,它以字符串的形式保存在
char commandline[LINE_SIZE]
这个字符串当用,那么我现在就可以在这个字符串当中把指令名一个一个的分割出来放在我们自己定义的argv当中,然后更新一下argv(个数)
这就是一个简单的分割,我们可以用双指针或者滑动窗口来解决,我们用string这个库函数中自带的strtok这个函数
实例:
cpp
int spliststring(char* line,char *_argv[])
{
//获取命令
int i=0;
argv[i++]=strtok(line,BLEAK);
while(_argv[i++]=strtok(NULL,BLEAK));
return i-1;
}
测试
for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
测试结果
四、调用指令
我们现在已经获取当了指令,并且指令的地址已经保存在了argv这个指针数组当用。
我们学习过exec-什么的函数,查看库函数和参数
我们发现用execvp这个函数比较方便,只需要argv这个数组的第一个元素,和后面的元素
那么我们就创建一个子进程来调用,如果用父进程来调用,等指令进程跑起来就结束了,因为代码被替换了。
创建进程和调用指令代码:
实例
cpp
void NormalExcute()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
return ;
}
else if(id==0)
{
//子进程调用命令
execvp(argv[0],argv);
exit(44);
}
else
{
int status=0;
pid_t ret=waitpid(-1,&status,0);
if(ret==id)
{
lastcode=WEXITSTATUS(status);
if(lastcode<0)
{
perror("waitpid");
return;
}
else if(lastcode>0)
{
cout<<"指令错误!!"<<endl;
}
else{
}
}
}
}
运行结果:
五、内建指令(特殊指令)
当我们cd ..到上一级目录当中时,我们会发现没有反应也没有报错,这是因为我们的环境变量表中的PWD没有改变,因为这个环境变量表是从父进程那里写时拷贝过来的,所以没有更新,我们需要将它给跟新一下。只需要将这个指令给特殊处理一下就可以了。
chdir是当指定的工作目录当中去。
cpp
int buildCommand(char* _argv[],int _argc)
{
if(_argc==2&&strcmp((_argv[0]),"cd")==0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",buf);
return 1;
}
}
还有echo当我们输入?时,如果不特殊处理就会直接打印?,我们xshell却是将子进程的结束码打印出来,我们的而我们的结束码在父进程waitpid等待到子进程时给写入status里面了,只需要调用WEXITSTATUS(status)这个宏就可以记录下来返回给lastcode;这个在上面的创建子进程时父进程当中有所写到,lastcode是定义的一个全局变量。
代码实例:
cpp
int buildCommand(char* _argv[],int _argc)
{
if(_argc==2&&strcmp((_argv[0]),"cd")==0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",buf);
return 1;
}
else if(_argc&&strcmp(_argv[0],"echo")==0)
{
if(strcmp(_argv[1],"$?")==0)
{
printf("%d\n",lastcode);
lastcode=0;
}
else if(*_argv[1]=='$')
{
char* val=getenv(_argv[1]+1);
if(val)printf("%s\n",val);
}
else{
printf("%s\n",_argv[1]);
}
return 1;
}
else if(_argc==2&&strcmp(_argv[0],"exprot")==0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
return 0;
}
因为exprot,添加环境变量时,env表不会改变,使用为我们写时拷贝的父进程env表,所以不会改变,我们只需要自己闯将一个就可以了。
六、完整代码
cpp
#include<iostream>
#include<unistd.h>
#include<assert.h>
#include<stdlib.h>
#include<string.h>
#include<iostream>
#include<sys/wait.h>
using namespace std;
#define LINE_SIZE 1024
#define LEFT "["
#define RIGHT "]"
#define RED "$"
#define ARGV_SIZE 32
#define BLEAK " \t"
int lastcode=0;
int argc=0;
char* argv[ARGV_SIZE];
char s[LINE_SIZE];
char buf[LINE_SIZE];
char commandline[LINE_SIZE];
//string commandline;
//string buf;
char myenv[LINE_SIZE];
char* getusername()
{
return getenv("LOGNAME");
//return getcwd(buf,sizeof(buf));
}
void getpwd()
{
getcwd(buf,sizeof(buf));
}
char* Strsuffix(char sr[])
{
//取buf的最后
size_t len=strlen(buf)-1;
while(len)
{
if(buf[len-1]!='/')len--;
else break;
}
//char s[LINE_SIZE];
//char* s[LINE_SIZE];
strncpy(sr,&buf[len],len);
return sr;
}
char * gethostname()
{
return getenv("HOSTNAME");
}
void interact(char line[],int size)
{
getpwd();
printf(LEFT"%s@%s %s"RIGHT RED" ",getusername(),gethostname(),Strsuffix(s));
//cout<<LEFT<<*getusername()<<"@"<<*gethostname()<<" "<<buf<<RIGHT;
//获取输入指令
char* ss = fgets(line,size,stdin);
assert(ss);
(void)ss;
line[strlen(line)-1]='\0';
//cout<<line;
}
int spliststring(char* line,char *_argv[])
{
//获取命令
int i=0;
argv[i++]=strtok(line,BLEAK);
while(_argv[i++]=strtok(NULL,BLEAK));
return i-1;
}
void NormalExcute()
{
pid_t id=fork();
if(id<0)
{
perror("fork");
return ;
}
else if(id==0)
{
//子进程调用命令
execvp(argv[0],argv);
exit(44);
}
else
{
int status=0;
pid_t ret=waitpid(-1,&status,0);
if(ret==id)
{
lastcode=WEXITSTATUS(status);
if(lastcode<0)
{
perror("waitpid");
return;
}
else if(lastcode>0)
{
cout<<"指令错误!!"<<endl;
}
else{
}
}
}
}
int buildCommand(char* _argv[],int _argc)
{
if(_argc==2&&strcmp((_argv[0]),"cd")==0)
{
chdir(_argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",buf);
return 1;
}
else if(_argc&&strcmp(_argv[0],"echo")==0)
{
if(strcmp(_argv[1],"$?")==0)
{
printf("%d\n",lastcode);
lastcode=0;
}
else if(*_argv[1]=='$')
{
char* val=getenv(_argv[1]+1);
if(val)printf("%s\n",val);
}
else{
printf("%s\n",_argv[1]);
}
return 1;
}
else if(_argc==2&&strcmp(_argv[0],"exprot")==0)
{
strcpy(myenv,_argv[1]);
putenv(myenv);
return 1;
}
return 0;
}
int main()
{
//char a[]="..";
//chdir(a);
while(1)
{
// 1.创建前缀
interact(commandline,sizeof(commandline));
// 2. 交互问题,获取命令行, ls -a -l > myfile / ls -a -l >> myfile / cat < file.txt
argc= spliststring(commandline,argv);
// 3. 子串分割的问题,解析命令行
if(argc==0)continue;
int n=buildCommand(argv,argc);
if(!n)NormalExcute();
// 4. 指令的判断
// debug
//for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
//内键命令,本质就是一个shell内部的一个函数<Paste>
}
return 0;
}
总结
shell的运行就是就是进程的调用。