【Linux 进程】 自定义shell

news2024/12/27 13:25:24

目录

关于shell

1.打印提示符&&获取用户命令字符​编辑

2.分割字符串

3.检查是否为内建命令

cd命令

export命令

echo命令

1.输出最后一个执行的命令的状态退出码(返回码)

2.输出指定环境变量

4.执行外部命令


关于shell

Shell 是计算机操作系统的一种命令行解释器,它允许用户与操作系统进行交互,执行各种操作和任务。Shell 接受用户输入的命令,并将其解释成操作系统能够理解的形式,然后将这些命令发送给操作系统内核执行。

Shell 的作用包括但不限于:

  1. 命令执行: 用户可以使用 Shell 来执行各种命令,包括系统命令、应用程序命令、脚本等,以完成各种任务和操作。

  2. 文件操作: 用户可以使用 Shell 进行文件和目录的创建、复制、移动、删除等操作,以及文件内容的查看、编辑等操作。

  3. 系统管理: 用户可以使用 Shell 进行系统资源的管理,包括进程管理、用户管理、权限管理等。

  4. 环境配置: 用户可以使用 Shell 来配置系统环境,包括设置环境变量、执行初始化脚本等。

  5. 脚本编程: 用户可以使用 Shell 编写脚本,实现自动化任务和流程控制,提高工作效率。

总的来说,Shell 提供了一个灵活而强大的界面,使用户能够通过简单的命令和脚本来与操作系统进行交互和控制,从而完成各种任务和操作。

1.打印提示符&&获取用户命令字符

首先,要自定义shell就必须要接收命令,可以看到我们在系统的shell中提示符有三部分:1.用户名 2.主机名 3.所在目录。所以我们自己的shell就必须先把这三个提示符打印出来,然后是接收用户输入的命令字符。而考虑到接收命令是一直持续的,所以我们用死循环来控制。

那如何在程序中获取用户名、主机名以及所在目录呢?这就需要环境变量了。

环境变量(environment variables)一般是指在操作系统中用来指定操作系统运行环境的一些参数,是操作系统为了满足不同的应用场景预先在系统内预先设置的一大批全局变量。

在操作系统里,我们可以输入env、printenv、echo等命令来打印出环境变量:

可以看到系统输出了很多我们不认识的东西,但我们只需要找到需要的用户名(USER)、主机名(HOSTNAME)以及当前目录(PWD)。

然后我们就可以编写程序了。

从程序中获取环境变量,我们需要用到一个函数

char *getenv(const char *name);

这个函数将返回我们传给的 name,然后它将环境列表中搜索名为name的环境变量,如果name不是环境列表的一部分,它将返回 NULL,否则返回具有所求环境变量值的C字符串。

 const char *getUsername()
  {
      const char *name = getenv("USER");
      if(name) return name;
      else return NULL;
  }
  const char *getHostname()
  {
      const char* hostname = getenv("HOSTNAME");
      if(hostname) return hostname;
      else return NULL;
  }
  const char *getCwd()
  {
      const char* cwd = getenv("PWD");
      if(cwd) return cwd;
      else return NULL;
  }     

我们编写了上面三个函数,分别可以返回用户名、主机名和当前目录路径。

下面我们创建一个函数getUserCommand,使其能够打印出提示符信息并获取用户命令字符,在此之前,我们需要在主函数中创建一个字符数组username以便存放用户命令字符和后续操作。

#define NUM 1024
char usercommand[NUM];

那么如何从进程中接收用户输入的字符呢?有的人可能会想到用scanf,但是我们要直到,scanf碰到空格会自动停止,所以我们需要用到fgets,以便从输入流中截取字符。

fgets函数返回值为我们传过去的s字符指针,如果获取字符失败,则返回NULL,参数中第一个元素是我们存储字符的字符串,第二个是要截取的字符串的长度,第三个是标准流,我们需要的是标准输入流stdin,这样getUserCommand函数的需要的参数也就确定了,一个是存放字符的数组usercommand,另一个是数组的长度sizeof(usercommand)

可这样就结束了吗?我们打印出获取的字符串会发现,总是会多一个换行符,这是因为每次输入的回车也被接收了,只需要将字符数组中的最后一个元素换成'\0'就好,然后可以返回字符数组的长度,以便判断是否成功获取到命令字符。

int getUserCommand(char *command,int num)
  {
      printf("[%s@%s %s] $",getUsername(),getHostname(),getCwd());
      char * r = fgets(command,num,stdin);
      if(r == NULL) return -1;
      command[strlen(command)-1] = '\0';
      return strlen(command);
  }
//如果函数返回 -1 或 0 ,则代表获取命令字符失败

2.分割字符串

在C++里,我们通常可以用substr来进行字符串的分割,但是在这我们用的是C,所以只能用strtok来对字符串进行分割。

str是我们想要分割的字符串,delim是我们想要在哪截断的符号,如果没有找到delim或者扫描的字符串到达末尾空字符时,strtok将返回NULL,否则返回指向以delim开头的字符串(不包括delim)。

需要注意的是,strtok会在内部保留一个静态指针,用于记录当前分割的位置,通过将第一个参数设置为 NULL,来告诉strtok函数接着上次的位置进行分割。

在此之前,我们需要在主函数中创建一个存储字符串指针的数组,以便后续操作,定义SIZE代表这个数组最多能存储多少个命令。

#define SIZE 64
char *argv[SIZE];  

接着我们确定函数的参数,第一个是之前接收的字符数组command,第二个是存储命令的argv数组,这里in代表command,out代表argv。

void commandSplit(char *in,char* out[])
  {
      int argc = 0;
      out[argc++] = strtok(in,SEP);
      while(out[argc++] = strtok(NULL,SEP));
  #ifdef DEBUG
      //用于测试 commandSplit是否生效,如果DEBUG被定义,则运行代码,未定义不运行
      for(int i = 0;out[i];i++)
      {
          printf("%d:%s\n",i,out[i]);
      }
 #endif
 }
通过定义DEBUG来测试commandSplit函数能否成功分割字符串。

3.检查是否为内建命令

内建命令是指直接内置在操作系统的命令行解释器(如Bash、CMD)中的命令,而不是外部可执行文件。这些命令不需要从磁盘加载,而是作为解释器的一部分而存在。它们通常提供了一些基本的操作,比如文件系统操作、环境控制等。在linux系统中,常见的内建命令有:cd、echo、pwd、exit、export等。

没有内建命令,我们的shell可以说就是个空格,接下来我们将在我们的shell中中加入内建命令,来让其可以做到一些基本的功能。

int doBuildin(char *argv[]);
//返回值
cd命令

因为正常用户输入的命令都是第一个空格前的字符串代表的是命令,后面的代表命令选项,例如ls -a -l,所以我们需要先判断命令的字符串是否等于"cd",后面的命令也同样如此,如果相等,开始下一步。

因为cd命令后面跟的都是路径,所以我们判断argv中的第二个字符串是否为空,如果为空的话,就让path等于家目录,如果不为空,就让path等于它,然后进行我们的cd命令。

if(strcmp(argv[0],"cd") == 0)
    {
       char *path = NULL;                                                                                                                                                      
       if(argv[1] == NULL) path = homepath();
       else path = argv[1];
       cd(path);
       return 1;
     }

进入自己的cd函数后,我们需要通过一个函数chdir来改变当前的目录地址

然后我们需要记录下已经改变后的目录地址,然后将当前目录地址设置为环境变量

然后我们需要记录下已经改变后的目录地址,然后将当前目录地址设置为环境变量

如此我们就实现了第一个内建命令cd,它可以在改变目录地址的同时,修改环境变量中的PWD变量,使其跟当前目录同步。

export命令

export 是一个命令行命令,用于设置环境变量。在Unix/Linux系统中,环境变量是在操作系统中存储的一组键值对,它们可以影响系统和用户进程的行为。通过使用 export 命令,你可以将一个变量设置为环境变量,使其在当前会话中对所有后续运行的程序可见。

export命令的实现相对简单,只需判断第二个字符是否为空,如果为空的话,直接返回,不为空就设置一个全局变量enval,先将argv[1]拷贝给enval,再用putenv命令将enval中的内容添加到环境变量中。

else if(strcmp(argv[0],"export") == 0)
    {
       if(argv[1] == NULL) return 1;
       strcpy(enval,argv[1]);
       putenv(enval);
       return 1;
   }
echo命令

echo 是一个命令行命令,用于在终端输出文本或变量的值。它是一个非常简单但功能强大的命令,常用于脚本编程、调试以及与其他命令的组合使用。

首先要判断第二个字符串的第一个字符是否为$,并且第二个字符串的长度要大于1,如果满足继续向下走,不满足直接输出argv[1]的内容并返回。

向下走:将指针argv[1]+1,使其跳过第一个字符$,然后将剩下的指针赋值给val,然后对val进行判断,如果val的值等于"?",执行下面第一个,否则执行第二个。

1.输出最后一个执行的命令的状态退出码(返回码)
2.输出指定环境变量
else if(strcmp(argv[0],"echo") == 0)
    {
        if(argv[1] == NULL)
        {
            printf("\n");
            return 1;
        }
        if(*(argv[1]) == '$' && strlen(argv[1]) > 1)
        {
            char *val = argv[1]+1;
            if(strcmp(val,"?") == 0)  //如果是? 打印最后一个执行的命令的状态退出码
            {
                printf("%d\n",lastcode);
                lastcode = 0;
            }
            else
            {
                const char *enval = getenv(val);
                if(enval) printf("%s\n",enval);
                else printf("\n");
            }
            return 1;
        }

但是这个函数不足的点在于,无论用户输入的内建命令成功与否,输入echo $?的结果都是0,除非上一个执行的命令是外部命令。

4.执行外部命令

上面的代码处理了当用户输入的命令是内建命令的情况,那么当用户输入外部命令时应该怎么办呢?

首先,我们创建一个execute函数用来封装处理外部程序,参数为命令字符数组argv,

然后我们可以用exec系列的函数来实现在程序内处理外部命令,那么这几个exec函数应该选哪个呢?

我们选择execvp函数,因为我们可以直接传递命令名称,让execvp去系统里找可执行文件,所以选择'p',而我们的命令名臣存储在了argv这一个数组中,所以选择'v'。

在execute函数中,我们先用fork创建子进程,让子进程去执行命令,然后父进程用waitpid来等待子进程的退出,并获取其退出码,无论子进程是否正常退出,则其状态退出码将存储在status中,然后用WEXITSTATUS来获取子进程的退出状态码。

int execute(char *argv[])
{
        pid_t id = fork();
        if(id < 0) return -1;
        else if(id == 0)  //child
        {
            //exec command
            execvp(argv[0],argv);
            exit(1);
        }
        else    //father
        {
            int status;
            pid_t rid = waitpid(id,&status,0);
            if(rid>0)
            {
                lastcode = WEXITSTATUS(status);
            }                 
        }
}

总代码

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include<sys/types.h>
#include<sys/wait.h>
#define NUM 1024
#define SIZE 64
#define SEP " "
//#define DEBUG 1
//
char cwd[1024];

char enval[1024];  // for test

int lastcode = 0; 


char *homepath()
{
    char *home = getenv("HOME");
    if(home)  return home;
    else
        return (char*)".";
}
const char *getUsername()
{
    const char *name = getenv("USER");
    if(name) return name;
    else return NULL;
}
const char *getHostname()
{
    const char* hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return NULL;
}
const char *getCwd()
{
    const char* cwd = getenv("PWD");
    if(cwd) return cwd;
    else return NULL;
}
int getUserCommand(char *command,int num)
{
    printf("[%s@%s %s] $",getUsername(),getHostname(),getCwd());
    char * r = fgets(command,num,stdin);
    if(r == NULL) return -1;
    command[strlen(command)-1] = '\0';
    return strlen(command);
}
void commandSplit(char *in,char* out[])
{
    int argc = 0;
    out[argc++] = strtok(in,SEP);
    while(out[argc++] = strtok(NULL,SEP));
#ifdef DEBUG
    //用于测试 commandSplit是否生效,如果DEBUG被定义,则运行代码,未定义不运行
    for(int i = 0;out[i];i++)
    {
        printf("%d:%s\n",i,out[i]);
    }
#endif
}

int execute(char *argv[])
{
        pid_t id = fork();
        if(id < 0) return -1;
        else if(id == 0)  //child
        {
            //exec command
            execvp(argv[0],argv);
            exit(1);
        }
        else    //father
        {
            int status;
            pid_t rid = waitpid(id,&status,0);
            if(rid>0)
            {
                lastcode = WEXITSTATUS(status);
            }                 
        }
}
void cd(const char *path)
{
    chdir(path);
    char tmp[1024];
    getcwd(tmp,sizeof(tmp));
    sprintf(cwd,"PWD=%s",tmp);
    putenv(cwd);
}

//什么叫做内建命令:内建命令就是bash自己执行的,类似与自己内部的一个函数
//1->yes 0->no  -1->error
int doBuildin(char *argv[])
{
    if(strcmp(argv[0],"cd") == 0)
    {
        char *path = NULL;
        if(argv[1] == NULL) path = homepath();
        else path = argv[1];
        cd(path);
        return 1;
    }
    else if(strcmp(argv[0],"export") == 0)
    {
        if(argv[1] == NULL) return 1;
        strcpy(enval,argv[1]);
        putenv(enval);
        return 1;
    }
    else if(strcmp(argv[0],"echo") == 0)
    {
        if(argv[1] == NULL)
        {
            printf("\n");
            return 1;
        }
        if(*(argv[1]) == '$' && strlen(argv[1]) > 1)
        {
            char *val = argv[1]+1;
            if(strcmp(val,"?") == 0)  //如果是? 打印最后一个执行的命令的状态退出码
            {
                printf("%d\n",lastcode);
                lastcode = 0;
            }
            else
            {
                const char *enval = getenv(val);
                if(enval) printf("%s\n",enval);
                else printf("\n");
            }
            return 1;
        }
        else
        {
            printf("%s\n",argv[1]);
            return 1;
        }
    }
    else if(0){}
    return 0;
}
int main()
{
    while(1)
    {
        char usercommand[NUM];
        char *argv[SIZE];
        //1.打印提示符&&获取用户命令字符串获取成功
        int n = getUserCommand(usercommand,sizeof(usercommand));
        if(n <= 0) continue;  //跳出循环
        //2.分割字符串
        commandSplit(usercommand,argv);
        //3.check build-in command
        n = doBuildin(argv);
        if(n) continue;
        //4.执行对应的命令
        execute(argv);
    }
}

这样我们就完成了一个简单的自定义shell程序了,还有很多内容可以进行开发。

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

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

相关文章

【精】hadoop、HIVE大数据从0到1部署及应用实战

目录 基本概念 Hadoop生态 HIVE hdfs(hadoop成员) yarn(hadoop成员) MapReduce(hadoop成员) spark flink storm HBase kafka ES 实战 安装并配置hadoop 环境准备 准备虚拟机 安装ssh并设置免密登录 安装jdk 安装、配置并启动hadoop 添加hadoop环境变量&…

翻译《The Old New Thing》 - Why does the CreateProcess function do autocorrection?

Why does the CreateProcess function do autocorrection? - The Old New Thing (microsoft.com)https://devblogs.microsoft.com/oldnewthing/20050623-03/?p35213 Raymond Chen 在 2005 年 6 月 23 日 为什么 CreateProcess 函数会进行自动更正&#xff1f; 译注&#xff…

智能工业相机哪家好?

一、什么是智能工业相机 在工业自动化的浪潮中&#xff0c;智能工业相机扮演着至关重要的角色。它们如同工业领域的“眼睛”&#xff0c;为生产过程提供精准的视觉监测和数据采集。然而&#xff0c;面对众多的智能工业相机品牌&#xff0c;如何选择一款真正适合的产品成为了众多…

Python面试十问

一、深浅拷贝的区别&#xff1f; 浅拷⻉&#xff1a; 拷⻉的是对象的引⽤&#xff0c;如果原对象改变&#xff0c;相应的拷⻉对象也会发⽣改变。 深拷⻉&#xff1a; 拷⻉对象中的每个元素&#xff0c;拷⻉对象和原有对象不在有关系&#xff0c;两个是独⽴的对象。 浅拷⻉(c…

Python量化炒股的数据信息获取— 获取上市公司股东和股本信息

Python量化炒股的数据信息获取— 获取上市公司股东和股本信息 获取上市公司股东和股本信息&#xff0c;即获取上市公司的十大股东信息、十大流通股东信息、股东股份质押信息、股东股份冻结信息、股东户数信息、大股东减持信息和上市公司股本变动信息。 获取上市公司的十大股东…

一、运维概述

章节目标 了解运维的基本概念了解企业的运行模式了解操作系统发展史以及作用 一、运维的基本概念 1、什么是运维&#xff1f; 在技术人员&#xff08;写代码的&#xff09;之间&#xff0c;一致对运维有一个开玩笑的认知&#xff1a;运维就是修电脑的、装网线的、背锅的岗位…

力扣hot100:199. 二叉树的右视图/437. 路径总和 III(dfs/回溯/树上前缀和/哈希表)

文章目录 一、LeetCode&#xff1a;199. 二叉树的右视图二、LeetCode&#xff1a;437. 路径总和 III 一、LeetCode&#xff1a;199. 二叉树的右视图 LeetCode&#xff1a;199. 二叉树的右视图 差点因为是个中等题打退堂鼓。其实比较简单。 右视图实际上只需要找到&#xff0c…

编译 x264 for iOS

文章目录 编译在 FFMpeg 启用 x264其他编译选项报错处理 环境 &#xff1a; macOS 14.3.1 x264 - 20191217-2245 编译 1、下载 x264 源码 http://download.videolan.org/pub/videolan/x264/snapshots/ 这里我下载x264-snapshot-20191217-2245.tar.bz2 &#xff08;截止2024-…

sql注入工具-​sqlmap

介绍&#xff1a; sqlmap是一款开源的自动化SQL注入工具&#xff0c;用于自动化检测和利用Web应用程序中的SQL注入漏洞。它具有强大的参数化查询和自定义注入脚本的功能&#xff0c;可以通过检测和利用SQL注入漏洞来获取数据库的敏感信息&#xff0c;如用户名、密码和其他重要…

Dockerfile镜像实例

目录 一、构建SSH镜像 1. 建立工作目录 2. 生成镜像 3. 启动容器并修改root密码 二、systemctl镜像 1. 建立工作目录 2. 生成镜像 3. 运行镜像容器 ​编辑 4. 测试容器systemct 三、Nginx镜像 1. 建立工作目录 2. 编写Dockerfile脚本 3. 编写run.sh启动脚本 4. …

Java之SimpleDateFormat

系列文章目录 文章目录 系列文章目录前言前言 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站,这篇文章男女通用,看懂了就去分享给你的码吧。 在多线程环境下,当多个线程同时使用相同的SimpleDateFormat对象(如stat…

数据结构---时间复杂度+空间复杂度

算法(algorithm)简单说就是解决问题的方法。方法有好坏&#xff0c;同样算法也是&#xff0c;有效率高的算法&#xff0c;也有效率低的算法。衡量算法的好坏一般从时间和空间两个维度衡量&#xff0c;也就是本文要介绍的时间复杂度和空间复杂度。有些时候&#xff0c;时间与空间…

(三)JVM实战——对象的内存布局与执行引擎详解

对象的内存布局 对象的实例化 对象的创建方式 - new的方式 - Class的newInstance():反射的方式 - Construct的newInstance() - clone:实现Cloneable接口,默认浅拷贝 - 使用反序列化&#xff1a;将二进制流转化为内存对象 创建对象的步骤 - 判断对象对应的类是否加载、链接、初…

RabbitMQ之生产批量发送

为什么要用生产批量发送&#xff1f; 批量发送消息&#xff0c;可以提高MQ发送性能。但是 RabbitMQ 并没有提供了批量发送消息的 API 接口,使用 spring-amqp 的 BatchingRabbitTemplate 实现批量能力。 SimpleBatchingStrategy 发送策略满足以下规则会进行发送&#xff1a; ba…

扩展学习|国内外用户画像相关进展一览

文献来源&#xff1a;徐芳,应洁茹.国内外用户画像研究综述[J].图书馆学研究,2020(12):7-16.DOI:10.15941/j.cnki.issn1001-0424.2020.12.002. 一、用户画像的概念 用户画像概念一经提出,便被广泛应用到精准营销等领域。后来,作为一种描绘用户特征、表达用户诉求的有效工具,用户…

Angular进阶-NVM管理Node.js实现不同版本Angular环境切换

一、NVM介绍 1. NVM简介 Node Version Manager&#xff08;NVM&#xff09;是一个用于管理多个Node.js版本的工具。它允许用户在同一台机器上安装和使用多个Node.js版本&#xff0c;非常适合需要同时进行多个项目的开发者。NVM是开源的&#xff0c;支持MacOS、Windows和Linux…

LLM应用:工作流workflow创建自定义模版使用

参考: https://www.coze.cn/ 本案例是在coze平台上操作的,也有其他工具支持工作流的创建例如dify;也例如图像生成的comfyui工作流工具 创建自定义模版 可以根据自己需求创建自己的工作流工具;本文案例是创建一个联网搜索的LLM应用: 创建工作流页面: https://www.coze.c…

Java面试——不安全的集合类

​ 系统性学习&#xff0c;移步IT-BLOG-CN Java 中有许多的集合&#xff0c;常用的有List&#xff0c;Set&#xff0c;Queue&#xff0c;Map。 其中 List&#xff0c;Set&#xff0c;Queue都是Collection&#xff08;集合&#xff09;&#xff0c;List中<>的内容表示其中…

Linux CPU 飙升 排查五步法

排查思路-五步法 1. top命令定位应用进程pid 找到最耗时的CPU的进程pid top2. top-Hp[pid]定位应用进程对应的线程tid 找到最消耗CPU的线程ID // 执行 top -Hp [pid] 定位应用进程对应的线程 tid // 按shift p 组合键&#xff0c;按照CPU占用率排序 > top -Hp 111683.…

华为手机ip地址怎么切换

随着移动互联网的普及&#xff0c;IP地址成为了我们手机上网的重要标识。然而&#xff0c;在某些情况下&#xff0c;我们可能需要切换手机的IP地址&#xff0c;以更好地保护个人隐私、访问特定地区的内容或服务&#xff0c;或者出于其他网络需求。华为手机作为市场上的热门品牌…