【Linux】自主shell

news2024/11/16 3:19:33

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

自主shell

  • 输出命令行
  • 获取用户输入的命令
  • 分割命令行字符串
  • 执行命令
  • 内建命令
    • cd
    • echo $?

输出命令行

我们先来看一下 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 的理解

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2133969.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ROS2 Humble如何初步使用Livox-mid-360激光雷达 (viewer/rviz)以及解决一些问题

Livox-mid-360激光雷达 1、设置静态ip 首先在Ubuntu22.04下将本机IP地址置于和雷达IP同一局域网下 不用改wifi的ip&#xff0c;改以太网的ip sudo ifconfig //查看你的以太网有线 sudo ifconfig sudo ifconfig enx207bd2b27267 192.168.1.50//将enx207…

华为OD机试 - 开源项目热度榜单(Python/JS/C/C++ 2024 E卷 100分)

华为OD机试 2024E卷题库疯狂收录中&#xff0c;刷题点这里 专栏导读 本专栏收录于《华为OD机试真题&#xff08;Python/JS/C/C&#xff09;》。 刷的越多&#xff0c;抽中的概率越大&#xff0c;私信哪吒&#xff0c;备注华为OD&#xff0c;加入华为OD刷题交流群&#xff0c;…

【Echarts】使用多横坐标轴展示近十五天天气预报

现在手机都有天气app,使用echarts展示十五天天气预报的需要你遇到过这样离大谱的需求吗&#xff1f;如果没有或许你能从中找到些许思路。 效果 看效果是不是有点那么个意思,开局一张图,代码全靠ctrl c。不多说上代码。 vue模板引擎代码 <template><div ref"xA…

2024122读书笔记|《人生歪理,歪的很有道理》——生活奇奇怪怪,你要可可爱爱

2024122读书笔记|《人生歪理&#xff0c;歪的很有道理》——生活奇奇怪怪&#xff0c;你要可可爱爱 偶然邂逅的一本书&#xff0c;很可爱治愈的一本书&#xff0c;多嘴鸭我更愿意叫它大鹅&#xff0c;是的就是铁锅炖大鹅的那个大鹅&#x1f60d;&#x1f60d;&#x1f929;&…

python清除一个月以前的ES索引文档数据

python清除一个月以前的ES索引文档数据 先查看一下mysql 数据&#xff0c;看一下那一列是日期字段看到是 edittime 列以下是 python 脚本 vim delete_old_noticeresult.py import datetime from elasticsearch import Elasticsearch, RequestError import logging# 配置日志 …

《C++》解密--顺序表

一、线性表 线性表是n个具有相同特性的数据元素的有限序列。 线性表是一种在实际中广泛使用的数据结构&#xff0c;常见的线性表&#xff1a;顺序表、链表、栈...... 线性表在【逻辑上】是线性结构…

构建“零工市场小程序”,服务灵活就业“大民生”

如今&#xff0c;灵活就业已成为现代劳动力市场的重要组成部分。然而&#xff0c;这一就业形态也面临着信息不对称、匹配效率低下等一系列挑战。为有效解决这些问题&#xff0c;构建一个高效、便捷的“零工市场小程序”显得尤为重要。 二、零工市场现状与挑战 市场规模与增长趋…

单调队列的实现

这是C算法基础-数据结构专栏的第二十五篇文章&#xff0c;专栏详情请见此处。 引入 单调队列就是满足单调性的队列&#xff0c;它最经典的应用就是给定一个序列和一个窗口&#xff0c;使窗口在序列中从前向后滑动&#xff0c;求出窗口在每个位置时&#xff0c;其中元素的最大/小…

DC_(n)Xyz

intra-band contiguous EN-DC 配置需要通过DC_(n)Xyz表示&#xff0c;其中第一个字母y表示contiguous E-UTRA carriers数量&#xff0c;第二个字母z表示contiguous NR carrier数量&#xff0c;而(n)X 就代表 E-UTRA band X 和NR band nX这个组合。上图38.101-3 Table 5.3B.0-1 …

Unity同时启动多个Editor

HardLinkShellExt tool https://schinagl.priv.at/nt/hardlinkshellext/linkshellextension.html 作用&#xff1a; 1.网络Online项目方便调试&#xff0c;MMO项目 2.方便发布不同平台的包&#xff0c;快速开发测试 使用方法&#xff1a;

网络药理学:2、文章基本思路、各个数据库汇总与比对、其他相关资料(推荐复现的文章、推荐学习视频、论文基本框架、文献基本知识及知网检索入门)

一、文章基本思路&#xff08;待更&#xff09; 一篇不含分子对接和实验的纯网络药理学文章思路如下&#xff1a; 即如下&#xff1a; 二、 各个数据库&#xff08;待更&#xff09; 三、其他相关资料 1.推荐复现的文章 纯网络药理学分子对接&#xff1a;知网&#xff1…

Java 每日一刊(第6期):整数运算

文章目录 前言Java 的整数类型基本的整数运算符整数除法与取模自增与自减运算整数的进制表示整数溢出问题位运算整数的优化技巧类型自动提升&#xff08;Type Promotion&#xff09;强制类型转换&#xff08;Type Casting&#xff09;本期小知识 在有限的符号中&#xff0c;我们…

【开放词汇检测】基于MMDetection的MM-Grounding-DINO实战

文章目录 摘要安装基础环境新建虚拟环境安装pytorch安装openmim、mmengine、mmcv安装 MMDetection验证安装配置OV-DINO环境 MMDetection的MM-Grounding-DINO详细介绍测试结果Zero-Shot COCO 结果与模型Zero-Shot LVIS ResultsZero-Shot ODinW&#xff08;野生环境下的目标检测&…

Android视频编辑:利用FFmpeg实现高级功能

在移动设备上进行视频编辑的需求日益增长&#xff0c;用户期望能够在智能手机或平板电脑上轻松地编辑视频&#xff0c;以满足社交媒体分享或个人存档的需求。Android平台因其广泛的用户基础和开放的生态系统&#xff0c;成为视频编辑应用的理想选择。FFmpeg&#xff0c;作为一个…

Centos7安装MySql(特详细)

文章目录 前言一、mysql下载1.打开mysql官网&#xff0c;找到download2.打开MySQL Community(GPL) Downloads3.打开MySql Community Server4.打开Archives5.下载 二、安装1.文件上传2.文件解压3.配置文件4.添加环境变量5.初始化6.启动7.登录并修改密码8.允许远程连接 前言 每次…

【人工智能】OpenAI发布GPT-o1模型:推理能力的革命性突破,这将再次刷新编程领域的格局!

在人工智能领域&#xff0c;推理能力的提升一直是研究者们追求的目标。就在两天前&#xff0c;OpenAI正式发布了其首款具有推理能力的大语言模型——o1。这款模型的推出&#xff0c;不仅标志着AI技术的又一次飞跃&#xff0c;也为开发者和用户提供了全新的工具来解决复杂问题。…

51单片机快速入门之独立按键

51单片机快速入门之独立按键 这里我们需要用上一个仿真软件,只因不想硬件焊接:PROTEUS DESIGN SUITE PROTEUS DESIGN SUITE: PROTEUS DESIGN SUITE是一款由LabCenter Electronics开发的电子设计自动化&#xff08;EDA&#xff09;软件&#xff0c;广泛应用于电气工程和电子工…

debian服务器上搭建git服务及添加文件提交拉取的操作记录、在Ubuntu上搭建Jenkins服务以及Ubuntu中的PPA源及PPA的安装使用

一、debian服务器上搭建git服务及添加文件提交拉取的操作记录 需要新建一个代码仓库&#xff0c;准备找台业务量不大的服务器上找个空间大的文件夹搭建一个。整个过程&#xff1a; 1&#xff0c;在服务器端安装git服务&#xff0c;新建git用户并设置密码&#xff0c;创建仓库&a…

深度学习-神经网络

文章目录 一、基本组成单元&#xff1a;神经元二、神经网络层三、偏置与权重四、激活函数1.激活函数的作用2.常见的激活函数1).Sigmoid2).Tanh函数3).ReLU函数 五、优点与缺点六、总结 神经网络&#xff08;Neural Network, NN&#xff09;是一种模拟人类大脑工作方式的计算模型…

北大阿里:新出炉的LLM偏好对齐方法综述

最近大家都聚集在 Open AI 新的&#x1f353;o1发布和 self-play RL 的共识上。 我想不管是草莓、self-play RL还是数据合成下的new scaling law&#xff0c;也不论这条路是否能够最终走通&#xff0c;仅对于当下以及未来LLM在偏好对齐来说&#xff0c;如文中所述&#xff0c;相…