当前位置: 首页 > news >正文

【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;  // 识别到内建命令,设置标记为1cd();     // 调用处理 "cd" 命令的函数}// 检查是否为 "echo" 命令,并且第二个参数是否为 "$?"else if (strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0){yes = 1;  // 识别到内建命令,设置标记为1printf("%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){// childexecvp(gArgv[0], gArgv);exit(errno);}else{// fatherint 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

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

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • Adobe Premiere Pro(Pr)安装包软件下载
  • 2024年热门硬盘数据恢复软件大盘点:高效恢复您的宝贵数据
  • 【数据结构】二叉树OJ题_对称二叉树_另一棵的子树
  • NAS新品“翻车”后,绿联科技要上市了
  • Chapter 5: 二叉树详解
  • docker默认存储地址 var/lib/docker 满了,换个存储地址操作流程
  • HardeningMeter:一款针对二进制文件和系统安全强度的开源工具
  • 项目收获总结--MyBatis的知识收获
  • linux在ssh的时候询问,yes or no 如何关闭
  • 自动驾驶车道线检测系列—3D-LaneNet: End-to-End 3D Multiple Lane Detection
  • Python爬虫进阶----2(细心,耐心才能爬好)
  • PostgreSQL的Json数据类型如何使用
  • Flask校验
  • Ansible服务实现自动化运维
  • 微信小程序开发入门指南
  • [PHP内核探索]PHP中的哈希表
  • 2017前端实习生面试总结
  • CSS中外联样式表代表的含义
  • HTTP中GET与POST的区别 99%的错误认识
  • react-native 安卓真机环境搭建
  • storm drpc实例
  • Wamp集成环境 添加PHP的新版本
  • XForms - 更强大的Form
  • 从setTimeout-setInterval看JS线程
  • 等保2.0 | 几维安全发布等保检测、等保加固专版 加速企业等保合规
  • 将回调地狱按在地上摩擦的Promise
  • 买一台 iPhone X,还是创建一家未来的独角兽?
  • 那些被忽略的 JavaScript 数组方法细节
  • 使用 Xcode 的 Target 区分开发和生产环境
  • 首页查询功能的一次实现过程
  • 与 ConTeXt MkIV 官方文档的接驳
  • Android开发者必备:推荐一款助力开发的开源APP
  • ​3ds Max插件CG MAGIC图形板块为您提升线条效率!
  • ​什么是bug?bug的源头在哪里?
  • ​业务双活的数据切换思路设计(下)
  • ​总结MySQL 的一些知识点:MySQL 选择数据库​
  • # include “ “ 和 # include < >两者的区别
  • #define用法
  • #etcd#安装时出错
  • #设计模式#4.6 Flyweight(享元) 对象结构型模式
  • $jQuery 重写Alert样式方法
  • (14)学习笔记:动手深度学习(Pytorch神经网络基础)
  • (4)事件处理——(2)在页面加载的时候执行任务(Performing tasks on page load)...
  • (delphi11最新学习资料) Object Pascal 学习笔记---第2章第五节(日期和时间)
  • .net core开源商城系统源码,支持可视化布局小程序
  • .NET 中 GetHashCode 的哈希值有多大概率会相同(哈希碰撞)
  • .net(C#)中String.Format如何使用
  • .NET框架
  • .NET中的十进制浮点类型,徐汇区网站设计
  • .NET中使用Protobuffer 实现序列化和反序列化
  • @value 静态变量_Python彻底搞懂:变量、对象、赋值、引用、拷贝
  • [C#]使用深度学习算法opencvsharp部署RecRecNet广角图像畸变矫正校正摄像广角镜头畸变图像
  • [C++] sqlite3_get_table 的使用
  • [ComfyUI]Flux+MiniCPM-V强强联手艺术创意,媲美GPT4V级国产多模态视觉大模型
  • [go 反射] 进阶