Linux——shell程序的简单实现

news2024/11/15 20:10:35

shell程序的简单实现

本章思维导图:

在这里插入图片描述注:本章思维导图对应的.xmind.png文件都已同步导入至资源,可免费查阅


在学习完有关进程的知识后,我们就可以开始尝试自己实现一个简单的shell程序了。

注:在编写简单的shell程序之前,你首先需要掌握:

👉进程控制

👉环境变量

👉进程替换

1. 实现交互 interact()

首先,和真正的shell程序一样,我们启动程序,shell就会打印出命令行提示符,并等待用户的输入

在这里插入图片描述

因此,我们首先要做的,就是要正确打印出命令行提示符,并等待接收用户输入的命令。

注:

命令行提示符的基本格式为:[用户名@主机名 当前路径]&

  • 需要注意,如果当前用户为root 用户,那么&就应该变为#

那么,我们该如何获取我们需要的有用户名、主机名和路径信息呢?答案便是通过环境变量来获取

  • 环境变量USER记录了当前的用户信息
  • 环境变量HOSTNAME记录了当前的主机信息
  • 环境变量PWD记录了当前的路径信息

可以利用系统调用getenv()来获取对应的信息,并进行打印

等待并接受用户的输入这一操作十分简单,定义一个字符数组,并用函数fgets()进行接收即可。

这样,我们就实现了第一部分的功能:

//形参out为一个输出型参数,用于接收用户输入的命令
void interact(char* out)
{
  printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
    
  fgets(out, SIZE, stdin);
  out[strlen(out) - 1] = '\0';	//fgets()会将用户输入的换行符读入,因此要将这个符号去除
}

2. 分割命令 split()

在进程替换一节中我们提到,如果要将当前的进程替换为另一个程序,那么就需要使用exec系列函数来进行进程程序替换:

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ..., char * const envp[]);

int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);
int execvpe(const char *file, char *const argv[], char *const envp[]);
  • 命令参数要么以参数列表的形式arg, ...传入,要么以字符串数组argv的方式传入

  • 但是我们在第一步interact()的过程中只接受了用户的一长串命令,这并不能直接作为参数传入程序替换函数中

  • 因此,我们就需要对之前输入的字符串以空格‘ ’为分隔符进行分割

如何分割?——可以用库函数strtok解决

char * strtok ( char * str, const char * delimiters );
  • delimiters分割符
  • 返回值即为被分割的字符串,分割结束返回NULL
  • 关于参数str,当要对用一个字符串多次调用时:
    • 第一次调用时,即为要被分割字符串str
    • 之后的所有调用,参数str都为NULL

如此,我们便可以实现功能分割功能了:

//参数command为用户输入的命令
//参数out为输出型参数,用于存储被分割的字符串集合
void split(char* command, char** out)
{
  int i = 0;
  out[i++] = strtok(command, " ");
  while (out[i++] = strtok(NULL, " "));
}

3. 执行命令

获得了正确的命令参数后,我们就可以开始程序替换了。

但是应该注意,如果程序替换成功,那么原程序之后的所有代码便都不会再执行了。

因此,为了确保shell能够一直处理用户输入的命令,我们应该创建一个子进程来进行进程程序替换

我们可以很容易的写出这样的代码:

//参数argv即为存储命令字符串的数组
void execute(char** argv)
{
    //创建子进程
    pid_t pid = fork();
    if (pid == 0)
    {
      //子进程进行进程程序替换
      execvp(argv[0], argv);
      exit(1);
    }
	
    //子进程退出后父进程进行等待,并获取子进程的退出码
    int status;
    pid_t rid = waitpid(pid, &status, 0);
    EXIT = WEXITSTATUS(status);
}

我们再对上面两部分代码进行整合,就可以得到我们shell的简单版本了:

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

#define SIZE 1024
#define ARGC 64

int EXIT = 0;	//进程退出码

void interact(char* out)
{
  printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
  fgets(out, SIZE, stdin);
  out[strlen(out) - 1] = '\0';
}

void split(char* command, char** out)
{
  int i = 0;
  out[i++] = strtok(command, " ");
  while (out[i++] = strtok(NULL, " "));
}

void execute(char** argv)
{
    pid_t pid = fork();
    if (pid == 0)
    {
      execvp(argv[0], argv);
      exit(1);
    }

    int status;
    pid_t rid = waitpid(pid, &status, 0);
    EXIT = WEXITSTATUS(status);
}

int main()
{
  while(1)
  {
    //获取命令行参数
    char command[SIZE] = {0};
    interact(command);
    if (strlen(command) == 0)
      continue;

    //将命令行拆分成多个字符串
    char* argv[ARGC];
    split(command, argv);
    
   
    execute(argv);
  }

  return 0;
}

我么可以执行来看看:

在这里插入图片描述

可以发现我们执行catlsclear这些命令的时候没有出现问题,但是当我们执行cdechoexport这些命令的时候,却得不到正确的结果。这是为什么?

  • 应该清楚,我们是用子进程进行的进程替换,子进程执行完后便会退出终止。
  • 因此,子进程的改变不会影响到父进程,即不会影响到shell进程
  • 例如我们使用cd命令修改当前路径,我们修改的只是子进程的路径,而其父进程shell并未受任何影响
  • 同样,对于export,我们只是对子进程添加了环境变量,父进程的环境变量同样不会改变

所以,当遇到类似cd这种命令时,我们要对其进行特殊处理

3.1 执行内建命令

在Linux中,诸如echocdexport这样的命令我们称其为内建命令

我们可以利用枚举的方法来对内建命令进行处理

//参数argv即为命令字符串集合
//返回值如果为0,说明不是内建命令;如果是1,说明是内建命令
int buildCommand(char** argv)
{
  int ret = 0;
    
  //处理“cd”
  if (strcmp(argv[0], "cd") == 0)
  {
    ret = 1;
    char* path = argv[1];	//命令cd后面跟的就是新的路径
    char put[SIZE];
    char absolutePath[SIZE];

    if (path == NULL)
      path = getenv("HOME");
    
    chdir(path);
    getcwd(absolutePath, SIZE);	//将新路径存入字符数组absolutePath
    
    snprintf(put, SIZE, "%s%s", "PWD=", absolutePath);	//修改环境变量PWD
    putenv(put);
  }
  //处理”export“
  else if (strcmp(argv[0], "export") == 0)
  {
    ret = 1;
    char env[SIZE];
    if (argv[1])
    {
      strcpy(env, argv[1]);
      putenv(env);
    }
      
    /*
    一定不能直接写成:putenv(argv[1]);
    否则当输入新的命令时,argv[1]的值就会改变,环境变量也会跟着变
    */
  }
  //处理“echo”
  else if (strcmp(argv[0], "echo") == 0)
  {
    ret = 1;
    if (argv[1] == NULL)
      printf("\n");
    else
    {  
      if (argv[1][0] == '$')
      {
        //"echo $?"即输出最近一个进程的退出码
        if (argv[1][1] == '?')
        {
          printf("%d\n", EXIT);
          EXIT = 0;
        }
        //否则输出对应的环境变量
        else 
        {
          char* env = getenv(argv[1] + 1);
          if (env == NULL)
            printf("\n");
          else 
            printf("%s\n", env);
        }
      } 
      //否则为向屏幕输出字符串
      else 
        printf("%s\n", argv[1]);                    
     }
  }
    
  return ret;
}

3.2 执行非内建命令

利用buildCommand()的返回值

  • 如果返回值为0,那么就说明该命令为非内建命令,开始创建子进程进行进程替换
  • 如果返回值为1,那么就说明该命令为内建命令,已经经过处理,直接等待下一条命令的输入即可
//处理内建命令
int ret = buildCommand(argv);

//执行命令
if (!ret)
    execute(argv);

4. 实现代码

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

#define SIZE 1024
#define ARGC 64

int EXIT = 0;

void interact(char* out)
{
  printf("[%s@%s %s]$ ", getenv("USER"), getenv("HOSTNAME"), getenv("PWD"));
  fgets(out, SIZE, stdin);
  out[strlen(out) - 1] = '\0';
}

void split(char* command, char** out)
{
  int i = 0;
  out[i++] = strtok(command, " ");
  while (out[i++] = strtok(NULL, " "));
}

int buildCommand(char** argv)
{
  int ret = 0;
  if (strcmp(argv[0], "cd") == 0)
  {
    ret = 1;
    char* path = argv[1];
    char put[SIZE];
    char absolutePath[SIZE];

    if (path == NULL)
      path = getenv("HOME");
    
    chdir(path);
    getcwd(absolutePath, SIZE);
    
    snprintf(put, SIZE, "%s%s", "PWD=", absolutePath);
    putenv(put);
  }
  else if (strcmp(argv[0], "export") == 0)
  {
    ret = 1;
    char env[SIZE];
    if (argv[1])
    {
      strcpy(env, argv[1]);
      putenv(env);
    }
  }
  else if (strcmp(argv[0], "echo") == 0)
  {
    ret = 1;
    if (argv[1] == NULL)
      printf("\n");
    else
    {  
      if (argv[1][0] == '$')
      {
        if (argv[1][1] == '?')
        {
          printf("%d\n", EXIT);
          EXIT = 0;
        }
        else 
        {
          char* env = getenv(argv[1] + 1);
          if (env == NULL)
            printf("\n");
          else 
            printf("%s\n", env);
        }
      } 
      else 
        printf("%s\n", argv[1]);                    
     }
  }


  return ret;
}

void execute(char** argv)
{
    pid_t pid = fork();
    if (pid == 0)
    {
      execvp(argv[0], argv);
      exit(1);
    }

    int status;
    pid_t rid = waitpid(pid, &status, 0);
    EXIT = WEXITSTATUS(status);
}

int main()
{
  while(1)
  {
    //获取命令行参数
    char command[SIZE] = {0};
    interact(command);
    if (strlen(command) == 0)
      continue;

    //将命令行拆分成多个字符串
    char* argv[ARGC];
    split(command, argv);
    
    //处理内建命令
    int ret = buildCommand(argv);

  	//执行命令
    if (!ret)
      execute(argv);
  }

  return 0;
}

本篇完
如果错误敬请指正

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

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

相关文章

R语言-检验正态性

1.为什么要检验正态性 首先需要明确正态性与正态分布是有区别的&#xff0c;正态分布&#xff08;标准分布&#xff09;是统计数据的分布方式&#xff0c;是个钟形曲线&#xff0c;已平均值为对称轴&#xff0c;数据在对称轴两侧对称分布。正态性是检验实际数据与标准正态分布…

基于Java SSM框架实现在线考试系统项目【项目源码+论文说明】

基于java的SSM框架实现在线考试系统演示 摘要 21世纪的今天&#xff0c;随着社会的不断发展与进步&#xff0c;人们对于信息科学化的认识&#xff0c;已由低层次向高层次发展&#xff0c;由原来的感性认识向理性认识提高&#xff0c;管理工作的重要性已逐渐被人们所认识&#…

字符金字塔(C语言刷题)

个人博客主页&#xff1a;https://blog.csdn.net/2301_79293429?typeblog 专栏&#xff1a;https://blog.csdn.net/2301_79293429/category_12545690.html 题目描述 请打印输出一个字符金字塔&#xff0c;字符金字塔的特征请参考样例 输入描述: 输入一个字母&#xff0c;保…

06.搭建一个自己的私有仓库-Gitea

06.搭建一个自己的私有仓库-Gitea | DLLCNX的博客 如果你是一位程序员或者IT相关领域的从业者&#xff0c;那么肯定知道git&#xff0c;而且也或多或少接触了不少开源仓库以及公司的私有仓库&#xff0c;但是我们有没有想过自己也搭建一个私有仓库呢。 这么多开源仓库&#xf…

C++练习题1-9

文章目录 NO1、选出妃子、宫女和嬷嬷No2、根据数字判断月份No3、循环计数No4、循环选数No5、玩转字符No6、计算字符串长度No7、显示字符串中的字符No8、字符串反转No9、二维数组的应用 NO1、选出妃子、宫女和嬷嬷 其他要求&#xff1a; 超女用结构体表示不要嵌套if输入所有数据…

【数据结构】72变的双端队列

双端队列 前言一、双端队列1.1 双端队列的定义1.2 输入受限的双端队列1.3 输出受限的双端队列1.5 输入输出都受限的双端队列1.6 小结 二、双端队列的使用2.1 双端队列的出队序列——暴力求解2.1.1 栈的出栈序列2.1.2 输入受限的双端队列2.1.3 输出受限的双端队列2.1.4 输入输出…

JCL中常用的DD语句

JCL中的DD语句介绍 ​ DD语句&#xff0c;主要定义数据集用的&#xff0c;也叫做DATASET DEFINE&#xff0c;分为定义设备的UNIT、VOLUME、SPACE&#xff0c;定义数据集的DSN、DISP、DCB,详细可以看英文版的《MVS JCL Reference》&#xff0c;还有一些特殊的DD&#xff0c;暂时…

一文掌握!九大提升 ECS 实例操作系统安全性技巧

云布道师 引言&#xff1a;【弹性计算技术公开课——ECS 安全季】第二节课程由阿里云弹性计算技术专家陈怀可带来&#xff0c;本文内容整理自他的课程&#xff0c;供各位阅览。 安全事件案例回顾与操作系统安全概念介绍 在介绍操作系统安全概念前&#xff0c;我们先来看一下…

每次打开都是:已在调试程序中暂停的处理

点击F12&#xff0c;把这个勾选去掉就可以了。

网安培训第二期——sql注入+中间件+工具

文章目录 宽字节注入插入注入二次注入PDO模式(动态靶机&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;&#xff01;)sql注入读取文件sql注入导出文件linux命令 10.12笔记sqlmapsqlmap参数 10.13笔记sqlmap 文件读写前后缀常用tamper及适用场景 10.…

操作系统-线程的概念(什么是线程 为什么线程共享进程资源 为什么线程切换开销低 引入线程的变化 线程属性 为啥要引入线程)

文章目录 总览什么是线程&#xff0c;为什么要引入线程引入线程机制的变化线程的属性 总览 什么是线程&#xff0c;为什么要引入线程 此时qq进程内的视频文字聊天传输文件可以同时进行&#xff0c;如果进程内部是顺序执行的话&#xff0c;那么将某一时刻只能执行一个功能&…

C语言王道练习题第七周两题

第一题 Description 输入一个学生的学号&#xff0c;姓名&#xff0c;性别&#xff0c;用结构体存储&#xff0c;通过 scanf 读取后&#xff0c;然后再 通过 printf 打印输出 Input 学号&#xff0c;姓名&#xff0c;性别&#xff0c;例如输入 101 xiongda m Output 输出…

基于YOLOv8与ByteTrack的车辆行人多目标检测与追踪系统【python源码+Pyqt5界面+数据集+训练代码】深度学习实战、目标追踪、运动物体追踪

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

开源项目Git Commit规范与ChangeLog

一&#xff0c;conventional commit(约定式提交) Conventional Commits 是一种用于给提交信息增加人机可读含义的规范。它提供了一组用于创建清晰的提交历史的简单规则。 1.1 作用 自动化生成 CHANGELOG基于提交类型&#xff0c;自动决定语义化的版本变更向项目相关合作开发…

数学与计算机:一场幽默风趣的盲约

数学与计算机&#xff1a;一场幽默风趣的盲约 Mathematics and Computers: A Humorous and Witty Blind Date 大家好&#xff0c;今天我们将要探讨一个比猫和键盘之间的深刻关系更有趣的话题——数学和计算机的浪漫邂逅。这可不是一场普通的相亲&#xff0c;而是一场逻辑与算法…

Vue+OpenLayers7入门到实战:快速搭建Vue+OpenLayers7地图脚手架项目。从零开始构建Vue项目并整合OpenLayers7.5.2

返回《Vue+OpenLayers7》专栏目录:Vue+OpenLayers7 前言 本章针对Vue初学者,对Vue不熟悉,甚至还不会Vue的入门学生读者。 本章会详细讲解从NodeJS环境到npm环境的各个步骤,再到使用vue-cli脚手架快速生成项目,以及添加OpenLayers7地图库依赖,编写简单的xyz高德地图显示…

基于python豆瓣电影评论的情感分析和聚类分析,聚类分析有手肘法进行检验,情感分析用snownlp

基于Python的豆瓣电影评论的情感分析和聚类分析是一种用于探索电影评论数据的方法。 情感分析 情感分析旨在从文本中提取情感信息&#xff0c;并对其进行分类&#xff0c;如正面、负面或中性。在这里&#xff0c;我们使用了一个名为snownlp的Python库来进行情感分析。Snownlp是…

Excel:将截面数据转换成面板数据

原始截面数据如下&#xff1a; 步骤&#xff1a;数据——自表格/区域 点击确定&#xff0c;出现下图&#xff1a; 然后&#xff0c;在这个界面选择&#xff1a;“转换”——“逆透视列”下选择逆透视其他列。会出现面板数据形式。 然后&#xff0c;点击“主页”——关闭并上载即…

二叉搜索树操作题目:二叉搜索树中的搜索操作

文章目录 题目标题和出处难度题目描述要求示例数据范围 解法一思路和算法代码复杂度分析 解法二思路和算法代码复杂度分析 题目 标题和出处 标题&#xff1a;二叉搜索树中的搜索操作 出处&#xff1a;700. 二叉搜索树中的搜索操作 难度 2 级 题目描述 要求 给定二叉搜索…

【java】6案例演示

关键字的定义和特点&#xff1a; 定义&#xff1a;被 Java 语言赋予了特殊含义&#xff0c;用做专门用途的字符串&#xff08;单词&#xff09; 特点&#xff1a;关键字中所有字母都为小写 保留字介绍 Java 保留字&#xff1a;现有 Java 版本尚未使用&#xff0c;但以后版本可…