【Linux】C语言模拟实现shell命令行(程序替换原理)

目录

一、自动化构建工具(makefile)

二、输出提示符

三、获取用户输入的数据

四、将用户输入的指令字符串进行分割:

五、执行用户输入的命令

六、发现cd命令用不了(内建命令)

原因在于:

七、处理内建命令cd:

八、存在一个小问题:

八、处理内建命令export

九、获取最近一次进程的退出码

十、处理内建命令echo

十一、让ls指令输出的内容带上颜色

十二、完整代码


上一章节我们学习了程序替换,现在我们就可以通过程序替换来模拟实现shell命令行;

前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家

点击跳转到网站

一、自动化构建工具(makefile)

cpp 复制代码
myprocess:myshell.c
	gcc -o $@ $^ -g -std=c99
.PHONY:clean
clean:
	rm -f myprocess

二、输出提示符

我们在命令行终端处时,一般会有这个输出提示符:

cpp 复制代码
#include<stdio.h>
#include<stdlib.h>

//获取主机名
const char* HostName()
{
    char* hostname = getenv("HOSTNAME");
    if(hostname)return hostname;
    else return "None";
}

//获取当前登录的用户名
const char* UserName()
{
    char* username = getenv("USER");
    if(username)return username;
    else return "None";
}

//获取当前工作目录
const char* CurrentWorkDir()
{
char* currentworkdir = getenv("PWD");
    if(currentworkdir)return currentworkdir;
    else return "None";
}


int main()
{
    //输出提示符
    printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir());
    return 0;
}

三、获取用户输入的数据

cpp 复制代码
void Interactive(char out[],int size)
{
    fgets(out,SIZE,stdin);//stdin是标准输入流,意思就是从键盘获取数据保存到commandline中
    out[strlen(out)-1] = '\0';//因为fgets会读取换行符,所以这步我们去掉换行。
}

int main()
{
    //输出提示符
    printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir());

    //获取用户输入的命令
    char commandline[SIZE];
    Interactive(commandline,SIZE);
    printf("test:%s\n",commandline);

    return 0;
}

四、将用户输入的指令字符串进行分割:

cpp 复制代码
//对字符串进行分割
void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in,SEP);//对字符串commandline以空格作为分隔符进行切割,"ls -a -l"
    while(argv[i++] = strtok(NULL,SEP));//进行第二次切割时,strtok第一个参数需要传入NULL。
}


int main()
{
    //1、输出提示符
    printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir());

    //2、获取用户输入的命令
    char commandline[SIZE];
    Interactive(commandline,SIZE);
    printf("test:%s\n",commandline);

    //3、对命令行字符串进行切割
    Split(commandline);

    
    return 0;
}

五、执行用户输入的命令

执行命令我们是用程序替换的原理,去执行对应的命令,而程序替换过后,就不会再执行之后的代码,为了避免这一点,所以我们创建子进程取进行程序替换。

cpp 复制代码
void Execute()
{
    //因为程序替换后,不会在执行之后的代码,所以这里创建子进程去执行最合适
    pid_t id = fork();
    if(id == 0)
    {
        //子进程通过程序替换执行命令
        execvp(argv[0],argv);
        exit(1);
    }
    //父进程进行等待
    pid_t rid = waitpid(id,NULL,0);
    printf("run done,rid: %d\n",rid);
}

int main()
{
    //1、输出提示符
    printf("[%s@%s %s]$ ",UserName(),HostName(),CurrentWorkDir());

    //2、获取用户输入的命令
    char commandline[SIZE];
    Interactive(commandline,SIZE);
    printf("test:%s\n",commandline);

    //3、对命令行字符串进行切割
    Split(commandline);

    //4、执行分割好的命令
    Execute();
    return 0;
}

这样我们就能运行起来单次命令了,所以我们套个while循环,就能循环输入了:

cpp 复制代码
int main()
{
    while (1)
    {
        // 1、输出提示符
        printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());

        // 2、获取用户输入的命令
        char commandline[SIZE];
        Interactive(commandline, SIZE);
        printf("test:%s\n", commandline);

        // 3、对命令行字符串进行切割
        Split(commandline);

        // 4、执行分割好的命令
        Execute();
    }
    return 0;
}

六、发现cd命令用不了(内建命令)

当我们使用cd命令时,发现没有起作用,比如cd -,没有返回上一次的工作目录:

原因在于:

有些命令不应该让子进程去执行的,而是应该由shell自己去执行,就不如上述的cd命令,这种命令叫内建命令。

所以我们在执行命令之前应该要先处理内建命令

七、处理内建命令cd:

cpp 复制代码
char* Home()
{
    return getenv("HOME");
}

int BuildinCmd()
{
    int ret = 0;
    //检查是否为内建命令,是 1,否 0
    if(strcmp("cd",argv[0]) == 0)
    {
        //执行
        ret = 1;
        char* target = argv[1];
        if(!target) target = Home();//如果只输入cd,则argv[1]的值为0,则会进入if语句,默认跳转到Home()工作目录
        //通过系统调用chdir,改变当前工作目录
        chdir(target);
        //虽然具体的工作目录变了,但是命令行提示符中工作目录我们没有实时更新,所以还没有变
        //此时需要处理一下,修改PWD环境变量,这样下次循环时,命令行提示符获取的就是当前路径。
        snprintf(pwd,SIZE,"PWD=%s",target);
        putenv(pwd);
    }
    return ret;
}

int main()
{
    while (1)
    {
        // 1、输出提示符
        printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());

        // 2、获取用户输入的命令
        char commandline[SIZE];
        Interactive(commandline, SIZE);

        // 3、对命令行字符串进行切割
        Split(commandline);

        // 4、处理内建命令
        int n = BuildinCmd();
        if(n)continue;
        // 5、执行分割好的命令
        Execute();
    }
    return 0;
}

八、存在一个小问题:

问题如下图:

该现象的原因在于,用户输入指令cd ..后target字符串的内容就变成了"..",后续调用chdir,因为chdir是系统调用,内部是处理了"."和".."的,所以这里会正常执行,但后面调用snprintf函数时,pwd的值就变成了"..",这样putenv改变的环境变量的内容就变成了"..",下次循环命令行提示的地方读取到的环境变量值就为".."。

因为在这之前,我们已经用chdir函数改变了当前工作目录了,所以我们后面使用一个接口叫:getcwd(),该接口返回的就是当前工作目录,用该接口的返回传给getenv,这样环境变量就能正常修改了

cpp 复制代码
int BuildinCmd()
{
    int ret = 0;
    //检查是否为内建命令,是 1,否 0
    if(strcmp("cd",argv[0]) == 0)
    {
        //执行
        ret = 1;
        char* target = argv[1];
        if(!target) target = Home();//如果只输入cd,则argv[1]的值为0,则会进入if语句,默认跳转到Home()工作目录
        //通过系统调用chdir,改变当前工作目录
        chdir(target);
        char temp[1024];
        getcwd(temp,1024);
        //虽然具体的工作目录变了,但是命令行提示符中工作目录我们没有实时更新,所以还没有变
        //此时需要处理一下,修改PWD环境变量,这样下次循环时,命令行提示符获取的就是当前路径。
        snprintf(pwd,SIZE,"PWD=%s",temp);
        putenv(pwd);
    }
    return ret;
}

八、处理内建命令export

export是用来导入新的环境变量的,也是一个内建命令,因为只有将新环境变量导入给自己,这样才能被子进程继承下去。

直接else if 接着判断即可:

九、获取最近一次进程的退出码

十、处理内建命令echo

cpp 复制代码
 else if(strcmp("echo",argv[0]) == 0)
    {
        ret = 1;
        if(argv[1] == NULL)
        {
            printf("\n");
        }
        else{
            if(argv[1][0] == '$')//$用于查看环境变量的值
            {
                if(argv[1][1] == '?')//$?:查看进程退出码
                {
                    printf("%d\n",lastcode);
                    lastcode = 0;
                }
                else
                {
                    char* e = getenv(argv[1]+1);//echo $PWD
                    if(e) printf("%s\n",e);
                }
            }
            else{
                //如果不是以$开头,则正常打印内容
                printf("%s\n",argv[1]);
            }
        }
    }

十一、让ls指令输出的内容带上颜色

十二、完整代码

cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>

#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "

// 全局进程
char *argv[MAX_ARGC];
//工作目录
char pwd[SIZE];
//环境变量
char env[SIZE];
//进程退出信息
int lastcode = 0;

// 获取主机名
const char *HostName()
{
    char *hostname = getenv("HOSTNAME");
    if (hostname)
        return hostname;
    else
        return "None";
}

// 获取当前登录的用户名
const char *UserName()
{
    char *username = getenv("USER");
    if (username)
        return username;
    else
        return "None";
}

// 获取当前工作目录
const char *CurrentWorkDir()
{
    char *currentworkdir = getenv("PWD");
    if (currentworkdir)
        return currentworkdir;
    else
        return "None";
}

// 获取用户输入的命令
void Interactive(char out[], int size)
{
    fgets(out, SIZE, stdin);     // stdin是标准输入流,意思就是从键盘获取数据保存到commandline中
    out[strlen(out) - 1] = '\0'; // 因为fgets会读取换行符,所以这步我们去掉换行。
}

// 对字符串进行分割
void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in, SEP); // 对字符串commandline以空格作为分隔符进行切割,"ls -a -l"
    while (argv[i++] = strtok(NULL, SEP)); // 进行第二次切割时,strtok第一个参数需要传入NULL。并且最后会填入NULL
    if(strcmp(argv[0], "ls") == 0)
    {
        argv[i - 1] = "--color";//即在字符串末尾加上--color选项
        argv[i] = NULL;
    }
}

void Execute()
{
    // 因为程序替换后,不会在执行之后的代码,所以这里创建子进程去执行最合适
    pid_t id = fork();
    if (id == 0)
    {
        // 子进程通过程序替换执行命令
        execvp(argv[0], argv);
        exit(1);
    }
    // 父进程进行等待,并查看退出信息
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id)
    {
        //使用宏解析出退出码
        lastcode = WEXITSTATUS(status);
    }
   // printf("run done,rid: %d\n", rid);
}

char* Home()
{
    return getenv("HOME");
}

int BuildinCmd()
{
    int ret = 0;
    //检查是否为内建命令,是 1,否 0
    if(strcmp("cd",argv[0]) == 0)
    {
        //执行
        ret = 1;
        char* target = argv[1];
        if(!target) target = Home();//如果只输入cd,则argv[1]的值为0,则会进入if语句,默认跳转到Home()工作目录
        //通过系统调用chdir,改变当前工作目录
        chdir(target);
        char temp[1024];
        getcwd(temp,1024);
        //虽然具体的工作目录变了,但是命令行提示符中工作目录我们没有实时更新,所以还没有变
        //此时需要处理一下,修改PWD环境变量,这样下次循环时,命令行提示符获取的就是当前路径。
        snprintf(pwd,SIZE,"PWD=%s",temp);
        putenv(pwd);
    }
    else if(strcmp("export",argv[0]) == 0)
    {
        ret = 1;
        if(argv[1])
        {
            strcpy(env,argv[1]);
            putenv(env);
        }
    }
    else if(strcmp("echo",argv[0]) == 0)
    {
        ret = 1;
        if(argv[1] == NULL)
        {
            printf("\n");
        }
        else{
            if(argv[1][0] == '$')//$用于查看环境变量的值
            {
                if(argv[1][1] == '?')//$?:查看进程退出码
                {
                    printf("%d\n",lastcode);
                    lastcode = 0;
                }
                else
                {
                    char* e = getenv(argv[1]+1);//echo $PWD
                    if(e) printf("%s\n",e);
                }
            }
            else{
                //如果不是以$开头,则正常打印内容
                printf("%s\n",argv[1]);
            }
        }
    }
    return ret;
}

int main()
{
    while (1)
    {
        // 1、输出提示符
        printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());

        // 2、获取用户输入的命令
        char commandline[SIZE];
        Interactive(commandline, SIZE);

        // 3、对命令行字符串进行切割
        Split(commandline);

        // 4、处理内建命令
        int n = BuildinCmd();
        if(n)continue;
        // 5、执行分割好的命令
        Execute();
    }
    return 0;
}
相关推荐
大树8814 小时前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠14 小时前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质14 小时前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
bush414 小时前
嵌入式linux学习记录十四、术语
linux·嵌入式
载数而行52014 小时前
Linux 11 动态监控指令top
linux
小宇宙Zz14 小时前
Maven依赖冲突
java·服务器·maven
Inhand陈工15 小时前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
酣大智15 小时前
ARP代理--工作原理
运维·网络·arp·arp代理
不会C语言的男孩16 小时前
Linux 系统编程 · 第 8 章:进程基础
linux·c语言
shushangyun_16 小时前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化