学习Linux到目前为止,我们都知道命令是由shell执行的,但是具体如何执行的我们看不到,因此我们今天来自己写一个shell来执行我们的指令,让大家对shell的底层有一个进阶的理解,文章的最后会给出完整代码喔~
目录
[2.echo 退出码](#2.echo 退出码)
一、打印命令行提示符
知识前置:shell本质上是一个死循环,因为要不断地处理一条又一条指令,因此编写的shell功能全部都要放进一个死循环中,直到我们主动退出才结束
命令行提示符的格式为 用户名@主机名 路径,因此要打印出来就必须获取这三个数据,很显然它们都属于环境变量,分别对应USER、HOSTNAME和PWD,要获取环境变量,用getenv函数即可,为了方便管理,我们将打印命令行提示符封装成一个函数
cpp
//1.打印命令行提示符
PrintCommandLine();
cpp
void PrintCommandLine()
{
printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetPwd()); //用户名@主机名 当前路径
}
cpp
const char *GetUserName()
{
char *name = getenv("USER");
if(name == NULL)
{
return "None";
}
return name;
}
const char *GetHostName()
{
char *hostname = getenv("HOSTNAME");
if(hostname == NULL)
{
return "None";
}
return hostname;
}
const char *GetPwd()
{
char *pwd = getenv("PWD");
if(pwd == NULL)
{
return "None";
}
return pwd;
}
至此命令行提示符的打印就完成了,但是shell本质是一个死循环,它还得需要一个等待我们输入指令的功能,否则就会一直打印,导致满屏的命令行提示符
二、获取键盘输入
键盘输入本质就是输入一个字符串,需要用一个字符数组来接收
cpp
#define MAXSIZE 128
//...
char command_line[MAXSIZE] = {0};
对于指令的输入我们通常存在两种情况,一种是输入指令后回车,一种是啥也不输入直接回车,因此,要获取键盘输入,我们需要用到系统调用 fgets ,啥也不输入的时候返回NULL, 我们依旧对获取键盘输入的功能封装一个函数,且函数的返回值为输入的指令字符串长度,当返回0的时候,命中我们刚刚说的第二种情况,直接continue即可,否则我们尝试打印出刚刚输入的指令做一个验证测试,看看是否输出的与我们输入的一致
但同时要注意的是,无论哪一种情况,我们都要至少输入一次回车键,回车键相当于换行符\n,因此为了保证字符串和输出的正确性,我们需要将最后的换行符改成'\0'
cpp
//2.获取键盘输入
if(GetCommand(command_line, sizeof(command_line)) == 0)
continue;
printf("%s\n", command_line);
cpp
int GetCommand(char commandline[], int size)
{
if(fgets(commandline, size, stdin) == NULL)
return 0;
//用户输入的时候,至少会按一次回车\n,改'\0'
commandline[strlen(commandline)-1] = '\0';
return strlen(commandline);
}
运行结果如下:

三、解析字符串
前面我们输入进去的指令是一整个字符串,我们要把它们拆分("ls -a -l" -> "ls" "-a" "-l" ),并放入命令行参数表中
对于字符串的拆分,C语言中有一个封装好的函数,strtok

第一个参数为要拆分的字符串,第二个参数为拆分符号,遇到该符号就进行拆分,对于同一个字符串的第二次拆分,则将第一个参数设为NULL,否则会一直拆分第一个而后面的不拆分,具体代码演示如下:
cpp
#include<stdio.h>
#include<string.h>
int main()
{
char str[] = "aaa bbb ccc ddd";
const char* sep = " ";
char *p = strtok(str, sep);
printf("%s\n", p);
while(p)
{
p = strtok(NULL, sep);
if(p == NULL)
{
break;
}
printf("%s\n",p);
}
return 0;
}

拆分的字符串,我们要放到全局的环境变量表中,这是shell内部要维护的第一张表,同时设置一下切割分隔符
cpp
#define MAXARGS 32
//shell内部维护的第一张表:命令行参数表
char *gargv[MAXARGS];
int gargc = 0;
const char *sep = " ";
将解析字符串封装函数
cpp
//3.解析字符串
ParseCommand(command_line);
cpp
int ParseCommand(char commandline[])
{
//输入新的指令要重置命令行参数表
gargc = 0;
memset(gargv, 0, sizeof(gargv));
//分割字符串
gargv[0] = strtok(commandline, sep);
while((gargv[++gargc] = strtok(NULL, sep)));
//打印测试
printf("gargc: %d\n", gargc);
int i = 0;
for(; gargv[i]; i++)
printf("gargv[%d]: %s\n", i, gargv[i]);
return 0;
}
我们打印出分割结果测试一下效果

四、执行指令
前面我们完成了指令的输入和解析,还差一个执行,要执行指令,就需要fork子进程来进行程序替换
cpp
//4.执行指令
ExcuteCommand();
cpp
int ExcuteCommand()
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0)
{
//子进程 程序替换
execvp(gargv[0], gargv);
exit(1);
}
else{
//父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
printf("wait child process success!\n");
}
}
}
有了命令行参数表gargv,我们就可以用程序替换函数execvp了,对于程序替换有问题的可以回看博主的文章《进程程序替换》
我们来看看运行结果

结果如我们所料,我们把所有的测试打印全部注释再试试看

到这里为止,shell的基本框架搭成了
五、增加内建命令
刚刚我们输入的指令都如期由子进程替换执行成功了,但是我们来看下面这种情况

这里演示了两种无法成功执行的情况,echo $?是输出上一个程序执行结束的退出码,这里没有打印出来;cd ..是返回上一个目录,但是我们用pwd查看,却发现路径没有变化。
原因就是它们都属于内建命令,内建命令的特点是由Shell自身解析执行,不需要创建新进程再替换,接下来一一解决这两个问题
执行命令前先写一个函数判断是否是内建命令,如果是则直接执行并返回1,如果不是则返回0,并创建子进程替换执行命令
1.cd 路径的改变
要改变路径,可以使用chdir函数

直接将gargv1放入参数即可,因为gargv0是cd,后面一个必跟路径
cpp
//4.执行指令
if(CheckBuiltinExcute() > 0)
continue;
ExcuteCommand();
cpp
int CheckBuiltinExcute()
{
if(strcmp(gargv[0], "cd") == 0)
{
//内建命令
if(gargc == 2)
{
//新的目标路径
chdir(gargv[1]);
}
return 1;
}
return 0;
}
试一下执行结果

可以发现路径改变了,但是为什么命令行提示符的路径却没有改变呢?
回看我们之前获取路径的函数
cpp
const char *GetPwd()
{
char *pwd = getenv("PWD");
if(pwd == NULL)
{
return "None";
}
return pwd;
}
会发现我们获取的是环境变量的PWD记录,但是环境变量的这个值是静态的,即使我们用chdir切换了目录但是没更新PWD环境变量,它依旧会返回旧路径,不准
要实时改变这个路径,就不能依赖环境变量,而需要一个能直接与内核交互的系统调用 getcwd!

这个系统调用的第一个参数属于典型的输出型参数,我们提供数组参数,它会将实时路径给我传递到数组中,那我们就创建一个接收的数组
cpp
//我们shell所处的工作路径
char cwd[MAXSIZE];
利用getcwd系统调用优化Getpwd函数
cpp
const char *GetPwd()
{
//char *pwd = getenv("PWD");
char *pwd = getcwd(cwd, sizeof(cwd));
if(pwd == NULL)
{
return "None";
}
return cwd;
}
试试优化后的效果

达到了我们的预期
但到这里还剩下最后一个问题,Linux命令行提示符中的路径只保留了最后一个,而我们是直接显示出了一长串的绝对路径,这是过于冗余的,我们接下来就是要解决这个问题,只取它的最后一个"/"后的路径,这里我用c++来实现
cpp
static std::string rfindDir(const std::string &p)
{
if(p == "/") return p;
const std::string psep = "/";
auto pos = p.rfind(psep);
if(pos == std::string::npos)
{
return std::string();
}
return p.substr(pos+1);
}
在打印的位置将本来要传的绝对路径先传入这个函数当中,最后截取结束后再打印出来
cpp
void PrintCommandLine()
{
printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); //用户名@主机名 当前路径
}
记得对rfindDir的返回值还要加一个c_str(),这是C++为了兼容C语言的打印而设计的接口
来看看成果,为了区分原shell和我们自己写的shell,我们的分隔符是不一样的,前者是$,我的是#


到这里就解决了cd路径的改变问题,所以其实真正的shell还是非常复杂的,我这仅仅是极简版本,还有很多内建命令和其他各种快捷键功能没实现,我们接下来再解决一下echo问题
2.echo 退出码
echo依旧是内建命令,承接实现"cd"的代码继续扩充。
首先先解决获取退出码的问题,先定一个全局变量Last_Exitcode
cpp
//上一个进程结束的退出码
int Last_ExitCode = 0;
编写输入指令为echo的情况,并在每个指令执行结束的地方重置退出码
cpp
int CheckBuiltinExcute()
{
if(strcmp(gargv[0], "cd") == 0)
{
//内建命令
if(gargc == 2)
{
//新的目标路径
chdir(gargv[1]);
Last_ExitCode = 0;
}
return 1;
}
else if(strcmp(gargv[0], "echo") == 0)
{
if(gargc == 2)
{
if(gargv[1][0] == '$')
{
if(strcmp(gargv[1]+1, "?") == 0)
{
printf("lastcode:%d\n", Last_ExitCode);
}
Last_ExitCode = 0;
}
return 1;
}
}
return 0;
}
cpp
//父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
//获取退出码
Last_ExitCode = WEXITSTATUS(status);
//printf("wait child process success!\n");
}
来看看实现成果

六、总结与源码
至此,一个简易的shell被我们手搓出来了,独立完成其实非常考验知识储备和代码能力,可作为一个教学意义极高的训练典例,其实除了命令行参数表以外,shell内部还管理了另一张表就是环境变量表,它也承担着非常重要的角色,我将手搓的源码放在下面供大家自行在此基础拓展
cpp
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<iostream>
#include<string>
#define MAXSIZE 128
#define MAXARGS 32
//shell内部维护的第一张表:命令行参数表
char *gargv[MAXARGS];
int gargc = 0;
const char *sep = " ";
//我们shell所处的工作路径
char cwd[MAXSIZE];
//上一个进程结束的退出码
int Last_ExitCode = 0;
static std::string rfindDir(const std::string &p)
{
if(p == "/") return p;
const std::string psep = "/";
auto pos = p.rfind(psep);
if(pos == std::string::npos)
{
return std::string();
}
return p.substr(pos+1);
}
const char *GetUserName()
{
char *name = getenv("USER");
if(name == NULL)
{
return "None";
}
return name;
}
const char *GetHostName()
{
char *hostname = getenv("HOSTNAME");
if(hostname == NULL)
{
return "None";
}
return hostname;
}
const char *GetPwd()
{
//char *pwd = getenv("PWD");
char *pwd = getcwd(cwd, sizeof(cwd));
if(pwd == NULL)
{
return "None";
}
return cwd;
}
void PrintCommandLine()
{
printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str()); //用户名@主机名 当前路径
}
int GetCommand(char commandline[], int size)
{
if(fgets(commandline, size, stdin) == NULL)
return 0;
//用户输入的时候,至少会按一次回车\n,改'\0'
commandline[strlen(commandline)-1] = '\0';
return strlen(commandline);
}
int ParseCommand(char commandline[])
{
//输入新的指令要重置命令行参数表
gargc = 0;
memset(gargv, 0, sizeof(gargv));
//分割字符串
gargv[0] = strtok(commandline, sep);
while((gargv[++gargc] = strtok(NULL, sep)));
// printf("gargc: %d\n", gargc);
// int i = 0;
// for(; gargv[i]; i++)
// printf("gargv[%d]: %s\n", i, gargv[i]);
return 0;
}
int CheckBuiltinExcute()
{
if(strcmp(gargv[0], "cd") == 0)
{
//内建命令
if(gargc == 2)
{
//新的目标路径
chdir(gargv[1]);
Last_ExitCode = 0;
}
return 1;
}
else if(strcmp(gargv[0], "echo") == 0)
{
if(gargc == 2)
{
if(gargv[1][0] == '$')
{
if(strcmp(gargv[1]+1, "?") == 0)
{
printf("lastcode:%d\n", Last_ExitCode);
}
Last_ExitCode = 0;
}
return 1;
}
}
return 0;
}
int ExcuteCommand()
{
pid_t id = fork();
if(id < 0) return -1;
else if(id == 0)
{
//子进程 程序替换
execvp(gargv[0], gargv);
exit(1);
}
else{
//父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
//获取退出码
Last_ExitCode = WEXITSTATUS(status);
//printf("wait child process success!\n");
}
}
}
int main()
{
char command_line[MAXSIZE] = {0};
while(1)
{
//1.打印命令行提示符
PrintCommandLine();
//2.获取键盘输入
if(GetCommand(command_line, sizeof(command_line)) == 0)
continue;
// printf("%s\n", command_line);
//3.解析字符串
ParseCommand(command_line);
//4.执行指令
if(CheckBuiltinExcute() > 0)
continue;
ExcuteCommand();
}
return 0;
}