目录
二、打印命令行提示符(PrintCommandLine函数)
前言:
大家如果看过我之前的文章的话,就已经知道了几乎关于进程所有的操作和属性(当然关于信号和通信我们以后再提)。此时已经完全可以实现一个简易的shell程序了,注意,本篇代码含量很高,前方高能,做好准备!此外,还将补充内键命令。
一、总览
shell(bash)也是进程,我们完成它,一定要从最小的步骤开始,也要知道每一步是干什么的。分为4个大步:
#include<iostream>
#include<cstdio>
using namespace std;
int main()
{
while(true)
{
PrintCommandLine(); //1.命令行提示符
//GetCommandLine(); //2.获取用户命令
//ParseCommandLine(); //3.分析命令
//ExecuteCommand(); //4.执行命令
}
return 0;
}
二、打印命令行提示符(PrintCommandLine函数)
命令行提示符中,我们要获取用户名,主机名和路径,这些通过环境变量获取(getenv)。
(注意:这个shell用C++来完成)
我们使用snprintf函数对一个长字符串赋值:
#include<iostream>
#include<cstdio>
using namespace std;
const int basesize = 1024;
string GetUserName()
{
string name = getenv("USER");
return name.empty() ? "None" : name;
}
string GetHostName()
{
string hostname= getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{
string pwd = getenv("PWD");
return pwd.empty() ? "None" : pwd;
}
string MakeCommandLine()
{
//[gan@localhost lesson16]$
char command_line[basesize];
//这里使用#来和本身的shell作区分
snprintf(command_line, basesize, "[%s@%s %s]#", \
GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());
return command_line;
}
void PrintCommandLine() //1.命令行提示符
{
printf("%s", MakeCommandLine().c_str());
fflush(stdout);
}
int main()
{
while(true)
{
PrintCommandLine(); //1.命令行提示符
//GetCommandLine(); //2.获取用户命令
//ParseCommandLine(); //3.分析命令
//ExecuteCommand(); //4.执行命令
}
return 0;
}
因为打印命令行中没有\n,所以使用fflush强制刷新一下。
我们加上睡眠函数和回车,先来观察一下代码执行(不要偷懒,一步一步调试,否则后面就不知道哪里出错了):
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<stdlib.h>
using namespace std;
const int basesize = 1024;
string GetUserName()
{
string name = getenv("USER");
return name.empty() ? "None" : name;
}
string GetHostName()
{
string hostname= getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{
string pwd = getenv("PWD");
return pwd.empty() ? "None" : pwd;
}
string MakeCommandLine()
{
//[gan@localhost lesson16]$
char command_line[basesize];
//这里使用#来和本身的shell作区分
snprintf(command_line, basesize, "[%s@%s %s]#", \
GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());
return command_line;
}
void PrintCommandLine() //1.命令行提示符
{
printf("%s", MakeCommandLine().c_str());
fflush(stdout);
}
int main()
{
while(true)
{
sleep(1);
PrintCommandLine(); //1.命令行提示符
printf("\n");
//GetCommandLine(); //2.获取用户命令
//ParseCommandLine(); //3.分析命令
//ExecuteCommand(); //4.执行命令
}
return 0;
}
这里路径我们先以绝对路径的方式打印,以后再来优化。
三、获取用户命令(GetCommandLine函数)
之后要获取用户命令,其实就是一段符串。用一种新方式获取字符串,fgets方式,从指定的文件描述符(后面讲)中获取指定大小的字符串函数:
bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
//我们认为:我们要将用户输入的命令行 当做一个完整的字符串
char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
if (!result)
{
return false;
}
return true;
}
int main()
{
char command_buffer[basesize];
while(true)
{
PrintCommandLine(); //1.命令行提示符
if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
{
//此时接受的是false
continue;
}
printf("%s", command_buffer);
//ParseCommandLine(); //3.分析命令
//ExecuteCommand(); //4.执行命令
}
return 0;
}
使用fgets接收参数,之后我们进行打印调试代码。
我们并没有在printf中输入"\n"但是每次都在换行,是因为我们每次最后都会输入回车,所以我们要让最后的回车消失。
bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
//我们认为:我们要将用户输入的命令行 当做一个完整的字符串
char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
if (!result)
{
return false;
}
command_buffer[strlen(command_buffer) - 1] = 0; //将最后的 \n 覆盖
return true;
}
int main()
{
char command_buffer[basesize];
while(true)
{
PrintCommandLine(); //1.命令行提示符
if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
{
//此时接受的是false
continue;
}
//ParseCommandLine(); //3.分析命令
//ExecuteCommand(); //4.执行命令
}
return 0;
}
四、分析命令(ParseCommandLine函数)
接下来分析命令,假设最多传入64个参数,我们再定义三个全局变量:
const int argvnum = 64;
//全局命令行参数表
char* gargv[argvnum];
int gargc = 0;
使用strtok对字符串进行分割,为保证安全每次命令行解析都对参数列表进行清空。
这里的makefile文件如下(使用C++11标准):
CC:=g++ myshell:myshell.cc $(CC) -o $@ $^ -std=c++11 .PHONY:clean clean: rm -f myshell
const int basesize = 1024;
const int argvnum = 64;
//全局命令行参数表
char* gargv[argvnum];
int gargc = 0;
void ParseCommandLine(char command_buffer[], int len) //3.分析命令
{
(void)len;
//为保证安全每次命令行解析都对参数列表进行清空
memset(gargv, 0, sizeof(gargv));
gargc = 0;
const char* sep = " "; //定义分隔符
gargv[gargc++] = strtok(command_buffer, sep); //对字符串切分
while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
//这里会多加一个1,所以自减1
gargc--;
}
void debug()
{
printf("argc: %d\n", gargc);
for (int i = 0; gargv[i]; ++i)
{
printf("argv[%d] = %s\n", i, gargv[i]);
}
}
int main()
{
char command_buffer[basesize];
while(true)
{
PrintCommandLine(); //1.命令行提示符
if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
{
//此时接受的是false
continue;
}
ParseCommandLine(command_buffer, strlen(command_buffer)); //3.分析命令
debug();
//ExecuteCommand(); //4.执行命令
}
return 0;
}
我们先debug一下:
当然也有可能直接输入回车,需要再更正代码,直接在GetCommandLine中判断command_buffer是否为0,是返回false,否返回true:
bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
//我们认为:我们要将用户输入的命令行 当做一个完整的字符串
char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
if (!result)
{
return false;
}
command_buffer[strlen(command_buffer) - 1] = 0; //将最后的 \n 覆盖
//当用户直接输入回车时判断长度是否为0
if(strlen(command_buffer) == 0) return false;
return true;
}
五、执行命令(ExecuteCommand函数)
不能让shell自己执行命令,因为一旦用户输入错误,shell自身就可能崩溃,所以我们要创建子进程执行命令。
六、shell前部分代码总览(不完善但能运行)
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
//全局命令行参数表
char* gargv[argvnum];
int gargc = 0;
string GetUserName()
{
string name = getenv("USER");
return name.empty() ? "None" : name;
}
string GetHostName()
{
string hostname= getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{
string pwd = getenv("PWD");
return pwd.empty() ? "None" : pwd;
}
string MakeCommandLine()
{
//[gan@localhost lesson16]$
char command_line[basesize];
//这里使用#来和本身的shell作区分
snprintf(command_line, basesize, "[%s@%s %s]#", \
GetUserName().c_str(), GetHostName().c_str(), GetPwd().c_str());
return command_line;
}
void PrintCommandLine() //1.命令行提示符
{
printf("%s", MakeCommandLine().c_str());
fflush(stdout);
}
bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
//我们认为:我们要将用户输入的命令行 当做一个完整的字符串
char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
if (!result)
{
return false;
}
command_buffer[strlen(command_buffer) - 1] = 0; //将最后的 \n 覆盖
//当用户直接输入回车时判断长度是否为0
if(strlen(command_buffer) == 0) return false;
return true;
}
void ParseCommandLine(char command_buffer[], int len) //3.分析命令
{
(void)len;
//为保证安全每次命令行解析都对参数列表进行清空
memset(gargv, 0, sizeof(gargv));
gargc = 0;
const char* sep = " "; //定义分隔符
gargv[gargc++] = strtok(command_buffer, sep); //对字符串切分
while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
//这里会多加一个1,所以自减1
gargc--;
}
bool ExecuteCommand() //4.执行命令
{
//创建子进程 让子进程执行命令
pid_t id = fork();
if (id < 0) return false;
if (id == 0)
{
//子进程
//1.执行命令
execvp(gargv[0], gargv);
//2.退出
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0); //以阻塞等待方式
if (rid > 0)
{
return true;
}
return false;
}
int main()
{
char command_buffer[basesize];
while(true)
{
PrintCommandLine(); //1.命令行提示符
if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
{
//此时接受的是false
continue;
}
ParseCommandLine(command_buffer, strlen(command_buffer)); //3.分析命令
ExecuteCommand(); //4.执行命令
}
return 0;
}
七、内键命令
1.cd命令
此时代码如果执行cd切换路径的代码,会发现切换不了。每一个进程都有一个叫做当前路径的概念(cwd),所以切换的是子进程的路径也就是把其cwd改变了(chdir函数改变,这我们之前都讲过)。
cd命令不是让子进程执行的,而是让父进程执行的。
这些不能由子进程执行的命令,叫做內键命令!
所以有一小部分指令不需要创建子进程,而是自己调用函数。当我们更改路径以后,需要把自己的PWD环境变量改变,chdir不会改变PWD。
这里必须再次说明cwd和PWD不是同一个东西,PWD是环境变量,CWD是进程属性。
在shell命令行中执行,cd本质就是chdir,先改变cwd,之后由OS改变PWD!
chdir
:改变调用它的进程的 CWD。
PWD
:由 shell 维护,需手动更新。
进程所在路径(PWD)实在自己的PCB中的,也就是环境变量中:
所以我们需要使用shell自己更新。 使用getcwd获取当前工作路径:
当然还需要把环境变量更新,使用putenv更新(其可以新增也可以更新)。
所以我们先来完善更改路径的函数(这里也就是更新PWD环境变量):
//全局shell工作路径
char pwd[basesize];
char pwdenv[basesize]; //定义环境全局变量
string GetPwd()
{
if(!getcwd(pwd, sizeof(pwd))) return "None";
//修改环境变量
snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
putenv(pwdenv);
return pwd;
//string pwd = getenv("PWD");
//return pwd.empty() ? "None" : pwd;
}
环境变量是由shell自己维护的,我们写的shell也并不是从头写的,而是从父进程(bash)继承的,所以当putenv时会发生写时拷贝。
1.1改善命令行参数提示符
我们将命令行提示符前面打印绝对路径修改为最后所在目录名:
string LastDir()
{
string curr = GetPwd();
if (curr == "/" || curr == "None") return curr;
size_t pos = curr.rfind("/");
if (pos == std::string::npos) return curr;
return curr.substr(pos + 1);
}
string MakeCommandLine()
{
//[gan@localhost lesson16]$
char command_line[basesize];
//这里使用#来和本身的shell作区分
snprintf(command_line, basesize, "[%s@%s %s]#", \
GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
return command_line;
}
2.export命令和echo命令
其实当我们export和echo不需要创建子进程,也是内键命令。但是我们目前还是使用的shell的环境变量,我们现在开始自己创建一个自己的环境变量表。这里因为无法读取系统配置,所以先从父shell中继承下来环境变量。
所以进程地址空间中有一段“命令行参数环境变量”就是专门存储这部分内容的:
因为export是导入环境变量,所以其是一个内键命令。 输入env会进行进程替换,但是我们目前先查当前进程的环境变量,所以将env先设置成内键命令。
const int envnum = 64;
//我的系统环境变量
char* genv[envnum];
void AddEnv(const char* item)
{
int index = 0;
while(genv[index++]);
genv[--index] = (char*)malloc(strlen(item) + 1);
strncpy(genv[index], item, strlen(item) + 1);
genv[++index] = nullptr;
}
bool CheckAndExecBuiltCommand()
{
if (strcmp(gargv[0], "cd") == 0)
{
if (gargc == 2)
{
chdir(gargv[1]);
return true;
}
}
else if (strcmp(gargv[0], "export") == 0)
{
if (gargc == 2)
{
//export也是内键命令
AddEnv(gargv[1]);
return true;
}
}
else if (strcmp(gargv[0], "env") == 0)
{
if (gargc == 1)
{
for (int i = 0; genv[i]; ++i)
{
printf("%s\n", genv[i]);
}
return true;
}
}
return false;
}
//初始化环境变量
//作为一个shell获取环境变量应该从系统配置来
//我们今天就直接从父shell中获取环境变量
void InitEnv()
{
//我们从父进程拷贝下来环境变量
extern char** environ;
int index = 0;
while(environ[index])
{
genv[index] = (char*)malloc(strlen(environ[index]) + 1);
strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
index++;
}
genv[index] = nullptr;
}
但是此时我们子进程并不会继承当前父进程,因为当前代码并没有让子进程获取,所以我们程序替换的函数就需要使用execvpe(可以传入环境变量):
bool ExecuteCommand() //4.执行命令
{
//创建子进程 让子进程执行命令
pid_t id = fork();
if (id < 0) return false;
if (id == 0)
{
//子进程
//1.执行命令
//execvp(gargv[0], gargv);
execvpe(gargv[0], gargv, genv);
//2.退出
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0); //以阻塞等待方式
if (rid > 0)
{
return true;
}
return false;
}
我们写一个打印环境变量的程序other,之后调用,看子进程是否继承:
2.1实现echo $?
最后,我们将echo $?这个內键命令实现。先设置一个全局变量lastcode记录上次退出码,并在CheckAndExecBuiltCommand函数中添加:
else if (strcmp(gargv[0], "echo") == 0)
{
if (gargc == 2)
{
//echo $?
//echo $PATH
//echo hello
if (strcmp(gargv[1], "$?") == 0)
{
//打印退出码
printf("%d\n", lastcode);
}
else
{
printf("%s\n", gargv[1]);
}
lastcode = 0;
return true;
}
else
{
lastcode = 3;
}
}
至此,我们已经完成了一个简易的shell,还有很多并没有完善,但是我们已经知道了其大致的原理。
八、shell的全部代码
#include<iostream>
#include<cstdio>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
//全局命令行参数表
char* gargv[argvnum];
int gargc = 0;
int lastcode = 0;
//全局shell工作路径
char pwd[basesize];
char pwdenv[basesize]; //定义环境全局变量
//我的系统环境变量
char* genv[envnum];
string GetUserName()
{
string name = getenv("USER");
return name.empty() ? "None" : name;
}
string GetHostName()
{
string hostname= getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
string GetPwd()
{
if(!getcwd(pwd, sizeof(pwd))) return "None";
//修改环境变量
snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
putenv(pwdenv);
return pwd;
//string pwd = getenv("PWD");
//return pwd.empty() ? "None" : pwd;
}
string LastDir()
{
string curr = GetPwd();
if (curr == "/" || curr == "None") return curr;
size_t pos = curr.rfind("/");
if (pos == std::string::npos) return curr;
return curr.substr(pos + 1);
}
string MakeCommandLine()
{
//[gan@localhost lesson16]$
char command_line[basesize];
//这里使用#来和本身的shell作区分
snprintf(command_line, basesize, "[%s@%s %s]#", \
GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
return command_line;
}
void PrintCommandLine() //1.命令行提示符
{
printf("%s", MakeCommandLine().c_str());
fflush(stdout);
}
bool GetCommandLine(char command_buffer[], int size) //2.获取用户命令
{
//我们认为:我们要将用户输入的命令行 当做一个完整的字符串
char* result = fgets(command_buffer, size, stdin); //从标准输入流获取
if (!result)
{
return false;
}
command_buffer[strlen(command_buffer) - 1] = 0; //将最后的 \n 覆盖
//当用户直接输入回车时判断长度是否为0
if(strlen(command_buffer) == 0) return false;
return true;
}
void ParseCommandLine(char command_buffer[], int len) //3.分析命令
{
(void)len;
//为保证安全每次命令行解析都对参数列表进行清空
memset(gargv, 0, sizeof(gargv));
gargc = 0;
const char* sep = " "; //定义分隔符
gargv[gargc++] = strtok(command_buffer, sep); //对字符串切分
while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
//这里会多加一个1,所以自减1
gargc--;
}
bool ExecuteCommand() //4.执行命令
{
//创建子进程 让子进程执行命令
pid_t id = fork();
if (id < 0) return false;
if (id == 0)
{
//子进程
//1.执行命令
//execvp(gargv[0], gargv);
execvpe(gargv[0], gargv, genv);
//2.退出
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0); //以阻塞等待方式
if (rid > 0)
{
if(WIFEXITED(status))
{
lastcode = WEXITSTATUS(status);
}
else
{
lastcode = 100;
}
return true;
}
return false;
}
void AddEnv(const char* item)
{
int index = 0;
while(genv[index++]);
genv[--index] = (char*)malloc(strlen(item) + 1);
strncpy(genv[index], item, strlen(item) + 1);
genv[++index] = nullptr;
}
bool CheckAndExecBuiltCommand()
{
if (strcmp(gargv[0], "cd") == 0)
{
if (gargc == 2)
{
chdir(gargv[1]);
}
else
{
lastcode = 1;
}
return true;
}
else if (strcmp(gargv[0], "export") == 0)
{
if (gargc == 2)
{
//export也是内键命令
AddEnv(gargv[1]);
}
else
{
lastcode = 2;
}
return true;
}
else if (strcmp(gargv[0], "env") == 0)
{
if (gargc == 1)
{
for (int i = 0; genv[i]; ++i)
{
printf("%s\n", genv[i]);
}
lastcode = 0;
return true;
}
}
else if (strcmp(gargv[0], "echo") == 0)
{
if (gargc == 2)
{
//echo $?
//echo $PATH
//echo hello
if (strcmp(gargv[1], "$?") == 0)
{
//打印退出码
printf("%d\n", lastcode);
}
else
{
printf("%s\n", gargv[1]);
}
lastcode = 0;
return true;
}
else
{
lastcode = 3;
}
}
return false;
}
//初始化环境变量
//作为一个shell获取环境变量应该从系统配置来
//我们今天就直接从父shell中获取环境变量
void InitEnv()
{
//我们从父进程拷贝下来环境变量
extern char** environ;
int index = 0;
while(environ[index])
{
genv[index] = (char*)malloc(strlen(environ[index]) + 1);
strncpy(genv[index], environ[index], strlen(environ[index]) + 1);
index++;
}
genv[index] = nullptr;
}
int main()
{
//启动前初始化环境变量表
InitEnv();
char command_buffer[basesize];
while(true)
{
PrintCommandLine(); //1.命令行提示符
if (!GetCommandLine(command_buffer, basesize)) //2.获取用户命令
{
//此时接受的是false
continue;
}
ParseCommandLine(command_buffer, strlen(command_buffer)); //3.分析命令
//判断是否为内键命令
if(CheckAndExecBuiltCommand())
{
continue;
}
ExecuteCommand(); //4.执行命令
}
return 0;
}
总结:
这酸爽!大家如果能比着实现一遍的话,实力绝对突飞猛进!如果能自己敲一遍更是无敌!进程讲解很多了,至少你已经知道了很多关于进程的知识,在生活中也能理解很多现象。但是我们说过一句话叫做Linux下一切皆文件!但是此时我们还没有提到关于任何关于文件的知识。因为这个也是Linux的核心内容,接下来,我们就要开始文件的篇章了,不要走开,敬请期待!