「Linux」使用C语言制作简易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

  • 8
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值