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*用到底,否则可能会出现argv0没有'\0'的问题,我这么写也可以,但是不推荐。

内建命令

1.cd

如何判断是不是内建命令?---直接判断 if(strcmp(cd,argv0)) ...

比如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(argv1),发现export之后,输入env,显示出来的是env??为什么呢??原因:

char* env\[\]表里面没有字符串,存放的都是指针!

导环境变量,不是将字符串拷贝进去!而是指针指向过去,也就是说上面那种写法,打印环境变量就从argv1里面去取。

如果用的是putenv(tmpi),下一次输出环境变量的时候这块指向的内容早就改变了! 对应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*的怎么写。

相关推荐
Soneswy1 小时前
Linux系统篇--重定向与文件操作命令
linux
有毒的教程1 小时前
Linux底层存储路由配
linux·运维·网络
Linux系统运维工程师_郭主任2 小时前
运维人还在被拒offer?现在能考的,只有 RHCE 10.0。
linux
自养Agent3 小时前
MCP Server Boundary Audit Report
linux·服务器·人工智能·windows·大模型·llm
Coisinier4 小时前
docker命令管理镜像及容器
linux·运维·nginx·docker·容器·操作系统
A心有千千结4 小时前
通过 SSH 反向代理让服务器借用本机网络访问 GitHub 的最佳实践
linux·服务器·网络·git·ssh·github
书生执笔画浮沉4 小时前
Linux 内核模块为什么不能自己解决兼容性问题?
linux·c·kernel·update·灵感
15Moonlight5 小时前
Linux网络(03):Socket 编程 TCP
linux·网络·tcp/ip
广州灵眸科技有限公司5 小时前
单芯片同时跑 Linux 与 RT-Thread:RV1126B的AMP异构方案完整拆解
linux·运维·服务器
小雪崩5 小时前
嵌入式学习:Linux驱动开发总结
linux·驱动开发·学习