【Linux】打造你自己的 Linux Shell:编写简易 Shell 的入门教程

举报
Yui_ 发表于 2024/12/07 16:08:24 2024/12/07
【摘要】 【Linux】打造你自己的 Linux Shell:编写简易 Shell 的入门教程

Shell 是一种提供用户与操作系统交互的命令行解释器,它接受用户的命令并调用操作系统的功能来执行这些命令。Shell 既可以作为一种交互式的命令行工具,又可以作为编写和运行脚本的编程环境。广泛使用于 Unix 和 Linux 系统中,Shell 也在其他操作系统中有类似的实现。

为了实现这么一个简易版本的自定义shell我们需要的知识有进程控制,进程等待,进程程序替换。学完这些我们就能给实现一个自己的简易shell。这些前置知识可翻阅我的往期文章。

1.准备阶段

在准备阶段我们就需要把下面的代码都写上,至于为什么在后续的代码会讲解。

1.2 头文件

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

这些都是必要的头文件,后续的函数都会用到。

1.2 提供环境变量的函数

使用const是因为,这些字符串都是只读的,不需要修改。

char* HOME()
{
    char* home = getenv("HOME");
    if(home == NULL) return "None";
    else return home;
}

const char* USER()
{
    char* user = getenv("USER");
    if(user == NULL) return "None";
    else return user;
}

const char* HOSTNAME()
{
    char* hostname = getenv("HOSTNAME");
    if(hostname == NULL) return "None";
    else return hostname;
}

const char* PWD()
{
    char* pwd = getenv("PWD");
    if(pwd == NULL) return "None";
    else return pwd;
}

1.3 标识常量与全局变量

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

char* argv[SIZE];
char env[SIZE];
char pwd[SIZE];
int lastcode = 0;

2.实现shell

为了实现一个这么一个简易shell,首先我们先来观察它的操作页面。

观察这张图片我们可以ubuntu@VM-20-9-ubuntu:~/myShell$这段信息那么它们分别是什么意思呢?从左到右:
ubuntu是用户名,VM-20-9-ubuntu是主机名,~/myShell是当前路径。这些信息都可以在环境变量中找到。

echo $USER
echo $HOSTNAME
echo $PWD
ubuntu@VM-20-9-ubuntu:~/myShell$ echo $USER
ubuntu
ubuntu@VM-20-9-ubuntu:~/myShell$ echo $PWD
/home/ubuntu/myShell
ubuntu@VM-20-9-ubuntu:~/myShell$ echo $HOSTNAME
VM-20-9-ubuntu

在C语言中我们可以通过函数getenv()来帮助我们拿到这些环境变量。那么上面我们写的提供环境变量的函数就起到了作用了。

printf("[%s@%s%s]$",USER(),HOSTNAME(),PWD());
//这个$是普通用户的意思,root用户为#

交互页面完成后就是获取用户输入的命令字符串了。我们可以在一个函数里实现。

2.1 交互界面

int Interactive(char in[],int size)
{
    printf("[%s@%s%s]$",USER(),HOSTNAME(),PWD());
    //开始输入命令行
    fgets(in,size,stdin);
    //去除最后的'\n’
    in[strlen(in)-1] = 0;
    return strlen(in);
}

提问:为什么需要返回值?
回答:用来判断用户是否进行了输入,字符串长度为0表示未输入。

2.2 对字符串进行切割

在获取了用户输入的命令后,我们就需要对字符串进行分割了。
当用户输入了ls -a -l时。我们就需要将这个命令分割为"la","-a","-l"。这是为了后续的进程程序替换而准备的。需要作为参数传给exec函数。

void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in,SEP);
    while(argv[i++] = strtok(NULL,SEP));
    if(strcmp(argv[0],"ls") == 0)
    {
        argv[i-1] = (char*)"--color";
        argv[i] = NULL;
    }
    
}

可能大家strtok函数用的不多,不知道它是如何使用的,这就需要大家自己取去搜索下咯。
这里我们主要讲这段代码:

    if(strcmp(argv[0],"ls") == 0)
    {
        argv[i-1] = (char*)"--color";
        argv[i] = NULL;
    }

加这段代码的目的是为了让我们在使用了ls后看到这文件有颜色区分。
所以我们需要让argv[i-1] = (char*)"--color"

2.3 处理内建命令

提问:什么是内建命令?
回答:内建命令(Built-in Command) 是指由 shell 自身直接提供和执行的命令,而不是系统上独立的可执行程序(如 /bin/ls 这样的外部命令)。内建命令是 shell 的一部分,执行时不需要启动新进程。这使它们在执行某些操作时更加高效,尤其是那些涉及 shell 本身的行为或配置的操作。
因为这是shell自身提供的命令,所以我们无法直接调用可执行程序,需要自己实现。

常见的内建命令
不同的 shell(如 Bash、Zsh、Sh 等)可能提供不同的内建命令,但以下是一些常见的 Bash 内建命令:

  • cd:更改当前工作目录。
  • exit:退出当前 shell。
  • echo:打印文本到终端。
  • alias:为命令创建别名。
  • set:设置或显示 shell 变量。
  • export:将 shell 变量导出为环境变量。
  • pwd:显示当前工作目录。
  • history:显示命令历史记录。
  • read:从标准输入读取输入。
  • kill:向进程发送信号(如终止信号)。
  • type:显示命令的类型(内建命令或外部命令)。
    本篇文章不会实现太多的内建命令,只会涉及比较常见的几个内建命令的实现。
int Bulidcmd()
{
    int ret = 0;
    //ret 为0表示非内建命令,ret为1表示内建命令
    if(strcmp("cd",argv[0]) == 0)
    {
        ret = 1;
        char* target = argv[1];
        if(target == NULL) target = HOME();
        chdir(target);
        char temp[SIZE];
        getcwd(temp,SIZE);
        int result = snprintf(pwd,SIZE,"PWD=%s",temp);
        //格式化输入数据到指定的字符串当中。
        if(result<0)
        {
            perror("snprintf()");
            exit(1);
        }
        else if(result>=SIZE)
        {
            fprintf(stderr,"error");
            exit(1);
        }
        putenv(pwd);
    }
    else if(strcmp("export",argv[0]) == 0)
    {
        ret = 0;
        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);
                    if(e) printf("%s\n",e);
                }
            }
            else
            {
                printf("%s\n",argv[1]);
            }
        }
    }
    return ret;
}

可能这段代码的难点就是一些函数大家可能没有见过。

2.3.1 chdir()

chdir 是一个C语言中的标准库函数,用于更改当前工作目录。它的全称是 “change directory”(更改目录),常用于改变进程的当前工作路径。

2.3.2 getcwd()

getcwd 是 C 语言中的标准库函数,用于获取当前工作目录的绝对路径。它的全称是 “get current working directory”(获取当前工作目录)。该函数可以帮助程序在更改目录后获取当前的路径,或者在程序中随时查看当前的工作目录。

2.3.3 putenv()

putenv 是 C 语言中的标准库函数,用于设置或修改环境变量。它的全称是 “put environment”(设置环境)。通过 putenv,你可以在程序运行时动态地添加或修改环境变量。

2.4 执行非自建命令

就是普通的进程程序替换。

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);
}

最后我们需要写一个死循环来执行这些函数

int main()
{
    while(true)
    {
        //1.提供用户的交互界面,并获取用户输入的命令字串
        char commandline[SIZE];
        int n = Interactive(commandline,SIZE);
        if(n == 0) continue;
        //2.对字符串进行分割
        Split(commandline);
        //3.处理内建命令
        n = Bulidcmd();
        if(n) continue;
        //执行非自建命令
        Execute();
    }
    return 0;
}

3. 运行结果

ubuntu@VM-20-9-ubuntu:~/SHELL$ ./shell #后续为自定义shell的执行
[ubuntu@None/home/ubuntu/SHELL]$pwd
/home/ubuntu/SHELL
[ubuntu@None/home/ubuntu/SHELL]$ls
makefile  shell  shell.c  shell.c~
[ubuntu@None/home/ubuntu/SHELL]$cd 
[ubuntu@None/home/ubuntu]$pwd
/home/ubuntu
[ubuntu@None/home/ubuntu]$cd ^H^H^H^C

目前自定义shell存在的缺点:

  • 内建命令未实现完整。
  • 无法进行删除。

4.代码

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

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

char* argv[SIZE];
char env[SIZE];
char pwd[SIZE];
int lastcode = 0;

char* HOME()
{
    char* home = getenv("HOME");
    if(home == NULL) return "None";
    else return home;
}

const char* USER()
{
    char* user = getenv("USER");
    if(user == NULL) return "None";
    else return user;
}

const char* HOSTNAME()
{
    char* hostname = getenv("HOSTNAME");
    if(hostname == NULL) return "None";
    else return hostname;
}

const char* PWD()
{
    char* pwd = getenv("PWD");
    if(pwd == NULL) return "None";
    else return pwd;
}

int Interactive(char in[],int size)
{
    printf("[%s@%s%s]$",USER(),HOSTNAME(),PWD());
    //开始输入命令行
    fgets(in,size,stdin);
    //取出最后的'\n’
    in[strlen(in)-1] = 0;
    return strlen(in);
}

void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in,SEP);
    while(argv[i++] = strtok(NULL,SEP));
    if(strcmp(argv[0],"ls") == 0)
    {
        argv[i-1] = (char*)"--color";
        argv[i] = NULL;
    }
    
}

int Bulidcmd()
{
    int ret = 0;
    //ret 为0表示非内建命令,ret为1表示内建命令
    if(strcmp("cd",argv[0]) == 0)
    {
        ret = 1;
        char* target = argv[1];
        if(target == NULL) target = HOME();
        chdir(target);
        char temp[SIZE];
        getcwd(temp,SIZE);
        int result = snprintf(pwd,SIZE,"PWD=%s",temp);
        if(result<0)
        {
            perror("snprintf()");
            exit(1);
        }
        else if(result>=SIZE)
        {
            fprintf(stderr,"error");
            exit(1);
        }
        putenv(pwd);
    }
    else if(strcmp("export",argv[0]) == 0)
    {
        ret = 0;
        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);
                    if(e) printf("%s\n",e);
                }
            }
            else
            {
                printf("%s\n",argv[1]);
            }
        }
    }
    return ret;
}

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);
}

int main()
{
    while(true)
    {
        //1.提供用户的交互界面,并获取用户输入的命令字串
        char commandline[SIZE];
        int n = Interactive(commandline,SIZE);
        if(n == 0) continue;
        //2.对字符串进行分割
        Split(commandline);
        //3.处理内建命令
        n = Bulidcmd();
        if(n) continue;
        //执行非自建命令
        Execute();
    }
    return 0;
}

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。