【Linux】自定义Shell - 指南


前言

在上一篇文章中,我们详细介绍了进程创建、进程终止、进程等待和进程程序替换的内容,内容还是挺多的,希望大家可以多去练习熟悉一下,那么本篇文章将带大家详细讲解自定义Shell的内容,接下来一起看看吧!


一. shell原理

当我们执行程序创建进程时,我们的shell命令行解释器也就是bash都做了什么工作呢?

  1. 输出命令行提示符
  2. 获取并解析输入的指令
  3. 判断是否为内建命令,如果不是则创建子进程执行命令

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。
在这里插入图片描述
然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序,并等待这个进程结束。所以要写一个shell,需要循环以下过程:

  1. 获取命令行
  2. 解析命令行
  3. 建立一个子进程(fork)
  4. 替换子进程(execvp)
  5. 父进程等待子进程退出(wait)

二. 自定义shell的实现

2.1 输出命令行提示符

在这里插入图片描述
bash在每次都会输出命令行提示符,然后等待我们用户输入。

看这个命令行提示符,它包含以下信息:

  • 用户名USER
  • 主机名HOSTNAME
  • 当前工作路径PWD
  • 这些在环境变量表中都能够找到,所以就可以使用getenv来获取。

这样我们需要获取环境变量USERHOSTNAMEPWD

#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
const char* GetUserName()
{
const char* name = getenv("USER");
return name == NULL ? "None" : name;
}
const char* GetHostName()
{
const char* hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
const char* GetPWD()
{
//connst char* pwd = getenv("PWD");    
const char* pwd = getcwd(cwd,sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv);
}
return pwd == NULL ? "None" : pwd;
}
std::string DirName(const char* pwd)
{
#define SLASH "/"
std::string dir = pwd;
if(dir==SLASH) return SLASH;
auto pos = dir.rfind(SLASH);
if(pos==std::string::npos) return "BUG?";
return dir.substr(pos+1);
}
void MakeCommandLine(char cmd_prompt[], int size)
{
snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPWD()).c_str());
}
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt,sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}

在这里插入图片描述
可以看到效果就跟上面的差不多了,为了区分系统的shell,所以将$改为#

2.2 获取用户输入的信息

接下来就应该获取命令行信息了
在这里插入图片描述
我们输入一行指令,然后再按回车就可以执行命令,所以我们输入的命令中是可以带空格的。我们不应该使用scanf/cin进行输入了,可以使用fgets来输入一行字符串。

bool GetCommandLine(char* out, int size)
{
char* c = fgets(out, size, stdin);
if(c==NULL) return 1;
out[strlen(out)-1] = '\0'; //清理\n    
if(strlen(out)==0) return false;
return true;
}

2.3 命令行解析

获取了用户输入的信息,但是获得的只是一个字符串,而我们想执行用户输入的命令,就要先对这个字符串进行解析;生成对应的命令行参数表,才能去执行。

命令行参数个数g_argc,命令行参数表g_argv;我们可以设置成全局的,这样每次只用修改g_argcg_argv即可。

我们可以使用strtok函数来分割字符串,可以以空格为分隔符进行分割,获得的每一个字符串存储到命令行参数表g_argv,再++命令行参数个数g_argc即可。

char *strtok(char *str, const char *delim);

功能:将字符串 strdelim 中的字符分割成若干子串(token)。
特点:修改原字符串,把匹配到的分隔符改成 ‘\0’

使用流程

步骤调用方式说明
第一次strtok(str, delim)传入非 NULL 字符串,返回第一个 token
后续strtok(NULL, delim)传入 NULL,继续从上一次位置往后找
#define MAXARGC 128                       
char* g_argv[MAXARGC];
int g_argc = 0;
bool CommandParse(char* commandline)
{
#define SEP " "
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--;
return g_argc > 0 ? true : false;
}

2.4 创建子进程执行命令

解析命令行并且生成命令行参数表之后,现在就可以去执行命令了,那么怎么执行命令呢?

我们的shell并不是自己去执行,而是创建子进程,然后让子进程去执行命令shell等待子进程退出

// last exit code
int lastcode = 0;
int Execute()
{
pid_t id = fork();
if(id==0)
{
// child
execvp(g_argv[0], g_argv);
exit(1);
}
// father
int status = 0;
pid_t rid = waitpid(id, &status, 0);
//(void)rid; // rid使用一下
if(rid>0)
{
lastcode = WEXITSTATUS(status);
}
return 0;
}

在这里插入图片描述

三. 自定义shell的扩展实现

3.1 环境变量表

上面的shell我们可以运行了,但是没有考虑内建命令环境变量表等这些东西,所以我们把它完善一下。

bash启动时,它的环境变量表从我们系统的配置文件中来,但是这里没办法从系统配置文件中读,所以我们这里就只能从父进程bash获取环境变量表,通过environ来获取从父进程继承下来的环境变量表。

但是我们也要有我们自己的一张环境变量表,所以要手动维护一张自己的环境变量表,并且把它们导出来,通过putenv来导出这些环境变量。

#define MAX_ENVS 100    
char* g_env[MAX_ENVS+1];
int g_envs = 0;
void InitEnv()
{
extern char** environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
// 本来要从配置文件来
// 1. 获取环境变量
for(int i=0;environ[i];i++)
{
// 申请空间
g_env[i] = (char*)malloc(strlen(environ[i]+1));
strcpy(g_env[i], environ[i]);
g_envs++;
}
g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
g_env[g_envs] = NULL;
// 2. 导成环境变量
for(int i=0;g_env[i];i++)
{
putenv(g_env[i]);
}
}

3.2 内建命令

内建命令指的是bash不创建子进程去执行,而是bash自己去执行的命令,我们现在知道的内建命令有cd、export、echo、alias等。

cd

cd命令肯定不能让子进程去执行,因为修改的子进程的路径,而不是shell自己的路径,所以要让shell自己去执行,我们可以使用chdir系统调用来修改当前工作路径:

在这里插入图片描述

关于cd命令

  • cd:进入用户的家目录
  • cd ~:进入用户的家目录
  • cd where:进入指定路径
  • cd -:进入上次的工作路径
char cwd[1024];
char cwdenv[1024];
char oldpwd[1024];
const char* GetOldPwd()
{
const char* oldpwd = getenv("OLDPWD");
return oldpwd == NULL ? "None" : oldpwd;
}
const char* GetHome()
{
const char *home = getenv("HOME");
return home == NULL ? "" : home;
}
void UpdateOldPwd()
{
snprintf(oldpwd, sizeof(oldpwd), "OLDPWD=%s", cwd);
//printf("%s\n",oldpwd);
putenv(oldpwd);
}
bool Cd()
{
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
UpdateOldPwd();
GetPWD();
}
else{
std::string where = g_argv[1];
if(where == "-"){
chdir(GetOldPwd());
UpdateOldPwd();
GetPWD();
}else if(where == "~"){
chdir(GetHome());
UpdateOldPwd();
GetPWD();
}else{
chdir(where.c_str());
UpdateOldPwd();
GetPWD();
}
}
return true;
}

echo

echo命令也是内建命令,我们知道,echo $?可以查看最近一次进程退出时的退出码。

查看最近一次进程退出时的退出码,这些退出码在哪里呢?
肯定不会在子进程中,那就在bash

所以在我们自定义的shell中,我们可以定义一个全局变量,每次执行一次命令就对其进行一次修改。

// last exit code
int lastcode = 0;
void Echo()
{
if(g_argc == 2)
{
// echo "hello world"
// echo $?
// echo $PATH
std::string opt = g_argv[1];
if(opt == "$?")
{
std::cout << lastcode << std::endl;
lastcode = 0;
}
else if(opt[0] == '$')
{
std::string env_name = opt.substr(1);
const char* env_value = getenv(env_name.c_str());
if(env_value)
std::cout << env_value << std::endl;
}
else
{
std::cout << opt << std::endl;
}
}
}

export

该命令的作用就是设置环境变量

void Export()
{
std::string addenv = g_argv[1];
auto pos = addenv.find("=");
std::string name = addenv.substr(0,pos);
std::string val = addenv.substr(pos+1);
bool vis = false;
int i = 0;
for(;g_env[i];i++)
{
std::string s = g_env[i];
auto j = s.find('=');
std::string envname = s.substr(0,j);
if(envname == name){
vis = true;
break;
}
}
if(vis)
{
std::string oldenv = g_env[i];
std::string oldval = oldenv.substr(oldenv.find('=')+1);
if(addenv.size() > oldenv.size())
{
free(g_env[i]);
g_env[i] = (char*)malloc(addenv.size()+1);
}
strcpy(g_env[i], addenv.c_str());
putenv(g_env[i]);
}
else
{
if(g_envs < MAX_ENVS)
{
g_env[g_envs] = (char*)malloc(addenv.size()+1);
strcpy(g_env[g_envs], addenv.c_str());
putenv(g_env[g_envs]);
g_env[g_envs++] = NULL;
}
}
}

alias

经过测试我们可以发现,bash支持ll,而我们的shell是不支持的;

我们知道ll是别名,如果想要我们的shell也支持别名,我们就要在shell中新增一张别名表,维护好这张别名表,就可以支持对其它命令取别名了。

可以使用unordered_map或者map来存储这张别名表。

std::unordered_map<std::string,std::string> alias_list;
  void Alias()
  {
  std::string name = g_argv[1];
  for(int i=2;g_argv[i];i++)
  {
  std::string s = g_argv[i];
  std::string ss = " ";
  ss += s;
  name+=ss;
  }
  //std::cout << name << std::endl;
  auto pos = name.find('=');
  std::string name1 = name.substr(0,pos);
  std::string name2 = "";
  if(name[pos+1]=='"' || name[pos+1]=='\'')
  {
  name2 = name.substr(pos+2);
  name2.pop_back();
  }
  else{
  name2 = name.substr(pos+1);
  }
  alias_list.insert({name1,name2});
  }
  bool AliasManage()
  {
  std::string cmd = g_argv[0];
  if(alias_list.count(cmd))
  {
  return CommandParse((char*)alias_list[cmd].c_str());
  }
  return true;
  }
  bool CheckAndExecBuiltin()
  {
  std::string cmd = g_argv[0];
  if(cmd == "cd")
  {
  Cd();
  return true;
  }
  else if(cmd == "echo")
  {
  Echo();
  return true;
  }
  else if(cmd == "export")
  {
  Export();
  return true;
  }
  else if(cmd == "alias")
  {
  Alias();
  return true;
  }
  return false;
  }
  int main()
  {
  // shell 启动的时候,从系统中获取环境变量
  // 我们的环境变量信息应该从父shell统一来
  InitEnv();
  while(1)
  {
  // 1. 输出命令行提示符
  PrintCommandPrompt();
  //printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetPWD());
  // 2. 获取用户输入的命令
  char commandline[COMMAND_SIZE];
  if(!GetCommandLine(commandline,sizeof(commandline)))
  {
  continue;
  }
  // 3. 命令行分析  "ls -a -l" -> "ls" "-a" "-l"
  if(!CommandParse(commandline))
  continue;
  // 4. 别名处理
  if(!AliasManage())
  continue;
  // 5. 检测并处理内建命令
  if(CheckAndExecBuiltin())
  continue;
  // 6. 执行命令
  Execute();
  //PrintArgv();
  //printf("echo %s\n", commandline);
  }
  return 0;
  }

在这里插入图片描述

除此之外,还有非常多的内建命令,这里就不一一实现了,大家感兴趣的话可以自己动手试试!

3.3 源代码

#include <iostream>
  #include <cstdio>
    #include <cstring>
      #include <cstdlib>
        #include <sys/types.h>
          #include <sys/wait.h>
            #include <unistd.h>
              #include <cstring>
                #include <unordered_map>
                  #define COMMAND_SIZE 1024
                  #define FORMAT "[%s@%s %s]# "
                  // 下面是shell定义的全局数据
                  // 1. 命令行参数表
                  #define MAXARGC 128
                  char* g_argv[MAXARGC];
                  int g_argc = 0;
                  // 2. 环境变量表
                  #define MAX_ENVS 100
                  char* g_env[MAX_ENVS+1];
                  int g_envs = 0;
                  // 3. 别名映射表
                  std::unordered_map<std::string,std::string> alias_list;
                    char cwd[1024];
                    char cwdenv[1024];
                    char oldpwd[1024];
                    // last exit code
                    int lastcode = 0;
                    const char* GetUserName()
                    {
                    const char* name = getenv("USER");
                    return name == NULL ? "None" : name;
                    }
                    const char* GetHostName()
                    {
                    const char* hostname = getenv("HOSTNAME");
                    return hostname == NULL ? "None" : hostname;
                    }
                    const char* GetPWD()
                    {
                    //connst char* pwd = getenv("PWD");
                    const char* pwd = getcwd(cwd,sizeof(cwd));
                    if(pwd != NULL)
                    {
                    snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
                    putenv(cwdenv);
                    }
                    return pwd == NULL ? "None" : pwd;
                    }
                    const char* GetOldPwd()
                    {
                    const char* oldpwd = getenv("OLDPWD");
                    return oldpwd == NULL ? "None" : oldpwd;
                    }
                    const char* GetHome()
                    {
                    const char *home = getenv("HOME");
                    return home == NULL ? "" : home;
                    }
                    void UpdateOldPwd()
                    {
                    snprintf(oldpwd, sizeof(oldpwd), "OLDPWD=%s", cwd);
                    //printf("%s\n",oldpwd);
                    putenv(oldpwd);
                    }
                    void InitEnv()
                    {
                    extern char** environ;
                    memset(g_env, 0, sizeof(g_env));
                    g_envs = 0;
                    // 本来要从配置文件来
                    // 1. 获取环境变量
                    for(int i=0;environ[i];i++)
                    {
                    // 申请空间
                    g_env[i] = (char*)malloc(strlen(environ[i]+1));
                    strcpy(g_env[i], environ[i]);
                    g_envs++;
                    }
                    g_env[g_envs++] = (char*)"HAHA=for_test"; //for_test
                    g_env[g_envs] = NULL;
                    // 2. 导成环境变量
                    for(int i=0;g_env[i];i++)
                    {
                    putenv(g_env[i]);
                    }
                    }
                    //command
                    bool Cd()
                    {
                    if(g_argc == 1)
                    {
                    std::string home = GetHome();
                    if(home.empty()) return true;
                    chdir(home.c_str());
                    UpdateOldPwd();
                    GetPWD();
                    }
                    else{
                    std::string where = g_argv[1];
                    if(where == "-"){
                    chdir(GetOldPwd());
                    UpdateOldPwd();
                    GetPWD();
                    }else if(where == "~"){
                    chdir(GetHome());
                    UpdateOldPwd();
                    GetPWD();
                    }else{
                    chdir(where.c_str());
                    UpdateOldPwd();
                    GetPWD();
                    }
                    }
                    return true;
                    }
                    void Echo()
                    {
                    if(g_argc == 2)
                    {
                    // echo "hello world"
                    // echo $?
                    // echo $PATH
                    std::string opt = g_argv[1];
                    if(opt == "$?")
                    {
                    std::cout << lastcode << std::endl;
                    lastcode = 0;
                    }
                    else if(opt[0] == '$')
                    {
                    std::string env_name = opt.substr(1);
                    const char* env_value = getenv(env_name.c_str());
                    if(env_value)
                    std::cout << env_value << std::endl;
                    }
                    else
                    {
                    std::cout << opt << std::endl;
                    }
                    }
                    }
                    void Export()
                    {
                    std::string addenv = g_argv[1];
                    auto pos = addenv.find("=");
                    std::string name = addenv.substr(0,pos);
                    std::string val = addenv.substr(pos+1);
                    bool vis = false;
                    int i = 0;
                    for(;g_env[i];i++)
                    {
                    std::string s = g_env[i];
                    auto j = s.find('=');
                    std::string envname = s.substr(0,j);
                    if(envname == name){
                    vis = true;
                    break;
                    }
                    }
                    if(vis)
                    {
                    std::string oldenv = g_env[i];
                    std::string oldval = oldenv.substr(oldenv.find('=')+1);
                    if(addenv.size() > oldenv.size())
                    {
                    free(g_env[i]);
                    g_env[i] = (char*)malloc(addenv.size()+1);
                    }
                    strcpy(g_env[i], addenv.c_str());
                    putenv(g_env[i]);
                    }
                    else
                    {
                    if(g_envs < MAX_ENVS)
                    {
                    g_env[g_envs] = (char*)malloc(addenv.size()+1);
                    strcpy(g_env[g_envs], addenv.c_str());
                    putenv(g_env[g_envs]);
                    g_env[g_envs++] = NULL;
                    }
                    }
                    }
                    void Alias()
                    {
                    std::string name = g_argv[1];
                    for(int i=2;g_argv[i];i++)
                    {
                    std::string s = g_argv[i];
                    std::string ss = " ";
                    ss += s;
                    name+=ss;
                    }
                    //std::cout << name << std::endl;
                    auto pos = name.find('=');
                    std::string name1 = name.substr(0,pos);
                    std::string name2 = "";
                    if(name[pos+1]=='"' || name[pos+1]=='\'')
                    {
                    name2 = name.substr(pos+2);
                    name2.pop_back();
                    }
                    else{
                    name2 = name.substr(pos+1);
                    }
                    alias_list.insert({name1,name2});
                    }
                    std::string DirName(const char* pwd)
                    {
                    #define SLASH "/"
                    std::string dir = pwd;
                    if(dir==SLASH) return SLASH;
                    auto pos = dir.rfind(SLASH);
                    if(pos==std::string::npos) return "BUG?";
                    return dir.substr(pos+1);
                    }
                    void MakeCommandLine(char cmd_prompt[], int size)
                    {
                    snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), DirName(GetPWD()).c_str());
                    }
                    void PrintCommandPrompt()
                    {
                    char prompt[COMMAND_SIZE];
                    MakeCommandLine(prompt,sizeof(prompt));
                    printf("%s", prompt);
                    fflush(stdout);
                    }
                    bool GetCommandLine(char* out, int size)
                    {
                    char* c = fgets(out, size, stdin);
                    if(c==NULL) return 1;
                    out[strlen(out)-1] = '\0'; //清理\n
                    if(strlen(out)==0) return false;
                    return true;
                    }
                    // 3. 命令行解析
                    bool CommandParse(char* commandline)
                    {
                    #define SEP " "
                    g_argc = 0;
                    g_argv[g_argc++] = strtok(commandline, SEP);
                    while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
                    g_argc--;
                    return g_argc > 0 ? true : false;
                    }
                    void PrintArgv()
                    {
                    for(int i = 0; g_argv[i]; i++)
                    {
                    printf("argv[%d]->%s\n", i, g_argv[i]);
                    }
                    printf("argc: %d\n", g_argc);
                    }
                    bool AliasManage()
                    {
                    std::string cmd = g_argv[0];
                    if(alias_list.count(cmd))
                    {
                    return CommandParse((char*)alias_list[cmd].c_str());
                    }
                    return true;
                    }
                    bool CheckAndExecBuiltin()
                    {
                    std::string cmd = g_argv[0];
                    if(cmd == "cd")
                    {
                    Cd();
                    return true;
                    }
                    else if(cmd == "echo")
                    {
                    Echo();
                    return true;
                    }
                    else if(cmd == "export")
                    {
                    Export();
                    return true;
                    }
                    else if(cmd == "alias")
                    {
                    Alias();
                    return true;
                    }
                    return false;
                    }
                    int Execute()
                    {
                    pid_t id = fork();
                    if(id==0)
                    {
                    // child
                    execvp(g_argv[0], g_argv);
                    exit(1);
                    }
                    // father
                    int status = 0;
                    pid_t rid = waitpid(id, &status, 0);
                    //(void)rid; // rid使用一下
                    if(rid>0)
                    {
                    lastcode = WEXITSTATUS(status);
                    }
                    return 0;
                    }
                    int main()
                    {
                    // shell 启动的时候,从系统中获取环境变量
                    // 我们的环境变量信息应该从父shell统一来
                    InitEnv();
                    while(1)
                    {
                    // 1. 输出命令行提示符
                    PrintCommandPrompt();
                    //printf("[%s@%s %s]# ", GetUserName(), GetHostName(), GetPWD());
                    // 2. 获取用户输入的命令
                    char commandline[COMMAND_SIZE];
                    if(!GetCommandLine(commandline,sizeof(commandline)))
                    {
                    continue;
                    }
                    // 3. 命令行分析  "ls -a -l" -> "ls" "-a" "-l"
                    if(!CommandParse(commandline))
                    continue;
                    // 4. 别名处理
                    if(!AliasManage())
                    continue;
                    // 5. 检测并处理内建命令
                    if(CheckAndExecBuiltin())
                    continue;
                    // 6. 执行命令
                    Execute();
                    //PrintArgv();
                    //printf("echo %s\n", commandline);
                    }
                    return 0;
                    }

最后

本篇关于自定义Shell的内容到这里就结束了,其中还有很多细节值得我们去探究,需要我们不断地学习。如果本篇内容对你有帮助的话就给一波三连吧,对以上内容有异议或者需要补充的,欢迎大家来讨论!

posted @ 2025-12-24 12:11  gccbuaa  阅读(11)  评论(0)    收藏  举报