Linux 自主 shell 编写(C 语言实现)

embedded/2024/9/19 23:40:10/ 标签: linux, c语言, 服务器, 运维

Linux 自主 shell 编写(C 语言实现)

  • 效果
  • 主要步骤
    • 打印命令行提示符
    • 获取用户命令字符串
    • 切割用户命令字符串
    • 执行命令
    • 循环
  • 至此源码(简易半成品)
  • 细节
    • 内建命令问题
      • cd
    • 退出码问题
    • echo 查看退出码
  • 完整源码
    • makefile
    • myshell.c

效果

效果嘛和 命令行解释器 一模一样,这里就不贴图了

只是把 # (超管)$ (普通用户) 符号改为 > 以作区分

注意哦: 删除键不能直接使用,要配合 ctrl 键才行

主要步骤

打印命令行提示符

在 Linux 终端(命令行)里,首先看到的是 命令行提示符

[exercise@localhost my_shell]$ 

shell 一旦跑起来,定是要先打印 命令行提示符 的,但是这玩意对于不同的用户是不一样的呀,所以不能单纯的打印出来,而是要获取用户名,主机名等等,如何获取?目前来说对各种 系统接口还不熟,那就直接使用 环境变量

命令行执行 env 命令,就可以看到很多 环境变量 ^ ^

系统环境变量 很多,不容易直接得到想要的,所以可以使用库函数 getenv 来获取,需要包含头文件 #include <stdlib.h> ,函数原型如下:

char *getenv(const char *name);

那么 用户名主机名工作目录 分别在 USERHOSTNAMEPWD 内,直接使用 getenv 函数获取即可

最后使用 snprintf() 函数拼接成 命令行提示符 的格式即可,函数原型:

int snprintf(char *str, size_t size, const char *format, ...);

获取用户命令字符串

C 语言 获取键盘字符串 可以使用库函数 scanf() ,但它遇到空格可就不继续读取了,而它的高端玩法还不熟

咱就老老实实使用 fgets 函数,原型:

char *fgets(char *s, int size, FILE *stream);

切割用户命令字符串

这一步是必要的,因为日后一定是需要 进程替换 的,进程替换 就一定需要将用户命令以空格为分隔符打散分开,是库函数参数的原因,是刚需

如何实现呢?倒是也很简单,我们可以直接将空格替换为 '\0' ,那么一个长串就变为若干个子串

如果要执行用户输入的命令,是要创建子进程来完成的;那我们就需要为进程传递 命令行参数 来实现,毕竟不同的选项具有不同的功能,所以切割的字串分别放入 命令行参数表 argv[] 里即可,argv 的每一个元素都是一个指针,指向被切完成的子串(最后一个指针为 NULL

那么只需要将 argv 的第一个元素指向第一个子串,第二个元素指向第二个子串,以此类推

但这比较麻烦,咱可以使用库函数 strtok() 完成; 命令行参数表 也可以设置为全局的,好调用

执行命令

获取用户的命令后,不执行等啥呢?

当然啦,执行命令不是自己当前进程来执行,而是 创建子进程,在利用 进程替换,此时子进程就可以执行你想要的全新的代码

循环

一个 shell 怎么能只运行一条命令呢?所以我们需要将上述过程循环起来,这样就能无限制运行命令

至此,简易到不能再简易的 shell 就实现好了

至此源码(简易半成品)

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32char* gArgv[NUM];const char* getUserName()
{const char* username = getenv("USER");if (username == NULL) return "None";return username;
}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 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);
}int GetUserCommand(char command[], size_t size)
{char* s = fgets(command, size, stdin);if (s == NULL) return -1;command[strlen(command) - 1] = ZERO;return strlen(command);
}void SplitCommand(char command[], int size)
{gArgv[0] = strtok(command, SEP);int index = 1;while ((gArgv[index++] = strtok(NULL, SEP)));}void Die()
{exit(1);
}void ExecuteCommmand()
{pid_t id = fork();if (id < 0) Die();else if (id == 0){// childexecvp(gArgv[0], gArgv);exit(errno);}else {// parentint status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){}}
}int main()
{int quit = 0;while (!quit){// 自己需要输出一个命令行MakeCommandLineAndPrint();// 获取用户命令字符串char usercommand[SIZE];int num = GetUserCommand(usercommand, sizeof(usercommand));if (num < 0) return 1;// 分割用户命令字符串SplitCommand(usercommand, sizeof(usercommand));// 执行命令ExecuteCommmand();}return 0;
}

细节

上面的代码虽然说可以运行,但有很多漏洞和细节尚未修补实现,接下来一一填补:

内建命令问题

cd

举个例子吧,上面的代码先跑起来,不说其他的,试试 cd 命令能不能正常运行

不能正常运行!!! 这个的漏洞不是一般的大,并不是不能使用 cd 命令,而是命令 cd 对咱这个 shell 不起任何作用

而如果你们运行上面的残本代码,会发现当前的工作路径是一串绝对路径,要想切割最后一个目录拿过来倒也容易,但 cd 还是无法生效啊

为什么?

其实很简单,我们是实现 shell 的方法是 创建子进程,然后拿想要的进程去替换这个子进程;由于进程的独立性,子进程会影响父进程吗?肯定不会,那子进程执行 cd 命令和你父进程有什么关系呢?子进程执行 cd 命令的时候父进程在干嘛?在那 wait 呢!!!

所以这样实现父进程 shell 的工作路径改不了的,那如何能改?当然是父进程自己执行咯

所以像 cd 这样的命令是 内建命令

既如此,观察上述代码,在执行命令之前 需要检查是否有 内建命令

如何检查?

直接判断不就行了,它有几个 内建命令,咱就判断几次,如果用户输入的是 cd 命令,shell 就自己执行

如何执行?这种涉及系统的东西当然要 系统调用 嘛,chdir 可以将当前进程的工作路径,切换至你想要的路径,那咱们就可以直接 将用户输入的路径 传进 chdir 的参数里即可

注意如果直接运行 cd 命令,是返回用户家目录的;所以如果切割后的子串只有 cd ,第二个元素路径为 NULL 的话,可直接返回 用户家目录(可函数实现)

改完之后记得要修改 shell 下一次打印出来的命令行路径,因为这是被我封装为函数的,直接修改较为麻烦,但我是从环境变量里获取的,所以直接修改环境变量即可:
首先使用函数 getcwd ,此函数可以直接获取真正的工作路径,然后拼接 PWD ,再使用函数 putenv() 来刷新环境变量

// 内建命令 cd 的执行过程
void Cd()
{// 获取 cd 路径const char* path = gArgv[1];if (path == NULL) path = getHome();// 此时 path 一定存在,那么可以直接使用 系统调用 修改工作路径chdir(path);// 获取此时的工作路径char temp[SIZE * 2];getcwd(temp, sizeof(temp));//  拼接 PWD 环境变量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();}// 继续判断其他内建命令...return yes;
}

至此,最大的坑已经被补上了,至于命令行解释器里,当前工作目录的切割,使用宏函数可直接实现(后附完整源码),这里不做解释

// 宏函数
#define SkipPath(pCwd) do { pCwd += (strlen(pCwd) - 1); while (*pCwd != '/') --pCwd; } while (0)

退出码问题

父进程是一定要得到子进程的退出码的,不然有问题无法准确反馈给用户

具体实现也是进程替换的内容,非常简单,看源码

echo 查看退出码

当然是下面这个命令啦:

echo $?

和上面 cd 命令一样,需要在 CheckBuildIn 函数里进行判断是否有 echo $? 命令,逻辑编写十分简单,在 CheckBuildIn 函数里编写即可:

else if (strcmp(enter_cmd, "echo") == 0 && strcmp(gArgv[1], "$?") == 0)
{yes = 1;printf("%d\n", lastcode);lastcode = 0;
}

完整源码

CentOS 7.9 平台 gcc 编译测试,进入 可执行文件 MyShell 所在目录下, ./MyShell 即可运行

makefile

bin=MyShell
src=myshell.c$(bin):$(src)gcc $^ -o $@
.PHONY:clean
clean:rm -f $(bin)

myshell.c

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <string.h>
#include <errno.h>#define SIZE 512
#define ZERO '\0'
#define SEP " "
#define NUM 32
#define SkipPath(pCwd) do { pCwd += (strlen(pCwd) - 1); while (*pCwd != '/') --pCwd; } while (0)char* gArgv[NUM];
char Cwd[SIZE];
int lastcode = 0;const char* getHome()
{const char* home = getenv("HOME");if (home == NULL) return "/";return home;
}const char* getUserName()
{const char* username = getenv("USER");if (username == NULL) return "None";return username;
}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 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 size)
{char* s = fgets(command, size, stdin);if (s == NULL) return -1;command[strlen(command) - 1] = ZERO;return strlen(command);
}void SplitCommand(char command[], int size)
{(void)size;gArgv[0] = strtok(command, SEP);int index = 1;while ((gArgv[index++] = strtok(NULL, SEP)));}void Die()
{exit(1);
}void ExecuteCommmand()
{pid_t id = fork();if (id < 0) Die();else if (id == 0){// childexecvp(gArgv[0], gArgv);exit(errno);}else {// parentint 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()
{// 获取 cd 路径const char* path = gArgv[1];if (path == NULL) path = getHome();// 此时 path 一定存在,那么可以直接使用 系统调用 修改工作路径chdir(path);// 获取此时的工作路径char temp[SIZE * 2];getcwd(temp, sizeof(temp));//  拼接 PWD 环境变量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){// 自己需要输出一个命令行MakeCommandLineAndPrint();// 获取用户命令字符串char usercommand[SIZE];int num = GetUserCommand(usercommand, sizeof(usercommand));if (num < 0) return 1;else if (num == 0) continue;// 分割用户命令字符串SplitCommand(usercommand, sizeof(usercommand));// 检查命令是否为内建命令num = CheckBuildIn();if (num) continue;// 执行命令ExecuteCommmand();}return 0;
}

http://www.ppmy.cn/embedded/111579.html

相关文章

根据NVeloDocx Word模板引擎生成Word(一)

自从我们基于免费开放的E6开发平台&#xff0c;实现了根据Word模版生成Word文档的模版引擎后&#xff0c;也实实在在帮助到了一些有需要的朋友。但是由于制作Word模版的过程对于很多人会是一个很头疼的事情&#xff0c;虽然提供了详细的文档&#xff0c;但是我们也经常接到他们…

HCIA--实验十:路由的递归特性

递归路由的理解 一、实验内容 1.需求/要求&#xff1a; 使用4台路由器&#xff0c;在AR1和AR4上分别配置一个LOOPBACK接口&#xff0c;根据路由的递归特性&#xff0c;写一系列的静态路由实现让1.1.1.1和4.4.4.4的双向通信。 二、实验过程 1.拓扑图&#xff1a; 2.步骤&am…

【网络安全】服务基础第二阶段——第二节:Linux系统管理基础----Linux统计,高阶命令

目录 一、Linux高阶命令 1.1 管道符的基本原理 1.2 重定向 1.2.1 输出重定向 1.2.2 输入重定向 1.2.3 wc命令基本用法 1.3 别名 1.3.1 which命令基本语法 1.3.2 alias命令基本语法 1.4 压缩归档tar 1.4.1 第一种&#xff1a;gzip压缩 1.4.2 第二种&#xff1a;bzip…

Docker续9:使用docker-compose部署nmt项目,在haproxy中代理mysql负载均衡

为之前的docker-compose添加mysql负载均衡 [rootlocalhost pes]# rm -rf mysql/data/auto.cnf //先删除这个文件&#xff0c;这个文件保证了mysql的唯一&#xff0c;因为待会要做一个从mysql&#xff0c;所以必须删除 [rootlocalhost pes]# cp -r mysql/data mysql/data0…

【使用CPU和使用GPU的算法】

一、通常耗费 GPU 的算法 想象 GPU 是一个有很多小工人同时干活的工厂。像训练很厉害的图像识别网络、处理复杂的图形渲染以及处理大量数据的时候&#xff0c;就好比有很多相似的任务可以同时进行。比如&#xff0c;图像识别的时候要对很多像素点同时做计算&#xff0c;这就很…

大数据Flink(一百一十六):Flink SQL的时间属性

文章目录 Flink SQL的时间属性 一、Flink 三种时间属性简介 二、Flink 三种时间属性的应用场景 三、​​​​​​​SQL 指定时间属性的两种方式 四、​​​​​​​​​​​​​​SQL 处理时间DDL定义 五、​​​​​​​​​​​​​​SQL 事件时间DDL定义 Flink SQL的时…

7项国家机密被境外公司窃取!

近日&#xff0c;国家安全机关公开了一起境外公司非法搜集窃取我稀土领域国家秘密案。该公司通过利诱中方人员&#xff0c;窃取7项机密级国家秘密。 “内鬼”为51万美元泄露国家7项机密&#xff01; 该公司中方雇员叶某某在外籍员工指挥下&#xff0c;利诱收买国内某稀土公司…

基于STM32设计的水渠闸门远程控制系统(华为云IOT)(226)

文章目录 一、前言1.1 项目介绍【1】开发背景【2】项目实现的功能【3】项目硬件模块组成【4】ESP8266工作模式配置1.2 设计思路【1】整体设计思路【2】整体构架【3】上位机开发思路1.3 项目开发背景【1】选题的意义【2】可行性分析【3】参考文献【4】摘要1.4 开发工具的选择【1…

应急响应实战---是谁修改了我的密码?

前言&#xff1a;此次应急响应为真实案例&#xff0c;客户反馈无法通过密码登录服务器&#xff0c;疑似服务器被入侵 0x01 如何找回密码&#xff1f; 客户服务器为windows server2019&#xff0c;运维平台为PVE平台&#xff1b;实际上无论是windows系统或者是linux系统&#…

语言中的溢出

任何数据类型都有最大和最小值&#xff0c;会溢出是由物理特性决定。编译型静态类型语言一般明确定义类型大小&#xff0c;这种定义出来的类型都存在溢出的可能。有的动态语言为减少程序员溢出的困扰&#xff0c;尽可能为数据扩展成更大的数值&#xff0c;Python就是这样的好人…

js 笔记

javascript编程语言 html css标记语言 html定义网页内容 css是描述网页布局 js控制网页行为 Livescript 为了确保不同浏览器上运行的javascript标准一致&#xff0c;所以几个公司共同制定了js的标准…

Modbus通信

Modbus是一种经典的工业通信协议&#xff0c;由Modicon&#xff08;现为施耐德电气&#xff09;在1979年首次发布。它广泛应用于各种工业自动化系统中&#xff0c;尤其是在PLC&#xff08;可编程逻辑控制器&#xff09;与其他设备之间的通信。Modbus的主要特点是其简单性和开放…

API 架构(RPC风格、RESTful风格)

API 架构风格笔记 RPC风格的接口案例RESTful风格的接口案例两者比对 RPC风格的接口案例 RPC&#xff08;Remote Procedure Call&#xff0c;远程过程调用&#xff09;风格的接口设计主要关注于远程服务调用的过程&#xff0c;而不强调资源的表述。以下是RPC风格接口的一个案例…

FAT32文件系统详细分析 (格式化SD nandSD卡)

FAT32 文件系统详细分析 (格式化 SD nand/SD 卡) 目录 FAT32 文件系统详细分析 (格式化 SD nand/SD 卡)1. 前言2.格式化 SD nand/SD 卡3.FAT32 文件系统分析3.1 保留区分析3.1.1 BPB(BIOS Parameter Block) 及 BS 区分析3.1.2 FSInfo 结构扇区分析3.1.3 引导扇区剩余扇区3.1.4 …

为什么自动驾驶技术的实现离不开4G+5G多卡聚合?

如今&#xff0c;汽车制造商和零部件巨头都在研究自动驾驶相关技术。要实现汽车的自动驾驶&#xff0c;不乏相关技术与道路环境的结合和变化。但要实现这一目标&#xff0c;最重要的环节无疑是建设网络。 在4G时代&#xff0c;随着网络带宽和速度的提高&#xff0c;可以实现实…

佰朔资本:未来钢铁行业产业格局有望稳中趋好

组织指出&#xff0c;未来钢铁作业工业格式有望稳中趋好&#xff0c;叠加当时部分公司已经处于价值小看区域&#xff0c;现阶段仍具结构性出资机会&#xff0c;尤其是拥有较高毛利率水平的优特钢企业和本钱管控力度强、具有规划效应的龙头钢企&#xff0c;未来存在估值修改的机…

zabbix6.4连接钉钉发出警告

zabbix6.4配置钉钉告警 注册钉钉 建一个内部群 添加自定义机器人 配置zabbix服务端 打开脚本告警的配置 # vim /etc/zabbix/zabbix_server.conf AlertScriptsPath/usr/lib/zabbix/alertscripts 准备脚本 安装一个依赖包 # dnf -y install python3-requests # vim /usr/li…

PCB电路板缺陷-目标检测数据集(包括VOC格式、YOLO格式)

PCB电路板缺陷-目标检测数据集&#xff08;包括VOC格式、YOLO格式&#xff09; 数据集&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1tm2ArZc4_rBA5NGFNDUPgA?pwd19iz 提取码&#xff1a;19iz 数据集信息介绍&#xff1a; 共有 1232 张图像和一一对应的标注文件 …

基于Ubuntu2404搭建mysql8配置远程访问

使用系统为Ubuntu2404&#xff0c;mysql8版本为8.0.36 安装mysql apt install -y mysql-server设置开机自启动 systemctl enable --now mysql修改密码&#xff0c;似乎是bug&#xff0c;修改密码第一次不成功&#xff0c;第二次可以 mysql use mysql; update user set Host…

PHP创意无限一键生成小程序名片生成系统源码

创意无限&#xff0c;一键生成 —— 小程序名片生成系统&#xff0c;开启你的个性化商务新时代&#xff01; 一、告别千篇一律&#xff0c;拥抱个性化名片 你还在使用那些千篇一律的传统纸质名片吗&#xff1f;是时候做出改变了&#xff01;现在有了“创意无限一键生成小程序名…