【Linux】第二个小程序--简易shell

news2025/1/16 8:07:38

请看上面的shell,其本质就是一个字符串,我们知道bash本质上就是一个进程,只不过命令行就是一个输出的字符串,

我们输入的命令“ls -a -l”实际上是我们在输入行输入的字符串,所以,如果我们想要做一个简易的shell的时候,首先要输出“[ghs@hecs-406886 myshell]$”这样的字符串,然后再接收并解析写入的命令字符串,然后一条命令就可以执行了。在“[ghs@hecs-406886 myshell]$”这样一行字符串中,ghs表示用户名,hecs-406886表示当前主机的主机名,myshell表示当前所处的路径,[ @ ]$为提示符。

所以,我们也需要构建一个类似的命令行,首先,创建一个myshell.c,

在main函数中,第一步我们需要自己输出一个命令行:命令行包括用户名、主机名、当前路径, 这些内容可以从环境变量中获取,

可以通过getenv函数获取环境变量内容,我们自定义三个函数来获取上面三个环境变量:

const char* getusername()
{
    const char* name = getenv("USER");
    if(name == NULL) return "None";
    return name;
}
const char* gethostname()
{
    const char* hostname = getenv("HOSTNAME");
    if(hostname == NULL) return "None";
    return hostname;
}
const char* getcwd()
{
    const char* cwd=getenv("PWD");
    if(cwd == NULL)return "None";
    return cwd;
}

先介绍一个函数snprintf:把指定参数按照特定格式写到指定长度的内存里,

然后综合上面三个函数,封装出MakeCommandLineAndPrint()函数,制作并打印命令行:

void MakeCommandLineAndPrint()
{
    char line[SIZE];
    const char* username = GetUserName();
    const char* hostname = GetHostName();
    const char* cwd = GetCwd();
    snprintf(line,sizeof(line),"[%s@%s %s]>",username,hostname,cwd);
    printf("%s",line);
    fflush(stdout);
}

第二步就是获取用户命令字符串,输入的指令(“ls -l -a”)站在开发者的角度本质是一个字符串,我们想一下,可以用scanf获取指令吗?不可以!因为指令选项个数不定。其实,我们想按行来获取字符串,在C语言中,我们可以使用fgets函数,它可以按行从特定的文件流中获取指定内容,获取内容指向由指针s指向的缓冲区,

char *fgets(char *s, int size, FILE *stream);

定义usercommand数组用于存储字符串,然后封装GetUserCommand()函数用于获取用户命令字符串,

int GetUserCommand(char command[],size_t n)
{
    char* s = fgets(command, n ,stdin);
    if(s == NULL) return -1;
    command[strlen(command)-1] = ZERO;
    return strlen(command);
}

第三步就是命令行字符串分割,使用‘ ’(空格)作为分隔符,我们想要得到一个数组,这个数组叫做char *argv[],对于这样“ls -l -a”一个字符串,定义指针指向第一个‘l’,再定义另一个指针也指向第一个‘l’,往后遍历,遇到第一个空格把其置为‘\0’,把指向第一个‘l’的指针放到agrv数组的第一个位置,然后指针往后走,指向‘-’,再定义一个指针向后走,遇到第一个空格把其置为‘\0’,把指向第一个‘-’的指针放到agrv数组的第二个位置,依次类推。为了实现这个功能,我们使用strtok函数:

#define NUM 32
char* gArgv[NUM];
void SplitCommand(char command[],size_t n)
{
    gArgv[0] = strtok(command,SEP);
    int index = 1;
    while((gArgv[index++] = strtok(NULL,SEP)));//故意写成=,表示先赋值再判断,分割之后,strtok 
                                               //会返回NULL,刚好让gArgv最后一个元素是NULL, 
                                               //并且while判断结束
}

下一步就要执行命令,我们需要创建子进程去执行,在子进程中我们要选择使用哪一个程序替换函数,第一,由于上面分割出来的命令是不带路径的,我要执行的命令全是系统的默认路径,应该让它在环境变量中找,所以选择的函数一定带p,第二,由于我提供的是一个命令数组argv,所以一定要带v,所以,选择execvp函数,这个函数在失败时返回-1,同时会设置错误码errno。

pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{
    //child
    execvp(gArgv[0],gArgv);
    exit(errno);
}

但是,上面这个代码只能执行一次,为了能一直执行下去,我们创建一个循环:

int main()
{
    int quit = 0;
    while(!quit)
    {
         //1.我们需要自己输出一个命令行
         MakeCommandLineAndPrint();

         //2.获取用户命令字符串
         char usercommand[SIZE];
         int n = GetUserCommand(usercommand,sizeof(usercommand));
         if(n <= 0) return 1;

         //3.命令行字符串分割
         SplitCommand(usercommand,sizeof(usercommand));
         
         //n.执行命令
         ExecuteCommand();
    }
    return 0;
}

上面我们完成一个粗犷的shell,但是有一点:

我们无法使用cd ..完成路径回退,这是为什么呢?因为我们上面的程序是在子进程中进行的,cd ..是在子进程中进行的,是把子进程的路径进行了回退,但是和父进程无关,父进程没有回退,cd这样的命令应该让父进程进程回退,而不应该让子进程回退,因此,需要对这些内建命令进行单独处理,在执行命令之前,要检查命令是否是内建命令

//3.命令行字符串分割
SplitCommand(usercommand,sizeof(usercommand));
//4.检查命令是否是内建命令
n = CheckBuilding();
if(n) continue; 
void Cd()
{
   const char* path = gArgv[1];
   if(path == NULL) path = GetHome();
   //path一定存在
   chdir(path);
}

上面代码的意思是,如果“cd”,那么回退到用户家目录,否则把命令行里的路径更改为当前路径。 

int CheckBuilding()
{
    int yes = 0;
    const char* enter_cmd = gArgv[0];
    if(strcmp("cd",enter_cmd) == 0)
    {
        yes = 1;
        Cd();
    }

    return yes;
}

当时,运行上面代码后,发现命令行的当前路径提示一直不变,只有pwd里的路径才改变,导致这样的原因是没有对环境变量进行更新,需要导入环境变量,将当前的路径放到temp中,然后将cwd导入环境变量中。

char cwd[SIZE*2];
void Cd()
{
   const char* path = gArgv[1];
   if(path == NULL) path = GetHome();
   //path一定存在
   chdir(path);
   //刷新环境变量
   char temp[SIZE*2];
   getcwd(temp,sizeof(temp));
   snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
   putenv(cwd);
}

执行结果虽然正确了,但是我们只想让最后一个路径显示出来(和正常的shell一样),因此,我定义了一个宏函数:

#define SkipPath(p) do{ p += strlen(p)-1; while(*p != '/') p--; }while(0)

这个宏将穿进去的路径字符串指向最后一个/,

最后的效果如上图。

但是还有一个问题,当我们回退到根目录后,命令行不显示路径了,需要做一下特殊处理:

此外,当进程退出时,我们也想用echo看一下退出码,由于echo也是一种内建命令,因此也需要在第四步特殊判断一下:

int CheckBuilding()
{
    int yes = 0;
    const char* enter_cmd = gArgv[0];
    if(strcmp("cd",enter_cmd) == 0)
    {
        yes = 1;
        Cd();
    }
    else if(strcmp(enter_cmd,"echo") == 0 && strcmp("$?",gArgv[1]) == 0)
    {
        yes = 1;
        printf("%d\n",lastcode);
        lastcode = 0;
    }

    return yes;
}

至此,简易的shell完成。

下面附上完整代码:

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

#define ZERO '\0'
#define SIZE 512
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += strlen(p)-1; while(*p != '/') p--; }while(0)

char cwd[SIZE*2];
char* gArgv[NUM];
int lastcode = 0;

const char* GetHome()
{
    const char* home = getenv("HOME");
    if(home == NULL) return "/";
    return home;
}

const char* GetUserName()
{
    const char* name = getenv("USER");
    if(name == NULL) return "None";
    return name;
}

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

//临时
const char* GetCwd()
{
    const char* cwd=getenv("PWD");
    if(cwd == NULL)return "None";
    return cwd;
}

void MakeCommandLineAndPrint()
{
    char line[SIZE];
    const char* username = GetUserName();
    const char* hostname = GetHostName();
    const char* cwd = GetCwd();

    SkipPath(cwd);
    snprintf(line,sizeof(line),"[%s@%s %s]>",username,hostname,strlen(cwd)==1 ? "/":cwd+1);
    printf("%s",line);
    fflush(stdout);
}
int GetUserCommand(char command[],size_t n)
{

    char* s = fgets(command, n ,stdin);
    if(s == NULL) return -1;
    command[strlen(command)-1] = ZERO;
    return strlen(command);
}
void SplitCommand(char command[],size_t n)
{
    gArgv[0] = strtok(command,SEP);
    int index = 1;
    while((gArgv[index++] = strtok(NULL,SEP)));//故意写成=,表示先赋值再判断,分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL,并且while判断结束
}
void Die()
{
    exit(-1);
}

void ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) Die();
    else if(id == 0)
    {
        //child
        execvp(gArgv[0],gArgv);
        exit(errno);
    }
    else 
    {
        //father
        int status = 0;
        pid_t rid = waitpid(id,&status,0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);
        }
    }
}
void Cd()
{
   const char* path = gArgv[1];
   if(path == NULL) path = GetHome();
   //path一定存在
   chdir(path);
   //刷新环境变量
   char temp[SIZE*2];
   getcwd(temp,sizeof(temp));
   snprintf(cwd,sizeof(cwd),"PWD=%s",temp);
   putenv(cwd);
}
int CheckBuilding()
{
    int yes = 0;
    const char* enter_cmd = gArgv[0];
    if(strcmp("cd",enter_cmd) == 0)
    {
        yes = 1;
        Cd();
    }
    else if(strcmp(enter_cmd,"echo") == 0 && strcmp("$?",gArgv[1]) == 0)
    {
        yes = 1;
        printf("%d\n",lastcode);
        lastcode = 0;
    }

    return yes;
}

int main()
{
    int quit = 0;
    while(!quit)
    {
         //1.我们需要自己输出一个命令行
         MakeCommandLineAndPrint();

         //2.获取用户命令字符串
         char usercommand[SIZE];
         int n = GetUserCommand(usercommand,sizeof(usercommand));
         if(n <= 0) return 1;

         //3.命令行字符串分割
         SplitCommand(usercommand,sizeof(usercommand));
         //4.检查命令是否是内建命令
         n = CheckBuilding();
         if(n) continue; 
         //5.执行命令
         ExecuteCommand();
    }
    return 0;
}

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

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

相关文章

vscode开发ESP32问题记录

vscode 开发ESP32问题记录 1. 解决vscode中的波浪线警告 1. 解决vscode中的波浪线警告 参考链接&#xff1a;https://blog.csdn.net/fucingman/article/details/134404485 首先可以通过vscode 中的IDF插件生成模板工程&#xff0c;这样会自动创建.vscode文件夹中的一些json配…

Jackson @JsonUnwrapped注解扁平化 序列化反序列化数据

参考资料 Jackson 2.x 系列【7】注解大全篇三JsonUnwrapped 以扁平的数据结构序列化/反序列化属性Jackson扁平化处理对象 目录 一. 前期准备1.1 前端1.2 实体类1.3 Controller层 二. 扁平化序列反序列化数据2.1 序列化数据2.2 反序列化数据 三. 前缀后缀处理属性同名四. Map数…

RabbitMQ3.7.8集群分区(脑裂现象)模拟及恢复处置全场景测试

测试环境准备: MQ服务器集群地址&#xff0c;版本号为3.7.8&#xff1a; 管理控制台地址:http://173.101.4.6:15672/#/queues 集群状态 rabbitmqctl cluster_status 集群操作相关命令: 创建一个RabbitMQ集群涉及到如下步骤&#xff1a; 安装RabbitMQ&#xff1a; 在每台要在集…

【Linux】Ubuntu 文件权限管理

Linux 系统对文件的权限有着严格的控制&#xff0c;用于如果相对某个文件执行某种操作&#xff0c;必须具有对应的权限方可执行成功&#xff0c;这也是Linux有别于Windows的机制&#xff0c;也是基于这个权限机制&#xff0c;Linux可以有效防止病毒自我运行。因为运行的条件是必…

软件架构复用

1.软件架构复用的定义及分类 软件产品线是指一组软件密集型系统&#xff0c;它们共享一个公共的、可管理的特性集&#xff0c;满足某个特定市场或任务的具体需要&#xff0c;是以规定的方式用公共的核心资产集成开发出来的。即围绕核心资产库进行管理、复用、集成新的系统。核心…

【随笔】Git 高级篇 -- 相对引用2(十三)

&#x1f48c; 所属专栏&#xff1a;【Git】 &#x1f600; 作  者&#xff1a;我是夜阑的狗&#x1f436; &#x1f680; 个人简介&#xff1a;一个正在努力学技术的CV工程师&#xff0c;专注基础和实战分享 &#xff0c;欢迎咨询&#xff01; &#x1f496; 欢迎大…

HTML:框架

案例&#xff1a; <frameset cols"5%,*" ><frame src"left_frame.html"><frame src"right_frame.html"> </frameset> 一、<frameset>标签 <frameset>标签&#xff1a;称为框架标记&#xff0c;将一个HTML…

Linux 学习之路 - 进程篇 - PCB介绍1-标识符

目录 一、基础的命令 <1> ps axj 命令 <2> top 命令 <3> proc 目录 二、进程的标识符 <1>范围 <2>如何获取标识符 <3>bash进程 三、创建进程 一、基础的命令 前面介绍了那么多&#xff0c;但是我们没有观察到进程相关状态&#x…

什么是智慧公厕?智慧旅游下的智慧公厕功能和特点

智慧旅游下的智慧公厕功能和特点&#xff1f;智慧旅游是景区、公园、游乐场、文化场馆等领域的一种信息化解决方案&#xff0c;智慧公厕是智慧旅游极为重要的一部分&#xff0c;能大大提升游客满意度。智慧公厕采用物联网、互联网、大数据、云计算等技术&#xff0c;实现旅游景…

深入浅出 -- 系统架构之微服务架构选型参考图

技术选型架构图 是一个用于展示项目中所采用的各种技术和组件之间关系的图表。 它通常包括以下几个部分&#xff1a; 1. 项目名称和描述&#xff1a;简要介绍项目的背景和目标。 2. 技术栈&#xff1a;列出项目中使用的主要技术和工具&#xff0c;如编程语言、框架、数据库…

Unity开发一个FPS游戏之三

在前面的两篇博客中&#xff0c;我已实现了一个FPS游戏的大部分功能&#xff0c;包括了第一人称的主角运动控制&#xff0c;武器射击以及敌人的智能行为。这里我将继续完善这个游戏&#xff0c;包括以下几个方面&#xff1a; 增加一个真实的游戏场景&#xff0c;模拟一个废弃的…

[C#]OpenCvSharp利用MatchTemplate实现多目标匹配

【效果展示】 原图 模板图 匹配结果&#xff1a; 【实现部分代码】 using System; using System.Collections.Generic; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Threading.Tasks; using…

Flutter仿Boss-4.短信验证码界面

效果 简述 在移动应用开发中&#xff0c;处理短信验证码是确保用户身份验证和安全性的重要步骤。本文将介绍如何使用Flutter构建一个短信验证码界面&#xff0c;让用户输入通过短信发送到他们手机的四位验证码。 依赖项 在这个项目中&#xff0c;我们将使用以下依赖项&#…

关于Tomcat双击startup.bat 闪退的解决⽅法

详解Tomcat双击startup.bat 闪退的解决⽅法 作为⼀个刚学习Tomcat的程序猿来说&#xff0c;这是会经常出现的错误。 1.环境变量问题 1.1 ⾸先需要确认java环境是否配置正确&#xff0c;jdk是否安装正确 winR打开cmd&#xff0c;输⼊java 或者 javac 出现下图所⽰就说明jdk配置正…

单元测试 mockito(二)

1.返回指定值 2.void返回值指定插桩 3.插桩的两种方式 when(obj.someMethod()).thenXxx():其中obj可以是mock对象 doXxx().wien(obj).someMethod():其中obj可以是mock/spy对象 spy对象在没有插桩时是调用真实方法的,写在when中会导致先执行一次原方法,达不到mock的目的&#x…

走进车厂 | 移远通信以前沿车载技术,照亮智能网联汽车产业创新发展之路

无钥匙自动解锁方便快捷、实时路况导航精准高效、语音指令轻松控制车辆、车载娱乐系统丰富多样……随着智能化、数字化浪潮的不断推进&#xff0c;现如今的汽车出行焕然一新。 正如我们所见&#xff0c;汽车产业正在经历前所未有的变革。物联网、车联网等前沿技术的发展和应用&…

GDAL源码剖析(九)之GDAL体系架构

GDAL源码剖析&#xff08;九&#xff09;之GDAL体系架构_gdal 源码-CSDN博客 在GDAL库中包含栅格数据的读写&#xff0c;矢量数据的读写&#xff0c;以及栅格和矢量数据的相关算法。下面主要对GDAL中栅格数据和矢量数据的体系架构做一个简单的说明。本人英文很烂&#xff0c;有…

力扣热题100_链表_21_合并两个有序链表

文章目录 题目链接解题思路解题代码 题目链接 21. 合并两个有序链表 将两个升序链表合并为一个新的 升序 链表并返回。新链表是通过拼接给定的两个链表的所有节点组成的。 示例 1&#xff1a; 输入&#xff1a;l1 [1,2,4], l2 [1,3,4] 输出&#xff1a;[1,1,2,3,4,4] 示例…

NIUSHOP完美运营版商城 虚拟商品全功能商城 全能商城小程序 智慧商城系统 全品类百货商城

完美运营版商城/拼团/团购/秒杀/积分/砍价/实物商品/虚拟商品等全功能商城 干干净净 没有一丝多余收据 还没过手其他站 还没乱七八走的广告和后门 后台可以自由拖曳修改前端UI页面 还支持虚拟商品自动发货等功能 挺不错的一套源码 前端UNIAPP 后端PHP 一键部署版本 源码免费…

【opencv】教程代码 —TrackingMotion 角点检测

角点检测执行角点检测并在可能的角点周围画一个圆对图像中的角点位置进行检测和细化Shi-Tomasi方法检测图像角点 1. cornerDetector_Demo.cpp 角点检测 /*** function cornerDetector_Demo.cpp* brief Demo code for detecting corners using OpenCV built-in functions* 使用 …