「Linux」使用C语言制作简易Shell

在这里插入图片描述

💻文章目录

  • 📄前言
  • 简易shell实现
    • shell的概念
    • 系统环境变量
    • shell的结构定义
    • 内建命令
    • 完整代码
  • 📓总结


📄前言

对于很多学习后端的同学来讲,学习了C语言,发现除了能写出那个经典的“hello world”以外,其他什么都做不了,如果你在烦恼着这些事的话,不妨来学习下如何实现Linux中的shell吧,如此不仅能提高你C语言功力,也能增进你对系统的理解。

文章知识点要求

  • 系统环境变量
  • 多进程编程(fork函数)
  • 程序替换
  • 程序等待

如果你还没有学习过这些知识,可以我的以前写的文章中学习。

简易shell实现

shell的概念

shell是一种作为命令解释器的应用程序,用于用户与操作系统之间的交互。因为系统内核的指令过于复杂,而且如果让用户直接执行,会轻易使系统出现错误,而shell就是为了这种情况而诞生的。

在这里插入图片描述

系统环境变量

在写代码之前我们还得了解一下系统的环境变量该如何在C语言中获取。我们有两种方式获取系统中的环境变量,用getenv函数来获取某一个环境变量的值,或者使用全局变量environ,这是一个指向了环境变量表的指针。

如果需要需要修改或增加环境变量,则可以使用putenv。

  • getenv(const char* name ) -> char* :参数为环境变量的名字,返回名字对于的内容。
  • putenv(char* string) -> int :参数为需要新增的环境变量.
  • char** environ ; 包含在unistd.h,可以通过便利来遍历所有环境变量

使用实例

#include <stdlib.h> 
#include <stdio.h>
#include <string.h>

int main()
{
    // putenv 
    char ptr[] = "PATH=/usr/bin;/home;/tmp;";
    int n = putenv(ptr);
    for(int i = 0; environ[i]; ++i)
    {
        printf("%s\n", environ[i]);
    }
    printf("%s\n", getenv("PATH"));
    return 0;
}

shell的结构定义

总所周知,shell是无时无刻都在运行着的,并且如果我们在shell中运行一个程序错误了,一般也不会波及到shell。那么,我们可以通过让父进程来检查输入命令,子进程来实现用户指令的方式来实现。

#include <cstdlib>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdio.h>
#include <stdlib.h>

#define NUM 1024	
#define SIZE 64
#define SEP " "		//需要切割的字符

const char* getCwd()	//获取当前路径
{
    const char* str = getenv("PWD");
    if(!str)    
        return "none";

    int n = strlen(str) - 1;
    while (n) 
    {
        if(str[n] == '/')
            break;
        --n;
    }

    return str+n+1;	//返回当前文件夹的名字
}

void commandSplit(char* usercommand, char** argv)	//切割字符串
{
    int n = 0;
    argv[n++] = strtok(usercommand, SEP);	
    while(argv[n++] = strtok(NULL, SEP));		//继续切割
}

int execute(char** argv)		//执行命令
{
    pid_t id = fork();
    if(id == -1)    
        return -1;
    else if(id == 0)
    {
        execvp(argv[0], argv);	//子进程执行命令。
        exit(1);
    }
    else 
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);    //阻塞等待
    }
    return 0;
}
    
int getUserCommand(char* usercommand, size_t n)
{
    printf("> %s:", getCwd());
    char* cmd = fgets(usercommand, n, stdin);
    if(!cmd)    
        return -1;

    n = strlen(cmd);
    usercommand[n-1] = '\0';

    return n;
}

int main()
{
    while (1) 
    {
        char usercommand[NUM];	
        char* argv[SIZE];
        int n = getUserCommand(usercommand, sizeof(usercommand));
        if(n <= 0) continue;	//输入错误
        commandSplit(usercommand, argv);	//切割
        execute(argv);		//执行
    }
}

在这里插入图片描述

我们可以看到程序可以正常的执行ls、pwd等命令,但如果使用cd echo 等命令则出现了异常,而cd、echo等命令就是内建命令。

内建命令

内建命令是类似于shell自己执行的命令,类似与内部函数的存在。如果要制作shell,必须得要完善一下内建命令。

char cwd[1024];	//存储当前的目录位置
int lastRet = 0;	//上一次子进程完成后的返回码

int cd(const char* path)
{//更改当前的PWD环境变量
    chdir(path);	//更改当前的工作目录
    char temp[512];
    getcwd(temp, sizeof(temp));
    sprintf(cwd, "PWD=%s", temp);		
    return putenv(cwd);		//更新env中的变量
}

int doBuildin(char* argv[])
{
    if(strcmp(argv[0], "cd") == 0)	
    {
        char* path = argv[1];
        if(!path)
            path = getenv("HOME");
        
        return cd(path);
    }
    else if(strcmp(argv[0], "export") == 0)
    {
        if(argv[1] == NULL) 
            return 0;
        /*需要注意putenv函数在enval释放后,新增的变量也会同时消失。*/
        char* enval = (char*)malloc(strlen(argv[1])+1);
        strcpy(enval, argv[1]);

        return putenv(enval);
    }
    else if(strcmp(argv[0], "echo") == 0)
    {
        if(*argv[1] == '$' && strlen(argv[1]) > 1)	//参数为变量的情况
        {
            char* val = argv[1] + 1;
            if(strcmp(val, "?") == 0)	//获取上一次执行的返回码
            {
                printf("%d\n", lastRet);
                lastRet = 0;
            }
            else 
            {
                const char* enval = getenv(val);	//获取环境变量
                if(!enval)  
                    printf("\n");
                else 
                    printf("%s\n", enval);
            }
            return 0;
        }
        else  	//非变量
        {
            printf("%s\n", argv[1]);
            return 0;
        }
    }
    return 1;
}   

运行结果

在这里插入图片描述

完整代码

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


#define NUM 1024	
#define SIZE 64
#define SEP " "		//需要切割的字符

char cwd[1024];		//存储当前工作目录
int lastRet = 0;		//存储上一次指令的执行结果

const char* getCwd()	//获取当前路径	
{
    const char* str = getenv("PWD");
    if(strcmp(str, getenv("HOME")) == 0)
        return "~";		//家目录的情况返回"~";

    int n = strlen(str) - 1;		//获取长度
    while (n) 
    {
        if(str[n] == '/')
            break;
        --n;
    }
        
    return n == 0 ? str : str + n + 1;	//返回当前文件夹的名字
}

void commandSplit(char* usercommand, char** argv)	//切割字符串
{
    int n = 0;
    argv[n++] = strtok(usercommand, SEP);	
    while(argv[n++] = strtok(NULL, SEP));		//继续切割
}

int execute(char** argv)		//执行命令
{
    pid_t id = fork();
    if(id == -1)    
        return -1;
    else if(id == 0)
    {
        execvp(argv[0], argv);	//子进程执行命令。
        exit(127);
    }
    else 
    {
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);    //阻塞等待
        lastRet = WEXITSTATUS(status);
    }
    return 0;
}


int cd(const char* path)
{
    chdir(path);	//改变工作目录
    char temp[512];
    getcwd(temp, sizeof(temp));	//获取当前目录
    sprintf(cwd, "PWD=%s", temp);	//打印当前目录到PWD
    return putenv(cwd);
}

int doBuildin(char* argv[])		//执行内建命令
{
    if(strcmp(argv[0], "cd") == 0)	//对比字符串,为0即相同
    {
        char* path = argv[1];
        if(!path)
            path = getenv("HOME");
        
        return cd(path);
    }
    else if(strcmp(argv[0], "export") == 0)
    {
        if(argv[1] == NULL) 
            return 0;
        /*注意:putenv在参数释放后,新增的变量内容也会随着消失,
        所以这里使用了动态内存开辟*/
        char* enval = (char*)malloc(strlen(argv[1])+1);
        strcpy(enval, argv[1]);

        return putenv(enval);
    }
    else if(strcmp(argv[0], "echo") == 0)	
    {
        if(!argv[1])	//空参数
        {
            return 0;
        }
        else if(*argv[1] == '$' && strlen(argv[1]) > 1)		//为参数是变量
        {
            char* val = argv[1] + 1;
            if(strcmp(val, "?") == 0)			//返回上一次指令的返回码
            {
                printf("%d\n", lastRet);
                lastRet = 0;
            }
            else 	//参数不是变量
            {
                const char* enval = getenv(val);
                if(!enval)  
                    printf("\n");
                else 
                    printf("%s\n", enval);
            }
            return 0;
        }
        else  
        {
            printf("%s\n", argv[1]);
            return 0;
        }
    }
    return 1;
}   
    
int getUserCommand(char* usercommand, size_t n)
{
    printf("> %s:", getCwd());
    char* cmd = fgets(usercommand, n, stdin);
    if(!cmd)    
        return -1;

    n = strlen(cmd);
    usercommand[n-1] = '\0';

    return n;
}

int main()
{
    while (1) 
    {
        char usercommand[NUM];	
        char* argv[SIZE];
        int n = getUserCommand(usercommand, sizeof(usercommand));	
        if(n <= 0) continue;	//输入错误
        commandSplit(usercommand, argv);	//切割

        n = doBuildin(argv);		//执行内建指令
        if(!n)  continue;			//执行内建执行后

        execute(argv);		//执行指令
    }
}

📓总结

📜博客主页:主页
📫我的专栏:C++
📱我的github:github

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

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

相关文章

43.0BaseDao抽取dao公共父类

43.1. 回顾 1. 把数据库表中查询的结果封装到一个实体类中。 命名规则:类名和表名一致 类中属性和表的字段对应。 表中的一条记录对应实体的一个对象 多条记录→集合 43.2. 正文 目录 43.1. 回顾 43.2. 正文 43.3. 抽取dao公共父类。 43.4. 引入数据源 43.3. 抽取dao公共…

文件夹重命名技巧:用关键词替换文件夹名称指定内容的右侧文字

在日常生活中&#xff0c;经常要管理大量的文件夹&#xff0c;这时候掌握一些文件夹重命名的技巧就非常实用。例如文件夹重命名时&#xff0c;经常要将一些通用的文字替换成其他关键词&#xff0c;以便更好地标识和分类文件夹。而用关键词替换文件夹名称指定内容的右侧文字&…

【论文阅读】1 SkyChain:一个深度强化学习的动态区块链分片系统

SkyChain 一、文献简介二、引言及重要信息2.1 研究背景2.2 研究目的和意义2.3 文献的创新点 三、研究内容3.1模型3.2自适应分类账协议3.2.1状态块创建3.2.2合并过程3.2.3拆分过程 3.3评价框架3.3.1性能3.3.1.1共识延迟3.3.1.2重新分片延迟3.3.1.3处理事务数3.3.1.4 约束 3.3.2 …

使用RobotFramework编写BDD代码

背景 行为驱动开发&#xff08;Behavior Driven Development&#xff09;即BDD&#xff0c;是一种敏捷开发方法&#xff0c;通常应用在自动化测试中&#xff0c;或者也可称为行为驱动测试。通过使用自然描述语言确定自动化脚本&#xff0c;通过这种方式&#xff0c;能够大大促…

EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测

EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测 目录 EI级 | Matlab实现TCN-BiLSTM-Multihead-Attention多头注意力机制多变量时间序列预测预测效果基本介绍程序设计参考资料 预测效果 基本介绍 1.【EI级】Matlab实现TCN-BiLSTM-Multihead-…

Openai通用特定领域的智能语音小助手

无穷尽的Q&A 钉钉...钉钉... 双双同学刚到工位,报销答疑群的消息就万马纷沓而来。她只能咧嘴无奈的摇摇头。水都还没有喝一口就开始“人工智能”的去回复。原本很阳光心情开始蒙上一层薄薄阴影。在这无休无止的Q&A中&#xff0c;就算你对工作有磐石一般强硬&#xff0…

XXL-Job详解(一):组件架构

目录 XXL-Job特性系统组成架构图调度模块剖析任务 “运行模式” 剖析执行器 XXL-Job XXL-JOB是一个分布式任务调度平台&#xff0c;其核心设计目标是开发迅速、学习简单、轻量级、易扩展。现已开放源代码并接入多家公司线上产品线&#xff0c;开箱即用。 特性 1、简单&#…

探究Kafka原理-6.CAP理论实践

&#x1f44f;作者简介&#xff1a;大家好&#xff0c;我是爱吃芝士的土豆倪&#xff0c;24届校招生Java选手&#xff0c;很高兴认识大家&#x1f4d5;系列专栏&#xff1a;Spring源码、JUC源码、Kafka原理&#x1f525;如果感觉博主的文章还不错的话&#xff0c;请&#x1f44…

解决keil右键Go To Definition跳转不过去的问题

解决&#xff1a; 在魔法棒中如图所示打上√

Pycharm新手开发指南

文章目录 前言一、常用功能介绍二、常用高效pycharm使用方法关于Python技术储备一、Python所有方向的学习路线二、Python基础学习视频三、精品Python学习书籍四、Python工具包项目源码合集①Python工具包②Python实战案例③Python小游戏源码五、面试资料六、Python兼职渠道 前言…

京东秒杀之秒杀实现

1 登录判断 用户在未登录状态下可以查看商品列别以及秒杀商品详情&#xff0c;但不可以在未登录状态进行秒杀商品的操作&#xff0c;当用户点击开始秒杀时&#xff0c;进行登陆验证 <!DOCTYPE html> <head><title>商品详情</title><meta http-eq…

习题补充整理

目录 一、自己封装response 二、在响应头中放数据 HttpResponse redirect ​编辑 render JsonResponse 三、函数和方法区别 ----》绑定方法区别 四、上传图片和开启media访问 五、页面静态化(解决访问率高的问题) 一、自己封装response 第一步&#xff0c;在app01下…

使用vue脚手架创建vue项目

Vue是一个流行的前端框架&#xff0c;可以用简洁的语法和组件化的思想开发单页面应用。Vue脚手架是一个官方提供的命令行工具&#xff0c;它可以帮你快速搭建和配置vue项目的基本结构和依赖。 本文介绍如何使用vue脚手架创建一个vue2项目&#xff0c;并选择一些常用的功能和插件…

【JavaFX漏扫开发基础】JavaFx项目基础

文章目录 前言一、创建项目二、运行程序三、application的启动方式和生命周期四、生命周期前言 创建第一个javafx项目,用Idea编辑器来创建。 提示:以下是本篇文章正文内容,下面案例可供参考 一、创建项目 初创项目,第一个javafx的窗口。 依次点击,新建项目 点击Jaca…

字节的“游戏心脏”,真的停止跳动了吗?

字节跳动在游戏行业的宏大叙事&#xff0c;终究变成了一场游戏一场梦。 近日&#xff0c;朝夕光年首次公开回应此前的裁员传闻&#xff0c;明确表示会有业务方向和组织调整&#xff0c;游戏业务将迎来大规模收缩&#xff0c;未来会更加聚焦部分创新型游戏及相关技术的探索。 …

spring RedisTemplate RedisLockRegistry opsForXxx 基本使用总结以及介绍

一、基本介绍 RedisTemplate 为 spring 对 redis 操作的高度封装&#xff0c;基本已经满足所有使用场景。 若存在其他拓展使用我们可以自行封装工具类对基本操作进行组装。 RedisLockRegistry 对 redis 锁的一些封装 二、不同环境下依赖以及基本配置 2.1 spring-boot 下依赖…

MySQL之 InnoDB逻辑存储结构

InnoDB逻辑存储结构 InnoDB将所有数据都存放在表空间中&#xff0c;表空间又由段&#xff08;segment&#xff09;、区&#xff08;extent&#xff09;、页&#xff08;page&#xff09;组成。InnoDB存储引擎的逻辑存储结构大致如下图。下面我们就一个个来看看。 页&#xff08…

WPF实战项目十八(客户端):添加新增、查询、编辑功能

1、ToDoView.xmal添加引用&#xff0c;添加微软的行为类 xmlns:i"http://schemas.microsoft.com/xaml/behaviors" 2、给项目添加行为 <i:Interaction.Triggers><i:EventTrigger EventName"MouseLeftButtonUp"><i:InvokeCommandAction Com…

【漏洞复现】熊海cms 存在sql注入 附poc

漏洞描述 熊海CMS 是由熊海开发的一款可广泛应用于个人博客,个人网站,企业网站的一套网站综合管理系统。 其采用前后端整合设计思路,php,Apache,mysql,前端使用Bootstrap和少许jquery前端框架开发; 网站样式设计简洁大方,整体功能点并不多,但功能正好够用;拥有一个…

【计网 面向连接的传输TCP】 中科大笔记 (十 二)

目录 0 引言1 TCP 的特性1.1 拓展&#xff1a;全双工、单工、半双工通信 2 TCP报文段结构3 TCP如何实现RDT4 TCP 流量控制4.1 题外话&#xff1a;算法感悟 5 TCP连接3次握手、断开连接4次握手5.1 连接5.2 断开连接 6 拥塞控制6.1 拥塞控制原理6.2 TCP拥塞控制 &#x1f64b;‍♂…