1、命令行提示符
首先应明确,shell是死循环软件,本质是交互式命令解释器
命令行提示符的格式:用户名 @主机名 当前路径
cpp
static std::string rfindDir(const std::string &p)
{
// 如果路径是根目录 /,直接返回/
if(p == "/")
return p;
const std::string psep = "/"; // 路径分隔符斜杠
// rfind:从后往前找第一个 '/'
auto pos = p.rfind(psep);
// npos代表没找到斜杠(比如单纯字符串test,没有/)
if(pos == std::string::npos)
return std::string(); // 返回空字符串
// substr(pos+1):截取斜杠后面所有字符
// 例:/home/ubuntu -> 找到最后一个/,后面ubuntu,返回ubuntu
return p.substr(pos+1);
}
const char *GetUserName()
{
// getenv("USER") 读取环境变量USER,存当前用户名
char *name = getenv("USER");
if(name == NULL) // 读取失败
return "None";
return name;
}
// 获取主机名
const char *GetHostName()
{
static char buf[128];
// 系统调用直接获取本机主机名
if (gethostname(buf, sizeof(buf)-1) == 0)
{
return buf;
}
strcpy(buf, "None");
return buf;
}
// 获取当前工作路径
const char *GetPwd()
{
char *pwd = getenv("PWD");
//char *pwd = getcwd(cwd, sizeof(cwd));
if(pwd == NULL)
return "None";
return pwd;
}
// 打印shell提示符
void PrintCommandLine()
{
// 拼接打印格式:[用户名@主机名 当前目录]#
printf("[%s@%s %s]# ", GetUserName(), GetHostName(), rfindDir(GetPwd()).c_str());
fflush(stdout); // 强制刷新输出缓冲区,不然提示符可能卡住不显示
}

2、获取键盘指令
cpp
int GetCommand(char commandline[], int size)
{
if(NULL == fgets(commandline, size, stdin))
return 0;
// 2.1 用户输入的时候,至少会摁一下回车\n abcd\n ,\n '\0'
commandline[strlen(commandline)-1] = '\0';
return strlen(commandline);
}
用fegts从键盘读取一行输入存到command,这里不能用scanf进行输入,scanf是以空格、回车为分隔符,无法一次性读取带多个空格的整行命令,命令参数的个数不固定,例如ls -a -l ,scanf无法一次性完整接收,fegts直接读取缓冲区一整行,完整保留所有空格,然后交给strtok分割
3、命令字符串切割
cpp
#define MAXARGS 32
char *gargv[MAXARGS];
int gargc = 0;
const char *gsep = " ";
void ParseCommand()
{
// 每次解析前清空全局参数表,防止上一条命令残留
memset(gargv, 0, sizeof(gargv));
gargc = 0;
// strtok第一次调用:传入原始字符串command_line
char* token = strtok(command_line, gsep);
while(token != NULL)
{
gargv[gargc++] = token;
// 后续分割:第一个参数传NULL,继续从上一次截断位置分割
token = strtok(NULL, gsep);
}
}
int main()
{
// 0. 从配置文件中获取环境变量填充环境变量表
// LoadEnv();
char command_line[MAXSIZE] = {0};
while(1)
{
// 1. 打印shell提示符
PrintCommandLine();
// 2. 获取用户输入命令
if (0 == GetCommand(command_line, sizeof(command_line)))
{
continue;
}
// 3. 分割整行命令,填充全局参数数组gargv
ParseCommand(command_line);
// 循环打印分割后的每一个命令参数
for (int i = 0; gargv[i]; i++)
{
printf("[%d]: %s\n", i, gargv[i]);
}
// 打印原始用户输入的完整命令字符串
printf("echo: %s\n", command_line);
}
return 0;
}

借助strtok将字符串分割成一个个的子串,第一次调用后返回第一个子串指针,后续循环分割第一个参数必须传NULL,函数自动记住上次分割位置,注意不能传入字符串常量char* str = "aaa bbb" ,strtok会原地修改字符串,把找到的分隔符(空格)覆盖为\0截断子串
4、外部命令的执行
cpp
int ExecuteCommand()
{
// 能不能让你的bash自己执行命令:ls -a -l
pid_t id = fork();
if(id < 0)
return -1;
else if(id == 0)
{
// ls -a -l
int fd = -1; // 重定向文件描述符
if(redir_type == NoneRedir)
{
// Do Nothing
}
else if(redir_type == OutputRedir)
{
// 子进程要进行输出重定向
fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);
dup2(fd, 1);
}
else if(redir_type == AppRedir)
{
// 追加重定向
fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);
dup2(fd, 1);
}
else if(redir_type == InputRedir)
{
fd = open(filename, O_RDONLY);
dup2(fd, 0);
}
else{}
// 参数1:程序名(无需绝对路径,自动搜索PATH)
// 参数2:命令参数数组gargv
// 参数3:自定义环境变量数组genv
execvpe(gargv[0], gargv, genv);
// exec系列函数执行成功不会返回;返回=替换失败,子进程退出码1
exit(1);
}
else
{
// 父进程
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
// WEXITSTATUS提取子进程正常退出的退出码,存入全局lastcode,对应$?
lastcode = WEXITSTATUS(status);
}
}
return 0;
}
int main()
{
// 存储用户输入整行命令的缓冲区
char command_line[MAXSIZE] = {0};
while(1)
{
// 打印命令行字符串
PrintCommandLine();
// 获取用户输入
if(0 == GetCommand(command_line, sizeof(command_line)))
continue;
ParseRedir(command_line);
// 解析字符串 -> "ls -a -l" -> "ls" "-a" "-l" 命令行解释器,就要对用户输入的命令字符串首先进行解析!
ParseCommand(command_line);
// 让子进程执行这个命令
ExecuteCommand();
}
return 0;
}
先通过fork创建子进程,复制当前shell程序,再调用execvp接口进行程序替换,不能父进程直接调用exec系列函数,exec 程序替换会完全覆盖当前进程代码、数据,shell 程序直接消失,无法循环接收下一条命令
5、内建命令
cpp
// 环境变量表,存储拷贝过来的父进程环境变量
char *genv[MAXARGS];
// 环境变量条目计数
int genvc = 0;
// 保存上一条执行命令的退出码,对应$?功能
int lastcode = 0;
// retunr val:
// 0 : 不是内建命令
// 1 : 内建命令&&执行完毕
int CheckBuiltinExecute()
{
// cd命令
if(strcmp(gargv[0], "cd") == 0)
{
// 内建命令
if(gargc == 2)
{
// 新的目标路径: gargv[1]
// 1. 更改进程内核中的路径
chdir(gargv[1]);
// 2. 更改环境变量
char pwd[1024];
getcwd(pwd, sizeof(pwd)); // /home/whb
snprintf(cwd, sizeof(cwd), "PWD=%s", pwd); // cwd: PWD=/home/home
putenv(cwd);
lastcode = 0;
}
return 1;
}
// echo命令
else if(strcmp(gargv[0], "echo") == 0) // cd , echo , env , export 内建命令
{
if(gargc == 2)
{
if(gargv[1][0] == '$')
{
// $? ? : 看做一个变量名字
if(strcmp(gargv[1]+1, "?") == 0)
{
printf("lastcode: %d\n", lastcode);
}
else if(strcmp(gargv[1]+1, "PATH") == 0)
{
printf("%s\n", getenv("PATH")); // putenv 和 getenv 是访问环境变量表!
}
lastcode = 0;
}
return 1;
}
}
return 0;
}
cd操作:chdir(path)系统调用:修改内核 PCB 记录的当前工作目录,getcwd() 获取最新路径,snprintf拼接PWD=xxx,putenv()更新环境变量PWD,操作系统不会同步更新PWD环境变量,只改内核路径,提示符读取getenv("PWD")会显示旧路径

echo命令:echo $?:读取全局变量lastcode,打印上一条外部命令退出码,echo $PATH:getenv()读取进程环境变量表打印,内建命令执行完成统一设置lastcode=0,代表执行成功

进程创建时自动继承父进程环境变量,系统全局指针extern char** environ存储,getenv/putenv底层操作进程内部环境变量表,修改后子进程 exec 时可通过execvpe传递