完整教程:《简易制作 Linux Shell:详细分析原理、设计与实践》

news/2025/11/15 18:41:42/文章来源:https://www.cnblogs.com/yangykaifa/p/19226036

前引:你是否好奇 Bash 是如何将你输入的命令变成操作系统的实际动作?本项目将一步步教你实现一个支持基本命令执行、管道、重定向和后台运行的 Linux Shell。通过亲手编写代码,你将加深对 Linux 进程模型、文件描述符、信号机制和系统调用的理解,同时提升你的系统编程能力!

目录

简易版:

【一】打印命令行

【二】输入命令行

【三】解析命令行

【四】父子进程创建

【五】进程替换

【六】子进程回收

【七】封装整理

挑战版:

【一】解决cd指令

【二】解决echo命令


简易版:

【一】打印命令行

在原本的shell中,我们每次都可以看见如下的打印,等待你输入指令:

所以可以看到需要获取一些用户当前的环境变量信息,因为这里属于实现,所以我们选择函数调用的方式(getenv(),头文件#include<stdlib.h>)来获取环境变量(获取内容自己个性化设置!):

【二】输入命令行

这里我们要开始输入命令行,在之前我们已经学了命令行的输入其实是一个个字符串,例如:

“ls” “pwd” “touch”等,我们每次输入都是输入的字符串,再根据空格去分割,我们使用 fgets()

函数原型:

char *fgets(char *str, int n, FILE *stream);

第一个参数:一个字符串指针,用来存放从流中读取到的字符串

第二个参数:最多读取的字符数

第三个参数:输入的文件流(stdin:标准输入流,通常是键盘输入)

例如:

#define MAX 32
char str[MAX];
//命令获取
fgets(str,sizeof(str)-1,stdin);
//注意去除用户输入的换行符
str[strlen(str)-1]='\0';

效果:将键盘输入的字符串存储到 str 里面,str存储的类似:“pwd ls rm”这样的一整个字符串

【三】解析命令行

现在我们已经利用C语言的库函数 fgets()函数将命令行存储到了 str 数组里面,现在我们通过字符串分割来根据空格和\0将每个命令解析出来,放在一个字符串数组里面:

这里采用的是strtok()库函数,头文件:<string.h>,分隔符会被替换成 \0

函数原型:

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

第一个参数:从 str 里面拿要分割的字符串

第二个参数:根据delim里面每个字符来进行分割,比如“./-+”

(注意:如果想多次分割,第一次传字符串,后续传 NULL否则会从头重新分割)

返回值:

成功:返回指向当前分割出的子串(token)的指针

没有更多子串:返回 NULL

例如:

#include
char* argv[MAX]={NULL};
const char* delim=" \0";
int i = 0;
//命令行提取
argv[i++]=strtok(str,delim);
while(argv[i-1])
{argv[i++]=strtok(NULL,delim);
}

效果:将刚才的一整个字符串,根据空格截取每段到一个字符串数组里面

【四】父子进程创建

既然我们现在获取了命令行参数,调用就很简单了,可以先分割父子进程:

pid_t d =fork();
if(d==0)
{....
}
else
{....
}

【五】进程替换

将当前子进程的代码数据采用 execvp()进行替换,它的第一次参数只需要是路径就行

注意:如果用户输入的是换行符,需要判断一下!

//argv就是提取的字符串数组
//进程替换
if(argv[0]==NULL)
{return 0;
}
int count =  execvp(argv[0],argv);
if(count<0)perror("execvp failed");
exit(0);

【六】子进程回收

这里我采用的是阻塞等待,也可以采用非阻塞等待,自定义!

//回收子进程
int count = waitpid(-1,NULL,0);
if(count<0)
{printf("子进程回收失败\n");
}

【七】封装整理

现在我们用函数来封装一下,更加的美观!(注意:拷贝传参自动带清零的效果!)

#include
#include
#include
#include
#include
#define LEFT "["
#define RIGHT "]"
#define MAX 32
int argc=0;
char str[MAX];
char* argv[MAX]={NULL};
const char* delim=" ";
//命令行打印
void command_printf()
{printf(LEFT"%s"":""%s"" ""#"RIGHT" ",getenv("USER"),getenv("HOME"));
}
//命令行获取
char* command_get(char str[MAX],int size)
{char* pc=fgets(str,size,stdin);//去除换行符str[strlen(str)-1]='\0';//测试//printf("命令行获取测试:\n");//int i=0;//while(str[i])//{//  printf("%c",str[i++]);//}//printf("\n");return pc;
}
//命令行提取
void command_extraction(char str[MAX],const char* delim,char* argv[MAX])
{int i=0;//printf("命令行提取测试:\n");argv[i++]=strtok(str,delim);while(argv[i-1]){argc++;//printf("argv[%d]=%s\n",i-1,argv[i-1]);argv[i++]=strtok(NULL,delim);}//printf("\n");return;
}
//命令行参数调用与回收
void command_use(char* argv[MAX])
{pid_t d = fork();if(d==0){//进程替换if(argv[0]==NULL){return;}int count =  execvp(argv[0],argv);if(count<0)perror("execvp failed");//子进程退出exit(EXIT_FAILURE);}else{//回收子进程int count = waitpid(-1,NULL,0);if(count<0){printf("子进程回收失败\n");}}return;
}
int main()
{while(1){   //命令行打印command_printf();//命令获取int size=sizeof(str)-1;char* pc=command_get(str,size);if(pc==NULL){printf("读取失败\n");}//命令行提取command_extraction(str,delim,argv);//命令行参数调用command_use(argv);}return 0;
}

效果展示:

挑战版:

现在我们已经完成了基本的shell功能,但是像 echo $PATH、cd ../ 这些内置命令,例如:

原因:在 Linux/Unix 下,shell 命令分为两类:

(1)外部命令例如 lscatps 等,它们是磁盘上的可执行文件。当 shell 执行它们会 fork() 一个子进程,然后 execvp() 加载对应的程序

(2)内置命令(built-in)例如 cdecho(部分实现)、exportsourceexit 等。这些命令必须由 shell 自己直接执行,不能用 fork() 子进程执行,因为它们会影响 shell 自身的运行环境

【一】解决cd指令

cd 指令的效果就是改变当前的工作目录,而实现的 shell 每轮输出一次的指令效果,然后自己就挂掉了,因此不会影响到下一个子进程,所以 cd 命令不应该给子进程完成,而交给父进程,而父进程本身又是系统shell的子进程,所以我们需要父进程调用 chdir()函数:

补充知识:C语言字符串比较用strcmp()【狗头】continue只能用在循环里面【狗头】

int chdir(const char *path);
//参数为目标路径

执行逻辑:

//cd命令判断
if(strcmp(argv[0],"cd")==0 && argc==2)
{/如果是跳到当前目录if(strcmp(argv[1],"./")==0){return;}else{//剩余可以交给chdir函数const char*path=argv[1];int count = chdir(path);if(count==-1){printf("路径执行错误\n");return;}}
}

效果展示:

【二】解决echo命令

shell 不是直接把 $PATH 传给 echo 程序,而是先替成 /usr/local/sbin:/usr/local/bin:... 这样的真实值(命令展开)因此我们需要先判断第二个参数的开头是不是 $ 符号(是则getenv()替换)

原理:先用getenv()获取展开的环境变量,再替换argv[1],就可以直接打印出来

//echo命令判断
if(strcmp(argv[0],"echo")==0 && argc==2)
{//取第二个参数char* pc=argv[1];//防止只有一个¥if(pc[0]=='$' && strlen(pc)>1){//去除¥char* var_name = pc + 1;//获取展开的环境变量char* value=getenv(var_name);//替换if(value){argv[1]=value;}}
}

效果展示:

其它的也可以增加 export 指令,这里就不展示了!正确处理环境变量即可!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/966444.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

计算机网络5 - 指南

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

2025年境外商务出差保险哪里有卖:TOP10平台专业解析

2025年境外商务出差保险哪里有卖:TOP10平台专业解析在当今全球化的商业环境中,境外商务出差已成为众多企业和商务人士的常态。然而,对于有境外商务出差需求的人来说,面临着诸多难题。“选品难(产品繁杂无从下手)…

2025年开除申诉靠谱机构推荐:专业学术申诉机构评测指南!

2025年开除申诉靠谱机构推荐:专业学术申诉机构评测指南!留学途中遭遇学术紧急情况?面临开除、停学或学术不端听证会,一家靠谱的申诉支持机构至关重要。本文基于教育部涉外监管认证信息、机构服务响应速度、申诉成功…

Day39(9)F:\硕士阶段\Java\课程代码\后端\web-ai-code\web-ai-project01\jdbc-demo+springboot-web-quickstart

DQL条件查询-- =================== DQL: 条件查询 ====================== -- 1. 查询 姓名 为 柴进 的员工 select * from emp where name = 柴进;-- 2. 查询 薪资小于等于5000 的员工信息 select * from emp where…

# Android Compose 实现 左滑删除

Android Compose 实现 左滑删除Android Compose 实现 左滑删除 直接看源码 private enum class CardState {Collapsed /* 收缩 */, Expanded /* 展开的 */ // 哈哈哈,还能学点英文 (: } @Composable private fun Pers…

win10pro sn

win10pro snVK7JG-NPHTM-C97JM-9MPGT-3V66T

完整教程:PMBT2222A,215 开关晶体管功率二极管 NXP安世半导体 音频放大电路 LED驱动 应用

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

用递归的方式输出各位数字

include <stdio.h> include <stdlib.h> void f(int n){ if(n<10){ printf("%d ",n); return; }//输出到最后一位 else{int r=0;int x=n;int k=0;//n是k位数while(x!=0){r=x%10;x=x/10;k++;}i…

WebServer类 - 指南

WebServer类 - 指南pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "C…

EFCore中巧妙利用ToQueryString()实现批插(不借助第三方包)

dotnet10发布了,ef10也快发布了,但是还是只有批量更新(ExecuteUpdateAsync)和批量删除(ExecuteDeleteAsync)功能,没有批量插入。 今天给个办法,在不引用第三方库的情况下,巧妙利用ToQueryString()实现批插。 …

2025 年 11 月门窗十大品牌综合实力权威推荐榜单,产能、专利、环保三维数据透视

引言 2025 年 10 月门窗十大品牌综合实力权威推荐榜单由中国建筑金属结构协会、全国工商联家具装饰业商会联合发布。本次榜单突破传统单一性能评选模式,以《铝合金门窗》(GB/T 8478-2008)为技术底版,创新性构建 “…

20232426 2025-2026-1 《网络与系统攻防技术》实验五实验报告

20232426 2025-2026-1 《网络与系统攻防技术》实验五实验报告 一、实验目标选择指定域名,用要求工具查询DNS、IP相关信息及地理位置。 获取某平台好友IP并查询其具体地理位置。 用nmap扫描靶机,确认其活跃度、端口、…

AzuraCast:自托管一体化网络电台管理套件

AzuraCast是一个功能强大的自托管网络电台管理套件,提供完整的电台解决方案,包括流媒体广播、节目编排、用户管理和API接口,支持多种广播格式和协议,轻松搭建专业级网络电台。AzuraCast:自托管一体化网络电台管理…

2025年11月安徽省有实力的旧房翻新企业综合推荐排行榜

摘要 随着国内城市化进程加速和居民生活品质提升,2025年旧房翻新行业迎来爆发式增长,安徽省特别是合肥地区的旧房翻新需求显著上升。本文基于行业数据、用户口碑、技术实力等多维度评估,为您推荐2025年11月安徽省最…

【前缀和+差分+二分】LeetCode 2528. 最大化城市的最小电量

View Post【前缀和+差分+二分】LeetCode 2528. 最大化城市的最小电量题目 https://leetcode.cn/problems/maximize-the-minimum-powered-city/description/ 题解 以stations = [1,2,4,5,0], r = 1, k = 2为测试用例进行…

Springboot启动时记录进程ID

Springboot启动时记录进程ID 1. 背景说明 springboot项目打包成可执行jar包以后,需要通过java -jar xxx.jar启动项目.启动方式对非技术人员不太友好.所以需要项目构建时,生成一个start.bat和stop.bat的脚本.关闭采用ta…

019数据结构之栈——算法备赛 - 实践

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

详细介绍:【Linux】07.Ubuntu开发环境部署

pre { white-space: pre !important; word-wrap: normal !important; overflow-x: auto !important; display: block !important; font-family: "Consolas", "Monaco", "Courier New", …

GESP考试报名附考试报名流程

GESP 考试报名 附考试报名流程 一、考试时间安排 1.报名时间 一般为每次认证考试前1-2个月,具体时间以GESP官网公布时间为准。 2.考试时间 CCF每年安排4次GESP认证考试,分别在3月、6月、9月和12月。2025年的考试时间…

2025 最新电缆品牌权威推荐:耐火 / 阻燃 / 智能 / 光伏等全品类优质厂商榜单,附国际认证测评

引言 随着全球电力基建、新能源产业及智能建筑的高速发展,电缆作为核心传输载体,其品质直接决定工程安全与运行效率。据国际电线电缆制造商协会(ICEA)最新测评数据显示,全球合格电缆产品仅占市场总量的 68%,32% …