【Linux】shell简单模拟实现

目录

什么是shell?

输出命令行提示符

1.获取用户名

2.获取主机名

3.获取当前所处路径

输出命令行提示符

获取用户输入的命令

分割命令

检查命令是否是内建命令

执行命令

完整代码及最终效果


什么是shell?

Shell 是一个命令行解释器,它为用户提供了一个与操作系统交互的界面。用户通过 Shell 输入命令,Shell 负责解析这些命令并将其传递给操作系统执行。

Shell 的主要功能:

  1. 命令执行 :Shell 可以直接执行用户输入的命令。例如,ls 用于列出当前目录下的文件和文件夹。

  2. 脚本编写:Shell 支持编写脚本,这些脚本是由一系列命令组成的文件,能够自动化重复的任务。例如,可以编写一个 Shell 脚本来备份文件、安装软件等。

  3. 管道和重定向 :Shell 支持管道 (|) 和重定向 (>, <),使得用户可以将一个命令的输出作为另一个命令的输入,或者将输出重定向到文件中。

  4. 环境管理 :Shell 允许用户设置和管理环境变量,这些变量可以影响 Shell 的行为和程序的运行方式。例如,PATH 环境变量用于指定系统查找可执行文件的路径。

Shell 的工作原理:

  1. 用户输入:用户在终端中输入命令,Shell 接收这些命令并将其解析为一系列的指令。

  2. 解析命令:Shell 解析用户输入的命令,并将其分解成可执行的指令和参数。

  3. 执行命令:Shell 使用系统调用来创建进程,并在子进程中执行用户输入的命令。

  4. 处理输出:命令执行完成后,Shell 将命令的输出显示在终端上,并返回到用户的命令提示符,等待下一个命令。

本文将对shell进行简单对模拟

所用头文件、宏、全局变量:

#include <stdio.h>

#include <stdlib.h>

#include <unistd.h>

#include <errno.h> //错误码

#include <string.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 *gArgv[NUM];

char cwd[SIZE * 2];

int lastcode=0;

输出命令行提示符

根据上图命令提示符:

  1. 获取用户名

  2. 获取主机名

  3. 获取当前所处路径

1.获取用户名

介绍getenv: <stdlib.h>

getenv 函数用于获取环境变量的值。

cpp 复制代码
 char *getenv(const char *name);
  • 参数name 是环境变量的名称(以 C 字符串形式传递)。

  • 返回值 :如果环境变量存在,则返回其对应的值(也是 C 字符串形式)。如果环境变量不存在,则返回 NULL

env查看环境变量:

环境变量USER=(当前用户名)

cpp 复制代码
 const char *GetUserName()
 {
     const char *name = getenv("USER");
     if (name == NULL)//没找到用户
         return "None";
     return name;
 }

2.获取主机名

复制代码
 
cpp 复制代码
const char *GetHostName()
 {
     const char *hostname = getenv("HOSTNAME");
     if (hostname == NULL)
         return "None";
     return hostname;
 }

3.获取当前所处路径

复制代码
 
cpp 复制代码
const char *Getcwd()
 {
     const char *cwd = getenv("PWD");
     if (cwd == NULL)
         return "None";
     return cwd;
 }

输出命令行提示符

定义相关宏函数:

cpp 复制代码
 #define SkipPath(p)         \
     do                      \
     {                       \
         p += strlen(p) - 1; \
         while (*p != '/')   \
             p--;            \
     } while (0) // 宏函数
复制代码
主代码:
cpp 复制代码
 void MakeCommandLinePrint()
 {
     char line[SIZE];
     const char *username = GetUserName();
     const char *hostname = GetHostName();
     const char *cwd = Getcwd();
 ​
     SkipPath(cwd);
     snprintf(line, SIZE, "[%s@%s %s]>", username, hostname, strlen(cwd) == 1 ? "/" : cwd + 1); //+1不打印'/'
     printf("%s", line);
     fflush(stdout);
 }

snprintf:

cpp 复制代码
int snprintf(char *str, size_t size, const char *format, ...);
  1. char \*str:这是一个字符数组,用于存储格式化后的字符串。

  2. size_t size :指定 str 数组的大小(即最大写入长度)。snprintf 会确保不会写入超过这个长度的字符,以防止缓冲区溢出。

  3. const char \*format :格式字符串,类似于 printf 的格式字符串,用于定义输出的格式。

  4. ...:格式字符串中的格式说明符所对应的变量。

    返回值

    • snprintf 返回实际写入的字符数(不包括终止的空字符 '\0')。

    • 如果返回值大于或等于 size,说明输出被截断了(即实际需要的字符数超出了提供的缓冲区大小)。在这种情况下,str 数组将会以空字符 '\0' 结尾,确保字符串是以正确的格式终止的。

    优点

    • 安全性snprintf 提供了对缓冲区溢出的保护,通过指定缓冲区的大小来避免写入超过缓冲区限制的数据。

    • 灵活性 :可以格式化各种类型的数据,类似于 printf 函数,但输出到字符串中而不是直接到标准输出。

fflush:

功能fflush 用于刷新指定文件流的缓冲区,确保缓冲区中的数据被立即写入目标流(如终端或文件)。

cpp 复制代码
int fflush(FILE *stream);
  • FILE \*stream :指向 FILE 结构的指针,表示要刷新的流。可以是标准输入 (stdin)、标准输出 (stdout)、标准错误 (stderr),或者其他打开的文件流。如果 streamNULL,则 fflush 刷新所有输出流。

返回值

  • 成功 :返回 0

  • 失败 :返回 EOF,并且设置 errno 以指示错误类型。

效果展示:

此处的None是应为我个人使用的Unix系统进行执行,Unix系统获取主机名的环境变量并不叫HOSTNAME

获取用户输入的命令

cpp 复制代码
 char usercommand[SIZE];//定义数组获得用户输入的命令
 int n = GetUserCommand(usercommand, sizeof(usercommand));
 if (n <= 0)
     return 1; // 获取失败,重新获取
复制代码
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);
 }
 //#define ZERO '\0'

fgets:

cpp 复制代码
char *fgets(char *str, int n, FILE *stream);

参数

  • char \*str :指向用于存储读取内容的字符数组。fgets 将从文件流中读取的字符存储到这个数组中。

  • int n :指定要读取的最大字符数(包括终止的空字符 '\0')。实际读取的字符数最多为 n-1

  • FILE \*stream :指向 FILE 结构的指针,表示要读取的文件流。可以是标准输入 (stdin)、标准输出 (stdout)、标准错误 (stderr),或其他打开的文件流。

返回值

  • 成功 :返回 str(即指向字符数组的指针),并将读取的内容存储在这个数组中。

  • 遇到文件结束符 (EOF) 或错误 :返回 NULL,并且可能设置 errno 以指示错误。

特点

  • 缓冲区管理fgets 会在读取到换行符、文件结束符或达到最大字符数(n-1)时停止,自动添加空字符 '\0' 作为字符串结束符。

  • 换行符处理 :如果读取的行包含换行符,fgets 会将换行符包括在返回的字符串中,直到换行符之前的所有字符(最多 n-1 个字符)。

  • 安全性 :相比于 getsfgets 是更安全的,因为它允许指定缓冲区大小,防止缓冲区溢出。

分割命令

获取到用户输入的命令,要对用户对命令进行拆解

cpp 复制代码
 void SplitCommand(char command[], size_t n)
 {
     gArgv[0] = strtok(command, SEP); // 第一个参数
     int index = 1;
     while ((gArgv[index++] = strtok(NULL, SEP)))
         ;
 }
 //#define SEP " "

strtok:

cpp 复制代码
char *strtok(char *str, const char *delim);

参数

  • char \*str :待分割的字符串。如果这是第一次调用 strtok,则传入待分割的字符串。如果是后续调用,应传入 NULL,以便继续分割上次的字符串。

  • const char \*delim :包含所有分隔符字符的字符串。strtok 将根据这些字符分割输入字符串。

返回值

  • 成功:返回指向当前分割出的子串的指针。

  • 结束 :当没有更多的子串时,返回 NULL

功能描述

  • 首次调用 :在第一次调用 strtok 时,传入要分割的字符串 str 和分隔符 delimstrtok 会找到第一个分隔符,将它替换为 '\0'(字符串结束符),并返回指向第一个子串的指针。

  • 后续调用 :在后续调用中,传入 NULL 作为 str 参数,strtok 会继续使用上次传入的字符串,并返回下一个分隔符之间的子串,直到没有更多子串为止。

检查命令是否是内建命令

cpp 复制代码
 n = CheckBuildin();
 if (n)
     continue;
复制代码
 
cpp 复制代码
int CheckBuildin()
 {
     int yes = 0;  // 标记是否识别到内建命令,初始值为0(表示未识别)
     const char *enter_cmd = gArgv[0];  // 获取输入命令的第一个参数,即命令名称
 ​
     // 判断是否为内建命令
     // 检查是否为 "cd" 命令
     if (strcmp(enter_cmd, "cd") == 0)
     {
         yes = 1;  // 识别到内建命令,设置标记为1
         cd();     // 调用处理 "cd" 命令的函数
     }
     // 检查是否为 "echo" 命令,并且第二个参数是否为 "$?"
     else if (strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
     {
         yes = 1;  // 识别到内建命令,设置标记为1
         printf("%d\n", lastcode);  // 打印上一个命令的返回状态码
         lastcode = 0;  // 重置返回状态码为0,准备下一次使用
     }
 ​
     return yes;  // 返回识别标记,1表示识别到内建命令,0表示没有识别到
 }

cd 函数的主要功能是:

  • 更改当前进程的工作目录。

  • 更新 PWD 环境变量,以确保环境变量反映新的工作目录路径。这对于命令行提示符的显示(如果该程序是一个命令行工具的一部分)以及子进程继承环境变量是重要的。

cpp 复制代码
void cd()
 {
     const char *path = gArgv[1];  // 获取命令行参数中的路径(即 'cd' 命令后的路径)
     
     if (path == NULL)
         path = GetHome();  // 如果路径参数为空,则设置路径为用户主目录
 ​
     // 使用 chdir 函数更改当前工作目录
     // chdir(path) 将当前进程的工作目录更改为 path 指定的路径
     chdir(path);
 ​
     // 刷新环境变量以更新命令行提示符的路径
     char temp[SIZE * 2];  // 临时缓冲区,用于存储当前工作目录的路径
 ​
     // 使用 getcwd 函数获取当前工作目录的路径,并将其存储在 temp 中
     // getcwd(temp, sizeof(temp)) 将当前工作目录路径存储在 temp 中
     getcwd(temp, sizeof(temp));
 ​
     // 使用 snprintf 函数格式化路径为 "PWD=当前路径",并存储在 cwd 中
     // 这样可以更新环境变量 PWD 以反映当前工作目录
     snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
 ​
     // 使用 putenv 函数更新环境变量,将 cwd 变量的内容写入环境变量
     // 这会影响到环境变量的值,使得命令行提示符显示正确的路径
     putenv(cwd);
 }

执行命令

cpp 复制代码
// 退出码
void Die()
{
    exit(1);
}

void ExecuteCommand()
{
    pid_t id = fork();  // 创建一个新进程,返回值存储在 id 中

    if (id < 0)
    {
        // 如果 fork 返回负值,则表示进程创建失败,调用 Die() 函数处理错误
        Die();
    }
    else if (id == 0)
    {
        // 子进程部分
        // 使用 execvp 函数替换当前进程的映像为新命令的映像
        // gArgv[0] 是要执行的命令,gArgv 是命令及其参数的数组
        execvp(gArgv[0], gArgv);

        // execvp 如果成功,则不会返回;如果失败,返回到这里并退出进程
        // 使用 errno 作为退出状态码
        exit(errno);
    }
    else
    {
        // 父进程部分
        int status = 0;  // 用于存储子进程的退出状态
        pid_t rid = waitpid(id, &status, 0);  // 等待子进程结束,并获取其退出状态

        if (rid > 0)
        {
            // 使用 WEXITSTATUS 宏获取子进程的退出状态码
            lastcode = WEXITSTATUS(status);

            // 如果退出状态码不为0,则打印错误信息
            if (lastcode != 0)
            {
                // 打印命令名、错误描述和错误代码
                // strerror(lastcode) 将状态码转换为可读的错误描述
                printf("%s:%s:%d\n", gArgv[0], strerror(lastcode), lastcode);
            }
        }
    }
}
复制代码
 

execvp:

cpp 复制代码
int execvp(const char *file, char *const argv[]);

参数说明

  • file:要执行的程序的名称或路径。可以是一个可执行文件的名称(当前路径下的文件)或者是绝对路径/相对路径。

  • argv :一个字符串数组,包含要传递给程序的参数。数组的第一个元素 argv[0] 通常是程序的名称,数组的最后一个元素必须是 NULL,以标识参数的结束。

主要功能

  1. 替换当前进程 :当调用 execvp 成功时,当前进程的上下文(包括代码、数据、堆栈等)会被新程序的上下文所替代,因此 execvp 之后的代码不会被执行。换句话说,调用 execvp 后,执行的程序将成为当前进程。

  2. 参数传递 :通过 argv 数组传递给新程序的参数,可以让新程序在执行时获得所需的命令行参数。

  3. 搜索路径execvp 会在系统的 PATH 环境变量中查找指定的可执行文件,因此,你可以直接传递程序名称,而无需提供完整路径。此外,如果只给出文件名,execvp 将自动在 PATH 中的目录中搜索该文件。

返回值

  • 成功execvp 成功时不会返回(因为当前进程已经被新程序替换)。

  • 失败 :如果失败,则返回 -1,并设置 errno 以指示错误的类型。

在许多情况下,特别是在创建子进程的场景下,execvp 是调用新程序的常用方式。例如,用户输入命令后,可以使用 fork 创建一个子进程,并在子进程中调用 execvp 来执行用户指定的命令,从而使得 shell 能够运行各种程序。

完整代码及最终效果

复制代码
 
cpp 复制代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <errno.h> //错误码
#include <string.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 *gArgv[NUM];
char cwd[SIZE * 2];

int lastcode=0;

// 退出码
void Die()
{
    exit(1);
}

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

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

// 输出命令行提示符
void MakeCommandLinePrint()
{
    char line[SIZE];
    const char *username = GetUserName();
    const char *hostname = GetHostName();
    const char *cwd = Getcwd();

    SkipPath(cwd);
    snprintf(line, SIZE, "[%s@%s %s]>", username, hostname, strlen(cwd) == 1 ? "/" : cwd + 1); //+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)
{
    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
    {
        // father
        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:更改当前工作路径
    chdir(path);

    // 刷新环境变量
    char temp[SIZE * 2];
    getcwd(temp, sizeof(temp)); // getcwd:获取当前工作目录的路径,返回当前工作目录的路径名
    // 更新当前环境变量(不更新导致命令行提示符path不更新)
    snprintf(cwd, sizeof(cwd), "PWD=%s", temp);
    putenv(cwd);
}

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. 输出命令行提示符
        MakeCommandLinePrint();

        // 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;

        // n.执行命令
        ExecuteCommand();
    }

    return 0;
}
// ls -l --color

本篇讲解就到这啦,感谢翻阅!

相关推荐
xixixin_2 小时前
【Vite】前端开发服务器的配置
服务器·前端·网络
.生产的驴2 小时前
Vue3 加快页面加载速度 使用CDN外部库的加载 提升页面打开速度 服务器分发
运维·服务器·前端·vue.js·分布式·前端框架·vue
程序员JerrySUN2 小时前
Linux 内核核心知识热点题分析:10 个连环打通的难点
linux·运维·服务器
R_.L3 小时前
Linux : 线程【同步与互斥】
linux
再睡一夏就好3 小时前
从硬件角度理解“Linux下一切皆文件“,详解用户级缓冲区
linux·服务器·c语言·开发语言·学习笔记
zm5 小时前
TCP 粘包
服务器·网络·php
honey ball8 小时前
R & S的EMI接收机面板
linux·运维·网络
GBXLUO10 小时前
如何使用远程桌面控制电脑
服务器
柳如烟@10 小时前
在Rocky Linux 9.5上部署MongoDB 8.0.9:从安装到认证的完整指南
linux·运维·mongodb
搬码临时工10 小时前
电脑怎么远程访问服务器?4种常见的简单方法
运维·服务器·网络·异地访问