【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 函数用于获取环境变量的值。

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

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

env查看环境变量:

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

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

2.获取主机名

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

3.获取当前所处路径

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

输出命令行提示符

定义相关宏函数:

 #define SkipPath(p)         \
     do                      \
     {                       \
         p += strlen(p) - 1; \
         while (*p != '/')   \
             p--;            \
     } while (0) // 宏函数
主代码:
 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:

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 用于刷新指定文件流的缓冲区,确保缓冲区中的数据被立即写入目标流(如终端或文件)。

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

返回值

  • 成功:返回 0

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

效果展示:

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

获取用户输入的命令

 char usercommand[SIZE];//定义数组获得用户输入的命令
 int n = GetUserCommand(usercommand, sizeof(usercommand));
 if (n <= 0)
     return 1; // 获取失败,重新获取
 
 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:

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 是更安全的,因为它允许指定缓冲区大小,防止缓冲区溢出。

分割命令

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

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

strtok:

char *strtok(char *str, const char *delim);

参数

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

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

返回值

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

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

功能描述

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

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

检查命令是否是内建命令

 n = CheckBuildin();
 if (n)
     continue;
 
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 环境变量,以确保环境变量反映新的工作目录路径。这对于命令行提示符的显示(如果该程序是一个命令行工具的一部分)以及子进程继承环境变量是重要的。

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

执行命令

// 退出码
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:

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 能够运行各种程序。

完整代码及最终效果

 
#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

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

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值