自定义实现shell/bash

文章目录

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

函数和进程之间的相似性

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

一个C程序可以fork/exec另一个程序,并传给它一些参数。这个被调用的程序执行一定的操作,然后通过exit(n)来返回值。调用它的进程可以通过wait(&ret)来获取exit的返回值。

shell

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

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

我们可以看到每次在运行的时候,shell都会先打印前面的一堆东西,并且光标会停留在后面等待用户输入,所以我们父进程可以先把打印以及输入这个工作搞定。

那么这一堆东西如何获取打印呢?我们可以直接写死,但是换个主机可能就不满足要求了,我们可以分析一下,前面的这一堆东西是不是可以通过环境变量来获取呢?我们可以查一下环境变量

我们可以发现前面是用户@主机名 然后加当前路径,所以我们可以通过环境变量来动态获取这些内容。

我们可以先来三个函数,一个用来获取用户名,一个用来获取当前路径,一个用来获取主机名。

c 复制代码
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";
}

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

c 复制代码
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函数,还是比较简单的。

c 复制代码
#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自己去执行的。所以我们在创建子进程之前,需要判断一下是否是内建命令,如果是的话,就没必要创建子进程了,所以我们可以设计一个返回值来进行区分是否是内建命令。

c 复制代码
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这个函数。

c 复制代码
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的整体逻辑差不多就结束了,最后奉上全部代码。

c 复制代码
#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;
}
相关推荐
远望清一色16 分钟前
基于MATLAB的实现垃圾分类Matlab源码
开发语言·matlab
confiself25 分钟前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言
XiaoLeisj37 分钟前
【JavaEE初阶 — 多线程】Thread类的方法&线程生命周期
java·开发语言·java-ee
杜杜的man40 分钟前
【go从零单排】go中的结构体struct和method
开发语言·后端·golang
幼儿园老大*41 分钟前
走进 Go 语言基础语法
开发语言·后端·学习·golang·go
半桶水专家42 分钟前
go语言中package详解
开发语言·golang·xcode
llllinuuu43 分钟前
Go语言结构体、方法与接口
开发语言·后端·golang
cookies_s_s43 分钟前
Golang--协程和管道
开发语言·后端·golang
王大锤43911 小时前
golang通用后台管理系统07(后台与若依前端对接)
开发语言·前端·golang
为什么这亚子1 小时前
九、Go语言快速入门之map
运维·开发语言·后端·算法·云原生·golang·云计算