【Linux】:自定义shell(简易版)

朋友们、伙计们,我们又见面了,本期来给大家带来一期自定义shell,如果看完之后对你有一定的启发,那么请留下你的三连,祝大家心想事成!

C 语 言 专 栏:C语言:从入门到精通

数据结构专栏:数据结构

个  人  主  页 :stackY、

C + + 专 栏   :C++

Linux 专 栏  :Linux

目录

引言: 

1. 命令行提示符 

2. 获取用户指令

2.1 封装命令行和获取指令

3. 执行用户输入的命令

3.1 分割命令字符串

3.2 创建子进程执行指令

3.3  封装分割字符串和执行命令

4. 内建命令的执行

4.1 cd命令 

4.2 export命令

4.3 echo命令

5. 检查重定向

6. 完整代码


引言: 

Linux命令行的功能非常强大,我们用了这么长时间的shell,那么本期我们来自己实现一个简单版的shell。

1. 命令行提示符 

我们可以先看一下原本的shell:

要实现一个shell先得有一个命令行提示符,这个命令行提示符前面是用户名,中间是主机号,后面是工作目录,这些属性我们可以直接定义,但是在Linux环境变量中都保存着这些属性,并且是动态的,所以我们直接使用环境变量中的这些属性;

在程序中获取环境变量的接口叫做getenv:

#include <stdlib.h>
char *getenv(const char *name);

接下来我们先使用getenv实现三个获取命令行提示符的函数:

// 获取用户名
const char* getUsername()
{const char* name = getenv("USER");if(name) return name;return "none";
}
// 获取主机名
const char* getHostname()
{const char* hostname = getenv("HOSTNAME");if(hostname) return hostname;return "none";
}
// 获取当前工作目录
const char* getCwd()
{const char* cwd = getenv("PWD");if(cwd) return cwd;return "none";
}

获取完成之后,紧接着按照原本shell的格式来把他们拼接在一起打印出来看一看:

int main()
{// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());return 0;
}

可以看到我们的shell命令行提示符已经完成了,接下来就需要获取用户输入的命令了。

2. 获取用户指令

要获取用户输入的指令,我们先定义一个用来保存指令的字符数组,由于我们输入的指令可能不只是单纯的一个,还会输入一些带选项的指令,此时就不能使用scanf来读取,需要用按行读取的函数接口,我们选择fgets:

C语言会默认打开三个流:

  • ① 标准输入流:stdin
  • ② 标准输出流:stdout
  • ③ 标准错误流:stderr

所以我们从标准输入流中读取用户写入的命令:

#define NUM 1024int main()
{char usercommand[NUM];// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 获取用户输入命令char* r = fgets(usercommand, sizeof(usercommand), stdin);if(r == NULL) return 1;return 0;
}

当我们输入命令时,最后肯定会跟上一个'\n',所以我们需要将命令行中的最后一个位置的'\n'设置为'\0'。

int main()
{char usercommand[NUM];// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 获取用户输入命令char* r = fgets(usercommand, sizeof(usercommand), stdin);if(r == NULL) return 1;// 命令规范化usercommand[strlen(usercommand) - 1] = '\0';   // 不会存在越界问题return 0;
}

这样写是不存在越界的问题,假设我们什么命令都不输入,但是最后还是会敲一下回车,所以只有一个'\n',将这个'\n'改为'\0'也符合预期。

2.1 封装命令行和获取指令

将打印命令行提示符和获取用户指令封装称为一个函数,后面我们直接调用这个函数即可。

#include <stdio.h>
#include <stdlib.h>#define NUM 1024int getUserCommand(char *command, int sz)
{// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 获取用户输入命令char* r = fgets(command, sz, stdin);if(r == NULL) return -1;// 命令规范化command[strlen(command) - 1] = '\0';   // 不会存在越界问题return strlen(command);
}int main()
{char usercommand[NUM];getUserCommand(usercommand, sizeof(usercommand));//printf("%s", usercommand);return 0;
}

3. 执行用户输入的命令

获取完用户输入的指令,接下来就需要执行对应的指令了。 

3.1 分割命令字符串

要能正确执行对应的命令,就需要将输入的命令以及选项做合理的分割:分割命令字符串我们选用C语言中的strtok函数:

将分割好的字符串存放在一个字符指针数组中,最后以NULL结尾即可:

int main()
{char usercommand[NUM];// 打印命令行提示符 + 获取用户指令getUserCommand(usercommand, sizeof(usercommand));// 分割命令字符串char *argv[SIZE];int argc = 0;argv[argc++] = strtok(usercommand, SEP);while (argv[argc++] = strtok(NULL, SEP)){};return 0;
}

3.2 创建子进程执行指令

因为要执行指令就需要进行程序替换,如果单进程替换的话后面的代码就不能运行,所以创建子进程,让子进程去执行对应的指令。

在这里的程序替换选择的函数接口是:execvp

我们使用命令行输入指令肯定是想要我输完一个结果显示紧接着就可以输入下一个,所以需要设置一个循环的结构,并且为了效率,当获取用户指令失败、或者用户没有输入有效指令,就继续获取,就不用进行下面分割字符串、创建子进程的过程了。

#define SEP " "
#define SIZE 64int main()
{while (1){char usercommand[NUM];// 打印命令行提示符 + 获取用户指令int flag = getUserCommand(usercommand, sizeof(usercommand));if (flag <= 0)continue;// 分割命令字符串char *argv[SIZE];int argc = 0;argv[argc++] = strtok(getUserCommand, SEP);while (argv[argc++] = strtok(NULL, SEP)){};// 创建子进程执行命令pid_t id = fork();if (id < 0)return 1;else if (id == 0) // child{// 程序替换执行命令execvp(argv[0], argv);exit(1);}else // parent{pid_t rid = waitpid(id, NULL, 0);}}return 0;
}

3.3  封装分割字符串和执行命令

为了使用方便以及代码简介,直接将这两个接口封装称为一个函数即可:

// 分割命令字符串
void commandSplit(char *in, char *out[])
{int argc = 0;out[argc++] = strtok(in, SEP);while (out[argc++] = strtok(NULL, SEP)){};
}// 执行命令
int excuteCommand(char *argv[])
{// 创建子进程执行命令pid_t id = fork();if (id < 0)return -1;else if (id == 0) // child{// 程序替换执行命令execvp(argv[0], argv);exit(1);}else // parent{pid_t rid = waitpid(id, NULL, 0);}return 0;
}

4. 内建命令的执行

在Linux中有一批命令,需要我们的bash自己去执行,类似于自己内部的一个函数,这批命令就叫做内建命令,例如:cd、export、echo的特殊用法等,因此我们需要在执行命令之前,判断用户输入的命令是否为内建命令,如果为内建命令,直接让我们的父进程直接去执行内建命令,同样的我们将判断内建命令封装成一个函数,直接在这个函数里面调用执行对应的内建命令:

4.1 cd命令 

cd命令是更改我们的工作目录,更改工作目录的接口是chdir:

路径更改完之后,还需要更改环境变量中的PWD,所以先用获取当前路径的接口getcwd:

然后使用sprintf将当前路径导出到环境变量中。

cd命令还需要注意一个小细节,cd后面没有路径默认情况是cd到家目录。

char cwd[1024];  // 接受当前工作目录的缓冲区void cd(char *path)
{chdir(path); // 改变当前工作目录char tmp[1024]; // getcwd(tmp, sizeof(tmp)); // 获取当前工作目录sprintf(cwd, "PWD=%s", tmp); // 写入到缓冲区中putenv(cwd);   // 导出环境变量
}// 判断内建命令
int isBuildinCommand(char *argv[])  
{if (strcmp(argv[0], "cd") == 0)  // cd path{char *path = NULL;if(argv[1] == NULL) home = homePath();else path = argv[1];cd(path);return 1;}// 可以再列举更多内建命令return 0;
}

4.2 export命令

 我们使用export命令来在命令行中导出环境变量,所以我们设置全局的缓冲区来接收要导出的环境变量,然后使用putenv来导出环境变量。

int isBuildinCommand(char *argv[])
{if (strcmp(argv[0], "cd") == 0)  // cd path{char *path = NULL;if(argv[1] == NULL) path = getHomePath();else path = argv[1];cd(path);return 1;}else if(strcmp(argv[0], "export") == 0){if(argv[1] == NULL) return 1;strcpy(enval, argv[1]);  // 拷贝到缓冲区putenv(enval);  // 导出return 1;}// 可以再列举更多内建命令return 0;
}

4.3 echo命令

我们使用echo命令首先打印普通变量(echo 1244),其次打印退出码(echo $?),还会打印环境变量(echo $PATH),所以这些特殊用法都要实现:


// 判断内建命令
int isBuildinCommand(char *argv[])
{if (strcmp(argv[0], "cd") == 0) // cd path{char *path = NULL;if (argv[1] == NULL)path = getHomePath();elsepath = argv[1];cd(path);return 1;}else if (strcmp(argv[0], "export") == 0){if (argv[1] == NULL)return 1;strcpy(enval, argv[1]); // 拷贝到缓冲区putenv(enval);          // 导出return 1;}else if (strcmp(argv[0], "echo") == 0){if (argv[1] == NULL)   // echo{printf("\n");return 1;}else if (*(argv[1]) == '$' && strlen(argv[1]) > 1) // echo $...{char *val = argv[1] + 1;if (strcmp(val, "?") == 0){printf("%d\n", lastcode);lastcode = 0;}else{const char *env_val = getenv(val);if (env_val)printf("%s\n", env_val);elseprintf("\n");}return 1;}else          // echo 1234{printf("%s\n", argv[1]);return 1;}}// 可以再列举更多内建命令return 0;
}

5. 检查重定向

在获取完指令后先检测一下指令中是否存在重定向。

先获取重定向的类型,再拆分要重定向的文件。

获取完成之后在执行命令的时候直接使用重定向完成操作。

// 检查重定向
void checkRedir(char command[], int len)
{char *end = command + len - 1; // 从后向前面检查char *start = command;while (end > start){if (*end == '>') // 输出重定向       ls -a -l >> log.txt{if (*(end - 1) == '>') // 追加重定向{*(end - 1) = '\0';filename = end + 1;SkipSpace(filename);redir = AppendRedir;break;}else{*end = '\0';filename = end + 1;SkipSpace(filename);redir = OutputRedir;break;}}else if (*end == '<') // 输入重定向{*end = '\0';filename = end + 1;SkipSpace(filename);redir = InputRedir;break;}else{end--;}}
}

6. 完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <fcntl.h>#define NUM 1024
#define SEP " "
#define SIZE 64#define NoneRedir 0
#define OutputRedir 1
#define AppendRedir 2
#define InputRedir 3int redir = NoneRedir;
char *filename = NULL;char cwd[1024];
int lastcode = 0;
char enval[1024];// 获取用户名
const char *getUsername()
{const char *name = getenv("USER");if (name)return name;return "none";
}
// 获取主机名
const char *getHostname()
{const char *hostname = getenv("HOSTNAME");if (hostname)return hostname;return "none";
}
// 获取当前工作目录
const char *getCwd()
{const char *cwd = getenv("PWD");if (cwd)return cwd;return "none";
}
// 获取家目录
char *getHomePath()
{char *home = getenv("HOME");if (home)return home;elsereturn (char *)".";
}// 打印命令行提示符 + 获取用户指令
int getUserCommand(char *command, int sz)
{// 打印命令行提示符printf("[%s@%s %s]# ", getUsername(), getHostname(), getCwd());// 获取用户输入命令char *r = fgets(command, sz, stdin);if (r == NULL)return 1;// 命令规范化command[strlen(command) - 1] = '\0'; // 不会存在越界问题return strlen(command);
}// 分割命令字符串
void commandSplit(char *in, char *out[])
{int argc = 0;out[argc++] = strtok(in, SEP);while (out[argc++] = strtok(NULL, SEP)){};
}// 执行命令
int excuteCommand(char *argv[])
{// 创建子进程执行命令pid_t id = fork();if (id < 0)return -1;else if (id == 0) // child{int fd = 0;if (redir == InputRedir) // 输入重定向{fd = open(filename, O_RDONLY);dup2(fd, 0);}else if (redir == OutputRedir) // 输出重定向{fd = open(filename, O_WRONLY | O_CREAT | O_TRUNC, 0666);dup2(fd, 1);}else if (redir == AppendRedir) // 追加重定向{fd = open(filename, O_WRONLY | O_CREAT | O_APPEND, 0666);dup2(fd, 1);}else{// do nothing}// 程序替换执行命令execvp(argv[0], argv);exit(1);}else // parent{int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);}}return 0;
}void cd(char *path)
{chdir(path);                 // 改变当前工作目录char tmp[1024];              //getcwd(tmp, sizeof(tmp));    // 获取当前工作目录sprintf(cwd, "PWD=%s", tmp); // 写入到缓冲区中putenv(cwd);                 // 导出环境变量
}// 判断内建命令
int isBuildinCommand(char *argv[])
{if (strcmp(argv[0], "cd") == 0) // cd path{char *path = NULL;if (argv[1] == NULL)path = getHomePath();elsepath = argv[1];cd(path);return 1;}else if (strcmp(argv[0], "export") == 0){if (argv[1] == NULL)return 1;strcpy(enval, argv[1]); // 拷贝到缓冲区putenv(enval);          // 导出return 1;}else if (strcmp(argv[0], "echo") == 0){if (argv[1] == NULL){printf("\n");return 1;}else if (*(argv[1]) == '$' && strlen(argv[1]) > 1){char *val = argv[1] + 1;if (strcmp(val, "?") == 0){printf("%d\n", lastcode);lastcode = 0;}else{const char *env_val = getenv(val);if (env_val)printf("%s\n", env_val);elseprintf("\n");}return 1;}else{printf("%s\n", argv[1]);return 1;}}// 可以再列举更多内建命令return 0;
}#define SkipSpace(pos)        \do                        \{                         \while (isspace(*pos)) \pos++;            \} while (0)// 检查重定向
void checkRedir(char command[], int len)
{char *end = command + len - 1; // 从后向前面检查char *start = command;while (end > start){if (*end == '>') // 输出重定向       ls -a -l >> log.txt{if (*(end - 1) == '>') // 追加重定向{*(end - 1) = '\0';filename = end + 1;SkipSpace(filename);redir = AppendRedir;break;}else{*end = '\0';filename = end + 1;SkipSpace(filename);redir = OutputRedir;break;}}else if (*end == '<') // 输入重定向{*end = '\0';filename = end + 1;SkipSpace(filename);redir = InputRedir;break;}else{end--;}}
}int main()
{while (1){char usercommand[NUM];// 打印命令行提示符 + 获取用户指令int flag = getUserCommand(usercommand, sizeof(usercommand));if (flag <= 0)continue;// 检测重定向checkRedir(usercommand, sizeof(usercommand));// 分割命令字符串char *argv[SIZE];commandSplit(usercommand, argv);// 判断内建命令int sign = isBuildinCommand(argv);if (sign)continue;// 执行命令excuteCommand(argv);}return 0;
}

朋友们、伙计们,美好的时光总是短暂的,我们本期的的分享就到此结束,欲知后事如何,请听下回分解~,最后看完别忘了留下你们弥足珍贵的三连喔,感谢大家的支持!       

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

虚拟现实和增强现实技术系列—Expressive Talking Avatars

文章目录 1. 概述2. 背景介绍3. 数据集3.1 设计标准3.2 数据采集 4. 方法4.1 概述4.2 架构4.3 目标函数 5. 实验评测5.1 用户研究5.2 我们方法的结果5.3 比较与消融研究 1. 概述 支持远程协作者之间的交互和沟通。然而&#xff0c;明确的表达是出了名的难以创建&#xff0c;主…

SSRF中伪协议学习

SSRF常用的伪协议 file:// 从文件系统中获取文件内容,如file:///etc/passwd dict:// 字典服务协议,访问字典资源,如 dict:///ip:6739/info: ftp:// 可用于网络端口扫描 sftp:// SSH文件传输协议或安全文件传输协议 ldap://轻量级目录访问协议 tftp:// 简单文件传输协议 gopher…

媒体邀约专访与群访的区别?

传媒如春雨&#xff0c;润物细无声&#xff0c;大家好&#xff0c;我是51媒体网胡老师。 媒体邀约中的专访与群访在多个方面存在显著差异&#xff0c;以下是对这两种采访方式的详细比较&#xff1a; 一、定义与形式 专访&#xff1a; 定义&#xff1a;专访是指由媒体记者对单…

iOS 开发包管理之CocoaPods

CocoaPods&#xff08;Objective-C 时期&#xff0c;支持Objective-C和swift&#xff09;&#xff0c;CocoaPods下载第三方库源代码后会将其编译成静态库.a 文件 或动态库框架.framework 文件 的形式&#xff0c;并将它们添加到项目中&#xff0c;建立依赖关系&#xff0c;这种…

CPU与IO设备交互

距离cpu比较近的总线速度快&#xff0c;价格昂贵一些&#xff0c;根据重要程度选择总线&#xff0c;cpu不是通过总线直接和io设备相连接的&#xff0c;而是通过设备控制器进行连接的&#xff0c;暂时只需要关注cpu和设备控制器的直接进行的操作。 通过判断状态寄存器是否usy或者…

数据融合工具(15)线层、面层打折自动检测修复

一、内容导读 一个工具解决包括极小角在内的线层、面层要素的打折数据质量问题…… 小编提供了很多功能强大&#xff0c;应用场景广发的数据融合辅助工具集&#xff0c;能高效解决数据融合需要…… 数据融合工具&#xff08;1&#xff09;指定路径下同名图层合并 数据融合工具…

Linux云计算 |【第一阶段】SERVICES-DAY6

主要内容&#xff1a; Linux容器基础、Linux容器管理、podman命令行、管理容器进阶 实操前骤&#xff1a;安装 RHEL8.2 虚拟机 1.选择软件包&#xff1a;rhel-8.2-x86-dvd.iso&#xff1b; 2.内存2048M&#xff1b; 3.时区选择亚洲-上海&#xff0c;带GUI的服务器&#xff1b…

后端接口返回图片,前端的处理方法

接口返回如下图所示&#xff1a; 打印结果如下图所示&#xff1a; 出现问题的原因的axios默认返回的是json文本形式&#xff0c;二进制图片数据被强制转换成了 json 文本形式 处理方法&#xff1a; 首先&#xff0c;在axios中&#xff0c;将responseType默认返回数据类型json…

Python轻量级邮件发送库库之salmon使用详解

概要 电子邮件是现代通信的基础,在许多应用程序中,自动发送电子邮件是一个常见需求。salmon-mail 是一个基于 Python 的轻量级邮件发送库,它提供了简洁且强大的 API,用于处理电子邮件的发送和管理。本文将详细介绍 salmon-mail 库,包括其安装方法、主要特性、基本和高级功…

React 学习——行内样式、外部样式、动态样式

三种样式的写法 import "./index.css"; //同级目录下的样式文件 function App() {const styleCol {color: green,fontSize: 40px}// 动态样式const isBlock false;return (<div className"App">{/* 行内样式 */}<span style{{color:red,fontSiz…

科技引领水资源管理新篇章:深入剖析智慧水利解决方案,展现其在提升水资源利用效率、优化水环境管理方面的创新实践

本文关键词&#xff1a;智慧水利、智慧水利工程、智慧水利发展前景、智慧水利技术、智慧水利信息化系统、智慧水利解决方案、数字水利和智慧水利、数字水利工程、数字水利建设、数字水利概念、人水和协、智慧水库、智慧水库管理平台、智慧水库建设方案、智慧水库解决方案、智慧…

破解网络奥秘:一图胜千言,TCP重传、滑动窗口、流量与拥塞控制全解析

引言 TCP 是一个非常复杂且伟大的协议&#xff0c;它通过许多机制来确保传输的可靠性。为了实现这一目标&#xff0c;TCP需要处理各种问题&#xff0c;比如数据损坏、丢包、重复数据以及分片顺序混乱等。如果这些问题得不到解决&#xff0c;可靠传输就无从谈起。 众所周知&am…

C++系列-list的模拟实现

&#x1f308;个人主页&#xff1a;羽晨同学 &#x1f4ab;个人格言:“成为自己未来的主人~” 构造函数生成 template<class T>struct ListNode{ListNode<T>* _next;ListNode<T>* _prev;T _data;ListNode(const T& data T()):_next(nullptr),_prev…

在 Android 上实现语音命令识别:详细指南

在 Android 上实现语音命令识别:详细指南 语音命令识别在现代 Android 应用中变得越来越普遍。它允许用户通过自然语言与设备进行交互,从而提升用户体验。本文将详细介绍如何在 Android 上实现语音命令识别,包括基本实现、带有占位槽位的命令处理,以及相关的配置和调试步骤…

Linux嵌入式学习——数据结构——概念和Seqlist

数据结构 相互之间存在一种或多种特定关系的数据元素的集合。 逻辑结构 集合&#xff0c;所有数据在同一个集合中&#xff0c;关系平等。 线性&#xff0c;数据和数据之间是一对一的关系。数组就是线性表的一种。 树&#xff0c; 一对多 图&#xff0c;多对多 …

压测实操--kafka broker压测方案

作者&#xff1a;九月 环境信息&#xff1a; 操作系统centos7.9&#xff0c;kafka版本为hdp集群中的2.0版本。 kafka broker参数 num.replica.fetchers&#xff1a;副本抓取的相应参数&#xff0c;如果发生ISR频繁进出的情况或follower无法追上leader的情况则适当增加该值&…

Java---String类

乐观学习&#xff0c;乐观生活&#xff0c;才能不断前进啊&#xff01;&#xff01;&#xff01; 我的主页&#xff1a;optimistic_chen 我的专栏&#xff1a;c语言 &#xff0c;Java 欢迎大家访问~ 创作不易&#xff0c;大佬们点赞鼓励下吧~ 前言 在C语言中已经涉及到字符串了…

[前端]解决Iframe即使设置高度100%,但还是显示滚动条scrollbar的问题

前言 好烦,你看看这两个重复的滚动条. 一个是来自iframe,另一个来自父级的div(overflow: auto;) 我已经在css中设置了iframe的height: 100%;border: none;,但无论如何还是显示出了父级的scrollbar 解决 将iframe的display: block;即可. 或者vertical-align: bottom;

【启明智显分享】基于国产Model3芯片的7寸触摸屏助力智慧医疗,电子床头屏提升护理交互

未来医院必然是以信息化为基础&#xff0c;以物联网为特征&#xff0c;以医疗为核心的服务型医院。病房作为医院的重要服务场所&#xff0c;成为智慧医院建设的重要一环。 为提高医护人员与患者的互动交流&#xff0c;给医疗注入智慧元素&#xff0c;让患者享受智能服务&#…

java实现OCR图片识别,RapidOcr开源免费

先看一下识别效果&#xff08;自我感觉很牛逼&#xff09;&#xff0c;比Tess4J Tesseract省事&#xff0c;这个还需要训练&#xff0c;安装软件、下载语言包什么的 很费事&#xff0c;关键识别率不高 RapidOcr不管文字的横竖&#xff0c;还是斜的都能识别&#xff08;代码实现…