Linux 自主 shell 编写(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);
那么 用户名、 主机名 和 工作目录 分别在 USER
、HOSTNAME
和 PWD
内,直接使用 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;
}
相关文章:
Linux 自主 shell 编写(C 语言实现)
Linux 自主 shell 编写(C 语言实现) 效果主要步骤打印命令行提示符获取用户命令字符串切割用户命令字符串执行命令循环 至此源码(简易半成品)细节内建命令问题cd 退出码问题echo 查看退出码 完整源码makefilemyshell.c 效果 效果…...
pointpillar部署-TensorRT实现(一)
1. 主干部分 核心部分分为:PreProcessCuda前处理; TRT(ppOnnxPath, stream_)模型推理; PostProcessCuda(stream_)后处理 内存管理部分: cudaMallocManaged 统一内存管理,无须进行cpu内存申请,gpu内存申请,cpu到gpu的数据拷贝过程。cudaMallocManaged 即可完成同一个变量…...

ubuntu使用命令行查看硬件信息
ubuntu使用命令行查看硬件信息 CPU cat /proc/cpuinfo其中,model name就显示了cpu的型号,cpu cores显示cpu的所有物理核心数量。 内存 cat /proc/meminfo其中,MemTotal就显示总内存大小,这里为32GB内存,SwapTotal显…...

vue国际化vue-i18n搭配i18n-ally实现多语言国际化
i18n-ally 是一款 VS Code 插件,为开发者提供了一套强大而简便的工具,以轻松实现国际化(i18n)。本文将介绍如何使用 i18n-ally 插件,实现应用程序的多语言支持。 一:安装vscode插件。 首先,在 Visual Stu…...
Linux(1)--Linux简介
文章目录 1. 基本概念2. 版本2.1 RedHat红帽2.2 CentOS2.3 Ubuntu2.4 Debian2.5 Kali Linux 3. Linux应用场景 1. 基本概念 Linux,全称GNU/LInux,本质上是一个类UNIX系统。 普通用户使用Linux的比较少,大家普遍比较熟悉微软公司的Windows和…...
Python——破解rar压缩包密码
破解RAR压缩包密码一般是通过穷举法来实现的,即尝试所有可能的密码组合,直到找到正确的密码为止。 以下是使用Python编写的一个简单的RAR密码破解程序: import itertools import rarfiledef crack_rar_password(rar_file, password_length)…...
取指操作流程
取指操作,即指令获取(Instruction Fetch),是计算机执行程序时的一个基本且至关重要的步骤。这一过程虽然是自动进行的,但控制器的参与是不可或缺的,尽管它不需要针对每次取指操作接收一个明确的“取指”指令…...

Git:远程项目代码上传管理
本地代码上传至远端仓库,需要下载git,访问官网下载https://git-scm.com/downloads 一、初始化本地仓库 首先要在项目本地,打开Git Bash,输入以下代码! git init 然后进行全局设置用户名和邮箱,使用以下代码…...

MySQL数据库的介绍
目录 1.什么是MySQL数据库 2.MySQL数据库的设计 MySQL的进一步认识 MySQL的客户端 —— mysql MySQL的服务端 —— mysqld 3.MySQL数据库的架构 MySQL架构图 连接层 服务层 存储引擎层 文件系统层 4.MySQL的存储引擎 认识存储引擎 MySQL中的存储引擎 存储引擎之…...

div内英文不换行问题以及解决方案
div内英文不换行问题以及解决方案 div盒子中文字换行问题:div中放中文的代码:div中放英文的代码: 解决办法注意 div盒子中文字换行问题: div设置宽度以后,如果div中放的是中文,默认文字超过div宽度会自动换…...

『功能项目』DOTween动态文字【26】
打开上一篇25协程生成怪物模型的项目, 本章要做的事情是用DOTween插件做一个动态文字效果 首先在资源商店中免费下载一个DOTween插件 新建脚本:DowteenFlicker.cs 编写脚本: using DG.Tweening; using UnityEngine; using UnityEngine.UI;pu…...
经验笔记:框架(Framework)与库(Library)
框架(Framework)与库(Library)的经验笔记 引言 在现代软件开发过程中,框架(Framework)与库(Library)是两个不可或缺的概念。虽然它们都是为了提升开发效率和服务复用性…...
每日一题——第八十七题
题目:给出年月日,计算该日期是这一年的第几天 #include<stdio.h> #include<stdbool.h>bool isLeapYear(int year) {return (year % 4 0 && year % 100 ! 0) || (year % 400 0); }int dayOfYear(int year, int month, int day) {/…...

CTF——简单的《WEB》
文章目录 一、WEB1、easysql2、baby_web3、baby_sql4、upload_easy5、easygame拓展1.1拓展1.2 6、ht_ssti7、包容乃大 一、WEB 1、easysql 题目描述: sql注入漏洞 1.常用的sql注入测试语句 2.sql注入bypass 解题思路 这边提示基本给的也很完整的,不…...

【Nacos】报错之服务实例类型不允许改变
在使用Nacos配置服务的实例类型的时候,对服务的实例类型进行修改。 之前的非临时实例,修改为临时实例后,报错: com.alibaba.nacos.api.exception.NacosException: errCode: 400, errMsg: Current service DEFAULT_GROUPproduct-…...

SRS流媒体服务器从入门到精通(其一,环境搭建)
欢迎诸位来阅读在下的博文~ 在这里,在下会不定期发表一些浅薄的知识和经验,望诸位能与在下多多交流,共同努力! 江山如画,客心如若,欢迎到访,一展风采 文章目录 一、SRS简介二、SRS的应用场景三、环境搭建…...
Java Native Interface (JNI) 简介
Java Native Interface (JNI) 概述 Java Native Interface (JNI) 是 Java 提供的一种接口,用于允许 Java 应用程序与本地(Native)代码进行交互。通过 JNI,Java 代码可以调用 C/C 等其他语言编写的库,反之亦然。JNI 的主…...

navigator.mediaDevices.getUserMedia检查用户的摄像头是否可用,虚拟摄像头问题
在Web开发中,检查用户的摄像头是否可用是一个常见的需求,尤其是在需要视频聊天或录制视频的应用程序中。navigator.mediaDevices.getUserMedia() API 提供了这一功能,它允许你请求访问用户的媒体设备,如摄像头和麦克风。虽然这个A…...

跨境网红营销SOP流程1.0丨出海笔记
品牌出海利用红人营销基本是标配了,KOL 社交媒体是绝对的带货神器。比如美国歌手蕾哈娜Rihanna 的美妆品牌 Fenty Beauty 上市开卖后40天就达到了1亿美元,火遍全球美妆圈。例子和废话少说,其实大小红人都有用。 之前几位大神已经在出海笔记分…...

Jedis,SpringDataRedis
快速入门 导入依赖 <!--jedis--><dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId><version>3.7.0</version></dependency><!--单元测试--><dependency><groupId>org.ju…...

AI-调查研究-01-正念冥想有用吗?对健康的影响及科学指南
点一下关注吧!!!非常感谢!!持续更新!!! 🚀 AI篇持续更新中!(长期更新) 目前2025年06月05日更新到: AI炼丹日志-28 - Aud…...

基于距离变化能量开销动态调整的WSN低功耗拓扑控制开销算法matlab仿真
目录 1.程序功能描述 2.测试软件版本以及运行结果展示 3.核心程序 4.算法仿真参数 5.算法理论概述 6.参考文献 7.完整程序 1.程序功能描述 通过动态调整节点通信的能量开销,平衡网络负载,延长WSN生命周期。具体通过建立基于距离的能量消耗模型&am…...

JavaScript 中的 ES|QL:利用 Apache Arrow 工具
作者:来自 Elastic Jeffrey Rengifo 学习如何将 ES|QL 与 JavaScript 的 Apache Arrow 客户端工具一起使用。 想获得 Elastic 认证吗?了解下一期 Elasticsearch Engineer 培训的时间吧! Elasticsearch 拥有众多新功能,助你为自己…...

【Redis技术进阶之路】「原理分析系列开篇」分析客户端和服务端网络诵信交互实现(服务端执行命令请求的过程 - 初始化服务器)
服务端执行命令请求的过程 【专栏简介】【技术大纲】【专栏目标】【目标人群】1. Redis爱好者与社区成员2. 后端开发和系统架构师3. 计算机专业的本科生及研究生 初始化服务器1. 初始化服务器状态结构初始化RedisServer变量 2. 加载相关系统配置和用户配置参数定制化配置参数案…...

【第二十一章 SDIO接口(SDIO)】
第二十一章 SDIO接口 目录 第二十一章 SDIO接口(SDIO) 1 SDIO 主要功能 2 SDIO 总线拓扑 3 SDIO 功能描述 3.1 SDIO 适配器 3.2 SDIOAHB 接口 4 卡功能描述 4.1 卡识别模式 4.2 卡复位 4.3 操作电压范围确认 4.4 卡识别过程 4.5 写数据块 4.6 读数据块 4.7 数据流…...
服务器硬防的应用场景都有哪些?
服务器硬防是指一种通过硬件设备层面的安全措施来防御服务器系统受到网络攻击的方式,避免服务器受到各种恶意攻击和网络威胁,那么,服务器硬防通常都会应用在哪些场景当中呢? 硬防服务器中一般会配备入侵检测系统和预防系统&#x…...
Spring AI 入门:Java 开发者的生成式 AI 实践之路
一、Spring AI 简介 在人工智能技术快速迭代的今天,Spring AI 作为 Spring 生态系统的新生力量,正在成为 Java 开发者拥抱生成式 AI 的最佳选择。该框架通过模块化设计实现了与主流 AI 服务(如 OpenAI、Anthropic)的无缝对接&…...

select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...
大学生职业发展与就业创业指导教学评价
这里是引用 作为软工2203/2204班的学生,我们非常感谢您在《大学生职业发展与就业创业指导》课程中的悉心教导。这门课程对我们即将面临实习和就业的工科学生来说至关重要,而您认真负责的教学态度,让课程的每一部分都充满了实用价值。 尤其让我…...

Android 之 kotlin 语言学习笔记三(Kotlin-Java 互操作)
参考官方文档:https://developer.android.google.cn/kotlin/interop?hlzh-cn 一、Java(供 Kotlin 使用) 1、不得使用硬关键字 不要使用 Kotlin 的任何硬关键字作为方法的名称 或字段。允许使用 Kotlin 的软关键字、修饰符关键字和特殊标识…...