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

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

前引:你是否好奇 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.jsqmd.com/news/41242/

相关文章:

  • 计算机网络5 - 指南
  • 2025年境外商务出差保险哪里有卖:TOP10平台专业解析
  • 2025年开除申诉靠谱机构推荐:专业学术申诉机构评测指南!
  • Day39(9)F:\硕士阶段\Java\课程代码\后端\web-ai-code\web-ai-project01\jdbc-demo+springboot-web-quickstart
  • # Android Compose 实现 左滑删除
  • win10pro sn
  • 完整教程:PMBT2222A,215 开关晶体管功率二极管 NXP安世半导体 音频放大电路 LED驱动 应用
  • EFCore中巧妙利用ToQueryString()实现批插(不借助第三方包)
  • 2025 年 11 月门窗十大品牌综合实力权威推荐榜单,产能、专利、环保三维数据透视
  • 20232426 2025-2026-1 《网络与系统攻防技术》实验五实验报告
  • Springboot启动时记录进程ID
  • 019数据结构之栈——算法备赛 - 实践
  • 详细介绍:【Linux】07.Ubuntu开发环境部署
  • GESP考试报名附考试报名流程
  • 2025 最新电缆品牌权威推荐:耐火 / 阻燃 / 智能 / 光伏等全品类优质厂商榜单,附国际认证测评
  • 2025 最新电缆制造厂家推荐!电缆品牌权威榜单发布,耐火 / 智能 / 特种电缆优选企业全解析
  • 2025年Dynamics 365 CRM的工作行情如何?
  • iHaier2.0 智能协同办公模块(Doc-Collab)实现实用的方案详解
  • 字节序浅析
  • 2025 最新无缝钢管厂家推荐榜:国际测评认证 + 技术创新 + 全场景适配权威指南大口径无缝钢管/16Mn 无缝钢管/定制无缝钢管/厚壁无缝钢管公司推荐
  • 2025年硫酸钠流化床干燥机源头厂家权威推荐榜单:调味品振动流化床干燥机/大豆纤维流化床干燥机/味精振动流化床干燥机源头厂家精选
  • 2025 最新钢结构源头厂家推荐排行榜,聚焦优质供应与专业服务精选榜单美标 / 欧标钢结构 / 环保设备 / 水泥矿山 / 机械设备钢结构厂家推荐
  • 当下市面上靠谱的平移门服务商
  • 2025年11月中国伸缩门源头厂家口碑推荐榜单
  • 简单了解数组
  • 2025年目字扣订制厂家权威推荐榜单:塑料扣具/箱包插扣/五金插扣源头厂家精选
  • # 第10章 指针和结构体
  • 2025年步进式加热直饮水机订制厂家权威推荐榜单:奶茶店全自动烧水器/大型工业净水器/饭店专用开水器源头厂家精选
  • 2025 最新钢结构厂家推荐排行榜,涵盖全产业链服务与优质产能企业权威甄选钢结构建筑/钢结构房屋/钢结构屋面/钢结构网架/钢结构桁架/钢结构连廊公司推荐
  • 2025 年 11 月漆渣脱水系统,漆渣脱水机,漆渣脱水装置品牌最新推荐,产能、专利、环保三维数据透视!