Linux--自主编写shell

目录

准备知识

shell原理

shell与用户互动的过程

实现shell

0.用到的头文件和宏定义

1.首先我们需要自己输出一个命令行

2.获取用户命令行字符

3.命令行字符串分割

4.执行命令

5.设置循环

6.检测内建命令

7.完善细节--获取工作目录而非路径


准备知识

Linux--环境变量-CSDN博客

Linux--地址空间-CSDN博客

Linux--进程控制(1)-CSDN博客

Linux--进程控制(2)--进程的程序替换(夺舍)-CSDN博客


shell原理

在Linux中,shell的工作原理主要涉及到用户与操作系统之间的交互。shell是用户与Linux内核进行通信的桥梁,它负责解释和执行用户输入的命令,并将这些命令转换为内核可以执行的操作。

具体来说,当用户在命令行界面(CLI)中输入一个命令时,shell会首先接收这个输入。然后,shell会对命令进行解析,识别出命令名、参数和选项等组成部分。这个过程包括检查命令是否是shell内部的命令,或者是否是一个外部的应用程序。

如果命令是内部的,shell会直接执行相应的操作。如果命令是外部的,shell会在搜索路径中查找这个应用程序的可执行文件。搜索路径是一个包含可执行程序目录的列表,shell会按照顺序在这些目录中查找命令对应的可执行文件。

一旦找到可执行文件,shell会将其加载到内存中,并创建一个新的进程来执行这个命令。这个进程会调用系统调用,与Linux内核进行交互,完成命令所指定的操作。

最后,shell会将命令执行的结果输出到命令行界面,供用户查看。这个结果可以是命令的输出信息,也可以是命令执行的状态码,用于表示命令是否成功执行。


shell与用户互动的过程

举个例子:

用下图的时间轴来表示事件的发生次序。其中时间从左向右。shell由标识为sh的方块代表,它随着时间的流逝从左向右移动。shell从用户读入字符串"ls"。shell建立一个新的进程,然后在那个进程中运行ls程序并等待那个进程结束。

然后shell读取新的一行输入,建立一个新的进程,在这个进程中运行程序 并等待这个进程结束。

根据这些思路,和我们前面的学的技术,就可以自己来实现一个shell了。


实现shell

0.用到的头文件和宏定义

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

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)

char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;

这里补充一下,如果命令输入错入,要删除重新输入。删除:CTRL+删除键

退出自己写的shell:CTRL+c

1.首先我们需要自己输出一个命令行

light@VM-16-9-centos myshell\]$ 获取用户名 主机名 所处的工作目录 1.1获取用户名 * 使用 `getenv` 函数从环境变量 `USER` 中获取值,并将其存储在名为 `name` 的 `const char*` 类型的变量中。`getenv` 函数返回指向该环境变量值的指针,如果该环境变量不存在,则返回 `NULL`。 ```cpp const char *GetUserName() { const char *name = getenv("USER"); if(name == NULL) return "None"; return name; } ``` 1.2获取主机名 ```cpp const char *GetHostName() { const char *hostname = getenv("HOSTNAME"); if(hostname == NULL) return "None"; return hostname; } ``` 同样的,这里我们只需要从环境变量HOSTNAME中获取就行了。 1.3获取所处的工作目录 ```cpp const char *GetCwd() { const char *cwd = getenv("PWD"); if(cwd == NULL) return "None"; return cwd; } ``` 使用环境变量PWD获取的是绝对路径,但我们可以只截取当前目录的,这里为了和shell更好的区分就先不截取了。 1.4封装打印函数 封装打印函数我们要使用到snprintf函数: snprintf() 是一个 C 语言标准库函数,用于格式化输出字符串,并将结果写入到指定的缓冲区,与 sprintf() 不同的是,snprintf() 会限制输出的字符数,避免缓冲区溢出。安全性更高 ![](https://file.jishuzhan.net/article/1785444651105259521/9f2e4272718f68f2365ad3726535ed0c.webp) ```cpp void MakeCommandLineAndPrint() { char line[SIZE]; const char *username = GetUserName(); const char *hostname = GetHostName(); const char *cwd = GetCwd(); snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname,cwd); printf("%s", line); fflush(stdout); } ``` 我们将之前写的函数都封装在这个函数中,接收了两个参数,一个字符数组 `line` 和一个 `size_t` 类型的 `size`。字符数组 `line` 用于存储构建的命令行提示符。模仿shell的输出我们的命令行。调用 `fflush` 函数清空标准输出缓冲区,确保提示符字符串被立即打印到屏幕上,而不是被缓存在内部缓冲区中。 **效果演示:** ![](https://file.jishuzhan.net/article/1785444651105259521/fad4a6011e82d83346642fbbe1fa9a61.webp) *** ** * ** *** ### 2.获取用户命令行字符 ```cpp int GetUserCommand(char command[], size_t n) { char *s = fgets(command, n, stdin); if(s == NULL) return -1; command[strlen(command)-1] = ZERO; return strlen(command); } ``` 函数的目的是从标准输入(stdin)读取用户输入的命令,并将其存储在传入的字符数组 `command` 中。函数返回读取到的命令的长度,或者在发生错误时返回 `-1`。 * 使用 `fgets` 函数从标准输入(stdin)读取最多 `n-1` 个字符(保留一个位置给字符串结束符 '\\0')并存储在 `command` 数组中。`fgets` 函数返回指向 `command` 的指针,并将其赋值给 `s`。 * `command[strlen(command)-1] = ZERO;` 这一行尝试将 `command` 数组中的最后一个字符(通常是换行符 '\\n')替换为 `ZERO`。使用 `'\0'` 来替换换行符,因为我们我们使用fgets函数读取完字符串最后回车,换行符会被读取到。 **效果演示:** ![](https://file.jishuzhan.net/article/1785444651105259521/31059f94f70bbe223e22db125f974769.webp) ![](https://file.jishuzhan.net/article/1785444651105259521/f0f23c397dca2bae32af5c68631ddb22.webp) *** ** * ** *** ### **3.命令行字符串分割** 期望"ls -a -l -n" ----\>"ls" "-a" "-l" "-n" 并把它们放在命令行参数表中。 这里我们要是用一个函数strtok: `strtok` 是 C 语言中的一个标准库函数,用于分解字符串。它基于指定的分隔符集合来分割字符串,并返回指向下一个标记的指针。这个函数在处理文本文件或字符串时非常有用,特别是当你需要按照特定的分隔符(如逗号、空格等)来分割字符串时。 ![](https://file.jishuzhan.net/article/1785444651105259521/4fb63c2f6572c9c2e549c467fd540c81.webp) ```cpp #define NUM 32 char *gArgv[NUM]; void SplitCommand(char command[], size_t n) { // "ls -a -l -n" -> "ls" "-a" "-l" "-n" gArgv[0] = strtok(command, SEP); int index = 1; while((gArgv[index++] = strtok(NULL, SEP))); } ``` done, 故意写成=,表示先赋值,在判断. 分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL, 并且while判断结束。char \*gArgv\[NUM\];表示命令行参数表,这个是二维数组。 这里就不做演示,命令行参数被分割好后,会被依次放在命令行参数表中。 *** ** * ** *** ### 4.执行命令 执行命令这里我们就要用到替换函数了,我们有命令行参数表(数组),我们直接使用execvp函数就行了 **int execvp(const char \*file, char \*const argv\[\]);** p:用户可以不传要执行的文件路径(但是要穿文件名),查找这个程序,系统会自动在环境变量PATH中进行查找。 ```cpp int lastcode = 0; void Die() { exit(1); } void ExecuteCommand() { pid_t id = fork(); if (id < 0) Die(); else if (id == 0) { // child execvp(gArgv[0], gArgv); exit(errno); } else { // fahter int status = 0; pid_t rid = waitpid(id, &status, 0); if (rid > 0) { lastcode = WEXITSTATUS(status); if (lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode); } } } ``` 使用 `fork()` 函数创建一个新的子进程。`fork()` 返回两次:在父进程中返回子进程的PID,在子进程中返回0。如果创建失败直接杀掉。 **子进程执行命令** :如果 `fork()` 返回0,说明当前代码在子进程中执行。子进程调用 `execvp(gArgv[0], gArgv)` 来执行 `gArgv` 数组指定的命令。`execvp` 会用新的程序替换当前进程的映像,如果成功则不会返回;如果失败则返回-1,子进程会执行 `exit(errno)` 来退出,其中 `errno` 包含了出错信息。 **父进程等待子进程** :如果 `fork()` 返回的值大于0,说明当前代码在父进程中执行。父进程调用 `waitpid(id, &status, 0)` 来等待子进程结束。`waitpid` 会阻塞父进程,直到子进程结束或发生错误 **处理子进程退出状态** :如果 `waitpid` 成功返回(即 `rid > 0`),父进程会检查子进程的退出状态。`WEXITSTATUS(status)` 宏用于从 `status` 中提取子进程的退出状态码。如果退出状态码不是0(通常表示子进程正常结束),则打印出命令名、对应的错误描述和退出状态码。 **效果演示:** ![](https://file.jishuzhan.net/article/1785444651105259521/bc9cdffd792684611d6836fd31ab13da.webp) *** ** * ** *** ### 5.设置循环 为了能多次的执行命令, 我们需要设置循环 ![](https://file.jishuzhan.net/article/1785444651105259521/c42f83e49d3e2a4a194ca5b39e567d99.webp) *** ** * ** *** ### 6.检测内建命令 **1.无法进行目录的回退(内建命令)** ![](https://file.jishuzhan.net/article/1785444651105259521/823d7a58ca2e0f61fea40e84ac9bb030.webp) 这是因为我们是使用子进程执行的,但是这个进程是属于父进程的,子进程执行完就结束了与父进程是无关的。**像cd这种命令应该是让父进程执行的,而不是让子进程来执行。这种需要父进程执行的命令,叫做内建命令** **因此我们在执行命令的时候,需要检测是不是内建命令** 使用 `chdir` 函数来改变当前工作目录到目标路径。 ![](https://file.jishuzhan.net/article/1785444651105259521/49406a0d4bea98a0571d2b298f6e4b54.webp) ```cpp const char* GetHome() { const char* home = getenv("HOME"); if (home == NULL) return "/"; return home; } void Cd() { const char* path = gArgv[1]; if (path == NULL) path = GetHome(); // path 一定存在 chdir(path); // 刷新环境变量 char temp[SIZE * 2]; getcwd(temp, sizeof(temp)); snprintf(cwd, sizeof(cwd), "PWD=%s", temp); putenv(cwd); // OK } int CheckBuildin() { int yes = 0; const char* enter_cmd = gArgv[0]; if (strcmp(enter_cmd, "cd") == 0) { yes = 1; Cd(); } else if (strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0) { yes = 1; printf("%d\n", lastcode); lastcode = 0; } return yes; } ``` **`GetHome` 函数** 这个函数试图获取当前用户的主目录路径。它使用 `getenv` 函数来检索环境变量 `"HOME"` 的值,该环境变量通常包含了用户的主目录路径。如果 `getenv` 返回 `NULL`(即没有找到 `"HOME"` 环境变量),则函数返回 `"/"`,这通常代表根目录。 **`Cd` 函数** 这个函数实现了 `cd` 命令的功能,即改变当前工作目录。 1. 它首先获取 `gArgv[1]` 作为目标路径。`gArgv` 应该是一个全局数组,包含了命令行参数。 2. 如果 `gArgv[1]` 为 `NULL`(即没有提供路径参数),则调用 `GetHome` 函数来获取用户的主目录,并将其作为目标路径。 3. 使用 `chdir` 函数来改变当前工作目录到目标路径。 4. 然后,它获取当前工作目录的路径,并构建一个字符串 `"PWD=<当前工作目录>"`,其中 `PWD` 是一个常见的环境变量,用于存储当前工作目录的路径。 5. 最后,使用 `putenv` 函数将构建的字符串添加到环境变量中,从而"刷新"环境变量。 **`CheckBuildin` 函数** 这个函数检查 `gArgv[0]`(通常是命令名)是否是内置命令,并执行相应的操作。 1. 它首先初始化一个变量 `yes` 为 `0`,用于标记是否找到了内置命令。 2. 如果 `gArgv[0]` 是 `"cd"`,则 `yes` 被设置为 `1`,并调用 `Cd` 函数来执行 `cd` 命令。 3. 如果 `gArgv[0]` 是 `"echo"` 并且 `gArgv[1]` 是 `"$?"`,则 `yes` 也被设置为 `1`,并打印出 `lastcode` 的值(它是一个全局变量,用于存储上一个命令的退出状态码)。之后,将 `lastcode` 重置为 `0`。 4. 函数最后返回 `yes` 的值,如果找到了内置命令并成功执行,则返回 `1`,否则返回 `0`。 **效果演示:** 如果是内建命令,则跳过下面的执行命令,进入下一次循环 ![](https://file.jishuzhan.net/article/1785444651105259521/f65eb39475d5c00afeb7bec1bca5aab4.webp) ![](https://file.jishuzhan.net/article/1785444651105259521/b5b13ee96ff31d7baef573499745822e.webp) *** ** * ** *** ### 7.完善细节--获取工作目录而非路径 这里我们改写了一下打印。 ```cpp #define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0) void MakeCommandLineAndPrint() { char line[SIZE]; const char* username = GetUserName(); const char* hostname = GetHostName(); const char* cwd = GetCwd(); SkipPath(cwd); snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd + 1); printf("%s", line); fflush(stdout); } ``` 这个宏接受一个指向字符的指针 `p`,该指针指向一个字符串,这个字符串应该是一个文件路径。宏的作用是将 `p` 移动到该路径中最后一个斜杠 `'/'` 的位置。 使用 `snprintf` 函数构建命令行提示符。格式是 `"[用户名@主机名 当前工作目录]> "`。这里还做了一个小处理:如果 `cwd` 的长度是1(即只有斜杠 `'/'`),则打印根目录 `"/"`;否则,打印从最后一个斜杠后面的部分开始的工作目录。 **效果演示:** ![](https://file.jishuzhan.net/article/1785444651105259521/580d63d554486392df05ad5770de9b2d.webp) ![](https://file.jishuzhan.net/article/1785444651105259521/535a82605577b520621a24f904186614.webp)


完整代码

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

#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)

char cwd[SIZE*2];
char *gArgv[NUM];
int lastcode = 0;

void Die()
{
    exit(1);
}

const char *GetHome()
{
    const char *home = getenv("HOME");
    if(home == NULL) return "/";
    return home;
}

const char *GetUserName()
{
    const char *name = getenv("USER");
    if(name == NULL) return "None";
    return name;
}
const char *GetHostName()
{
    const char *hostname = getenv("HOSTNAME");
    if(hostname == NULL) return "None";
    return hostname;
}
// 临时
const char *GetCwd()
{
    const char *cwd = getenv("PWD");
    if(cwd == NULL) return "None";
    return cwd;
}

// commandline : output
void MakeCommandLineAndPrint()
{
    char line[SIZE];
    const char *username = GetUserName();
    const char *hostname = GetHostName();
    const char *cwd = GetCwd();

    SkipPath(cwd);
    snprintf(line, sizeof(line), "[%s@%s %s]> ", username, hostname, strlen(cwd) == 1 ? "/" : cwd+1);
    printf("%s", line);
    fflush(stdout);
}

int GetUserCommand(char command[], size_t n)
{
    char *s = fgets(command, n, stdin);
    if(s == NULL) return -1;
    command[strlen(command)-1] = ZERO;
    return strlen(command); 
}


void SplitCommand(char command[], size_t n)
{
    (void)n;
    // "ls -a -l -n" -> "ls" "-a" "-l" "-n"
    gArgv[0] = strtok(command, SEP);
    int index = 1;
    while((gArgv[index++] = strtok(NULL, SEP)));
}

void ExecuteCommand()
{
    pid_t id = fork();
    if(id < 0) Die();
    else if(id == 0)
    {
        // child
        execvp(gArgv[0], gArgv);
        exit(errno);
    }
    else
    {
        // fahter
        int status = 0;
        pid_t rid = waitpid(id, &status, 0);
        if(rid > 0)
        {
            lastcode = WEXITSTATUS(status);
            if(lastcode != 0) printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
        }
    }
}

void Cd()
{
    const char *path = gArgv[1];
    if(path == NULL) path = GetHome();
    // path 一定存在
    chdir(path);

    // 刷新环境变量
    char temp[SIZE*2];
    getcwd(temp, sizeof(temp));
    snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
    putenv(cwd); // OK
}

int CheckBuildin()
{
    int yes = 0;
    const char *enter_cmd = gArgv[0];
    if(strcmp(enter_cmd, "cd") == 0)
    {
        yes = 1;
        Cd();
    }
    else if(strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
    {
        yes = 1;
        printf("%d\n", lastcode);
        lastcode = 0;
    }
    return yes;
}

int main()
{
    int quit = 0;
    while(!quit)
    {
        // 1. 我们需要自己输出一个命令行
        MakeCommandLineAndPrint();

        // 2. 获取用户命令字符串
        char usercommand[SIZE];
        int n = GetUserCommand(usercommand, sizeof(usercommand));
        if(n <= 0) return 1;

        // 3. 命令行字符串分割. 
        SplitCommand(usercommand, sizeof(usercommand));

        // 4. 检测命令是否是内建命令
        n = CheckBuildin();
        if(n) continue;
        // 5. 执行命令
        ExecuteCommand();
    }
    return 0;
}
相关推荐
一袋米扛几楼981 分钟前
【密码学】CrypTool2 工具是什么?
服务器·网络·密码学
vin_zheng1 小时前
破解企业安全软件网络拦截实战记录
运维
林姜泽樾3 小时前
Linux入门第十二章,创建用户、用户组、主组附加组等相关知识详解
linux·运维·服务器·centos
xiaokangzhe3 小时前
Linux系统安全
linux·运维·系统安全
feng一样的男子4 小时前
NFS 扩展属性 (xattr) 提示操作不支持解决方案
linux·go
南棱笑笑生4 小时前
20260310在瑞芯微原厂RK3576的Android14查看系统休眠时间
服务器·网络·数据库·rockchip
xiaokangzhe4 小时前
Nginx核心功能
运维·nginx
松果1774 小时前
以本地时钟为源的时间服务器
运维·chrony·时间服务器
XDHCOM4 小时前
ORA-32152报错咋整啊,数据库操作遇到null number问题远程帮忙修复
服务器·数据库·oracle
Highcharts.js4 小时前
Highcharts React v4.2.1 正式发布:更自然的React开发体验,更清晰的数据处理
linux·运维·javascript·ubuntu·react.js·数据可视化·highcharts