【Linux】自主shell

学习了进程的相关知识后,我们可以试着实践一下,编写一个简单的 shell。我们的目的并不是完美还原一个 shell,而是通过编写 shell 的过程,更好地理解 shell 的工作方式

输出命令行

我们先来看一下 shell 的命令行都有哪些部分组成:[用户名@主机名 + 当前工作目录]提示符

[图片]

我们可以通过环境变量来获取这些信息,然后拼接为一个字符串打印出来

在这里插入图片描述

可以使用getenv函数来获取指定环境变量

#include <stdlib.h>
char *getenv(const char *name);

然后就可以编写输出命令行的函数了

#include <stdio.h>
#include <stdlib.h>

#define SIZE 512

const char* GetUserName()
{
        const char* s = getenv("USER");
        if (s == NULL) return "None";
        return s;
}

const char* GetHostName()
{
        const char* s = getenv("HOST");
        if (s == NULL) return "None";
        return s;
}

const char* GetPwd()
{
        const char* s = getenv("PWD");
        if (s == NULL) return "None";
        return s;
}

void MakeCommandAndPrint()
{
        char line[SIZE];

        const char* username = GetUserName();
        const char* hostname = GetHostName();
        const char* cwd = GetPwd();
}
int main()
{
        // 输出命令行
        MakeCommandAndPrint();
        return 0;
}

然后我们要怎么把他们拼起来呢?可以利用snprintf将这些数据格式化输出到 line 中

#include <stdio.h>
int snprintf(char *str, size_t size, const char *format, ...);
// 第一个参数是输出到哪里
// 第二个参数表示要输出多少字节
// 后面就和 printf 用法一样了

我们打印的时候不要加\n,命令行与用户输入的指令应在同一行。但是不加 ‘\n’ 就不会主动刷新缓冲区,我们需要在 printf 之后加一句fflush(stdout),刷新缓冲区

void MakeCommandAndPrint()
{
  char line[SIZE];

  const char* username = GetUserName();
  const char* hostname = GetHostName();
  const char* cwd = GetPwd();

  snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, cwd); // 拼接
  printf("%s", line); // 打印命令行
  fflush(stdout); // 刷新缓冲区
}

先来测试一下看看如何:

在这里插入图片描述

看上去还算个样子,只是当前工作目录显示的不太对,我们可以这样处理:

让 cwd 的指针反向遍历字符串,遇到第一个 ‘/’ 就停下,这样就可以定位到最后一层目录的位置了

这里就不写函数了,因为修改指针本身,就涉及到了二级指针传参了,比较麻烦。所以定义宏函数

#define SkipPath(p) do{ p += strlen(p)-1; while(*p != '/') p--;}while(0)

用 dowhile 的目的:这样就可以在 SkipPath 后面加分号或者其他操作,用起来更像一个普通函数

因为 cwd 指向的是 ‘/’,所以cwd+1才是我们要输出的目录

void MakeCommandAndPrint()                                                          
{                                                                                   
  char line[SIZE];                                                                  
                                                                                    
  const char* username = GetUserName();                                             
  const char* hostname = GetHostName();                                             
  const char* cwd = GetPwd();                                                       
                                                                                    
  SkipPath(cwd);                                                                    
  snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, cwd+1); // cwd+1      
  printf("%s", line);                                                               
  fflush(stdout);                                                                        
}

在这里插入图片描述

此时正常打印出了工作目录,但是有一个小漏洞:当处于根目录时,目录显示应该是/,但是我们上面把 ‘/’ 跳过了

在这里插入图片描述

所以应当做一个判断:

  • 当 strlen(cwd) == 1时,输出’/’
  • 否则输出 cwd+1
snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);

获取用户输入的命令

获取用户输入的命令之后,还要分割用户输入的命令。所以将获取的命令存入char usercommand[]中,大小为 SIZE = 512

然后写一个GetUserCommand来获取用户输入命令。如何获取一行数据,可以使用fgets

注意:这样会把换行符也读进去,记得把 usercommand 的最后一个字符换为 ‘\0’

#include <stdio.h>
char *fgets(char *s, int size, FILE *stream);
// 参数:
// 1.要将数据写到哪里
// 2.最大接受多少数据
// 3.数据来源,文件流
// 成功则返回字符串,失败则返回NULL

可以将usercommand作为输出型参数,传给GetUserCommand,最后可以顺便返回命令的长度

int GetUserCommand(char usercommand[], int n)                                     
{                                                                                 
  char* s =fgets(usercommand, n, stdin);                                          
  if (s == NULL) return -1;                                                       
                                                                                  
  usercommand[strlen(usercommand)-1] = '\0'; // 去除换行符                        
  return strlen(usercommand);
}

// main
// 获取用户输入命令
  char usercommand[SIZE];
  int n = GetUserCommand(usercommand, sizeof(usercommand));                                                    
  if (n <= 0) return 1;
  printf("%s\n", usercommand);// 测试

测试一下:

在这里插入图片描述

分割命令行字符串

得到了命令行字符串后,就可以进行分割了。“ls -a -b -c” -> “ls” “-a” “-b” “-c”,这样得到了一批命令行参数之后,我们可以维护一个命令行参数表 argv。

由于其他函数也会用到命令行参数,比如执行命令,所以直接将命令行参数表设为全局变量,就不用每次都传参了

#define NUM 32
char* gArgv[NUM];

关于如何分割字符串,可以使用 strtok

#include <string.h>
char *strtok(char *str, const char *delim);
//参数:
// 1.要分割的字符串,设置为 NULL 可以接着上次调用后继续分割
// 2.分割符,注意是字符串

以下是CommandSplit函数

void CommandSplit(char usercommand[])    
{    
  gArgv[0] = strtok(usercommand, " ");    
    
  int i = 1;    
  while(1)    
  {    
    gArgv[i] = strtok(NULL, " "); // 接着上次分割
    if (gArgv[i] == NULL) // argv表应该以 NULL 结尾
      break;    
    i++;                                                                                                                   
  }    
}

// main
// 分割命令行字符串
  CommandSplit(usercommand);
  // 测试
  for (int i = 0; gArgv[i]; i++)
    printf("%s\n", gArgv[i]);

测试:

在这里插入图片描述

执行命令

有了命令行参数表,就可以执行命令了,通过父进程创建子进程,子进程调用 exec* 函数进行进程替换来实现

exec*函数有很多,调用哪个合适呢?根据我们的命令行参数的形式来确定。argv是数组,而且里面并没有存放着文件的路径,所以用execvp很合适

以下是执行命令的函数ExecuteCmd

void ExecuteCmd()
{
  pid_t id = fork();
  if (id < 0) exit(1);                                                                                                    
  else if (id == 0)
  {
    // child
    execvp(gArgv[0], gArgv);
    exit(errno); // 执行失败
  }
  // father
  int status = 0;
  pid_t rid = waitpid(id, &status, 0);
  if (rid > 0)
  {
    // wait sucess
  }
}
// main
// 执行命令    
ExecuteCmd();

测试结果:

在这里插入图片描述

为了可以一直执行命令,将全部模块放到循环之中

int main()    
{    
  int quit = 0;    
  while(!quit)    
  {  
      // 输出命令行
      MakeCommandAndPrint();
      
      // 获取用户输入命令
      char usercommand[SIZE];
      int n = GetUserCommand(usercommand, sizeof(usercommand));
      if (n <= 0) return 1;
    
      // 分割命令行字符串
      CommandSplit(usercommand);
      
      // 执行命令
      ExecuteCmd();
  }
  return 0;                                                                                                               
}

但是执行一些命令时,出了问题,例如cd

在这里插入图片描述

这是因为:这里的 cd 命令是由子进程执行的,而我们看的是父进程的工作目录。你子进程改变了工作目录,关我父进程什么事?

像 cd 这种内建命令应该直接由 bash 执行,这就是内建命令

所以在执行命令之前,需要检测要执行的命令是不是内建命令

内建命令

检测命令是不是内建命令,可以把我们要执行的命令 argv[0] 与内建命令匹配,可以用 strcmp 匹配两个字符串

strcmp(argv[0], "cd");

如果是内建命令,就让父进程执行,不再创建子进程执行。如下是检测函数CheckBuildin

int CheckBuildin()    
  {    
    const char* enter_cmd = gArgv[0];    
    int yes = 0;    
    if (strcmp(enter_cmd, "cd") == 0)    
    {    
      yes = 1;    
      Cd();
    }
    return yes;
  }
// main
// 检测内建命令
n = CheckBuildin();
if (n) continue; // 父进程执行内建命令后,就不用子进程执行了
        
// 执行命令
ExecuteCmd();

cd

接下来我们来写 Cd 函数,模拟 cd 命令的实现

我们先得到要跳转的目录 path = gArgv[1],然后检测 path 是否存在

  1. 如果不存在,就表示用户只输入了 cd,没有输入目录。那就要默认跳转到家目录,getenv(“HOME”)可以得到家目录
  2. 存在,就改变当前进程的工作目录

经过以上操作,path一定不为空,这时就可以改变工作目录了,可以使用 chdir 函数

void Cd()                                                                            
{                                                                                    
  const char* path = gArgv[1];                                                       
  if (path == NULL)                                                                  
    path = GetHome();                                                                
  //到这里 path 一定存在
  chdir(path);                                                                                                                                     
}

测试一下:

在这里插入图片描述

这时我们发现,虽然当前目录确实切换了,但是命令行显示却没刷新。这是因为,我们命令行的当前目录是从环境变量中获得的,虽然目录改变,但是环境变量中的PWD没变,所以命令行打印出来的仍然是旧的目录

在这里插入图片描述

所以,在改变当前工作目录后,我们还要修改对应的环境变量,可以使用 putenv 来实现。注意,环境变量是"name=value"形式的

#include <stdlib.h>
int putenv(char *string);

我们可以维护一个全局变量 cwd 用来储存当前的环境变量PWD,每次调用Cd时,都要更改它,然后把它作为 putenv 的参数,修改环境变量PWD

char cwd[SIZE];

void Cd()                                                                            
{                                                                                    
  const char* path = gArgv[1];                                                       
  if (path == NULL)                                                                  
    path = GetHome();                                                                
  //到这里 path 一定存在
  chdir(path);
  // 修改当前工作目录后
  snprintf(cwd, sizeof(cwd), "PWD=%s", path);    
  putenv(cwd);                                                                                                     
}

到这里还没结束,因为 path 可能是相对路径,例如..,这样修改环境变量后,就变成了 PWD=..,显然是不合理的。所以我们要先获得当前工作目录的绝对路径,可以通过 getcwd 获得

#include <unistd.h>
char *getcwd(char *buf, size_t size);

可以开一个临时的字符数组 tmp,来存放绝对路径,然后将 tmp 的内容以环境变量的形式输出到 cwd 中

// 改变环境变量    
  char tmp[SIZE*2];    
  getcwd(tmp, sizeof(tmp));    
  snprintf(cwd, sizeof(cwd), "PWD=%s", tmp);                                                                                                                 
  putenv(cwd);

测试:

在这里插入图片描述

虽然比较麻烦,但最终还是实现 cd 功能了

echo $?

我们想要查看上一个命令执行成功还是失败,可以使用 echo $? 查看上一个进程的退出码。而 echo 也是一个内建命令,所以需要我们多加一个检测echo的情况。因为这里只会用到 echo $?,所以就简化一下,如下

int CheckBuildin()                                                                                                                                         
{                                                                                                                                                          
  const char* enter_cmd = gArgv[0];                                                                                                                        
  int yes = 0;                                                                                                                                             
  if (strcmp(enter_cmd, "cd") == 0)                                                                                                                        
  {                                                                                                                                                        
    yes = 1;                                                                                                                                               
    Cd();                                                                                                                                                  
  }                                                                                                                                                        
  else if(strcmp(gArgv[0], "echo") == 0 && strcmp(gArgv[1], "$?") == 0)                                                                                    
  {                                                                                                                                                        
    // ...                                                                                                                                                   
  }                                                                                                                                                        
  return yes;                                                                                                                                              
}

我们可以创建一个全局变量lastcode,用于储存上一个进程的退出码。在子进程执行完命令时,父进程通过 waitpid 获取子进程退出码,存于 lastcode。根据退出码的值,如果子进程执行命令失败,父进程就将相关错误信息打印出来

int lastcode = 0; // 全局变量

void ExecuteCmd() // 子进程执行命令                                                                                
{                                                                                                                   
  pid_t id = fork();                                                                                                
  if (id < 0) exit(1);                                                                                              
  else if (id == 0)                                                                                                 
  {                                                                                                                 
    // child                                                                                                        
    execvp(gArgv[0], gArgv);                                                                                        
    exit(errno); // 执行失败                                                                                        
  }                                                                                                                 
  // father                                                                                                         
  int status = 0;                                                                                                   
  pid_t rid = waitpid(id, &status, 0);                                                                              
  if (rid > 0)                                                                                                      
  {                                                                                                                 
    // wait sucess                                                                                                  
    lastcode = WEXITSTATUS(status); // 设置退出码                                                            
    if (lastcode)                                                                                                   
      printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode); // 打印错误信息
  }                                                                                                                                                          
}

有了lastcode,我们使用echo $?就可以知道上一个命令是否执行成功了

int CheckBuildin()                                                                                                                                         
{                                                                                                                                                          
  const char* enter_cmd = gArgv[0];                                                                                                                        
  int yes = 0;                                                                                                                                             
  if (strcmp(enter_cmd, "cd") == 0)                                                                                                                        
  {                                                                                                                                                        
    yes = 1;                                                                                                                                               
    Cd();
    lastcode = 0;                                                                                                                                   
  }                                                                                                                                                        
  else if(strcmp(gArgv[0], "echo") == 0 && strcmp(gArgv[1], "$?") == 0)                                                                                    
  {
    yes = 1;    
    printf("%d\n", lastcode);    
    lastcode = 0;                                                                                                                 
  }                                                                                                                                                        
  return yes;                                                                                                                                              
}

注意,内建命令也是命令,因此执行成功后,要将 lastcode 置零

测试:

在这里插入图片描述

至此,就完成了一个十分简陋的 shell 的编写,虽然功能不完善,但是重在加深对 shell 的理解

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿洵Rain

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值