手搓简易的Linux命令行解释器
对于我们所用的linux,由于他没有图形化界面,所以我们一般是用命令来使用他的,那么他是如何解析我们的命令的呢,今天我们就来实现一个简易的linux命令行解释器
输出命令行提示符
首先,我们可以发现命令行在最开头的地方会有一个命令行提示符(Command Prompt)
![]()
就这个图片而言,它是由当前登录用户名(root)主机名(hostname:iZ2ze49v4rphe7jq7w5ov8Z)和当前工作路径(~/666)以及一些符号(@ :#)构成
所以首先我们要实现这个输出的格式,可以发现我们需要获取3个环境变量:登录用户名,主机名和当前工作路径
1.获取登录用户名
static std::string GetUserName()
{
std::string username = getenv("USER");
return username.empty() ? "None" : username;
}
这里的static是不希望其他文件去使用该函数,而getenv是一个获取环境变量的函数
char *getenv(const char *name);
就这样就可以很简单的获取环境变量了,其他两个也是这样
2.获取主机名
static std::string GetHostName()
{
std::string hostname = getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
3.获取当前工作路径
static std::string GetPwd()
{
char temp[1024];
getcwd(temp, sizeof(temp));
// 顺便更新一下shell自己的环境变量pwd
snprintf(pwd, sizeof(pwd), "PWD=%s", temp);
putenv(pwd);
// /home/whb/code/code
std::string pwd_lable = temp;
const std::string pathsep = "/";
auto pos = pwd_lable.rfind(pathsep);
if(pos == std::string::npos)
{
return "None";
}
pwd_lable = pwd_lable.substr(pos+pathsep.size());
return pwd_lable.empty() ? "/" : pwd_lable;
}
对于cwd和PWD的问题,cwd相当于是进程的一个属性,每个进程都有自己的cwd来表示自己的工作路径。而PWD是一个环境变量,从内容上来说他和cwd实际上是一样的,PWD也是用进程的cwd去维护的,cwd可以用getcwd()获取
目前来看PWD需要cwd,他们两个内容又一样,那么PWD这个环境变量存在的意义是什么呢??
举个例子呢,就是在去写shell底层的时候其实cwd就够用了,只需要用getcwd()就可以直接获取,用chdir()就可以直接更新。但是对于用户而言,难道我用命令去查看当前工作目录的时候我要去调用getenv()这个函数吗?这显然不合理,所以就有了PWD这个环境变量
那现在还有一个问题:既然用户需要知道当前工作目录,那为什么要把cwd设置为进程的属性??直接把cwd搞成一个环境变量不就行了吗?
但是但是,要知道环境变量用户是可以随意修改的,那如果现在只有一个环境变量cwd去表示当前工作路径的话,用户去乱改cwd,就会导致内核管理出以一系列的安全问题。而现在有一个PWD来面向用户,即使用户随意去修改PWD最多出问题的也就用户自己的程序(就是用户自己搞出来的依赖PWD的一些代码或者脚本),而linux本身并不会出什么问题
获取用户输入的命令
bool GetCommandString(char cmd_str_buff[], int len)
{
if(cmd_str_buff == NULL || len <= 0)
return false;
char *res = fgets(cmd_str_buff, len, stdin);
if(res == NULL)
return false;
// ls -a -l\n -> ls -a -l\0
// ls -a -l>log.txt\n -> ls -a -l\0
cmd_str_buff[strlen(cmd_str_buff) - 1] = 0;
return strlen(cmd_str_buff) == 0 ? false : true;
}
这里我们用一个char类型的数组(cmd_str_buff)去将用户命令带出去。fget可以一次读取一行,输入回车停止输入,这里需要注意的是在回车结束的时候换行符其实也会被写入数组中,所以要将strlen(cmd_str_buff)-1为下标的换行符改为0(转为char类型就是'\0'),最后再返回结果即可
重定向检测和设置
在命令行中会有ls -a -l >log.txt这种命令,这种命令就是重定向,我们可以通过来识别命令中有没有>,>>,<这几个符号来判断有没有重定向操作
#define TrimSpace(start) do{\
while(isspace(*start))\
{\
start++;\
}\
}while(0)
// "ls -a -l > log.txt" -> "ls -a -l\0log.txt";
// filename = log.txt, redir_type = OUTPUT_REDIR;
void CheckRedir(char cmd[])
{
char *start = cmd;
char *end = cmd + strlen(cmd) - 1;
// ls -a -l >> log.txt
// ls -a -l
while(start <= end)
{
// > >> <
if(*start == '>')
{
if(*(start+1) == '>')
{
redir_type = APPEND_REDIR;
// 追加重定向
*start = '\0';
start += 2;
TrimSpace(start);
filename = start;
break;
}
else
{
// 输出重定向
redir_type = OUPUT_REDIR;
*start = '\0';
start++;
TrimSpace(start); // 移除空格
filename = start;
break;
}
}
else if(*start == '<')
{
//输入重定向
// cat < log.txt
redir_type = INPUT_REDIR;
*start = '\0';
start++;
TrimSpace(start);
filename = start;
break;
}
else
{
start++;
}
}
}
解析命令
bool ParseCommandString(char cmd[])
{
if(cmd == NULL)
return false;
#define SEP " "
//3. "ls -a -l" -> "ls" "-a" "-l"
gargv[gargc++] = strtok(cmd, SEP);
// 整个数字,最后以NULL结尾
while((bool)(gargv[gargc++] = strtok(NULL, SEP)));
// 回退一次,命令行参数的格式
gargc--;
return true;
}
将命令拆开即可
执行命令
执行命令通常是创建一个子进程,然后使用exec相关接口进行进程替换来执行命令,但是有些命令不能通过创建子进程执行,比如说cd,这样的命令用子进程执行会毫无意义
比如你执行一个ls的相关命令,用子进程再进行替换,然后在子进程中给你打印出相关文字后子进程再退出,但是如果是cd这样切换目录的命令的话,你想想更改目录的本质是什么?是通过什么来判断你的工作目录的?没错,就是你的cwd,但是如果创建子进程来执行cd,修改的是子进程的cwd,对父进程没有影响,也就是说你执行的cd根本就没用,所以这种命令只能用父进程来执行
还有一种是echo这种的命令,根本没必要开一个新的子进程来执行,直接printf就可以
像cd,echo这种直接在父进程中执行的命令称为内置命令,ls这种开子进程来执行的命令叫外部命令
所以我们首先来判断用户的命令是不是内置命令(这里我们以cd和echo来掩饰,实际上Bash中的内置命令有60到70个)
bool BuiltInCommandExec()
{
//内建命令: 是shell自己执行的命令,如同shell执行一个自己的函数
//gargv[0]
std::string cmd = gargv[0];
bool ret = false;
if(cmd == "cd")
{
// build
if(gargc == 2)
{
std::string target = gargv[1];
if(target == "~")
{
ret = true;
chdir(GetHomePath().c_str());
}
else{
ret = true;
chdir(gargv[1]);
}
}
else if(gargc == 1)
{
ret = true;
chdir(GetHomePath().c_str());
}
else
{
//BUG
}
}
else if(cmd == "echo")
{
if(gargc == 2)
{
std::string args = gargv[1];
if(args[0] == '$')
{
if(args[1] == '?')
{
printf("lastcode: %d\n", lastcode);
lastcode = 0;
ret = true;
}
else{
const char *name = &args[1];
printf("%s\n", getenv(name));
lastcode = 0;
ret = true;
}
}
else{
printf("%s\n", args.c_str());
ret = true;
}
}
}
return ret;
}
接下来来执行外部命令
void ForkAndExec()
{
pid_t id = fork();
if(id < 0)
{
//for : XXXXX
perror("fork"); // errno -> errstring
return;
}
else if(id == 0)
{
if(redir_type == OUPUT_REDIR)
{
int output = open(filename.c_str(), O_CREAT | O_WRONLY | O_TRUNC, 0666);
(void)output;
dup2(output, 1);
}
else if(redir_type == INPUT_REDIR)
{
int input = open(filename.c_str(), O_RDONLY);
(void)input;
dup2(input, 0);
}
else if(redir_type == APPEND_REDIR)
{
int appendfd = open(filename.c_str(), O_CREAT | O_WRONLY | O_APPEND);
(void)appendfd;
dup2(appendfd, 1);
}
else{
//Do Nothing
}
// 子进程打开文件了,也进行了重定向,execl*不影响历史打开的文件!
//子进程
execvp(gargv[0], gargv);
exit(0);
}
else
{
//父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
lastcode = WEXITSTATUS(status);
}
}
}
这里的if else里面是在搭建类似管道一样的东西
如果有重定向命令的话就搭建重定向的管道,再执行execvp去替换子进程去执行重定向的输入输出。如果没有重定向命令的话就不会搭建通道,会直接execvp
由于是子进程进行的重定向,父进程的文件标识符表并不会被弄乱
总结
这个简易的shell大致就是这样
这是所有的代码
main.cc · 邱宏林/邱宏林 - 码云 - 开源中国
https://gitee.com/Qhl-666/qiu-honglin/blob/master/main.cc
myshell.cc · 邱宏林/邱宏林 - 码云 - 开源中国
https://gitee.com/Qhl-666/qiu-honglin/blob/master/myshell.cc
myshell.h · 邱宏林/邱宏林 - 码云 - 开源中国
https://gitee.com/Qhl-666/qiu-honglin/blob/master/myshell.h
更多推荐



所有评论(0)