shell的模拟实现
我们知道shell是一个永不退出的程序,所以他应该是一个死循环,并且shell为了防止影响到自己,我们在命令行上输入的所有命令都是由shell的子进程来执行的,所以它应该要有创建子进程的相关函数,当然也会有进程替换的相关函数,因为我们直接创建子进程,父子进程是共享代码的,如果没有进程替换,shell根本无法让子进程执行特定的命令。
总的思路:
1.打印提示符 && 获取用户命令字符串
2.分割字符串
//“ls -a -l” —> “ls” “-a” “-l”
3.check buildin command 检查内键命令
4.执行命令
1.打印提示符 && 获取用户命令字符串
对于getUserCommand函数:
先介绍fgets函数:
#define NUM 1024
#define SIZE 64
int getUserCommand(char *command,int num)
{
printf("[%s@%s %s]",getUsername(),getHostname(),getCwd());
//不用scanf,遇到空格会停,不好用
char *r = fgets(command,num,stdin);//最终还是会输入\n
if(r == NULL) return -1;
//abcd\n
command[strlen(command) - 1] = '\0';//给最后加上\0
return strlen(command);
}
getUsername(),getHostname(),getCwd() 是为了读取用户名@主机名 当前所在文件夹
2.分割字符串
比如我要执行ls命令,ls命令后还可以携带选项,ls -a -l,shell需要获取指令与选项,并将它们分割(shell接收到的是一串字符串"ls -a -l",shell需要将字符串分割为更小的字符串"ls" “-a” “-l”)。
定义char usercommand[NUM];接收一整行的指令,该数组中的元素保存的是字符,char *argv[SIZE];//存储命令拆分后结果,该数组中的元素要保存的是分割后的字符串。
介绍strtok:
void commandSplit(char *in,char *out[])
{
int argc = 0;
out[argc++] = strtok(in," ");//分隔并放入out指针数组
while(out[argc++] = strtok(NULL," "));
}
3.check buildin command 检查内键命令
内建命令:
开头说到,shell的命令分为内部命令和其他命令,何为内建命令? 内建命令是一个需要shell自己执行的命令,即shell不创建子进程,自己亲自执行的命令。
shell对于一些命令是必须要亲自执行的:比如cd更改工作路径,将shell的工作路径修改后,由于子进程的工作路径与父进程相同,更改分进程的工作路径后,父进程创建出的子进程的工作路径也是被修改过的,体现给用户的感觉就是当前的工作路径改变了。
对于内建命令,shell是怎么实现的? shell中有许多内建命令,当shell接收到指令并解析后,需要判断用户输入的命令是否为内建命令,如果是就执行拦截操作,使子进程不再被创建,自己执行该指令。如果不是内建命令,则创建子进程执行该命令。
内建命令太多,代码里面只写了cd和export内建命令。
char cwd[1024];//全局变量
char enval[1024];//for test
char *homepath()
{
char *home = getenv("HOME");
if(home) return home;
else return (char*)".";
}
void cd(const char *path)
{
chdir(path);//chdir(),用户将当前的工作目录改变成以参数路径所指的目录
char tmp[1024];
getcwd(tmp,sizeof(tmp));//getcwd()会将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,参数size为buf的空间大小。
sprintf(cwd,"PWD=%s",tmp); //打印到字符串中
putenv(cwd);//putenv 函数会将cwd 直接填写到环境表中
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char* argv[])
{
if(strcmp(argv[0],"cd") == 0) //cd ...
{
char *path = NULL;
if(argv[1] == NULL) path = homepath();//纯cd,回到HOME
else path = argv[1];
cd(path);//进入要进入的路径
return 1;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval,argv[1]);//拷贝
putenv(enval);//直接填写到环境表中
return 1;
}
return 0;
}
对于void cd(const char *path),用 chdir() 函数来将 用户当前的工作目录改变成以参数路径所指的目录,
用 getcwd() 函数将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,
用 sprintf() 函数将 “PWD=工作目录” 字符串打印给cwd,
用putenv 将cwd 直接填写到环境表中。
4.执行命令
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
//exec command
execvp(argv[0],argv);//execvp()会从环境变量所指的目录中查找符合参数 file 的文件名, 找到后执行该文件, 然后将第二个参数argv 传给该执行的文件。
exit(1);
}
else //father
{
int status = 0;
pid_t res = waitpid(-1, &status, 0);//阻塞式等待
if(res > 0)//等待成功
{
printf("exit code: %d \n", WEXITSTATUS(status));
}
}
return 0;
}
用子进程进行进程替换,执行命令:
在进行进程替换时,使用ececvp()函数:
效果动态图
代码
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#define NUM 1024
#define SIZE 64
char cwd[1024];
char enval[1024];//for test
char *homepath()
{
char *home = getenv("HOME");
if(home) return home;
else return (char*)".";
}
const char *getUsername()
{
const char *name = getenv("USER");
if(name) return name;
else return "none";
}
const char *getHostname()
{
const char *hostname = getenv("HOSTNAME");
if(hostname) return hostname;
else return "none";
}
const char *getCwd()
{
const char *cwd = getenv("PWD");
if(cwd) return cwd;
else return "none";
}
int getUserCommand(char *command,int num)
{
printf("[%s@%s %s]",getUsername(),getHostname(),getCwd());
//不用scanf,遇到空格会停,不好用
char *r = fgets(command,num,stdin);//最终还是会输入\n
if(r == NULL) return -1;
//abcd\n
command[strlen(command) - 1] = '\0';
return strlen(command);
}
void commandSplit(char *in,char *out[])
{
int argc = 0;
out[argc++] = strtok(in," ");//分隔并放入out指针数组
while(out[argc++] = strtok(NULL," "));
}
void cd(const char *path)
{
chdir(path);//chdir(),用户将当前的工作目录改变成以参数路径所指的目录
char tmp[1024];
getcwd(tmp,sizeof(tmp));//getcwd()会将当前工作目录的绝对路径复制到参数buffer所指的内存空间中,参数size为buf的空间大小。
sprintf(cwd,"PWD=%s",tmp); //打印到字符串中
putenv(cwd);//putenv 函数会将c wd 直接填写到环境表中
}
// 什么叫做内键命令: 内建命令就是bash自己执行的,类似于自己内部的一个函数!
// 1->yes, 0->no, -1->err
int doBuildin(char* argv[])
{
if(strcmp(argv[0],"cd") == 0) //cd ...
{
char *path = NULL;
if(argv[1] == NULL) path = homepath();
else path = argv[1];
cd(path);
return 1;
}
else if(strcmp(argv[0],"export") == 0)
{
if(argv[1] == NULL) return 1;
strcpy(enval,argv[1]);
putenv(enval);
return 1;
}
return 0;
}
int execute(char *argv[])
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0) //child
{
//exec command
execvp(argv[0],argv);//execvp()会从环境变量所指的目录中查找符合参数 file 的文件名, 找到后执行该文件, 然后将第二个参数argv 传给该执行的文件。
exit(1);
}
else //father
{
int status = 0;
pid_t res = waitpid(-1, &status, 0);//阻塞式等待
if(res > 0)//等待成功
{
printf("exit code: %d \n", WEXITSTATUS(status));
}
}
return 0;
}
int main()
{
while(1)
{
char usercommand[NUM];//存储命令
char *argv[SIZE];//存储命令拆分后结果
//1.打印提示符 && 获取用户命令字符串
int n = getUserCommand(usercommand,sizeof(usercommand));
if(n <= 0) continue;//输入空也可以重新输入
//2.分割字符串
//"ls -a -l" ---> "ls" "-a" "-l"
commandSplit(usercommand,argv);
//3.check buildin command 检查内键命令
n = doBuildin(argv);
if(n) continue;
//4.执行命令
execute(argv);
}
}