【Linux笔记】自定义一个简单的shell

news2024/9/24 23:23:31

一、命令行解释器shell的原理

我们已经知道Linux给我们提供了一系列由exec开头的系统调用接口,可以让我们在自己所写的程序中调用各种指令或者我们自己写的其他程序:

而我们的shell命令行解释器也是接收用户输入的指令,然后执行:

那我们在自己所写的程序中执行一次指令,是不是就类似于一个只执行了一次的shell?

而如果我们自己写一个程序可以不断地接收用户输入的指令并执行,那是不是就等价于一个简易的shell?

所以,命令行解释器shell的原理其实就是一个死循环程序,它不断地接收用户输入的指令并执行对应的程序,直到用户退出shell。

二、先搭好大致的框架

1、先获取用户信息并打印出提示符

既然要模拟式下一个shell,那我们也要模拟的像点样子,我们平时在shell命令行中打印命令的时候,在命令的前面都有一个长长的提示符:

这个长长的提示符主要是提示一些用户的信息,包括用户名和主机名和当前所在的工作目录。

那我们也需要先获取一下。

这些用户名、主机名和工作目录其实都是一些环境变量,所以我们可以通过getenv系统调用获得:

而用户名这些信息,其实在系统的环境变量中都有:

所以要打印出这些信息其实并不难,我们直接在环境变量中获取就行了:

效果也是如预料之中的:

2、获取用户输入指令并分割成字符串数组

解决打印提示符的工作接下来就应该接收用户输入的命令了,需要注意的是我们平时在命令行中输入各种指令的时候,例如“ls -a -l”,都是会带一些空格的。所以我们不能直接使用scanf接收,因为scanf是默认遇到空格就结束的了,所以scanf最多只能接收到第一个ls。

所以我们就要使用另一个更适合的接口——fgets:

它的功能其实就是从一个输入流中读取数据,写入一个缓冲区中,我们可以从stdin(键盘)中读取数据,然后定义一个字符串,将数据保存到字符串中:

其结果也是符合我们的预期的:

获取完用户输入的指令之后我们要有干什么呢?

我们最后的目的是要执行指令的啊,我们先来考虑一下我们该使用哪一个程序题换接口才更方便呢?

因为我们现在是在自己写的程序里面去执行指令,而程序并不知道我们所输入的指令的路径在哪里,所以我们肯定是选择自带路径的,即带‘p’的。而可变参数列表又只适用于手动传参,所以我们可能也得选择带‘v’的。

所以最优的选择就是:

所以这就需要我们将用户输入的字符串以空格为分隔符,分割进一个字符串数组中。

而C语言也有这样的接口,能让我们对一个字符串以一个分隔符分割,然后放入到一个字符数组中,那就是strtok:

现在来回一下,这个strtok的第一次使用和后面的使用是不一样的,第一次使用我们需要传递的是字符串数组的起始位置,而之后传递就只需要传第一个NULL即可。

因为strtok一旦失败就返回NULL,所以我们可以像下面这样写:

运行结果:

3、执行指令

有了上面的准备工作,我们就终于可以来执行指令了。

我们观察到,在shell中执行一个指令后其实是执行完就退出了的:

既然是执行完就退出,那我们就肯定不能让我们的父进程执行,而是应该让子进程来执行,子进程执行完就直接退出,而父进程则负责回收子进程:

运行结果:

所以我们现在就成功的调用到了我们系统的指令了,而现在它只执行了一次,我们只需要将之前所写的逻辑放入到一个死循环中,就可以让我们自定义的shell一直运行了:

运行结果:

至此,我们自定义shell的雏型也就完成了,它可以执行很多我们系统的指令,也不会退出,已经满足了shell命令行的大部分功能了。

但还有一些指令,是现阶段的myshell不能完成的,比如我们可以试着运行一下cd命令:

我们会发现,myshell在执行完cd命令后,路径并没有发生改变,也就是说不能完成cd的任务。

这是因为像cd这样的命令,它是一个“内建命令”

三、处理“内建命令”

1、什么是内建命令

在Linux中,有一些命令是一定要父进程来执行的,不能由子进程来执行,这些命令就被称为“内建命令”。

就拿上面所提到的“cd”命令来说,它的本质是程序的工作目录发生了改变,之后执行任何指令都是在这个工作目录下执行。那它就必定不能交给子进程来执行,因为子进程一执行就退了,所以就算子进程的工作目录改变了也没用。

所以"cd"命令一定要是父进程执行。

Linux中其实有很多的内建命令,今天我们实现的是一个简易的shell,所以我这里只实现三个:cd、export,echo。

2、cd

执行cd命令是改变当前程序的工作目录,所以我们先要来认识一个接口:

这个接口就是用来改变当前进程的工作目录的,谁调用chdir,谁的工作目录就发生改变。

所以我们要做的就是让父进程来调用这个chdir。

我们可以封装一个函数来判断当前命令是否是内建命令如果是则执行并返回1,如果不是则返回0。

然后我们在创建子进程之前先判断一下就行了:

之后我们的cd内建命令就可以正常执行了:

但是这里还有一个问题,也就是虽然我们工作目录的确是改变了,但是我们提示符里面的工作目录却并没有改变:

原因在于我们在打印提示符的时候获取的工作目录实在环境变量里获取的:

而我们这里只是改变了工作目录,并没有对环境变量做更改,所以它每一次获取到的都是一样的。

如果想要让提示符内的路径也发生改变,那我们还得要先认识一个接口:

这个接口的作用就是将当前进程所在的绝对路径获取,并放入一个缓冲区内。

所以我们可以创建一个全局的cwd,每次改变path的时候,就获取一次当前进程的绝对路径,然后将获取到的路径放入cwd中,然后再将cwd导入到系统的环境变量表中:

做完这些工作之后,我们提示符里面的路径就也会发生改变了:

为什么cwd一定要用全局变量呢?这是因为环境变量的获取一定要有一个源头,如果cwd只是局部变量,那么子进程一退出,局部变量就被销毁了。那我们之后再查询env的时候,就查不到对应的环境变量了。

4、export

就像上面所说到的,环境变量在查询的时候一定要有一个“源头”,所以我们要导出的环境变量就一定不能存储在一个临时的空间里面。

所以我们要为我们写的myshell创建一个全局的环境变量表:

然后我们在导入环境变量的同时,把要导入的环境变量加入到我们创建的环境变量表中即可:

这样,我们导的环境变量就不会消失了,而且也能导入多个:

3、echo

这个echo就有很多情况要分了,如果echo后面跟的是“$+一个环境变量”,我们需要去环境变量表中查询出这个环境变量然后打印出来,如果后面跟的是一个字符串,那我们直接打印出这个字符串即可,而如果我们后面跟的是“$?”,那我们要打印的是最近一个程序结束时的退出码,这个也是我们等下需要特殊处理的东西。

打印字符串或者打印环境变量其实很好处理,如果是字符串那我们就直接打印好了,如果是环境变量那我们就用getenv获取后再打印出来:

运行结果:

对于退出码我们可以创建一个全局的变量latcode,默认设为0,然后在每次子进程结束后,父进程使用waitpid回收子进程的状态时将lastcode赋值即可:

运行结果:

四、整体代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/wait.h>
#include <unistd.h>
#include <sys/types.h>

char enval[100][100];
int env_num = 0;
int lastcode = 0; // 记录最近一个进程退出是的退出码,默认为0

char cwd[1024]; 
const char* getUserName() {
  const char *username = getenv("USER");
  if (username) {
    return username;
  }
  return "none";
}

const char* getHostName() {
  const char* hostname = getenv("HOSTNAME");
  if (hostname) {
    return hostname;
  }
  return "none";
}

const char* getPwd() {
  const char* pwd = getenv("PWD");
  if (pwd) {
    return pwd;
  }
  return "none";
}
// 处理内建命令
// 成功执行返回1,失败返回0
int dobuildin(char *argv[]) {
  if (strcmp(argv[0], "cd") == 0) {
    char *path = NULL;
    if (NULL == argv[0]) {
      // 如果后面没有跟路径,就让路径默认为'.'即当前目录
      path = ".";
    } else {
      path = argv[1];
    }


    chdir(path);
    // 获取当前进程的绝对路径
    char temp[1024];
    getcwd(temp, sizeof(temp));
    // 将temp写入cwd中
    sprintf(cwd, "PWD=%s", temp);
    // 将cwd中的环境变量导入到系统的环境变量表中
    putenv(cwd);
    return 1;
    } else if (strcmp(argv[0], "export") == 0) {
    if (argv[1] == NULL) {
      return 1;
    }
    strcpy(enval[env_num], argv[1]);
    putenv(enval[env_num]); // 注意这里要导入的是enval[env_num],而不能是argv[1]
    env_num++;
    return 1;
    } else if (strcmp(argv[0], "echo") == 0) {
      if (argv[1] == NULL) {
        printf("\n");
        return 1;
      }
      if (argv[1][0] == '$' && strlen(argv[1]) > 1) {
        if (argv[1][1] == '?') {
          // 打印上一个进程的退出码
          printf("%d\n", lastcode);
          // 因为内建命令执行时总是成功的,所以这里直接将lastcode设成0就行
          lastcode = 0; 
          
        } else {
          // 打印环境变量
          char *val = argv[1] + 1;
          char *reval = getenv(val);
          if (reval == NULL) {
            printf("\n");
            return 1;
          }
          printf("%s\n", reval);
        }
      } else {
        // 表示是字符串
        printf("%s\n", argv[1]);
      }
      return 1;
    }
  return 0;

}

int main() {
  char usercommand[1024];
  while (1) {
  char *argv[100] = { NULL };
  int argc = 0;
  printf("[%s@%s %s]¥ ", getUserName(), getHostName(), getPwd());
  char *r = fgets(usercommand, sizeof(usercommand), stdin);
  if (NULL == r || strlen(usercommand) == 0) {
    continue;
  }
  usercommand[strlen(usercommand) - 1] = '\0';

  // 分割用户输入的指令
  argv[argc++] = strtok(usercommand, " ");
  while (argv[argc++] = strtok(NULL, " "));

  // 检查是否是内建命令并执行
  int res = dobuildin(argv);
  if (res) {
    continue; // 如果成功执行就不用再往后执行了
  }
  
  

  // 执行指令
  pid_t id = fork();
  if (0 == id) {
    // child
    int n = execvp(argv[0], argv);
    if (-1 == n) {
      printf("-myShell: %s: command not found\n", argv[0]);
    }
    exit(1);
  } else {
    // father
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if (rid > 0) {
      lastcode = WEXITSTATUS(status);
    } else {
      return -1;
    }
  }
  }
  return 0;
}

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

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

相关文章

Error: Failed to download template from registry: fetch failed

第一次构建Nuxt项目时&#xff0c;出现在这样的错误&#xff01;&#xff01;&#xff01; 如果你也是这样得错误&#xff0c;修改hosts也没用。我试了 是因为你的npm安装了其他镜像源&#xff0c; 这个时候你就需要手动下载了&#xff1a; web端访问&#xff1a; https://ra…

[DL]深度学习_Feature Pyramid Network

FPN结构详解 一、概念介绍 Feature Pyramid Network (FPN)是一种用于目标检测和语义分割的神经网络架构。它的目标是解决在处理不同尺度的图像时&#xff0c;信息丢失和语义细节模糊的问题。 FPN的核心思想是通过在网络中添加一组横向连接来构建多尺度特征金字塔。这些横向连接…

01循环算法

1.求小数点的某一位&#xff0c;且超出float和double的精度问题 【题目描述】 分数a/b化为小数后&#xff0c;小数点后第n位的数字是多少&#xff1f; 【输入】 三个正整数a&#xff0c;b&#xff0c;n&#xff0c;相邻两个数之间用单个空格隔开。0<a<b<100&#…

RK3568驱动指南|第十二篇 GPIO子系统-第134章 三级节点操作函数实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…

Redis:原理速成+项目实战——Redis实战14(BitMap实现用户签到功能)

&#x1f468;‍&#x1f393;作者简介&#xff1a;一位大四、研0学生&#xff0c;正在努力准备大四暑假的实习 &#x1f30c;上期文章&#xff1a;Redis&#xff1a;原理速成项目实战——Redis实战13&#xff08;GEO实现附近商铺、滚动分页查询&#xff09; &#x1f4da;订阅…

【AI的未来 - AI Agent系列】【MetaGPT】0. 你的第一个MetaGPT程序

《MetaGPT智能体开发入门》开课&#xff0c;跟着课程&#xff0c;学习MetaGPT智能体开发。 0. 安装MetaGPT 请确保你的系统已安装Python 3.9。你可以通过以下命令进行检查&#xff1a; python3 --version下面是具体的安装命令&#xff1a; 安装命令 pip install metagpt如…

ZooKeeper 实战(四) Curator Watch事件监听

文章目录 ZooKeeper 实战(四) Curator Watch事件监听0.前言1.Watch 事件监听概念2.NodeCache2.1.全参构造器参数2.2.代码DEMO2.3.日志输出 3.PathChildrenCache3.1.全参构造器参数3.2.子节点监听时间类型3.2.代码DEMO 4.TreeCache4.1.构造器参数4.2.代码DEMO4.3.日志输出 ZooKe…

Flink standalone集群部署配置

文章目录 简介软件依赖部署方案二、安装1.下载并解压2.ssh免密登录3.修改配置文件3.启动集群4.访问 Web UI 简介 Flink独立模式&#xff08;Standalone&#xff09;是部署 Flink 最基本也是最简单的方式&#xff1a;所需要的所有 Flink 组件&#xff0c; 都只是操作系统上运行…

别再为创业失败找借口了!否则你永远无法创业成功!2024适合上班族的创业,2024个人创业做什么

每当聊起创业&#xff0c;很多人嘴上都很积极&#xff0c;行动都很低迷&#xff0c;事后就开始找各种理由开始否定创业这个路&#xff0c;要么就是大环境不好&#xff0c;要么就是行业太差&#xff0c;还有就是竞争太多&#xff0c;反正不会是自己的能力太差。 其实创业没有你想…

Postgres 中文周报:Postgres Weekly 537 期

本周报由 Cloudberry Database 社区编译自英文版《Postgres Weekly》&#xff0c;译文较原文有所调整。 推荐博文 &#x1f3c6; PostgreSQL: The DBMS of the Year 2023 PostgreSQL 荣获 DB-Engines 网站 2023 年度 DBMS 冠军。DB-Engines 收集了 480 款数据库系统信息并跟踪…

各版本 操作系统 对 .NET Framework 与 .NET Core 支持

有两种类型的受支持版本&#xff1a;长期支持 (LTS) 版本和标准期限支持 (STS) 版本。 所有版本的质量都是一样的。 唯一的区别是支持的时间长短。 LTS 版本可获得为期三年的免费支持和补丁。 STS 版本可获得 18 个月的免费支持和修补程序。 有关详细信息&#xff0c;请参阅 .N…

2024年美国大学生数学建模思路 - 复盘:校园消费行为分析

文章目录 0 赛题思路1 赛题背景2 分析目标3 数据说明4 数据预处理5 数据分析5.1 食堂就餐行为分析5.2 学生消费行为分析 建模资料 0 赛题思路 &#xff08;赛题出来以后第一时间在CSDN分享&#xff09; https://blog.csdn.net/dc_sinor?typeblog 1 赛题背景 校园一卡通是集…

mac 上 ssh: connect to host localhost port 22: Connection refused

1。 问题 在搭建hadoop环境的时候 发现ssh localhost 在报错 2. 解决 打开系统设置 -> 共享 -> -> 在左边服务中选择 远程登录 注意红框这些选项慎重选择&#xff01;&#xff01;&#xff01; 修改后&#xff0c;在终端再次 ssh localhost 发现登录成功了 如果…

SkipList 的索引过程,能否越两级搜索

“SkipList 的索引过程&#xff0c;能否越两级搜索&#xff1f;” 昨天&#xff0c;一个工作 7 年的粉丝&#xff0c;去某外包公司面试&#xff0c;被问到这个问题不知道该怎么回答。 今天正好有空&#xff0c;给大家分享一下这个问题的回答思路。 对了&#xff0c;这个问题…

【保姆级教程】【YOLOv8替换主干网络】【1】使用efficientViT替换YOLOV8主干网络结构

《博主简介》 小伙伴们好&#xff0c;我是阿旭。专注于人工智能、AIGC、python、计算机视觉相关分享研究。 ✌更多学习资源&#xff0c;可关注公-仲-hao:【阿旭算法与机器学习】&#xff0c;共同学习交流~ &#x1f44d;感谢小伙伴们点赞、关注&#xff01; 《------往期经典推…

08-微服务链路追踪案例

4.4.1&#xff1a;环境说明 dubbo provider: 192.168.58.153 dubbo consumer: 192.168.58.154 zookeeper: 192.168.58.1554.4.2: zookeeper 部署 ~$ apt install openjdk-11-jdk -y ~$ wget https://dlcdn.apache.org/zookeeper/zookeeper-3.8.3/apache-zookeeper-3.8.3-bin.…

【计算机组成原理】指令流水线的三种冒险情况(Hazards)

冒险 在计算机架构中&#xff0c;流水线冒险是指在指令流水线的执行过程中由于数据相关性或控制相关性而导致的一种性能问题。指令流水线是将指令执行过程划分为多个阶段&#xff0c;这样可以同时处理多条指令&#xff0c;从而提高指令执行的效率。然而&#xff0c;流水线执行…

新版云进销存ERP销售库存仓库员工管理系统源码

新版云进销存ERP销售库存仓库员工管理系统源码 系统介绍&#xff1a;2022版本,带合同报价单打印&#xff0c;修复子账号不显示新加客户的BUG&#xff0c;还有其他方面的优化。 简单方便。 功能强大&#xff0c;系统采用phpMYSQL开发&#xff0c;B/S架构&#xff0c;方便随地使用…

怎么找微信服务器的IP地址

首先&#xff0c;让微信客户端在PC端运行&#xff0c;在任务管理器->详细信息中&#xff0c;找到WeChat.exe的进程&#xff0c;找到PID 就是微信进程的ID号&#xff0c;如下图所示&#xff1a; 打开一个命令行窗口&#xff0c;cmd或者powershell窗口都可以&#xff0c;输入…

互联网医院系统|北京线上问诊|线上问诊系统功能解析

随着科技的不断发展&#xff0c;线上问诊系统作为一种快速、便捷的医疗服务方式在近年来越来越受欢迎。本文将重点介绍线上问诊系统的开发功能及其优势&#xff0c;帮助读者更好地了解这一医疗服务方式的价值和好处。 一、线上问诊系统的开发功能&#xff1a; 1、患者注册与登…