写一个自己的命令行解释器

news2024/11/24 2:34:26

写一个自己的命令行解释器

当我点开xshell运行服务器的时候bash就被加载到了内存中,此后我在bash上执行的所有程序都是作为bash的子进程。在bash这个进程内创建子进程,并让子进程去执行全新的代码,这不就是程序替换吗?

所以我们让子进程去执行程序替换,在我们的程序内执行命令,那我们不就是写了一个自己的命令行解释器吗?本文将带领各位读者通过实现一个简单的命令行解释器来巩固前面所学的部分知识。

一.搭建框架

当我们打开服务器的时候在最右边有提示符,包括用户名和服务器名称以及当前路径;并且支持多次输入,所以这里可以采用一个死循环,进程替换的事情由子进程来执行,所以肯定要用fork函数。

通过环境变量的学习我们知道,main函数也是有参数的,其中有一个argv指针数组,这个数组中存放的内容就是我输入的指令,argv[0]存放的是我要执行的命令的地址,后面的内容都是我指令附带的选项。所以我也可以创建一个数组存放我输入的指令(以空格为分割,将我要执行的程序和所带的选项分割开来),采用库函数strtok来切割。

至此我们可以根据需求搭建出这样的代码:

#include<stdio.h>
#incldue<string.h>
#include<unistd.h>
#include<assert.h>

#define NUM 1024
#define MAX 20

char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条

int main()
{
    while(1)
    {
        printf("用户名@服务器 当前路径:");
        fflush(stdout);//上面没带\n,这里要强制刷新
        
        //将键盘中输入的字符全部存入数组中
        char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
        assert(s);//暴力检查,s不能为空
        
        
        //分割,使用库函数stork
        myargv[0]=strtok(LineCommand," ");
        int i=1;
        while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
        
        //测试一下是否切割成功
		for(int i=0;myargv[i];i++)
        {
            printf("myargv[%d]:%s\n",i,myargv[i]);
        }
    }
    return 0;
}

可以看到命令是切割成功了,但是在输出的时候好像后面多打了一次换行。这是因为在外面输入指令的时候肯定会输入回车的。所以在存放命令的数组中的最后一个元素就是\n,如果不想这样可以在输入完指令以后将最后一个元素换成\0

当切割命令都没问题的时候,就可以开始用子进程执行进程替换来执行系统的指令了。

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

#define NUM 1024
#define MAX 20

char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条

int main()
{
    while(1)
    {
        printf("用户名@服务器 当前路径:");
        fflush(stdout);//上面没带\n,这里要强制刷新
        
        //将键盘中输入的字符全部存入数组中
        char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
        assert(s);//暴力检查,s不能为空
        
        LineCommand[strlen(LineCommand)-1]=0;//处理最后一个元素为'\n'
        
        //分割,使用库函数stork
        myargv[0]=strtok(LineCommand," ");
        int i=1;
        while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
        
        pid_t id=fork();
        assert(id!=-1);//fork失败返回-1
        if(id==0)
        {
            //子进程内部执行进程替换,我们有了数组,优先考虑使用带p的
            execvp(myargv[0],myargv);
            exit(-1);//如果程序替换失败就返回-1
    
        }
       
        //父进程要回收子进程资源
        int status=0;
        pid_t ret=waitpid(id,&status,0);//非阻塞式等待
        
    }
    return 0;
}

在这里插入图片描述

可以看到此时就已经可以执行指令了,但是这里还存在着几个问题

1.使用ls指令没有颜色区别:这是因为少了一个“–color=auto”选项的原因,我们可以对部分指令做适当的枚举来解决这个问题

2.cd ..无法回退到上级路径:这和当前进程的当前路径有关(当前路径就是这个进程的工作路径),可以通过chdir来更改

3.无法使用echo $?查询上次指令的退出码:要拿到上次的退出码我首先要保存上次的退出码,所以还要定义两个变量,此外还要通过枚举让 ? 变成输出上次的退出码而不是向屏幕中打印 ?变成输出上次的退出码而不是向屏幕中打印 ?变成输出上次的退出码而不是向屏幕中打印?

二.通过简单枚举来完善代码

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

#define NUM 1024
#define MAX 20

char LineCommand[NUM];//允许输入指令最大长度为1024
char*myargv[MAX];//执行程序+选项最多20条
int lastcode=0;//上个程序的退出码
int lastsig=0;//上个程序的退出信号

int main()
{
    while(1)
    {
        printf("用户名@服务器 当前路径:");
        fflush(stdout);//上面没带\n,这里要强制刷新
        
        //将键盘中输入的字符全部存入数组中
        char*s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//留一个位置存放\0
        assert(s);//暴力检查,s不能为空
        
        LineCommand[strlen(LineCommand)-1]=0;//处理最后一个元素为'\n'
        
        //分割,使用库函数stork
        myargv[0]=strtok(LineCommand," ");
        int i=1;
        
        //让ls选项带颜色标识
        if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
        {
            myargv[i++]="--color=auto";
        }
      
        while(myargv[i++]=strtok(NULL," "));//循环切割,先将切割后的结果赋值给myargv,再将这个值作为判断,strtok在结束时会返回空
        
       if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
        {
            if(myargv[1]!=NULL)
            {
                chdir(myargv[1]);//通过chdir系统调用,将当前的工作目录改为myargv数组下标为1的元素
                continue;//后面的语句不用再执行了,直接下一次循环
            }
        }
        
       if(myargv[0]!=NULL&&strcmp(myargv[0],"echo")==0)
        {
           if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0)
           {
				printf("%d %d\n",lastcode,lastsig);
                continue;
           }
        }
        
        pid_t id=fork();
        assert(id!=-1);//fork失败返回-1
        if(id==0)
        {
            //子进程内部执行进程替换,我们有了数组,优先考虑使用带p的
            execvp(myargv[0],myargv);
            exit(-1);//如果程序替换失败就返回-1
    
        }
       
        //父进程要回收子进程资源
        int status=0;
        pid_t ret=waitpid(id,&status,0);//非阻塞式等待
        
        lastcode=(status>>8)&0xff;
        lastsig=status&0x7f;
        
    }
    return 0;
}

在这里插入图片描述

三.实现重定向

命令行解释器是支持重定向的,但是就我们目前所写的代码来说还没有支持重定向。重定向的本质就是上层用的fd不变,在内核中更改fd对应struct file*的指向。如果不太懂可以去看看博主的基础IO:基础IO

也就是说只要使用dup2系统调用更改fd中struct file*的指向即可,当我们完善这个功能以后一个简单命令行解释器也就完成了。

追加重定向本质上也是另外一种输出重定向,所以可以将这两个放在一起写,具体实现如下:

#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/wait.h>
#include<sys/stat.h>
#include<fcntl.h>
#include<ctype.h>
#include<assert.h>
#include<errno.h>

#define NUM 1024 //定义最大输入指令大小
#define MAX 20
#define Skipspace(start) do{\
                             while(isspace(*start)) start++;\
                          } while(0)

//定义文件重定向的类型,否则后面无法区分

#define NON 0
#define APPEND 1
#define OUTPUT 2
#define INPUT 3


char LineCommand[NUM];//定义输入字符数组
char*myargv[MAX];



//设置退出结果和退出信号
int lastcode=0;
int lastsig=0;

//4-15,增加重定向功能,>输出重定向,>>追加重定向,<输入重定向
//重定向首先要分割文件名和指令,所以在标识重定向的位置要放\0

char*readfile;
int redirType=NON;


void redirect(char *commands)
{
  //从左到右开始扫描
 char * start=commands;
 char*end=commands+strlen(commands);
  while(start<end)
  {
      
    if(*start=='>')
    {
      //找到描述符,把这里换成\0
      *start='\0';
      start++;
      if(*start=='>')//说明是追加重定向,start还要向后挪动一个位置
      {
         start++;//后面可能有空格,要跳过空格
         redirType=APPEND;
      }     
      else{
        redirType=OUTPUT;
      }
      Skipspace(start);
      readfile=start;//前面定义的是以指针的方式不是没有道理的
      break;
    }
    else if(*start=='<')
    {
      //开头都是同样的处理
      *start='\0';
      ++start;

      redirType=INPUT;
      Skipspace(start);

      readfile=start;
      break;
    }
    else{
      start++;
    }
  }

}

int main()
{
  while(1)
  {

    redirType=NON;
    readfile=NULL;
    //写一个自己的shell,首先我的有提示符
      printf("用户名@服务器 当前路径:");
     fflush(stdout);

   //将用户输入的指令作为字符串存入数组中,用fgets函数获取输入的指令 我要将其切割出来
     char *s=fgets(LineCommand,sizeof(LineCommand)-1,stdin);//将stdin中输入的字符放到LineCommand中
     assert(s!=NULL);
    //清除最后一个\n
    LineCommand[strlen(LineCommand)-1]=0;

      redirect(LineCommand);
  //切割,argv存放的第一个字符串是程序
      myargv[0]=strtok(LineCommand," ");
     int i=1;
    if(myargv[0]!=NULL&&strcmp(myargv[0],"ls")==0)
    {
       myargv[i++]="--color=auto";
    }
    //
    // 在切割之前要把文件名和指令分开
    
     while(myargv[i++]=strtok(NULL," "));//循环切割

     if(myargv[0]!=NULL&&strcmp(myargv[0],"cd")==0)
     {
         //如果是cd命令,并且有输入cd到哪个路径,就将当前的工作路径改为myargv[1]
         if(myargv[1]!=NULL)
         {
           chdir(myargv[1]);

           continue;///后面的语句不用再执行了
        }
     }

#ifdef DEBUG 
     for(i=0;myargv[i];i++)
     {
       printf("myargv[%d]:%s\n",i,myargv[i]);
     }

#endif
     //创建子进程,让子进程替换
     pid_t id=fork();
     assert(id!=-1);

     if(id==0)
     {
          
       //因为指令由子进程进程替换来执行,所有重定向肯定也是交由子进程
       switch(redirType)
       {
         case NON:
           break;//不重定向
         case INPUT:
           {
             int flag=O_RDONLY;
             int fd=open(readfile,flag);
             if(fd<0)
             {
               perror("open");
               exit(errno);
             }
             dup2(fd,0);
           }
              break;
         case OUTPUT:
         case APPEND:
              {
                int flags=O_WRONLY|O_CREAT;
                if(redirType==APPEND) flags|=O_APPEND;
                else flags|=O_TRUNC;
                //先打开文件
                int fd=open(readfile,flags,0666);
                if(fd<0)
                {
                  perror("open");
                  exit(errno);
                }
               //重定向,更改标准输出
               dup2(fd,1);
              }
              break;
         default:
              //可能有错误
              printf("bug?\n");
              break;

       }


       //替换,选用带vp的来换
       execvp(myargv[0],myargv);
       exit(1);
     }

     int status=0;
     pid_t ret =  waitpid(id,&status,0);
     //父进程回收子进程,并获取退出码
    assert(ret>0);
    lastcode=(status>>8)&0xff;
    lastsig=(status)&0x7f;
     
    if(myargv[0]!=NULL&&strcmp(myargv[0],"echo")==0)
    {
      if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0)
      {
          printf("%d %d\n",lastcode,lastsig);
          continue;
      }
    }    
      
  }
 
return 0;
}

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

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

相关文章

腾讯云4核8g服务器支持多少人在线使用?

腾讯云轻量4核8G12M轻量应用服务器支持多少人同时在线&#xff1f;通用型-4核8G-180G-2000G&#xff0c;2000GB月流量&#xff0c;系统盘为180GB SSD盘&#xff0c;12M公网带宽&#xff0c;下载速度峰值为1536KB/s&#xff0c;即1.5M/秒&#xff0c;假设网站内页平均大小为60KB…

【Unity入门】17.脚本访问父子结点

【Unity入门】脚本访问父子结点 大家好&#xff0c;我是Lampard~~ 欢迎来到Unity入门系列博客&#xff0c;所学知识来自B站阿发老师~感谢 &#xff08;一&#xff09;父级节点 &#xff08;1&#xff09;访问父级节点 父子关系我们并不陌生&#xff0c;在cocos中常用node:get…

单链表的实现

链表的概念与结构 链表与我们通讯录中的顺序表是不同的&#xff0c;顺序表的空间是连续的&#xff0c;像数组一样可以通过下标访问。而链表是一种物理存储结构上非连续、非顺序的存储结构&#xff0c;数据元素的逻辑顺序是通过链表中的指针链接次序实现的。即&#xff1a;链表…

数据结构笔记:二叉树的遍历与技巧

引言 本篇是最近有遇到过的一个题目&#xff0c;关于二叉树的遍历&#xff0c;正好有一些经验与技巧&#xff0c;所以开一篇帖子记录一下。 二叉树遍历介绍 遍历是数据结构中常见的操作&#xff0c;主要是将所有元素都访问一遍。对于线性结构来说&#xff0c;遍历分为两种&a…

RecyclerView 静态布局实现过程解析:如何构建高性能的列表

作者&#xff1a;maxcion Recyclerview在日常开发中所使用的控件中绝对是顶流一般的存在&#xff0c;想嚼它这个想法一次两次了。在网上也看了很多关于Recyclerview源码解析的文章&#xff0c;大佬们写的都很深刻&#xff0c;但是对于像我们这种储备知识不足的小白读者来说&…

前端实现端到端测试(代码版)

端到端测试框架选取 playwright 、 cypress 、 selenium 对比 cypress使用 下载 cypress npm install cypress --save-dev package.json npm run cypress:open {"scripts": {"cypress:open": "cypress open"} }使用流程 入门官方文档 npm ru…

一本通 3.4.5 最小生成树

1348&#xff1a;【例4-9】城市公交网建设问题 【题目描述】 有一张城市地图&#xff0c;图中的顶点为城市&#xff0c;无向边代表两个城市间的连通关系&#xff0c;边上的权为在这两个城市之间修建高速公路的造价&#xff0c;研究后发现&#xff0c;这个地图有一个特点&…

SQL Server基础 第四章 select定制查询(select中的各种查询筛选条件)

本章主要介绍 select 语句查询数据的基本用法&#xff0c;其中包括查询指定字段信息、条件查询等。 目录 1、比较运算符、逻辑运算符 &#xff08;1&#xff09;查询phone大于500且不是单县的 &#xff08;2&#xff09;查询地址为烟台或者单县但是phone要大于666的 &#…

IMX6ull 之 HelloWorld Led点灯

一 GPIO点灯&#xff0c;嵌入式的helloworld 1 何为GPIO&#xff1f; GPIO只是一个CPU内提供的一种功能外设&#xff0c;CPU外部的I/O引脚会被赋予一种功能&#xff08;GPIO、UART、I2C等&#xff09;&#xff1b;该功能由CPU内外设提供&#xff0c;具体是什么功能由IOMUX…

刷题笔记4-22

目录 1.Java&#xff1a;(a,b)>Math.abs(a-3)-Math.abs(b-3)&#xff1b; 2.字符解释 3.C语言二维数组中a[i]表示ai的地址&#xff0c;而a[i]又可以表示为*&#xff08;ai&#xff09; 4.二维数组在传参时&#xff0c;必须给定列 5.软件开发&#xff1a;观察者模式 6.建…

shell脚本控制

shell脚本编程系列 处理信号 Linux利用信号与系统中的进程进行通信&#xff0c;通过对脚本进行编程&#xff0c;使其在收到特定信号时执行某些命令&#xff0c;从而控制shell脚本的操作。 Linux信号 shell脚本编程会遇到的最常见的Linux系统信号如下表所示&#xff1a; 在默…

【ros】6.ros激光雷达SLAM(建图定位)

百行业为先 &#xff0c;万恶懒为首。——梁启超 文章目录 :smirk:1. 激光SLAM:blush:2. 二维激光SLAM:satisfied:3. 三维激光SLAM &#x1f60f;1. 激光SLAM SLAM&#xff08;同步定位与地图构建&#xff09;是一种机器人感知技术&#xff0c;用于在未知环境中同时确定机器人…

java调用webservicer的方法

对于使用 Webservicer的方式&#xff0c;一般采用 Java API调用的方式。Webservicer是一个运行在浏览器中的客户端程序&#xff0c;它可以通过 Webservicer的接口来访问服务器上的服务。 使用 Java调用 Webservicer有两种方式&#xff1a; 下面是一个简单的例子&#xff1a; 2、…

零基础,零成本,部署一个属于你的大模型

前言 看了那么多chatGPT的文章&#xff0c;作为一名不精通算法的开发&#xff0c;也对大模型心痒痒。但想要部署自己的大模型&#xff0c;且不说没有算法相关的经验了&#xff0c;光是大模型占用的算力资源&#xff0c;手头的个人电脑其实也很难独立部署。就算使用算法压缩后的…

数据结构和算法学习记录——小习题-二叉树的遍历二叉搜索树

目录 二叉树的遍历 1-1 1-2 1-3 二叉搜索树 2-1 2-2 2-3 2-4 答案区 二叉树的遍历 1-1 假定只有四个结点A、B、C、D的二叉树&#xff0c;其前序遍历序列为ABCD&#xff0c;则下面哪个序列是不可能的中序遍历序列&#xff1f; .ABCD .ACDB .DCBA .DABC 1-2 对于…

最精简:windows环境安装tensorflow-gpu-2.10.1

Tensorflow 2.10是最后一个在本地windows上支持GPU的版本 1. 通过.whl文件方式安装2.创建anaconda虚拟环境3.安装对应的cuda与cudnn版本&#xff0c;local不必装cuda和cudnn4. 测试tensorflow gpu是否可用 1. 通过.whl文件方式安装 .whl文件的下载地址&#xff1a; tensorflow…

windows下使用vite创建vue项目

windows下使用vite创建vue项目 1 下载安装配置NodeJS1.1 下载1.2 安装1.3 配置1.4 npm镜像加速配置1.6 设置环境变量 2 Vite简单介绍3 Vite创建vue项目3.1 vite创建vue项目的命令3.2 vite创建vue项目步骤 1 下载安装配置NodeJS 1.1 下载 下载地址&#xff1a;https://nodejs.…

全注解下的SpringIoc 续2-bean的生命周期

spring中bean的生命周期 上一个小节梳理了一下Spring Boot的依赖注入的基本知识&#xff0c;今天来梳理一下spring中bean的生命周期。 下面&#xff0c;让我们一起看看bean在IOC容器中是怎么被创建和销毁的。 bean的生命周期大致分为四个部分&#xff1a; #mermaid-svg-GFXNEU…

数据分类分级 数据识别-识别日期类型数据

前面针对数据安全-数据分类分级方案设计做了分析讲解,具体内容可点击数据安全-数据分类分级方案设计,不再做赘述 上面图片是AI创作生成!如需咒语可私戳哦! 目录 前言需求日期格式代码日期类型数据对应正则表达式前言 要做数据分类分级,重要的是分类分级模版的合理性和数…

一致性 Hash 算法 及Java TreeMap 实现

1、一致性 Hash 算法原理 一致性 Hash 算法通过构建环状的 Hash 空间替线性 Hash 空间的方法解决了这个问题&#xff0c;整个 Hash 空间被构建成一个首位相接的环。 其具体的构造过程为&#xff1a; 先构造一个长度为 2^32 的一致性 Hash 环计算每个缓存服务器的 Hash 值&…