自定义实现shell/bash

news2024/11/18 16:19:51

文章目录

  • 函数和进程之间的相似性
  • shell
    • 打印提示符,以及获取用户输入
    • 分割用户的输入
    • 判断是否是内建命令
    • 执行相关的命令
  • 全部代码

正文开始前给大家推荐个网站,前些天发现了一个巨牛的 人工智能学习网站, 通俗易懂,风趣幽默,忍不住分享一下给大家。[点击跳转到网站]

函数和进程之间的相似性

exec/exit就像call/return一个C程序有很多函数组成。一个函数可以调用另外一个函数,同时传递给它一些参数。被调用的函数执行一定的操作,然后返回一个值。每个函数都有他的局部变量,不同的函数通过call/return系统进行通信。这种通过参数和返回值在拥有私有数据的函数间通信的模式是结构化程序设计的基础。Linux鼓励将这种应用于程序之内的模式扩展到程序之间
在这里插入图片描述
一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。

shell

有了程序替换我们可以让一个进程来调用其他进程,所以我们想如果我们在执行程序替换的时候创建一个子进程让子进程去执行,然后我们主进程(父进程)来和用户交互,我们是不是就可以实现一个简易的shell命令行解释器。

打印提示符,以及获取用户输入

在这里插入图片描述
我们可以看到每次在运行的时候,shell都会先打印前面的一堆东西,并且光标会停留在后面等待用户输入,所以我们父进程可以先把打印以及输入这个工作搞定。
那么这一堆东西如何获取打印呢?我们可以直接写死,但是换个主机可能就不满足要求了,我们可以分析一下,前面的这一堆东西是不是可以通过环境变量来获取呢?我们可以查一下环境变量
在这里插入图片描述

在这里插入图片描述
我们可以发现前面是用户@主机名 然后加当前路径,所以我们可以通过环境变量来动态获取这些内容。
我们可以先来三个函数,一个用来获取用户名,一个用来获取当前路径,一个用来获取主机名。

char *getUsername()
{
    char *name = getenv("USER");
    if (name)
        return name;
    return "none";
}

char *getHostname()
{
    char *name = getenv("HOSTNAME");
    if (name)
        return name;
    return "none";
}

char *getCwd()
{
    char *cwd = getenv("PWD");
    if (cwd)
        return cwd;
    return "none";
}

然后我们可以定义一个缓冲区,用来保存用户的输入,然后把打印起那么的一堆东西和输入放到一个函数里,进行一下封装。

int getComment(char comment[], int num)
{
    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());

    char *ch = fgets(comment, num, stdin);
    if (ch == NULL)
        return -1;
    int n = strlen(comment);
    comment[n - 1] = '\0';
    return n - 1;
}

这里面的细节还是挺多的,不管怎么说用户一定会输入一个回车,所以我们不用担心 comment[n - 1] = ‘\0’ 会越界访问,对于返回值问题,如果用户输入失败或者没有输入了内容返回值就会是一个 <= 0的数,就没必要进行后面创建子进程等的工作,直接跳过此次循环就OK。所以我们只要传进来一个缓冲区,就可以了。这个函数就会把用户输入的内容放到缓冲区中去。

分割用户的输入

用户输入进来的诸如"ls -a -l"等等,都是一整个字符串,但是我们进程程序替换是需要将这些个子串以空格分割进行分割成若干个子串,所以我们可以把这部分在进行封装成一个函数,我们把用户输入的内容传给这个函数,让这个函数把用户输入的内容分割成若干个子串,然后传出去就可以了,分割子串我们可以用到C语言的strtok函数,还是比较简单的。

#define SEP " "
void commentSplit(char *in, char *out[])
{
    int pos = 0;
    out[pos++] = strtok(in, SEP);
    while (out[pos++] = strtok(NULL, SEP));
}

判断是否是内建命令

我们学到的命令由两部分,有一部分是普通命令也就是shell创建子进程让子进程去执行的,还有一部分就是内建命令,如(cd,export,echo等)这些命令是shell的一个函数,是需要shell自己去执行的。所以我们在创建子进程之前,需要判断一下是否是内建命令,如果是的话,就没必要创建子进程了,所以我们可以设计一个返回值来进行区分是否是内建命令。

char cwd[NUM];
char add_env[NUM][NUM];
int envi = 0;
static int lastcode = 0;

char* getHomePath()
{
    char* path = getenv("HOME");
    if(path) return path;
    return "none";
}
void cd(const char* path)
{
    char tmp[NUM];
    //改变工作目录为当前的path
    chdir(path);
    //获取当前的工作目录
    getcwd(tmp,sizeof tmp);
    sprintf(cwd,"PWD=%s",tmp);
    putenv(cwd);
}
int dobuildcom(char* argv[])
{
    if(strcmp(argv[0],"cd") == 0)
    {
        char* path;
        if(argv[1] == NULL) path = getHomePath();
        else if(strcmp(argv[1], "~") == 0) 
        {
            path = getHomePath();
        }else path = argv[1];

        cd(path);
        return 1;
    }
    else if(strcmp(argv[0],"export") == 0)
    {
        if(argv[1] == NULL) return 1;
        strcpy(add_env[envi],argv[1]);
        putenv(add_env[envi]);
        envi++;
        return 1;
    }
    else if(strcmp(argv[0],"echo") == 0)
    {
        if(argv[0] == NULL) 
        {
            printf("\n");
            return 1;
        }
        char* tmp = argv[1];
        if(tmp[0] == '$')
        {
            tmp++;
            if(strcmp(tmp,"?") == 0) 
            {
                printf("%d\n",lastcode);
            }else
            {
                char* c = getenv(tmp);
                if(c) printf("%s\n",c);
                else printf("\n");

            }
        }
        else{
            printf("%s\n",tmp);
        }
        lastcode = 0;
        return 1;
    }

    return 0;
}

我们这里就实现了3个内建命令,这里面最值的一说的就是环境变量了,所以我们在cd或者使用export添加环境时,是不能使用临时变量的,因为环境变量拥有全局属性,如果使用局部变量的话当函数执行完之后,这个空间就会还给OS,所以我们在导环境环境变量的时候需要定义全局变量,然后把需要导的环境变量拷贝到全局变量中在进行添加环境变量。返回值是1就是内建命令,否则就不是。

执行相关的命令

我们知道了程序替换这个就比较简单了,我们只需要将分割好的子串给这个函数,然后我们有了子串数组我们那选择好程序替换函数就可以了,因为我们直接就有子串的数组,所以我们可以选择execvp这个函数。

void execute(char *argv[])
{
    pid_t id = fork();
    if (id == 0)
    {
        execvp(argv[0], argv);
        exit(0);
    }
    int status = 0;
    waitpid(id, &status, 0);
    lastcode = WEXITSTATUS(status);
}

我们在执行完一个命令后,我们有一个全局变量来记录最后一条命令执行的结果,所以我们可以获取一下退出码赋值给这个变量。

全部代码

到这里我们的自定义shell的整体逻辑差不多就结束了,最后奉上全部代码。

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

#define NUM 1024
#define SEP " "

extern int putenv(char* );

char cwd[NUM];
char add_env[NUM][NUM];
int envi = 0;
static int lastcode = 0;

char *getUsername()
{
    char *name = getenv("USER");
    if (name)
        return name;
    return "none";
}

char *getHostname()
{
    char *name = getenv("HOSTNAME");
    if (name)
        return name;
    return "none";
}

char *getCwd()
{
    char *cwd = getenv("PWD");
    if (cwd)
        return cwd;
    return "none";
}

char* getHomePath()
{
    char* path = getenv("HOME");
    if(path) return path;
    return "none";
}

int getComment(char comment[], int num)
{
    printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());

    char *ch = fgets(comment, num, stdin);
    if (ch == NULL)
        return -1;
    int n = strlen(comment);
    comment[n - 1] = '\0';
    return n - 1;
}

void commentSplit(char *in, char *out[])
{
    int pos = 0;
    out[pos++] = strtok(in, SEP);
    while (out[pos++] = strtok(NULL, SEP));
}

void execute(char *argv[])
{
    pid_t id = fork();
    if (id == 0)
    {
        execvp(argv[0], argv);
        exit(0);
    }
    int status = 0;
    waitpid(id, &status, 0);
    lastcode = WEXITSTATUS(status);
}

void cd(const char* path)
{
    char tmp[NUM];
    chdir(path);
    getcwd(tmp,sizeof tmp);
    sprintf(cwd,"PWD=%s",tmp);
    putenv(cwd);
}
int dobuildcom(char* argv[])
{
    if(strcmp(argv[0],"cd") == 0)
    {
        char* path;
        if(argv[1] == NULL) path = getHomePath();
        else if(strcmp(argv[1], "~") == 0) 
        {
            path = getHomePath();
        }else path = argv[1];

        cd(path);
        return 1;
    }
    else if(strcmp(argv[0],"export") == 0)
    {
        if(argv[1] == NULL) return 1;
        strcpy(add_env[envi],argv[1]);
        putenv(add_env[envi]);
        envi++;
        return 1;
    }
    else if(strcmp(argv[0],"echo") == 0)
    {
        if(argv[0] == NULL) 
        {
            printf("\n");
            return 1;
        }
        char* tmp = argv[1];
        if(tmp[0] == '$')
        {
            tmp++;
            if(strcmp(tmp,"?") == 0) 
            {
                printf("%d\n",lastcode);
            }else
            {
                char* c = getenv(tmp);
                if(c) printf("%s\n",c);
                else printf("\n");

            }
        }
        else{
            printf("%s\n",tmp);
        }
        lastcode = 0;
        return 1;
    }

    return 0;
}
int main()
{
    while (1)
    {
        char usercomment[NUM];
        char *argv[64] = {NULL};
        int n = getComment(usercomment, sizeof usercomment);
        if(n <= 0) continue;
        // 分割字符串
        commentSplit(usercomment, argv);
        //检查并内建命令
        n = dobuildcom(argv);
        if(n) continue;
        // 执行命令
        execute(argv);
    }
    return 0;
}

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

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

相关文章

数据结构和算法:分治

分治算法 分治&#xff08;divide and conquer&#xff09;&#xff0c;全称分而治之&#xff0c;是一种非常重要且常见的算法策略。分治通常基于递归实现&#xff0c;包括“分”和“治”两个步骤。 1.分&#xff08;划分阶段&#xff09;&#xff1a;递归地将原问题分解为两个…

C语言自定义类型变量——枚举(enum)

一.枚举的定义和声明 字面意思&#xff0c;枚举就是一一列举&#xff0c;把可能的取值一一列举&#xff0c;在我们现实生活中有许多可以列举的事物&#xff0c;例如&#xff1a;一周七天&#xff0c;一年四季&#xff0c;性别&#xff0c;月份&#xff0c;三原色等等。当我们需…

vim美化配置(懒人版)

文章目录 配置vim&#xff08;懒人版&#xff09;1.搜索资源2.安装3.自定义缩进4.卸载方法 配置vim&#xff08;懒人版&#xff09; 1.搜索资源 打开gitee&#xff0c;注意到上面的搜索框 搜索 vimforcpp 进入&#xff0c;找到安装方法中的链接 2.安装 复制粘贴到linux中的命…

【JavaWeb】Day36.MySQL概述——数据库设计-DDL(三)

查询 关于表结构的查询操作&#xff0c;工作中一般都是直接基于图形化界面操作。 1.查询当前数据库所有表 2.查看指定表结构 3.查询指定表的建表语句 注意&#xff1a;23版的点击导航中的转到DDL 修改 关于表结构的修改操作&#xff0c;一般也是直接基于图形化界面操作。 添…

Linux基础篇:Linux第三方软件仓库——可以让Linux变得有趣的软件仓库

Linux第三方软件仓库——可以让Linux变得有趣的软件仓库 一、epel源介绍 EPEL&#xff08;Extra Packages for Enterprise Linux&#xff09;源是一个由Fedora项目组维护的第三方软件仓库&#xff0c;为企业级Linux发行版&#xff08;如Red Hat Enterprise Linux&#xff08;…

2024年阿里云新购、升级及续费活动大全

随着云计算技术的不断发展和普及&#xff0c;越来越多的企业和个人开始选择将业务和数据迁移到云端。作为国内领先的云计算服务提供商&#xff0c;阿里云一直致力于为用户提供更加稳定、高效和安全的云服务。2024年&#xff0c;阿里云继续推出了丰富的新购、升级及续费活动&…

读所罗门的密码笔记12_群雄逐鹿(上)

1. 国际电信规则 1.1. 美国坚持互联网自由和极少的内容限制&#xff0c;这一立场肯定会遭到许多国家的反对 1.2. 除去两个各方针锋相对、无法妥协的议题&#xff0c;比如内容限制规定&#xff0c;实际上所有国家都已在打击垃圾邮件和常见网络安全威胁方…

【苍穹外卖】sql自动补全列名

第一步要设置IDEA与MySQL的链接 右侧的Database 加号 Data Source ----MySQL 填一下用户名密码就行&#xff0c;然后测试连接。可能会有时区问题&#xff0c;他让你点什么你就点 完了之后&#xff0c;他的表好像只有bank下面的那一个&#xff0c;要把所有的表都调出来&…

SpringBoot属性配置的多种方式

✅作者简介:大家好,我是Leo,热爱Java后端开发者,一个想要与大家共同进步的男人😉😉🍎个人主页:Leo的博客💞当前专栏: 循序渐进学SpringBoot ✨特色专栏: MySQL学习 🥭本文内容:SpringBoot属性配置的多种方式 📚个人知识库: Leo知识库,欢迎大家访问 目录 …

一些Java面试题

1、 Java语言有哪些特点 1、简单易学、有丰富的类库 2、面向对象&#xff08;Java最重要的特性&#xff0c;让程序耦合度更低&#xff0c;内聚性更高&#xff09; 3、与平台无关性&#xff08;JVM是Java跨平台使用的根本&#xff09; 4、可靠安全 5、支持多线程 2、面向对象和…

软件无线电原理

常规软件无线电接收器&#xff1a; 首先&#xff0c;来自天线的射频信号被放大&#xff0c;通常射频部分利用一个调谐器将感兴趣的频段区域的信号进行放大。这个放大的射频信号被送入一个混频器。来自本振的信号也被送入混频器&#xff0c;其频率由无线电的调谐控制决定。混频器…

【.Net】DotNetty

文章目录 概述NIO和BIO、AIODotNetty适用场景DotNetty的整体架构和模块DotNetty的使用示例来源 概述 本系列文章主要讲述由微软Azure团队研发的.net的版本的netty&#xff0c;Dotnetty。所有的开发都将基于.net core 3.1版本进行开发。 Dotnetty是什么&#xff0c;原本Netty是…

UART设计

一、UART通信简介 通用异步收发器&#xff0c; 特点&#xff1a;串行、异步、全双工通信 优点&#xff1a;通信线路简单&#xff0c;传输距离远 缺点&#xff1a;传输速度慢 数据传输速率&#xff1a;波特率&#xff08;单位&#xff1a;baud&#xff0c;波特&#xff09; …

重装系统之后,电脑连网卡都没反应怎么办?

前言 有些电脑比较奇葩&#xff0c;安装完成之后会出现网卡连驱动都没有&#xff0c;这时候要安装电脑驱动可是真的烦躁。怎么下手呢&#xff1f; 如果确定电脑的网卡型号还好&#xff0c;直接找个电脑下载个对应的网卡驱动&#xff0c;用U盘复制过去就能安装。 但如果不知道…

golang 选择排序

学习笔记&#xff5e; // Author sunwenbo // 2024/4/6 21:49 package mainimport "fmt"/* 选择排序基本介绍选择式排序也属于内部排序法&#xff0c;是从预排序的数据中按指定的规则选出某一元素&#xff0c;经过和其他元素重整&#xff0c;再依原则交换位置后达到…

Java集合——Map、Set和List总结

文章目录 一、Collection二、Map、Set、List的不同三、List1、ArrayList2、LinkedList 四、Map1、HashMap2、LinkedHashMap3、TreeMap 五、Set 一、Collection Collection 的常用方法 public boolean add(E e)&#xff1a;把给定的对象添加到当前集合中 。public void clear(…

qt-C++笔记之QLabel加载图片

qt-C笔记之QLabel加载图片 —— 2024-04-06 夜 code review! 文章目录 qt-C笔记之QLabel加载图片0.文件结构1.方法一&#xff1a;把图片放在项目路径下&#xff0c;在 .pro 文件中使用 DISTFILES添加图片文件1.1.运行1.2.qt_test.pro1.3.main.cpp 2.方法二&#xff1a;不在 .pr…

hydra九头蛇

一、hydra简介 Hydra是一款非常强大的暴力破解工具&#xff0c;它是由著名的黑客组织THC开发的一款开源暴力破解工具。Hydra是一个验证性质的工具&#xff0c;主要目的是&#xff1a;展示安全研究人员从远程获取一个系统认证权限。 目前该工具支持以下协议的爆破&#xff1a; A…

快速理解JS中的原型和原型链

快速理解JS中的原型和原型链 在我们学习JS的过程中&#xff0c;我们总会接触到一些词&#xff1a;“原型”&#xff0c;“原型链”。那么今天我就来带大家来学习学习原型和原型链的知识吧&#xff01; 在开始之前&#xff0c;我们明确一下我们接下来想要学习的目标&#xff1a…

Redis从入门到精通(八)Redis实战(五)分布式锁误删与原子性问题、Redisson

↑↑↑请在文章开头处下载测试项目源代码↑↑↑ 文章目录 前言4.4 分布式锁4.4.4 分布式锁的误删问题4.4.4.1 问题说明4.4.4.2 解决方案4.4.4.3 代码实现 4.4.5 Redis分布式锁的原子性问题4.4.5.1 问题说明4.4.5.2 解决方案4.4.5.3 代码实现 4.4.6 分布式锁小结 4.5 分布式锁-R…