Linux 学习-模拟实现【简易版bash】

article/2025/8/11 23:18:04

1、bash本质

在模拟实现前,先得了解 bash 的本质

bash 也是一个进程,并且是不断运行中的进程
证明:常显示的命令输入提示符就是 bash 不断打印输出的结果

输入指令后,bash 会创建子进程,并进行程序替换
证明:运行自己写的程序后,可以看到当前进程的 父进程 为 bash

 此时可以断定神秘的 bash 就是一个运行中的进程,因为进程间具有独立性,因此可以同时存在多个 bash,这也是多用户登录 Linux 可以同时使用 bash 的重要原因

系统自带的 bash 是一个庞然大物,我们只需根据其本质,实现一个简易版 bash 就行了


2、需求分析

bash 需要帮我们完成命令解释+程序替换的任务,因此它至少要具备以下功能:

  • 接收指令(字符串)
  • 对指令进行分割,构成有效信息
  • 创建子进程,执行进程替换
  • 子进程运行结束后,父进程回收僵尸进程
  • 输入特殊指令时的处理

3、核心内容

核心内容主要为 读取切割替换 这三部分,逐一实现,首先从指令读取开始

 3.1、指令读取

读取指令前,首先要清楚待读取命令可能有多长

  • 常见命令如 ls -a -l 长度不超过 10
  • 为了避免极端情况,这里预设命令最大长度为 1024
  • 使用数组进行指令存储(缓冲区)
char commandline[1024];//命令行

 考虑什么是指令?如何读取指令?

  • Linux 中的大部分指令由 指令 [选项] 构成,在 指令 和 [选择] 间有空格
  • 常规的 scanf 无法正常读取指令,因为空格会触发输入缓冲区刷新
  • 这里主要使用 fgets 逐行读取,可以读取到空格
void interact(char* cline,int size)//输出命令行{getpwd();printf("[%s@%s%s]#  " ,getusrname(),gethostname(),pwd); char *s=fgets(cline,size,stdin);//输入指令,有可能什么也没有输入直接回车   assert(s);(void)s;//”abcd\n\0" cline[strlen(cline)-1]='\0';//原来\n,在输入的时候也会加入到字符串中;checkdir(cline);//检查重定向}

 注意: 可能存在读取失败的情况,assert 断言解决;因为 fgets 也会把最后的 '\n' 读进去,为了避免出错,手动置为 '\0';

3.2、指令分割

获得指令后,就需要将指令进行分割

 为何要分割指令?

  • 程序替换时,需要使用 argv 表,这张表由 指令选项NULL 构成
  • 利用指令间的空格进行分割

如何分割指令?

  • C语言 提供了字符串分割函数 strtok,可以直接使用
  • 当然也可以手动实现分割

指令分割后呢?

  • 将分割好的指令段,依次存入 argv 表中,供后续程序替换使用
  • argv 表实际为一个指针数组,可以存储字符串

如 command 一样,表 argv 也需要考虑大小,这里设置为 64实际使用时也就分割为四五个指令段

strtok 是 C 语言中的一个字符串处理函数,用于将一个字符串分割成多个子字符串(tokens)。该函数定义在 string.h 头文件中。strtok 通常用于解析由分隔符(如空格、逗号等)分隔的字符串。

函数原型:

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

参数说明:

  • str:要分割的字符串。在第一次调用时,传入需要分割的原始字符串,之后的调用则传入 NULL,以继续分割上次 strtok 返回的部分。

  • delim:一个包含所有分隔符字符的字符串。例如,如果分隔符是空格和逗号,delim 可以是 " ,"

返回值:

  • 成功:返回指向分割出的子字符串的指针(tokens)。子字符串会从原始字符串中分割出来,并且这个分割后的子字符串是原始字符串的一部分,它们将共享内存空间。

  • 失败:如果没有更多的子字符串可供提取,strtok 返回 NULL

使用说明:

  1. 第一次调用:传入待分割的字符串。

  2. 后续调用:每次调用时,传入 NULL 以继续分割上次 strtok 返回的部分,直到没有更多的子字符串为止(返回 NULL)。

#define DEF_CHAR " "	//预设分割项,需为字符串void split(char* argv[ARGV_SIZE], char* ps)
{assert(argv && ps);//调用 C语言 中的 strtok 函数分割字符串int pos = 0;argv[pos++] = strtok(ps, DEF_CHAR);  //有空格就分割while(argv[pos++] = strtok(NULL, DEF_CHAR));  //不断分割argv[pos] = NULL; //确保安全
}

注意: 指令分割结束后,需要在添加 argv 表结尾 NULL

3.3、程序替换

获得实际可用的 argv 表后,就可以开始子进程程序替换操作了

这里使用的是函数 execvp,理由:

  • v 表示 vector,正好和我们的 argv 表对应
  • p 为 path,可以根据 argv[0](指令),在 PATH 中寻找该程序并替换

当然也可以使用 execve 系统级替换函数

//子进程进行程序替换
pid_t id = fork();
if(id == 0)
{//直接执行程序替换,这里使用 execvpexecvp(argv[0], argv);exit(168); //替换失败后返回
}

注意: 程序替换成功后,exit(168) 语句不会执行

4、特殊情况处理

对特殊情况进行处理,使 myBash 更加完善

4.1、ls 显示高亮

系统中的 bash 在面对 ls 等文件显示指令时,不仅会显示内容,还会将特殊文件做颜色高亮处理,比如在我的环境下,可执行文件显示为绿色

实现原理

  • 在指令结尾加上 --color=auto 语句,即可实现高亮处理这个问题很简单,在指令分割结束后,判断是否为 ls,如果是,就在 argv 表后尾插入语句 --color=auto 即可
//特殊处理
//颜色高亮处理,识别是否为 ls 指令
if(strcmp(argv[0], "ls") == 0)
{int pos = 0;while(argv[pos++]); //找到尾argv[pos - 1] = (char*)"--color=auto"; //添加此字段argv[pos] = NULL; //结新尾
}

 注意:

  • 因为 argv 表中的元素类型为 char*,所以在尾插语句时,需要进行类型转换
  • 尾插语句后,需要再次添加结尾,确保安全
4.2、内建命令

内建命令是比较特殊的命令,不同于普通命令直接进行程序替换,内建命令需要进行特殊处理,比如 cd 命令调用系统级接口 chdir 让 父进程(myBash) 进行目录间的移动

 5.3、cd

首先实现不同目录间的切换

切换的本质:令当前 bash 移动至另一个目录下,不能直接使用 子进程 ,因为需要移动的是 父进程(bash)

对于当前的 myBash 来说,cd 没有丝毫效果,因为此时 指令会被拆分后交给子进程处理,这个方向本身就是错误的

 特殊情况特殊处理,同 ls 高亮一样,对指令进行识别,如果识别到 cd 命令,就直接调用 chdir 函数令当前进程 myBash 移动至指定目录即可(不必再创建子进程进行替换)

//目录间移动处理
if(strcmp(argv[0], "cd") == 0)
{//直接调用接口,然后 continue 不再执行后续代码if(strcmp(argv[1], "~") == 0)chdir("/home");  //回到家目录else if(strcmp(argv[1], "-") == 0)chdir(getenv("OLDPWD"));else if(argv[1])chdir(argv[1]);  //argv[1] 中就是路径continue;  //终止此次循环
}
4.3、export

当添加环境变量时,环境变量具有全局属性,需要持久存在,所以要定义一个全局的数组存储环境变量的值。myenv 是一个全局的数组。

strcpy(myenv[count],_argv[1]);
putenv(myenv[count++]);
4.4、重定向

 重定向的本质:关闭默认输出/输入流,打开新的文件流,从其中写入/读取数据

重定向的三种情况:

  • echo 字符串 > 文件 向文件中写入数据,写入前会先清空内容
  • echo 字符串 >> 文件 向文件中追加数据,追加前不会先清空内容
  • 可执行程序 < 文件 从文件中读取数据给可执行程序

所以实现重定向的关键在于判断指令中是否含有 >>>< 这三个字符,如果有,就具体问题具体分析,完成重定向

具体实现步骤:

  • 判断字符串中是否含有目标字符,如果有,就置当前位置为 '\0‘,其后半部分不参与指令分割 
  • 后半部分就是文件名,在打开文件时需要使用
  • 根据不同的字符,设置不同的标记位,用于判断打开文件的方式(只写、追加、只读)
  • 判断是否需要进行重定向,如果需要,在子进程创建后,打开目标文件,并调用 dup2 函数进行标准流的替换

open 函数的打开选项

O_RDONLY	//只读
O_WRONLY | O_CREAT | O_TRUNC	//只写
O_WRONLY | O_CREAT | O_APPEND	//追加

 标准流交换函数 dup2

//给参数1传打开文件后的文件描述符,给参数2传递待关闭的标准流
//读取:关闭0号流
//写入、追加:关闭1号流
int dup2(int oldfd, int newfd);
void checkdir(char * cmd)48 {49   char *pos =cmd;50   while(*pos)51   {52     if(*pos=='>')53     {54         if(*(pos+1)=='>')//'>>'55         {56            *(pos++)='\0';57            *(pos++)='\0';58           while(*pos==' ') pos++;59 60           rdirfilename=pos;61           rdir = APPEND_RDIR;62           break;63         }64         else //'>'65         {66          *(pos++)='\0';                                                                                                                                              67        while(*pos==' ') pos++;68        rdirfilename=pos;69        rdir=OUT_RDIR;70         }72     }73     else if(*pos=='<')74     {75        *pos='\0';76         pos++;77         while(*pos==' ') pos++;78                                                                                                                                                                      79         rdirfilename=pos;80         rdir=IN_RDIR;81          break;82     }83     else{}84 85     pos++;86    }   87   88 }

5.源码:好好理解

#include<iostream>
#include<stdlib.h>
#include<unistd.h>
#include<sys/wait.h>
#include<sys/types.h>
#include<assert.h>
#include<string.h>
#include<stdlib.h>#include <sys/stat.h>#include <fcntl.h>extern char** environ;#define  NONE -1
#define  IN_RDIR 0 //输入
#define  OUT_RDIR 1//stdout
#define  APPEND_RDIR 2//stderrchar commandline[1024];//命令行
char *argv[32];//参数表
char pwd[1024];//路径长
char myenv[10][10];//环境变量表
int count=0;
int lastcode=0;//退出码char * rdirfilename=NULL; //重定向的文件
int rdir =NONE;const char* getusrname()
{ const  char* str=getenv("USER");return str;
}const char* gethostname()
{return getenv("HOSTNAME");
}void getpwd()
{getcwd(pwd,sizeof(pwd));//是一个接口函数,将路径写到pwd里面
}void checkdir(char * cmd)
{char *pos =cmd;while(*pos){if(*pos=='>'){if(*(pos+1)=='>')//'>>'{*(pos++)='\0';*(pos++)='\0';while(*pos==' ') pos++;rdirfilename=pos;rdir = APPEND_RDIR;break;}else //'>'{*(pos++)='\0';                                                                                                                  while(*pos==' ') pos++;rdirfilename=pos;rdir=OUT_RDIR;}}else if(*pos=='<'){*pos='\0';pos++;while(*pos==' ') pos++;rdirfilename=pos;rdir=IN_RDIR;break;}else{}pos++;}   }void interact(char* cline,int size)//输出命令行
{getpwd();printf("[%s@%s%s]#  " ,getusrname(),gethostname(),pwd);char *s=fgets(cline,size,stdin);//输入指令,有可能什么也没有输入直接回车assert(s);(void)s;//”abcd\n\0" cline[strlen(cline)-1]='\0';//原来\n,在输入的时候也会加入到字符串中;checkdir(cline);//检查重定向
}int splitstring(char * cline,char *_argv[])
{int i=0;argv[i++]=strtok(cline," ");//字符串分割while(_argv[i++]=strtok(NULL," "));//如果截取失败就会返回NULL,正好是参数表尾;return i-1; //含回指令的参数个数。NULL不算
}int buildCommand(char*_argv[],int _argc)
{if(_argc==2&&strcmp(_argv[0],"cd")==0){chdir(argv[1]);//改变当前进程的路径,但是并不影响环境变量当中的路径getpwd();sprintf(getenv("PWD"),"%s",pwd);return 1;}else if(_argc==2&&strcmp(_argv[0],"export")==0){strcpy(myenv[count],_argv[1]);putenv(myenv[count++]);return 1;}else if(_argc==2&&strcmp(_argv[0],"echo")==0){if(strcmp(_argv[1],"$?")==0){printf("%d\n",lastcode);lastcode=0;//查看完后置为0;}else if(*_argv[1]=='$'){char* val=getenv(_argv[1]+1);if(val) printf("%s\n",val);}else printf("%s\n",_argv[1]);return 1;}if(strcmp(_argv[0],"ls")==0){ _argv[_argc++]="--color=auto";_argv[_argc]=NULL;// return 1;}return 0;
}void NormalExcute(char* _argv[]){pid_t id=fork();if(id<0){perror("fork");return ;}else if(id==0){int fd=0;if(rdir==IN_RDIR){fd=open(rdirfilename,O_RDONLY);dup2(fd ,0);}else if(rdir==OUT_RDIR){fd=open(rdirfilename,O_CREAT|O_WRONLY|O_TRUNC,0666);dup2(fd,1);}else if(rdir==APPEND_RDIR){fd=open(rdirfilename, O_CREAT|O_WRONLY|O_APPEND,0666);dup2(fd,1);}execvp(_argv[0],_argv);exit(1);}else {int status=0;pid_t rid=waitpid(id,&status,0);//正常含回子进程的PIDif(rid==id){lastcode=WEXITSTATUS(status);}}
}int main()
{while(1){rdirfilename=NULL;rdir=NONE;    interact(commandline,sizeof(commandline));int argc=splitstring(commandline,argv);if(argc==0) continue;//for(int i=0;i<argc;i++) printf("argv[%d]:%s\n",i,argv[i]);int n=buildCommand(argv,argc);if(!n) NormalExcute(argv);}return 0;
}

http://www.hkcw.cn/article/YPLrCDivOR.shtml

相关文章

【Android SDK(adb命令环境)工具安装下载教程】

1、打开下载地址&#xff1a;SDK 平台工具版本说明 | Android Studio | Android Developers 2、下载Android SDK Platform-Tools压缩包&#xff0c;选择路径进行解压 3、复制SDK文件platform-tools保存的路径 4、配置adb环境变量&#xff1b;按下wini,在设置界面搜索”环境…

Redis可视化工具 RDM mac安装使用

第一步&#xff1a;https://pan.baidu.com/s/10vpdhw7YfDD7G4yZCGtqQg?at1673701651004将dmg下载 第二部&#xff1a;点击下载的dmg文件进行安装、mac可能会提示&#xff1a; 无法验证此App不包含恶意软件 解决方法&#xff1a; 打开系统偏好设置>安全性与隐私>通用&am…

Mac 使用 Crossover 加载 Windows Steam 游戏库,实现 Windows/Mac 共享移动硬盘

Mac 使用 Crossover 加载 Windows Steam 游戏库&#xff0c;实现 Windows/Mac 共享移动硬盘 1. 在Crossover上安装Steam2. Steam容器加载移动硬盘3. 配置Steam库 前言&#xff1a;本文介绍了如何在Crossover上安装Steam并加载外接移动硬盘&#xff0c;实现在Window上下载的游戏…

Mac上媲美TortoiseSVN 的Svn的强大客户端 — macSvn

什么是macSvn&#xff1f; 如果你使用过 svn 那肯定听说过 TortoiseSVN, 但是 TortoiseSVN 并不支持在 mac 上使用。而 macSvn 是一款专为macOS设计的SVN&#xff08;Subversion&#xff09;客户端,它和TortoiseSVN一样&#xff0c;提供了直观的图形化操作方式.操作非常方便! …

给Android Studio配置本地gradle和maven镜像地址,加快访问速度

Android Studio在创建工程后默认会访问Google自己的官网去下载gradle和maven依赖项&#xff0c;国内访问Google的速度相当慢&#xff0c;如果没有科学上网的话&#xff0c;甚至无法访问。本文记录如何解决这些问题。 配置本地gradle 下载gradle 首先需要去国内的网站下载gra…

Flutter 打包报错:Execution failed for task ‘:flutter_plugin_android_lifecycle的解决办法

本篇文章主要讲解&#xff1a;Flutter 打包报错&#xff1a;Execution failed for task :flutter_plugin_android_lifecycle的解决办法。 日期&#xff1a;2025年2月16日 作者&#xff1a;任聪聪 报错现象&#xff1a; 报文信息&#xff1a; FAILURE:Buildfailedwithexception…

uniapp从入门到精通(全网保姆式教程)~ 别再说你不会开发小程序了

目录 一、介绍 二、环境搭建&#xff08;hello world&#xff09; 2.1 下载HBuilderX 2.2 下载微信开发者工具 2.3 创建uniapp项目 2.4 在浏览器运行 2.5 在微信开发者工具运行 2.6 在手机上运行 三、项目基本目录结构 四、开发规范概述 五、全局配置文件&#xff0…

macOS包管理器HomeBrew的安装和使用(适合小白)

Homebrew 是 macOS 上广受欢迎的包管理器&#xff0c;它让安装、更新、卸载和管理开发工具及应用程序变得非常简单&#xff0c;通过HomeBrew&#xff0c;用户可以快速获取最新版本的软件包&#xff0c;而无需手动下载和安装。本文将简单介绍如何在 Mac 上安装 Homebrew 以及如何…

Android 15 适配之16K Page Size :为什么它会是最坑的一个适配点

首先什么是 Page Size &#xff1f;一般意义上&#xff0c;页面(Page)指的就是 Linux 虚拟内存管理中使用的最小数据单位&#xff0c;页面大小(Page Size)就是虚拟地址空间中的页面大小&#xff0c; Linux 中进程的虚拟地址空间是由固定大小的页面组成。 Page Size 对于虚拟内…

adblock:为AdGuard和uBlock Origin定制的个性化过滤规则

adblock&#xff1a;为AdGuard和uBlock Origin定制的个性化过滤规则 adblock Personal filters and rules for AdGuard/uBlock Origin 项目地址: https://gitcode.com/gh_mirrors/adb/adblock 项目介绍 adblock 项目是一个开源的过滤规则集合&#xff0c;专门为AdGuard…

Xcode16 iOS18 编译问题适配

问题1:ADClient编译报错问题 报错信息 Undefined symbols for architecture arm64:"_OBJC_CLASS_$_ADClient", referenced from:in ViewController.o ld: symbol(s) not found for architecture arm64 clang: error: linker command failed with exit code 1 (use …

Mac如何连上Windows共享文件夹

首先保证mac和windows在同一局域网下 接着打开mac的【finder】&#xff0c;点击【Go】->【Connect to Server】 接下来输入 windows的IP,格式如下 smb://ip&#xff0c;然后点击【Connect】 接下来输入账号密码登录即可 由于我们的是任何人都可以访问&#xff0c;所以我选的…

手拆STL

vector v e c t o r vector vector&#xff0c;动态数组。 先来看一下它的一些基本操作及其拆后残渣。 1.a.push_back(x)&#xff0c;将 x x x加入动态数组 a a a的末尾。 实现&#xff1a;a[cnt]x 2.a.size()&#xff0c;查询动态数组 a a a中元素的数量。 实现&#xff1a;cn…

CppCon 2014 学习: C++ Test-driven Development

“Elephant in the Room”这个比喻常用来形容那些大家都知道但没人愿意讨论的重大问题。 这段内容讲的是软件质量管理的经典做法和潜在的问题&#xff1a; 经典做法&#xff1a;开发完成后才进行人工测试&#xff08;manual testing after creation&#xff09;。隐喻“Cape o…

vscode编辑器怎么使用提高开发uVision 项目的效率,如何编译Keil MDK项目?

用vscode编译uVision 项目只需要安装一个Keil Assistant插件&#xff0c;即可用vscode开发“keil 项目”。极大提高开发速度&#xff01; 1.安装Keil Assistant插件 安装插件成功之后&#xff0c;应该会让安装一个东西&#xff0c;点击安装即可 2.配置安装包路径 3.打开 uVi…

w~大模型~合集7

我自己的原文哦~ https://blog.51cto.com/whaosoft/13960246 #语言模型是否会规划未来 token Transformer本可以深谋远虑&#xff0c;但就是不做,语言模型是否会规划未来 token&#xff1f;这篇论文给你答案。 「别让 Yann LeCun 看见了。」 Yann LeCun 表示太迟了&am…

Tomcat优化篇

目录 一、Tomcat自身配置 1.Tomcat管理页面 2. 禁用AJP服务 3.Executor优化 4.三种运行模式 5.web.xml 6.Host标签 7.Context标签 8.启动速度优化 9.其他方面 二、JMeter测试 笔者推荐 一、Tomcat自身配置 1.Tomcat管理页面 我们可以打开Tomcat的管理页面&#xff…

VectorStore 组件深入学习与检索方法

考虑到目前市面上的向量数据库众多&#xff0c;每个数据库的操作方式也无统一标准&#xff0c;但是仍然存在着一些公共特征&#xff0c;LangChain 基于这些通用的特征封装了 VectorStore 基类&#xff0c;在这个基类下&#xff0c;可以将方法划分成 6 种&#xff1a; 相似性搜…

深入理解短链服务:原理、设计与实现全解析

TinyURL 是全球最早提供短链服务的网站&#xff0c;被视为短链系统的鼻祖。如今&#xff0c;国内的主流互联网公司也纷纷推出了自己的短链平台&#xff0c;比如新浪的 t.cn、百度的 dwz.cn、腾讯的 url.cn 等。 随着业务复杂度的提升和数据量的剧增&#xff0c;短链服务不仅是…

OpenCV C++ 学习笔记(三):矩阵基本操作、遍历图像矩阵的方法及性能分析

文章目录 图像矩阵在内存中的存储矩阵基本操作高性能法——使用经典的C风格运算符[]&#xff08;指针&#xff09;迭代器法通过指定On-the-fly地址查找核心函数LUT性能分析 常用数据类型定义&#xff1a; cv::Size(cols, rows); cv::Size(width, height);cv::Scalar(gray) cv:…