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

Linux实现简易版Shell的代码详解

一、程序流程分析

我们日常使用Bash时,通过输入命令执行相应的操作,比如:

那么,Bash是如何进行工作的呢?观察一下,就会发现,首先Bash会打印命令行提示符,包括当前用户、主机名以及路径。之后会等待我们输入相关命令,然后根据命令执行相应程序。程序执行结束后,就会再次打印命令行提示符,等待我们再次输入指令…很明显是一个死循环。

总结一下,Bash的大体工作流程是:

1.打印命令行提示符(包括当前用户名、主机名、路径)
2.获取用户输入的命令行
3.解析命令行
4.执行命令
5.继续打印命令行提示符…

注意:当Bash执行非内建命令时,会创建一个子进程,由子进程完成相应的工作,Bash自己等待子进程工作结束。而对于内建命令(如cd,echo),需要Bash自己执行任务。

二、代码实现

接下来,我们开始按照上述工作流程,一步步实现我们的简易Shell。

1. 打印命令行提示符

CentOS的命令行提示符主要包含三个内容:当前用户名、主机名和当前所在路径。之前学习Linux环境变量时,我们了解到环境变量(USER、HOSTNAME、PWD)中存储着这些内容。所以我们使用getenv函数获取环境变量相应的值。

代码实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

#include <iostream>

#include <cstdio>

#include <cstring>

#include <cstdlib>

#include <unistd.h>

#include <sys/types.h>

#include <sys/wait.h>

//获取当前用户名

constchar* GetUserName()

{

constchar* name =getenv("USER");

returnname == nullptr ?"none": name;

}

//获取当前主机名

constchar* GetHostName()

{

constchar* name =getenv("HOSTNAME");

returnname == nullptr ?"none": name;

}

//获取当前工作路径

constchar* GetPwd()

{

constchar* pwd =getenv("PWD");

returnpwd == nullptr ?"none": pwd;

}

接下来,调用这些函数,形成一串命令行提示符并打印:

1

2

3

4

5

6

7

8

//创建并打印命令行提示符

voidPrintCommandPrompt()

{

charCommandPrompt[1024];

//这里为了区分Bash,用大括号

snprintf(CommandPrompt,sizeofCommandPrompt,"{%s@%s %s}@ ", GetUserName(), GetHostName(), GetPwd());

std::cout << CommandPrompt << std::flush;// 打印并刷新缓冲区

}

注意这里打印结束后,由于没有换行,所以缓冲区可能不会刷新,导致命令行提示符没有出现在屏幕上。因此这里我们需要主动刷新缓冲区。

进行测试:

1

2

3

4

5

intmain()

{

PrintCommandPrompt();

return0;

}

运行结果:

可以看到,用户名和主机名都正常打印,但在我们写的shell中,当前所在路径是绝对路径,太过冗长,所以可以对生成的路径进行一些处理:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

//创建并打印命令行提示符

voidPrintCommandPrompt()

{

charCommandPrompt[1024];

//处理当前工作路径

std::string pwd = GetPwd();

if(pwd !="/")// 如果是根目录,则直接输出

pwd = pwd.substr(pwd.rfind('/') + 1);// 查找最后一个"/",并从下一个位置开始分割

//这里为了区分Bash,用大括号

snprintf(CommandPrompt,sizeofCommandPrompt,"{%s@%s %s}@ ", GetUserName(), GetHostName(), pwd.c_str());

std::cout << CommandPrompt << std::flush;// 打印并刷新缓冲区

}

运行结果:

2. 获取用户输入的命令行

在Bash输入命令时,往往会带上一些选项,并用空格隔开。因此,使用scanf或cin读入时会以空格作为分隔符,达不到想要的效果。这里我们选择用fgets进行读取。

另外,主函数当中,命令的全部处理应该放在一个死循环当中,这样才能完成用户多次派发的任务。

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

//获取用户输入的命令行

boolGetCommandLine(char* command,intsize)

{

//从键盘读取命令

if(fgets(command, size, stdin) == nullptr)

{

returnfalse;

}

//注意清理末尾的'\n'

if(strlen(command) == 0)returnfalse;

command[strlen(command) - 1] ='\0';

returntrue;

}

intmain()

{

while(true)

{

//打印命令行提示符

PrintCommandPrompt();

//读入命令

charcommand[1024];

if(!GetCommandLine(command,sizeofcommand))// 若读取错误,就continue重新读取

{

std::cout <<"读取错误"<< std::endl;

continue;

}

//打印输入的命令行

std::cout << command << std::endl;

}

return0;

}

注意:使用fgets从键盘读取字符串时会附带末尾’\n’,需要进行处理。

运行测试:

可以看到,程序成功读入了我们的命令(包括空格),并且将命令回显出来。

3. 命令行解析

命令行解析的过程中,需要将用户读入的命令行进行分割,提取出要执行的程序名以及选项。这里我们创建两个全局变量g_argc和g_argv,分别存储解析到的命令行参数以及参数个数,方便后续指令的执行。

注:这里的命令行分割操作由strtok函数完成。

代码实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

//全局变量存储命令行参数及其个数

intg_argc = 0;

char* g_argv[128];

//命令行解析

boolCommandParse(char* command)

{

g_argc = 0;

for(char* p =strtok(command," "); p != nullptr; p =strtok(nullptr," "))

{

g_argv[g_argc++] = p;

}

returng_argc == 0 ?false:true;

}

intmain()

{

while(true)

{

//打印命令行提示符

PrintCommandPrompt();

//读入命令

charcommand[1024];

if(!GetCommandLine(command,sizeofcommand))// 若读取错误,就continue重新读取

{

std::cout <<"输入错误"<< std::endl;

continue;

}

// //打印输入的命令行

// std::cout << command << std::endl;

//命令行解析

if(!CommandParse(command))

{

std::cout <<"命令行解析失败"<< std::endl;

continue;

}

//打印解析结果

for(inti = 0; i < g_argc; i++)

{

std::cout << g_argv[i] << std::endl;

}

}

return0;

}

测试结果:

程序成功地按照空格将我们输入的命令行参数提取了出来。接下来,根据提取到的参数,就可以执行相关指令了。

4. 执行命令

对于非内建命令,Bash会创建子进程,并让子进程执行;而对于内建命令,则是由Bash自己执行。因此,执行命令之前,需要先判断该命令是否是内建命令,然后进行相应的操作。

为什么会有内建命令?

  • 效率:执行内建命令通常比执行外部命令更快,因为避免了创建新进程的开销。
  • Shell 功能:许多内建命令直接操作 Shell 的内部状态,例如改变当前工作目录 (cd)、设置环境变量 (export)、控制 Shell 行为等,这些功能如果作为外部命令实现会更加复杂或不可能。
  • 基本操作:一些非常基础和常用的操作需要作为内建命令提供,以确保 Shell 的基本功能可用

内建命令的处理

在Bash当中,可以使用type命令判断一个命令是否是内建命令,例如:

当然,除了cd和echo命令,还有printf、help等内建命令。本次实现中,为了能够让大家深刻理解shell运行原理,同时降低实现难度,博主就只针对cd和echo这两个内建命令进行简易实现。

内建命令的检查和处理:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

//内建命令处理

boolCheckAndExecBuiltin()

{

//取出命令行参数表的首元素,判断是否为内建命令,如果是,则直接执行

std::string str = g_argv[0];

if(str =="cd")

{

Cd();

returntrue;

}

elseif(str =="echo")

{

Echo();

returntrue;

}

//else if(...)

returnfalse;// 不是内建命令,直接返回

}

intmain()

{

while(true)

{

//打印命令行提示符

PrintCommandPrompt();

//读入命令

charcommand[1024];

if(!GetCommandLine(command,sizeofcommand))// 若读取错误,就continue重新读取

{

std::cout <<"输入错误"<< std::endl;

continue;

}

// //打印输入的命令行

// std::cout << command << std::endl;

//命令行解析

if(!CommandParse(command))

{

std::cout <<"命令行解析失败"<< std::endl;

continue;

}

// //打印解析结果

// for(int i = 0; i < g_argc; i++)

// {

// std::cout << g_argv[i] << std::endl;

// }

//内建命令的处理

if(CheckAndExecBuiltin())

{

continue;// 是内建命令,执行完毕就回去重新打印提示符

}

//不是内建命令,由子进程处理

}

return0;

}

cd的简易实现

cd的功能是改变当前工作路径。如果创建子进程,其只能修改它自己的工作路径,而无法修改Bash的工作路径。因此cd操作需要Bash亲自完成。我们获取到命令行参数后,可以通过调用chdir函数实现:

1

2

3

4

5

6

7

8

9

10

11

12

13

14

voidCd()

{

std::string dst;

if(g_argc == 1 || g_argv[1] == std::string("~"))// 处理进入家目录的情况

{

dst = GetHome();

if(dst =="")return;

}

else

{

dst = g_argv[1];

}

chdir(dst.c_str());

}

测试结果:

可以看到,使用cd后,当前路径貌似并没有发生改变。为什么呢?实际上chdir确实起到了效果,但是我们的命令行提示符中,当前工作路径是从环境变量中获取的,环境变量中的PWD并没有发生改变。因此,修改当前工作路径之后,要顺带着修改环境变量PWD的值。其次,当前工作路径改变后,修改环境变量之前,要获取到当前工作路径,就需要使用getcwd函数。进行键值处理后,使用putenv修改环境变量。

http://www.jsqmd.com/news/584550/

相关文章:

  • 程序员如何去阅读开源项目的源码?
  • 从零开始:用Speakeasy为你的Node.js应用添加双因素认证
  • 低成本验证创意:星图OpenClaw沙盒+Qwen3.5-9B试玩图片转代码
  • 腾讯HY-OmniWeaving:全能视频生成新突破
  • Nunchaku FLUX.1 CustomV3实战教程:多LoRA并行加载与动态权重切换操作指南
  • Skydive流量分析实战:从数据包捕获到深度协议解析的完整流程
  • 如何快速安装 git-flow-completion:三大Shell环境完整指南
  • 如何快速上手GSS引擎:5步实现响应式网页布局
  • 基于单片机的电话计费系统的设计
  • 搞定PS 2022的DR5插件‘未正确签署’报错,一条注册表命令就够了(附各版本对应表)
  • 千问3.5-27B效果实测:低质量扫描件文字区域检测与内容还原
  • 科研助手打造:OpenClaw调用Qwen3-14B实现文献综述自动化
  • 玩转红外遥控与步进电机的电子积木
  • Linux dd命令的深度解析与应用实践
  • AI模型优化与部署:从知识蒸馏到模型合并的完整解决方案
  • 基于STM32单片机的无线胎压监测系统
  • WuliArt Qwen-Image Turbo效果对比:FP16黑图频发 vs BF16稳定出图实测
  • 基于51单片机的太阳能LED路灯智能控制器:Proteus仿真与实现(包含原理图、流程图、物料...
  • 终极Windows Defender禁用工具:一键提升系统性能的完整解决方案
  • OpenClaw成本优化实践:百川2-13B-4bits量化模型本地调用方案
  • Crank.js未来展望:框架路线图和新功能预告
  • BHVCC生理学实验系统是什么 生理学实验系统软件
  • DSP开发实战:从系统设计到算法优化
  • Windows下OpenClaw安装避坑:Qwen3.5-9B模型接入全记录
  • Gemma-3-12B-IT WebUI进阶技巧:提示词工程+上下文管理+多轮对话优化
  • cbindgen实战手册:10个实用技巧提升跨语言开发效率
  • v基于STM32单片机的电子日历设计
  • OpenClaw成本控制:Qwen3.5-9B长任务token消耗优化
  • 如何用30美元自制AI智能眼镜:开源项目OpenGlass的完整指南
  • 代码随想录算法第三十一天| LeetCode56合并区间、LeetCode738单调递增的数字