前言
模拟Shell可以综合前面的知识,fork、环境变量、程序替换、进程等待、终止、内建命令等等,是一次很好的复习
一、原理
我们进行登录启动shell服务的时候,系统启动了一个bash进程,bash本身的环境变量是哪里来的?用户目录下存在.bash_profile这个配置文件!里面保存了导入环境变量的方法!

二、模拟的流程
1.模拟交互环境

用户名、主机名、工作目录都在环境变量中

可以用printf来控制格式了!
2.输入命令
本质上输入的是一个字符串,且这个服务应该一直在执行,while(1) {}
如果输入的是空串,不做处理,和shell中一样,如果收到了字符串,对字符串作分割 + 解析,如果是内建命令,使用内建命令的处理方式,如果是普通命令,就应该fork + exec系列函数!注意还有进程等待的问题。
当然,实际写代码的时候可能还会有一些问题,后面边写边解决。不推荐使用string类型,当然也可以就是非常麻烦,我这里是string和char*一起用的,也会讲纯char *的写法。
三、代码
交互环境
cpp
string Pwd;
const char *getusername()
{
return getenv("USER");
}
const char *gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
char buff[1024];
getcwd(buff,sizeof(buff));
string pwd = buff;
size_t pos = pwd.size() - 1;
while (pwd[pos--] != '/') ;
pos += 2;
pwd = pwd.substr(pos);
if (pwd == "root" || pwd == "caugyj")
{
Pwd = "~";
return ;
}
Pwd = pwd;
}
void Print()
{
getpwd();
char Lable = (getusername() == "root" ? '#' : '$');
printf(LEFT "%s@%s %s" RIGHT "%c"
" "
"(模拟)",
getusername(), gethostname(), Pwd.c_str(), Lable);
}
string cmdline;
void Interact()
{
Print();
getline(cin, cmdline);
while (cmdline.size() == 0)
{
Print();
getline(cin, cmdline);
}
}
这里简单提一下几个点:
1.printf("hello" "world")这种写法会被合并成printf("helloworld");
2.getpwd函数就是对环境变量ENV作逆序寻找路径分隔符,一定找得到,截取即可。这里存放了一个全局变量pwd,也可以用返回值。
getusername等函数就是简单封装了getenv
3.当用户不输入字符按Enter时,再次提供交互环境,知道输入了字符为止。
4.如果使用char*类型,可以用fgets
5.root身份下右括号坐标的符号是#号,而普通用户是$,在用户的目录下,显示的是~,对应cd ~的功能

命令分割
读取到一个完整的命令之后,比如ls -a -l,当然要进行分割,识别出ls,和-a,-l选项,能够放入exec系列函数
当然,分割有很多方式,如果是string可以自己手写或者使用stringstream,这里需要考虑一下如果是string类型的数组/vector是不方便进行exec系列函数的使用的!要么开一个char*的数组拷贝进去,要么就换成char *的
使用vector< string>手动分割
cpp
vector<string> tmp;
char* argv[1024];
void split()
{
string s;
for(size_t i = 0;i < cmdline.size();++i)
{
if(cmdline[i] == ' ')
{
if(s.size())
{
tmp.push_back(s);
s.clear();
}
}
else s += cmdline[i];
}
if(s.size()) tmp.push_back(s);
for(size_t i = 0; i < tmp.size();++i)
{
argv[i] = new char[tmp[i].size() + 1];
strcpy(argv[i],tmp[i].c_str());
argv[i][tmp[i].size()] = '\0';
}
argv[tmp.size()] = nullptr;
}
使用istringstream / stringstream
cpp
string s;
istringstream sin(cmdline);
while (sin >> s)
{
tmp.push_back(s);
}
for (size_t i = 0; i < tmp.size(); ++i)
{
argv[i] = new char[tmp[i].size() + 1];
strcpy(argv[i], tmp[i].c_str());
argv[i][tmp[i].size()] = '\0';
}
argv[tmp.size()] = nullptr;
使用strtok函数---C语言字符串分割函数
使用注意事项:
char *strtok(char *str, const char *delim);
调一次只会截出来一个字串,必须循环调用
且第一次调用用str,之后调用第一个参数都为NULL
strtok会修改原字符串
cpp
string s;
int i = 0;
char* cmd = const_cast<char*>(cmdline.c_str());
argv[i++] = strtok(cmd," ");
while(argv[i++] = strtok(nullptr," "));
当然,你如果一路都用char*会方便不少。
处理命令
当然要先判断是不是内建命令,不是就去执行正常命令的逻辑,先写正常命令的逻辑,方便一点。提一点注意事项,bash中可以用echo $?来获取上一次退出码,这里怎么实现?定义一个全局变量即可,每次进程等待时让这个全局变量等于WEXITSTATUS(st)即可。
cpp
int lastcode = 0;
void NormalCommand()
{
pid_t id = fork();
if(id < 0)
{
perror("fork");
return ;
}
else if(id == 0)
{
//子进程执行命令
execvpe(argv[0],argv,environ);
}
else
{
int status = 0;
pid_t ret = waitpid(id,&status,0);
if(ret == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
可以实验一下,另外输入ls -a -l不带颜色,是因为bash中重命名了,命令分割中额外判断一下就可以。
cpp
char *argv[1024];
vector<string> tmp;
void split()
{
string s;
istringstream sin(cmdline);
while (sin >> s)
{
tmp.push_back(s);
}
for (size_t i = 0; i < tmp.size(); ++i)
{
argv[i] = new char[tmp[i].size() + 1];
strcpy(argv[i], tmp[i].c_str());
argv[i][tmp[i].size()] = '\0';
}
argv[tmp.size()] = nullptr;
int pos = tmp.size();
if(tmp[0] == "ls")
{
argv[pos++] = const_cast<char*>("--color=auto");
}
argv[pos] = nullptr;
}
另外发现了一个很恶心的问题,还是不要string和char* 混用了,一律char*用到底,否则可能会出现argv[0]没有'\0'的问题,我这么写也可以,但是不推荐。
内建命令
1.cd
如何判断是不是内建命令?---直接判断 if(strcmp(cd,argv[0])) ...
比如cd,怎么写??需要修改路径--chdir函数

chdir本质是告诉内核修改task_struct中的数据,环境变量中没有改变,所以还需要修改环境变量!两种方式修改,第一种方式是把Pwd写道getenv的指针中,这里返回值竟然不是const char*,说明存放的就是指针,第二种方式是设置,1表示覆盖。其实也需要修改OLDPWD,可以自行修改
cpp
sprintf(getenv("PWD"),"%s",Pwd);
setenv("PWD",Pwd,1);
完整:
cpp
if(tmp.size() == 2 && tmp[0] == "cd")
{
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",Pwd.c_str());
//setenv("PWD",Pwd.c_str(),1);
return 1;
}
2.export
这里再说一下export的问题,export也是内建命令,很多人可能会直接这么写
putenv(argv[1]),发现export之后,输入env,显示出来的是env??为什么呢??原因:
char* env[]表里面没有字符串,存放的都是指针!
导环境变量,不是将字符串拷贝进去!而是指针指向过去,也就是说上面那种写法,打印环境变量就从argv[1]里面去取。
如果用的是putenv(tmp[i]),下一次输出环境变量的时候这块指向的内容早就改变了! 对应env
这就解释了全局变量不会出错的问题!因为它不会改变指向!
这是一个进程!while循环不断输入输出,完美的解释了上面的错误原因!
就需要额外开一个空间来存放导入的环境变量。如果想再完善,可以自行多次export的逻辑,就是在env后面加上换行再加上其他字符串
cpp
else if(tmp.size() == 2 && tmp[0] == "export")
{
strcpy(env,argv[1]);
putenv(env);
return 1;
}
3.echo
分三类即可,如果是echo ?,就打印lastcode,如果有 ,就打印环境变量的内容,没有就打印本身
cpp
else if(tmp.size() == 2 && tmp[0] == "echo")
{
if(tmp[1] == "$?")
{
printf("%d\n",lastcode);
lastcode = 0;
}
else if(tmp[1][0] == '$')
{
printf("%s\n",getenv(tmp[1].substr(1).c_str()));
}
else
{
printf("%s\n",tmp[1].c_str());
}
return 1;
}
还有其他的可以自行完善
清理
如果纯用char*当然不用清理,但是我这种写法每次结束后需要释放空间,还等把tmp置空。
cpp
void DEL()
{
for(size_t i = 0; i < tmp.size();++i)
{
delete[] argv[i];
}
tmp.clear();
}
管道
比如ps ajx | grep main这种,这里管道还没有学,大致说一下思路:
1.分析输入的命令行字符串,获取有多少个|,打散多个字符串
2.malloc申请空间,pipe先申请多个管道
3.循环创建多个子进程,每一个子进程的重定向情况,最开始,输出重定向,1->指定的一个管道。
中间:输入输出重定向,0标准输入到上一个的读,1标准输出重定向到下一个管道的写端
最后一个输入重定向,将标准输入重定向到最后一个管道的读端
4.分别让不同的子进程执行不同的命令,exec*不会影响该进程打开的文件
不会影响预先设置好的重定向
在进程间通信那章完善。
四、完整代码
cpp
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <bits/stdc++.h>
#include <string>
#include <algorithm>
#include <sys/wait.h>
using namespace std;
#define LEFT "["
#define RIGHT "]"
#define LABLE "$"
string Pwd;
const char *getusername()
{
return getenv("USER");
}
const char *gethostname()
{
return getenv("HOSTNAME");
}
void getpwd()
{
char buff[1024];
getcwd(buff,sizeof(buff));
string pwd = buff;
size_t pos = pwd.size() - 1;
while (pwd[pos--] != '/') ;
pos += 2;
pwd = pwd.substr(pos);
if (pwd == "root" || pwd == "caugyj")
{
Pwd = "~";
return ;
}
Pwd = pwd;
}
void Print()
{
getpwd();
char Lable = (getusername() == "root" ? '#' : '$');
printf(LEFT "%s@%s %s" RIGHT "%c"
" "
"(模拟)",
getusername(), gethostname(), Pwd.c_str(), Lable);
}
string cmdline;
void Interact()
{
Print();
getline(cin, cmdline);
while (cmdline.size() == 0)
{
Print();
getline(cin, cmdline);
}
}
char *argv[1024];
vector<string> tmp;
void split()
{
string s;
istringstream sin(cmdline);
while (sin >> s)
{
tmp.push_back(s);
}
for (size_t i = 0; i < tmp.size(); ++i)
{
argv[i] = new char[tmp[i].size() + 1];
strcpy(argv[i], tmp[i].c_str());
argv[i][tmp[i].size()] = '\0';
}
argv[tmp.size()] = nullptr;
int pos = tmp.size();
if(tmp[0] == "ls")
{
argv[pos++] = const_cast<char*>("--color=auto");
}
argv[pos] = nullptr;
}
extern char **environ;
int lastcode = 0;
void NormalCommand()
{
pid_t id = fork();
if (id < 0)
{
perror("fork");
return;
}
else if (id == 0)
{
// 子进程执行命令
execvpe(argv[0], argv, environ);
}
else
{
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if (ret == id)
{
lastcode = WEXITSTATUS(status);
}
}
}
char env[1024];
int Build_in_Command()
{
if(tmp.size() == 2 && tmp[0] == "cd")
{
chdir(argv[1]);
getpwd();
sprintf(getenv("PWD"),"%s",Pwd.c_str());
//setenv("PWD",Pwd.c_str(),1);
return 1;
}
else if(tmp.size() == 2 && tmp[0] == "export")
{
strcpy(env,argv[1]);
putenv(env);
return 1;
}
else if(tmp.size() == 2 && tmp[0] == "echo")
{
if(tmp[1] == "$?")
{
printf("%d\n",lastcode);
lastcode = 0;
}
else if(tmp[1][0] == '$')
{
printf("%s\n",getenv(tmp[1].substr(1).c_str()));
}
else
{
printf("%s\n",tmp[1].c_str());
}
return 1;
}
return 0;
}
void DEL()
{
for(size_t i = 0; i < tmp.size();++i)
{
delete[] argv[i];
}
tmp.clear();
}
int main()
{
while (1)
{
Interact();
split();
int r = Build_in_Command();
if(r == 0) NormalCommand();
DEL();
}
return 0;
}
注意
从写代码的复杂程序和空间上还是建议全部用char*,这样可以少开一个数组,并且也不需要手动new和delete,我写string是因为写习惯了,强烈建议全程使用char*,我也写了char*的怎么写。