Linux系统编程系列之模拟shell


前言

模拟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*的怎么写。

相关推荐
Xの哲學11 小时前
Linux SMP 实现机制深度剖析
linux·服务器·网络·算法·边缘计算
知识分享小能手11 小时前
Ubuntu入门学习教程,从入门到精通,Ubuntu 22.04的Linux网络配置(14)
linux·学习·ubuntu
皇族崛起12 小时前
【视觉多模态】- scannet 数据的 Ubuntu 百度网盘全速下载
linux·ubuntu·3d建模·dubbo
CAU界编程小白12 小时前
Linux系统编程系列之进程控制(下)
linux·进程控制
RisunJan12 小时前
Linux命令-ifconfig命令(配置和显示网络接口的信息)
linux·运维·服务器
LaoWaiHang13 小时前
Linux基础知识04:pwd命令与cd命令
linux
lbb 小魔仙13 小时前
【Linux】100 天 Linux 入门:从命令行到 Shell 脚本,告别“光标恐惧”
linux·运维·服务器
小张成长计划..13 小时前
【Linux】1:基本指令
linux
OliverH-yishuihan13 小时前
在win10上借助WSL用VS2019开发跨平台项目实例
linux·c++·windows