Linux——shell程序的简单实现

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;
}

本篇完
如果错误敬请指正

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux实现:从倒计时到进度条

文章目录 1.回车与换行2.缓冲区的概念3.倒计时4.进度条(第一版无应用场景)5.进度条(第二版有应用场景) 1.回车与换行 2.缓冲区的概念 强制刷新可以使用冲刷函数fflush #include <stdio.h> #include <unistd.h> int main() {printf("I am a \nhandsome man!&q…

leetcode 第三弹

链表声明&#xff1a; * Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNode() : val(0), next(nullptr) {}* ListNode(int x) : val(x), next(nullptr) {}* ListNode(int x, ListNode *next) : val(x), next(n…

【K12】tk窗口+plt图像功能-学习物理中的串并联研究【附源码说明】

程序源码 import tkinter as tk import matplotlib.pyplot as plt# 初始化 matplotlib 的字体设置 plt.rcParams[font.family] SimHei# 计算串联电路的函数 def calculate_series():try:# 获取用户输入的电阻值并转换为浮点数r1 float(entry_r1.get())r2 float(entry_r2.ge…

【CANoe使用大全】——Trace窗口

&#x1f64b;‍♂️【CANoe使用大全】系列&#x1f481;‍♂️点击跳转 文章目录 1.Trace作用2.Trace窗口打开方式2.1.Analysis—>Trace2.2.Measurement Setup ------> Trace 3.Trace窗口菜单栏介绍3.1. Detail View3.1. Statistic View3.3.Difference view3.4.Predefi…

【开发问题问题解决开发小技巧】通用资源管理01

【问题】新增应该输出提示但是出现乱码 查看会话发现是会话已结束&#xff0c;好家伙 重新登录会话依旧新增失败&#xff0c; 原来是提交的项没添加ORZ 【问题】会话保护 将会话保护改为“无限制” 执行修改提交但是一直在加载中&#xff0c;回滚后执行直接跳出来“未找到驱动程…

js打地鼠

文章目录 1实现效果2代码实现 1实现效果 游戏难度&#xff1a;简单&#xff0c;一般&#xff0c;困难&#xff0c;噩梦&#xff08;控制setInterval的time参数&#xff09; 按钮功能&#xff1a;结束&#xff08;可以通过修改gameScore的值来修改判定结束的分数&#xff09;&am…

MySQL十部曲之四:MySQL中的数据类型

文章目录 前言概述数字类型数字类型语法数字类型字面量十六进制字面量位字面量布尔字面量 数字类型的属性超出范围和溢出处理 时间和日期类型时间和日期类型语法DATE、DATETIME和TIMESTAMP的异同TIMESTAMP和DATETIME的自动初始化和更新时间和日期字面量 字符串类型字符串类型语…

知识圣殿,智慧熔炉

知识圣殿&#xff0c;智慧熔炉 知识殿堂&#xff0c;巍然屹立 一座灵魂熔炉&#xff0c;号称图书馆 万卷书香盈架&#xff0c;智慧如星河汇聚 每一册书页&#xff0c;流淌着人类文明的血脉 钢笔与墨水交织诗篇 思想发芽&#xff0c;真理绽放光焰 浩瀚知识海洋&#xff0c;波涛…

tensorboard+seaborn 画RL论文图片

概要 tensorboard记录数据&#xff0c;并保存为fie_name.csv 文件加载file_name.csv文件, 处理加载得到数据,然后通过seaborn 显示出来。 1. tensorboard 通常来说&#xff0c;我们一般会用 tensorboard 去记录一些数据。 所以我们先介绍一下 tensorboard 一些注意事项 seti…

mybatis-plus常用使用方法

** mybaits-plus常用使用方法 ** 常用三层分别继承方法 1.1mapper层&#xff08;接口定义层&#xff09;可以用BaseMapper<> 例如&#xff1a; 1.2.里面常用的封装方法有 1.3常用方法介绍 【添加数据&#xff1a;&#xff08;增&#xff09;】int insert(T entity);…

css不规则的文本环绕

<!DOCTYPE html> <html lang"en"><head><meta charset"UTF-8" /><meta name"viewport" content"widthdevice-width, initial-scale1.0" /><title>不规则的文本环绕</title><style>.b…

性能测试混合业务场景

已知从生产环境中统计出的接口比例如下所示&#xff1a; 接口接口比例接口140%接口220%接口330%接口410% 场景一&#xff1a;以上接口无上下依赖关系&#xff0c;设计出容量场景 接口1比例如下&#xff1a; 接口2比例如下&#xff1a; 接口3比例如下&#xff1a; 接口4比例如…

HFSS实战(三)——过孔via TDR仿真

文章目录 一、模型的处理二、TDR仿真2.1 修改求解模式2.2增加求解设置 三、查看仿真结果3.1 查看TDR结果3.2 查看S参数结果 四、结果分析4.1上升时间tr对仿真的影响 附&#xff1a;工程链接 在上一讲中&#xff0c;主要是通过观察S参数确定via的优化是否达到目标。但S参数只能看…

AI嵌入式K210项目(21)-AI模型文件导入至TF卡

文章目录 前言一、模型文件二、方法1三、方法2总结 前言 上一章节介绍了使用MicroPython进行开发&#xff0c;IDE中有很多的示例教程&#xff0c;相信大家已经迫不及待的想试试了&#xff0c;里面人目标检测的例程需要调用训练好的模型文件&#xff0c;这一章介绍如何将AI模型…

关于MySQL的基本查询(多表查询等)

1.创建student和score表 CREATE TABLE student ( id INT(10) NOT NULL UNIQUE PRIMARY KEY , name VARCHAR(20) NOT NULL , sex VARCHAR(4) , birth YEAR, department VARCHAR(20) , address VARCHAR(50) ); 创建score表。SQL代码如下&#xff1a; CREATE…

25考研政治备考计划

各位小伙伴大家好&#xff0c;今天给大家分享的是25考研政治复习备考计划。 政治没有基础阶段&#xff0c;直接就是强化&#xff0c;强化的内容也就是听课&#xff0c;刷题。 【时间安排】 *7-9月中 徐涛老师或腿姐强化课&#xff0c;推荐刷肖1000 *9月中-10月中 背腿姐的背…

BLIP-2: 基于冻结图像编码器和大型语言模型的语言-图像预训练引导

BLIP-2: 基于冻结图像编码器和大型语言模型的语言-图像预训练引导 项目地址BLIP-2的背景与意义BLIP-2的安装与演示BLIP-2模型库图像到文本生成示例特征提取示例图像-文本匹配示例性能评估与训练引用BLIP-2Hugging Face集成 在语言-图像预训练领域&#xff0c;BLIP-2的出现标志着…

Mac M1 Parallels CentOS7.9 Deploy 禅道

禅道官网下载地址: https://www.zentao.net/download/max4.10-83276.html 一、官网下载 二、解压安装 将下载好的包传至CentOS7.9虚拟机 zhinian192 ~ % scp Downloads/ZenTaoPMS-max4.10-zbox_arm64.tar.gz root10.211.55.36:~ ZenTaoPMS-max4.10-zbox_arm64.tar.gz …

LabVIEW振动信号分析

LabVIEW振动信号分析 介绍如何使用LabVIEW软件实现希尔伯特-黄变换&#xff08;Hilbert-Huang Transform, HHT&#xff09;&#xff0c;并将其应用于振动信号分析。HHT是一种用于分析非线性、非平稳信号的强大工具&#xff0c;特别适用于旋转机械等复杂系统的振动分析。开发了…

VR漫游:赋予用户720度身临其境的沉浸式体验

如今&#xff0c;VR全景技术已经成为了千行百业中不可或缺的一部分&#xff0c;随着用户对交互式、沉浸式体验的需求不断加强&#xff0c;VR漫游逐渐取代了传统图片&#xff0c;为人们带来了流畅且完整的VR宣传观看体验。 VR实景漫游真实的展示了现场环境&#xff0c;不仅增加了…