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

【Linux 学习计划】-- 简易版shell编写

目录

思路

创建自己的命令行

获取用户命令

分割命令

检查是否是内建命令

cd命令实现

进程程序替换执行程序

总代码

结语


思路

int main()
{while (1){// 1. 自己的命令行PrintCommandLine();// 2. 获取用户命令char command[SIZE];int n = GetUserCommand(command, sizeof(command));if (n <= 0)return 1;// 3. 分割指令SplitCommand(command);// for(int i = 0; gArgv[i]; i++)//     printf("[%d]: %s\n", i, gArgv[i]);// 4. 检查是否是内建命令n = CheckBuildin();if(n) continue;// 5. 进程程序替换执行命令ExecuteCommand();}return 0;
}

上图中的五个函数,就是我们的思路

首先就是先写命令行:

就是这个,这个获取环境变量 + 指针操作 + 打印即可,较为简单

接着就是需要获取用户输入的指令,并且将其分割,放进数组里面,这里会涉及到strtok函数的使用

最后将我们的函数分割完之后,就是判断是否是内建命令了

如果是内建命令的话,这里就只实现cd和echo $?,我们就单独函数实现

如果不是内建命令的话,我们就正常进行程序替换即可

这里最关键的点就是程序替换了,因为我们要使用的所有的命令都在磁盘上保存着,这时我们fork创建子进程,并用程序替换将磁盘上待替换的程序加载到内存中并将这个子进程给覆盖,最终跑起来

创建自己的命令行

#define SkipPath(p)         \do                      \{                       \p += strlen(p) - 1; \while (*p != '/')   \p--;            \} while (0)const char *GetHome()
{const char *home = getenv("HOME");return home;
}const char *GetHostName()
{const char *hostname = getenv("HOSTNAME");return hostname;
}const char *GetCwd()
{const char *cwd = getenv("PWD");return cwd;
}const char *GetUserName()
{const char *name = getenv("USER");return name;
}void PrintCommandLine()
{char line[SIZE];const char *UserName = GetUserName();const char *Cwd = GetCwd();const char *HostName = GetHostName();SkipPath(Cwd);snprintf(line, sizeof line, "[%s@%s %s]:>", UserName, HostName, strlen(Cwd) == 1 ? "/" : Cwd + 1);printf("%s", line);fflush(stdout);
}

这里没什么好说的,主要就是getenv获取环境变量,我们的参数就是从环境变量中来

需要讲的一点就是do...while(0)那个宏,这里我们如果直接获取PWD的话,就是所有都打出来:

这个是效果

但是整体是这样的

所以我们需要将指针移动到最后面那里,然后只显示最后一段

但是这里为什么用的是宏呢?因为这里是用 c 语言实现的,写一个函数的话还要二级指针,太麻烦了,宏方便点

而使用do...while(0)是为了形成一个代码块,我们可以整体使用,并且在下面调用的时候可以随便加 “ ; ”,这样调用起来就和函数一样了

最终效果如下:

获取用户命令

int GetUserCommand(char command[], size_t n)
{char *s = fgets(command, n, stdin);if (s == NULL)return 2;// 处理 \ncommand[strlen(command) - 1] = ZERO;// printf("命令: %s", command);return strlen(command);
}

其实如果是cpp的话,就直接getline了,但是c语言实现的话我们就用 fgets 获取一行

直接获取即可,完了之后放进提前开辟并传进函数里面的数组中

需要说的一点就是,我们用户输入的指令看起来是这样的:

ls -a -l

但其实最后还会加一个回车表示确定,所以实际上就是这样子的:

ls -a -l\n

综上,我们就需要将最后一个位置设置为\0,上面的ZERO其实就是\0,只不过设置成了宏而已

分割命令

void SplitCommand(char *command)
{gArgv[0] = strtok(command, SEP);int index = 1;while (gArgv[index++] = strtok(NULL, SEP));// 在最后一次判断的时候,发现没有字符串了,strtok 就会返回NULL// 正好让 gArgv 的最后一个位置变为 NULL,这样在后面进程程序替换的时候就可以直接用
}

这里其实就是strtok函数的使用技巧

先说说为什么要分割:

我们获取了程序之后,不是说所有的功能都是我们自己写,像ls、cd、mkdir等等,这些指令在我们电脑中的磁盘上人家都帮我们写好了,shell也是调用的这些文件

所以我们要做的,就是在用户输入指令的时候,我们能将这些程序加载进来

这里就需要用到进程程序替换了,并且我们现在只有用户输入的数据,我们到后面肯定是execvp用起来最好,所以分割完直接放函数里面当参数即可,就实现这个功能了

再来说说 strtok

当你第一次使用strtok的时候,传入一个数组当参数(第一个参数位置,第二个是分隔符)

当你从第二次开始,第一个参数直接传NULL,就代表使用你一刚开始用个的那个数组

这也就是为什么一开始传command数组,后面传NULL的原因

接着最妙的就是,当strtok检测到没有可以分割的了,就会返回NULL,而我们的数组(execvp函数要求)最后一个位置必须以NULL结尾

检查是否是内建命令

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;
}

直接就是if、else检查判断就行,有多少个内建命令就有多少个if、else

cd命令实现

void Cd()
{const char* path = gArgv[1];if(path == NULL) path = GetHome();chdir(path);// 更新环境变量char cwd[SIZE];char temp[SIZE];getcwd(temp, sizeof temp);snprintf(cwd, sizeof(cwd), "PWD=%s", temp);setenv("PWD", cwd, 1);
}

这里主要用到的是chdir函数,ps:chdir支持直接使用 . 或者 ..

先说获取path,程序走到这里,那么用户输入的指令一定是cd,那么就会有像cd ..这样的指令

那么我们获取完之后,分割的指令中,第二个就一定是path,直接获取就好(下标是1)

当然也有可能用户直接输入一个cd,后面啥也不跟,我们随便处理一下,给他返回到家目录即可

最后我们在chdir之后,还需要更新一下路径

这里我们先获取环境变量中的PWD,然后使用snprintf写入数组中,以PWD:%s的格式

最后直接用setenv函数修改(或者说更新)环境变量即可

当然你也可以在最后使用putenv,这样的话,我们的cwd数组就需要开辟在全局,不然函数运行结束之后,cwd数组也就没了,这样putenv找不到他,就会直接异常终止我们的程序,显示段错误(Segmentation fault)

setenv就不用,这里推荐这个,因为他更现代、好用

进程程序替换执行程序

void ExecuteCommand()
{pid_t id = fork();if (id == 0){// 子进程execvp(gArgv[0], gArgv);exit(errno);}else{// 父进程int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){if(WIFEXITED(status) == 0){lastcode = WEXITSTATUS(status);printf("exit code: %d, exit signal:%d\n", WEXITSTATUS(status), WTERMSIG(status));}}}
}

就是execvp函数的使用,创建子进程直接替换,然后父进程在外面waitpid阻塞等待即可,也不用父进程干什么事情

总代码

MyShell.c

点开这个链接,里面就是代码,放在gitee里面了

结语

这篇文章到这里就结束啦!!~( ̄▽ ̄)~*

如果觉得对你有帮助的,可以多多关注一下喔

http://www.xdnf.cn/news/903421.html

相关文章:

  • 【大模型LLM学习】Flash-Attention的学习记录
  • 阿里140 补环境日志
  • 华为 “一底双长焦” 专利公布,引领移动影像新变革
  • Caliper 负载(Workload)详细解析
  • 【NLP中向量化方式】序号化,亚编码,词袋法等
  • MySQL数据库基础(二)———数据表管理
  • 安卓基础(生成APK)
  • React 第五十六节 Router 中useSubmit的使用详解及注意事项
  • next,react封装axios,http请求
  • ✅ 常用 Java HTTP 客户端汇总及使用示例
  • 【零基础 快速学Java】韩顺平 零基础30天学会Java[学习笔记]
  • HTTP 请求协议简单介绍
  • 2025年SEVC SCI2区,潜力驱动多学习粒子群算法PDML-PSO,深度解析+性能实测
  • MySQL查询语句(续)
  • uniapp Vue2 获取电量的独家方法:绕过官方插件限制
  • Amazon Bedrock 助力 SolveX.AI 构建智能解题 Agent,打造头部教育科技应用
  • 当丰收季遇上超导磁测量:粮食产业的科技新征程
  • 智能手表健康监测系统的PSRAM存储芯片CSS6404LS-LI—高带宽、耐高温、微尺寸的三重突破
  • 微算法科技(NASDAQ:MLGO)基于信任的集成共识和灰狼优化(GWO)算法,搭建高信任水平的区块链网络
  • Guava LoadingCache 使用指南
  • Web前端基础:HTML-CSS
  • D3ctf-web-d3invitation单题wp
  • Q: dify前端使用哪些开发框架?
  • Houdini POP入门学习05 - 物理属性
  • 无头浏览器技术:Python爬虫如何精准模拟搜索点击
  • 每日八股文6.6
  • PowerBI企业运营分析—列互换式中国式报表分析
  • 【应用】Ghost Dance:利用惯性动捕构建虚拟舞伴
  • 单片机内部结构基础知识 FLASH相关解读
  • 数据集-目标检测系列- 口红嘴唇 数据集 lips >> DataBall