Linux自定义shell编写

news2024/12/27 16:10:47

Linux自定义shell编写

  • 一.最终版本展示
    • 1.动图展示
    • 2.代码展示
  • 二.具体步骤
    • 1.打印提示符
    • 2.解析命令行
    • 3.分析是否是内建命令
      • 1.shell对于内建名令的处理
      • 2.cd命令
      • 3.cd函数的实现
      • 4.echo命令的实现
      • 5.export命令的实现
      • 6.内建命令函数的实现
    • 4.创建子进程通过程序替换执行命令
    • 5.循环往复即可
  • 三.shell运行原理

经过了创建进程,终止进程,进程等待和进程程序替换之后,
我们就可以借助这些知识实现一个简单的shell命令行解释器了

温馨提示:
建议大家自己写一遍,这些代码分块之后每一个函数都很简单,
不过实现过程中可能会有各种各样非常细枝末节的地方被我们所忽视
因此可能会发生一看就懂,一写就废的情况…

一.最终版本展示

输入命令行时想要删除字符时不能直接按backspace,而是要按ctrl+backspace才能成功删除

1.动图展示

在这里插入图片描述

2.代码展示

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
//#define DEBUG 1
#define SEP " "

char cwd[1024]={'\0'};
int lastcode=0;//上一次进程退出时的退出码

char env[1024][1024]={'\0'};
int my_index=0;

const char* getUsername()
{
    const char* username=getenv("USER");
    if(username==NULL) return "none";
    return username;
}

const char* getHostname()
{

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

const char* getPwd()
{

    const char* pwd=getenv("PWD");
    if(pwd==NULL) return "none";
    return pwd;
}

//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
    int i=0;
    usercommand[i++]=strtok(command,SEP);
    while(usercommand[i++]=strtok(NULL,SEP));
}

//解析命令行
void GetCommand(char* command,char* usercommand[])
{
    command[strlen(command)-1]='\0';//清理掉最后的'\0'
    CommandSplit(usercommand,command);
#ifdef DEBUG
    int i=0;
    while(usercommand[i]!=NULL)
    {
        printf("%d : %s\n",i,usercommand[i]);
        i++;
    }
#endif
}

//创建子进程,完成任务
void Execute(char* usercommand[])
{
    pid_t id=fork();
    if(id==0)
    {
        //子进程执行部分
        execvp(usercommand[0],usercommand);
        //如果子进程程序替换失败,已退出码为1的状态返回
        exit(1);
    }
    else
    {
        //父进程执行部分
        int status=0;
        //阻塞等待
        pid_t rid=waitpid(id,&status,0);
        if(rid>0)
        {
            lastcode=WEXITSTATUS(status);
        }
    }
}

void cd(char* usercommand[])
{
    chdir(usercommand[1]);
    char tmp[1024]={'\0'};
    getcwd(tmp,sizeof(tmp));
    sprintf(cwd,"PWD=%s",tmp);
    putenv(cwd);
    lastcode=0;
}   

int echo(char* usercommand[])
{
    //1.echo后面什么都没有,相当于'\n'
    if(usercommand[1]==NULL)
    {
        printf("\n");
        lastcode=0;
        return 1;
    }
    //2.echo $?  echo $PWD echo $
    char* cmd=usercommand[1];
    int len=strlen(cmd);
    if(cmd[0]=='$' && len>1)
    {
        //echo $?
        if(cmd[1]=='?')
        {
            printf("%d\n",lastcode);
            lastcode=0;
        }
        //echo $PWD
        else
        {
            char* tmp=cmd+1;
            const char* env=getenv(tmp);
            //找不到该环境变量,打印'\n',退出码依旧为0
            if(env==NULL)
            {
                printf("\n");
            }
            else
            {
                printf("%s\n",env);
            }
            lastcode=0;
        }
    }
    else
    {
        printf("%s\n",cmd);
    }
    return 1;
}

void export(char* usercommand[])
{
    //export
    if(usercommand[1]==NULL)
    {
        lastcode=0;
        return;
    }
    strcpy(env[my_index],usercommand[1]);
    putenv(env[my_index]);
    my_index++;
}

int doBuildIn(char* usercommand[])
{
    //cd
    if(strcmp(usercommand[0],"cd")==0)
    {
        if(usercommand[1]==NULL) return -1;
        cd(usercommand);
        return 1;
    }
    //echo
    else if(strcmp(usercommand[0],"echo")==0)
    {
        return echo(usercommand);
    }
    //export
    else if(strcmp(usercommand[0],"export")==0)
    {
        export(usercommand);
    }
    return 0;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

二.具体步骤

1.打印提示符

在这里插入图片描述

const char* getUsername()
{
    const char* username=getenv("USER");
    if(username==NULL) return "none";
    return username;
}

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

const char* getPwd()
{
    const char* pwd=getenv("PWD");
    if(pwd==NULL) return "none";
    return pwd;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
    }
    return 0;
}

注意:
1.因为scanf默认读取到空格或者’\n’时就会停止继续读取,
可是我们命令行中要求读取到空格也不能停止,(否则我们的指令就无法带选项了,因为选项之间是用空格来分割的)
因此我们需要fgets函数

char command[1024]={'\0'};
fgets(command,sizeof(command),stdin);

从命令行当中读取一行字符串
在这里插入图片描述
2.因为我们用户输入的时候在最后的时候一定会输入一个’\n’,因此我们需要把’\n’置为’\0’

command[strlen(command)-1]='\0';//去除我们最后输入的'\n'

把’\n’置为’\0’的操作我们放到了下一个函数当中来完成

2.解析命令行

因为有些命令带有选项:例如 “ls -a -l”
我们在进行程序替换的时候需要分别传入"ls" “-a” "-l"这几个字符串,所以需要把用户输入的字符串分割为若干个字符串存放到一个指针数组当中,可以使用strtok字符串切割函数
在这里插入图片描述

#define DEBUG 1
#define SEP " "

//分割字符串填入usercommand数组当中
//例如: "ls -a -l" 分割为"ls" "-a" "-l"
void CommandSplit(char* usercommand[],char* command)
{
    int i=0;
    usercommand[i++]=strtok(command,SEP);
    while(usercommand[i++]=strtok(NULL,SEP));
}

//解析命令行
void GetCommand(char* command,char* usercommand[])
{
    command[strlen(command)-1]='\0';//清理掉最后的'\0'
    CommandSplit(usercommand,command);
#ifdef DEBUG
    int i=0;
    while(usercommand[i]!=NULL)
    {
        printf("%d : %s\n",i,usercommand[i]);
        i++;
    }
#endif
}

我们可以使用条件编译来方便我们自由选择是否需要打印
解析命令行后的usercommand数组中的内容

3.分析是否是内建命令

1.shell对于内建名令的处理

在这里插入图片描述
下面我们就一起来实现一下
cd,echo,export这几个内建命令

2.cd命令

在这里插入图片描述
可是如果shell进行进程程序替换了,那么shell执行完之后不就没了吗?
因此shell执行内建命令时直接封装函数调用系统调用接口即可

内建命令的个数是有限的,所以shell是可以对内建命令进行穷举的

因此我们就能更好地理解内建命令了:

内建命令:不需要创建子进程执行,shell自己执行,本质就是调用系统调用接口

3.cd函数的实现

对于cd而言,我们可以调用chdir这个系统调用接口来改变当前进程的工作目录
在这里插入图片描述
同时也可以设置一个全局的char类型的数组cwd来保存当前路径
每次cd之后用cwd来记录新路径,然后通过putenv来修改环境变量PWD
让我们第一步打印的提示符中的PWD路径可以动态调整

//cwd:存放当前路径,是一个全局变量
char cwd[1024]={'\0'};

void cd(char* usercommand[])
{
	//1.chdir改变当前进程的工作目录
    chdir(usercommand[1]);
    //2.获取当前进程所在工作目录到tmp数组当中
    char tmp[1024]={'\0'};
    getcwd(tmp,sizeof(tmp));
    //3.把tmp数组中的内容格式化为"PWD=tmp数组中的内容"放到cwd数组当中
    sprintf(cwd,"PWD=%s",tmp);
    //4.导入环境变量
    putenv(cwd);
    //5.最后一次进程的退出码置为0(是为了echo $?获取最后一次进程的退出码的实现,跟cd无关)
    lastcode=0;
}  

注意:cwd数组必须是全局变量,如果cwd是局部变量,那么出了cd这个函数之后cwd数组就会销毁,其中的内容也将会消失
而我们putenv导入环境变量时其实是把cwd的地址放入了环境变量当中,
而不是拷贝一份这个cwd放入环境变量当中,因此cwd数组销毁时,对应导入的环境变量中的内容就不是原来的内容了

4.echo命令的实现

echo命令也是一个内建命令

echo 空串      打印换行
echo $ ?      打印上次进程退出时的退出码
echo $环境变量  打印环境变量
echo 字符串    打印字符串
注意:
echo $       打印$这个字符串
因此即使判断了$,还要继续判断$后面还有没有字符
//全局变量
int lastcode=0;//上一次进程退出时的退出码

int echo(char* usercommand[])
{
      //1.echo后面什么都没有,相当于'\n'
      if(usercommand[1]==NULL)
      {
          printf("\n");
          lastcode=0;
          return 1;
      }
      //2.echo $?  echo $PWD echo $
      char* cmd=usercommand[1];
      int len=strlen(cmd);
      if(cmd[0]=='$' && len>1)
      {
          //echo $?
          if(cmd[1]=='?')
          {
              printf("%d\n",lastcode);
              lastcode=0;
          }
          //echo $PWD
          else
          {
              char* tmp=cmd+1;
              const char* env=getenv(tmp);
              //找不到该环境变量,打印'\n',退出码依旧为0
              if(env==NULL)
              {
                  printf("\n");
              }
              else
              {
                  printf("%s\n",env);
              }
              lastcode=0;
          }
      }
      else
      {
          printf("%s\n",cmd);
      }
      return 1;
}

5.export命令的实现

export导入环境变量
注意:
1.刚才介绍cd函数的时候.我们说明了环境变量导入时其实是导入的对应字符串的地址
因此我们环境变量字符串必须要保证全局有效

2.由于我们可以导入很多环境变量,因此env需要是一个二维数组,同时还需要一个index下标来标记该数组当中已经导入过的环境变量

char env[1024][1024]={'\0'};
int my_index=0;

void export(char* usercommand[])
{
	//1.export后面什么都没跟,什么都不执行,直接返回即可
    if(usercommand[1]==NULL)
    {
        lastcode=0;
        return;
    }
    //2.要导入的环境变量拷贝到env数组当中
    strcpy(env[my_index],usercommand[1]);
    //3.将env数组当中的环境变量导入该进程当中
    putenv(env[my_index]);
    my_index++;
}

6.内建命令函数的实现

写好了cd,echo,export这几个函数之后,我们只需要在内建命令函数当中调用这几个函数即可

//返回值=0,说明不是内建命令
//返回值=1,说明是内建命令并且执行成功
int doBuildIn(char* usercommand[])
{
    //cd
    if(strcmp(usercommand[0],"cd")==0)
    {
        if(usercommand[1]==NULL) return -1;
        cd(usercommand);
        return 1;
    }
    //echo
    else if(strcmp(usercommand[0],"echo")==0)
    {
        return echo(usercommand);
    }
    //export
    else if(strcmp(usercommand[0],"export")==0)
    {
        export(usercommand);
    }
    return 0;
}

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

4.创建子进程通过程序替换执行命令

使用execvp这个函数来进行程序替换
带上v:因为我们的命令都是放在数组当中的
带上p:因为我们输入的都是系统指令,带上p才可以自动在环境变量中查找我们的命令
否则就要显式传入路径

注意lastcode的设置

//创建子进程,完成任务
void Execute(char* usercommand[])
{
    pid_t id=fork();
    if(id==0)
    {
        //子进程执行部分
        execvp(usercommand[0],usercommand);
        //如果子进程程序替换失败,已退出码为1的状态返回
        exit(1);
    }
    else
    {
        //父进程执行部分
        int status=0;
        //阻塞等待
        pid_t rid=waitpid(id,&status,0);
        if(rid>0)
        {
            lastcode=WEXITSTATUS(status);
        }
    }
}

5.循环往复即可

main函数加上while(1)死循环即可

int main()
{
    while(1)
    {
        //1.打印提示符信息并获取用户的指令
        printf("[%s@%s %s]$ ",getUsername(),getHostname(),getPwd());
        char command[1024]={'\0'};
        fgets(command,sizeof(command),stdin);
        char* usercommand[1024]={NULL};
        //2.解析command字符串,放入usercommand指针数组当中
        GetCommand(command,usercommand);
        //3.检测并执行内建命令,如果是内建命令并成功执行,返回1,未成功执行返回-1,不是内建返回0
        int flag=doBuildIn(usercommand);
        //返回值!=0说明是内建命令,无需执行第4步
        if(flag!=0) continue;
        //4.创建子进程,交由子进程完成任务
        Execute(usercommand);
    }
    return 0;
}

三.shell运行原理

shell内部提取用户输入的命令行进行解析
判断是否是内建命令,
1.如果是内建命令的话,shell自己通过调用自己封装的函数来执行该命令
2.如果不是内建命令,shell创建子进程,通过程序替换来然子进程执行该命令
shell进程阻塞等待回收子进程的退出状态
然后循环往复

以上就是Linux自定义shell编写的全部内容,希望能对大家有所帮助!

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

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

相关文章

RabbitMQ是做什么的

rabbitMQ是做异步通讯的。用于解决同步同讯的拓展性差&#xff0c;级联失败的问题。 异步调用方式其实就是基于消息通知的方式&#xff0c;一般包含三个角色:。 消息发送者:投递消息的人&#xff0c;就是原来的调用方 消息代理:管理、暂存、转发消息&#xff0c;你可以把它理…

060:vue中markdown编辑器mavon-editor的应用示例

第060个 查看专栏目录: VUE ------ element UI 专栏目标 在vue和element UI联合技术栈的操控下&#xff0c;本专栏提供行之有效的源代码示例和信息点介绍&#xff0c;做到灵活运用。 &#xff08;1&#xff09;提供vue2的一些基本操作&#xff1a;安装、引用&#xff0c;模板使…

Hadoop之Yarn 详细教程

1、yarn 的基本介绍和产生背景 YARN 是 Hadoop2 引入的通用的资源管理和任务调度的平台&#xff0c;可以在 YARN 上运行 MapReduce、Tez、Spark 等多种计算框架&#xff0c;只要计算框架实现了 YARN 所定义的 接口&#xff0c;都可以运行在这套通用的 Hadoop 资源管理和任务调…

兔子目标检测数据集VOC格式3900张

兔子是一类可爱的哺乳动物&#xff0c;拥有圆润的脸庞和长长的耳朵&#xff0c;身体轻盈柔软。它们通常是以温和和友善的形象出现在人们的视野中&#xff0c;因此常常成为童话故事和卡通形象中的角色。 兔子是草食性动物&#xff0c;主要以各种草本植物为食&#xff0c;包括草…

数字 IC 笔试易混淆整理

signed 扩展 比较以下4段代码&#xff0c;给出W_DATA2的结果&#xff08;十进制或16进制或二进制&#xff09;&#xff1b; wire signed [3:0] W_DATA1 4b1000; wire signed [7:0] W_DATA2; assign W_DATA2 W_DATA1; wire [3:0] W_DATA1 4b1000; wire signed [7:0] W_DA…

【Docker】添加指定用户到指定用户组

运行Docker ps命令&#xff0c;报错&#xff1a;/v1.24/containers/json": dial unix /var/run/docker.sock: connect: permission denied 创建docker用户组 安装docker时默认已经创建好 sudo groupadd docker添加用户加入docker用户组 此处以用户user为例 sudo usermo…

@Zabbix监控网络设备Trap接口UPDOWN关联告警配置

网络设备Trap接口UPDOWN关联告警配置 文章目录 网络设备Trap接口UPDOWN关联告警配置SNMPTrap描述1.监控平台监控项配置2.监控平台日志接收3.监控平台触发器配置4.监控平台触发器功能测试1&#xff09;告警触发2&#xff09;告警恢复 5.告警解析 SNMPTrap描述 在Zabbix中&#x…

作为铭文跨链赛道龙头,SoBit 有何突出之处?

跨链桥赛道将是铭文市场长期的发展的刚需 在比特币网络中&#xff0c;Ordinals铭文铸造的铭文总量已经超过了5100万枚&#xff0c;并累计费用收入超5028 BTC。同时&#xff0c;仅BRC-20叙事方向的市值&#xff0c;就已经超过了30亿美元&#xff0c;并且随着铭文资产种类与数量的…

Java商城 免 费 搭 建:鸿鹄云商实现多种商业模式,VR全景到SAAS,应有尽有!

鸿鹄云商 b2b2c产品概述 【b2b2c平台】&#xff0c;以传统电商行业为基石&#xff0c;鸿鹄云商支持“商家入驻平台自营”多运营模式&#xff0c;积极打造“全新市场&#xff0c;全新 模式”企业级b2b2c电商平台&#xff0c;致力干助力各行/互联网创业腾飞并获取更多的收益。从消…

『JavaScript』全面解析JavaScript中的防抖与节流技术及其应用场景

&#x1f4e3;读完这篇文章里你能收获到 理解防抖&#xff08;Debouncing&#xff09;和节流&#xff08;Throttling&#xff09;的概念&#xff1a;了解这两种性能优化技术如何帮助我们更有效地处理频繁触发的事件掌握防抖与节流的实现方法&#xff1a;学习如何在JavaScript中…

Spark RDD操作性能优化技巧

Apache Spark是一个强大的分布式计算框架&#xff0c;用于处理大规模数据。然而&#xff0c;在处理大数据集时&#xff0c;性能优化成为一个关键问题。本文将介绍一些Spark RDD操作的性能优化技巧&#xff0c;帮助大家充分利用Spark的潜力&#xff0c;并获得更快的处理速度。 …

autosar SJBWY 开发

第一天&#xff1a; 解决tasking 增加任意目录源文件的问题&#xff1b; 展开 Advanced 下面 Browse...选你的源文件目录就好了&#xff1b;

2023启示录丨自动驾驶这一年

图片&#xff5c;《老人与海》插图 过去的20年&#xff0c;都没有2023年如此动荡。 大模型犹如一颗原子弹投入科技圈&#xff0c;卷起万里尘沙&#xff0c;传统模式瞬间被夷为平地&#xff0c;在耀眼的白光和巨大的轰鸣声之下&#xff0c;大公司、创业者、投资人甚至是每一位观…

Vue : v-if, v-show

目录 v-show v-if v-show <!DOCTYPE html> <html lang"en"> <head><meta charset"UTF-8"><meta name"viewport" content"widthdevice-width, initial-scale1.0"><title>Document</title>…

vue3+ts 代理的使用

简单封装request.ts import axios from "axios";// 1.创建axios对象 const serviceaxios.create();// 2.请求拦截器 service.interceptors.request.use(config>{return config; },error>{Promise.reject(error); })// 3.响应拦截器 service.interceptors…

如何从RTP包的AP类型包,获取h265的PPS、SPS、VPS信息

ffmpeg播放rtp流&#xff0c;为了降低首开延迟&#xff0c;需要在SDP文件中指定PPS、SPS、VPS信息。抓包后发现wireshark无法解析AP包。需要自己进行AP包解析。RTP协议AP包格式如下&#xff1a; 根据如上信息&#xff0c;我们可以解析AP包&#xff0c;效果如下 40 01&#xff…

Linux中账号和权限管理

目录 一.用户账号和组账号&#xff1a; 1.用户账号类型&#xff1a; 2.组账号类型&#xff1a; 3.系统区别用户的方法 &#xff1a; 4.用户账号文件&#xff1a; 二.Linux中账户相关命令&#xff1a; 1.useradd&#xff1a; 2.passwd&#xff1a; 3.usermod&#xff1a…

基于Java SSM框架实现家用电器销售系统项目【项目源码+论文说明】

基于java的SSM框架实现家用电器销售系统演示 摘要 家用电器销售网站采用B/S模式&#xff0c;促进了家用电器销售的安全、质量、快捷的发展。传统的管理模式还处于手工处理阶段&#xff0c;管理效率极低&#xff0c;随着用户的不断增多&#xff0c;传统基于手工管理模式已经无法…

Linux操作系统基础 – 文件管理和操作命令

Linux操作系统基础 – 文件管理和操作命令 Linux Operating System Essentials - File Manage and Manipulation Commands By JacksonML 1. 显示当前工作目录&#xff1a;pwd命令 很多教材都把这个命令放到书本靠后的位置&#xff0c;我个人认为应当提前讲述。 每当用户打…

MySQL 核心模块揭秘 |《发刊词》

1. 为什么要写专栏&#xff1f; 我还在做业务系统研发的时候&#xff0c;有一段时间&#xff0c;系统不稳定&#xff0c;慢 SQL 很多。我们团队花了很长时间持续优化 SQL。 我们有一个表格&#xff0c;从慢查询日志里整理出了很多慢 SQL。其中一些 SQL&#xff0c;按照我们的…