【Linux】第二个小程序--简易shell

请看上面的shell,其本质就是一个字符串,我们知道bash本质上就是一个进程,只不过命令行就是一个输出的字符串,

我们输入的命令“ls -a -l”实际上是我们在输入行输入的字符串,所以,如果我们想要做一个简易的shell的时候,首先要输出“[ghs@hecs-406886 myshell]$”这样的字符串,然后再接收并解析写入的命令字符串,然后一条命令就可以执行了。在“[ghs@hecs-406886 myshell]$”这样一行字符串中,ghs表示用户名,hecs-406886表示当前主机的主机名,myshell表示当前所处的路径,[ @ ]$为提示符。

所以,我们也需要构建一个类似的命令行,首先,创建一个myshell.c,

在main函数中,第一步我们需要自己输出一个命令行:命令行包括用户名、主机名、当前路径, 这些内容可以从环境变量中获取,

可以通过getenv函数获取环境变量内容,我们自定义三个函数来获取上面三个环境变量:

const char* getusername()
{const char* name = getenv("USER");if(name == NULL) return "None";return name;
}
const char* gethostname()
{const char* hostname = getenv("HOSTNAME");if(hostname == NULL) return "None";return hostname;
}
const char* getcwd()
{const char* cwd=getenv("PWD");if(cwd == NULL)return "None";return cwd;
}

先介绍一个函数snprintf:把指定参数按照特定格式写到指定长度的内存里,

然后综合上面三个函数,封装出MakeCommandLineAndPrint()函数,制作并打印命令行:

void MakeCommandLineAndPrint()
{char line[SIZE];const char* username = GetUserName();const char* hostname = GetHostName();const char* cwd = GetCwd();snprintf(line,sizeof(line),"[%s@%s %s]>",username,hostname,cwd);printf("%s",line);fflush(stdout);
}

第二步就是获取用户命令字符串,输入的指令(“ls -l -a”)站在开发者的角度本质是一个字符串,我们想一下,可以用scanf获取指令吗?不可以!因为指令选项个数不定。其实,我们想按行来获取字符串,在C语言中,我们可以使用fgets函数,它可以按行从特定的文件流中获取指定内容,获取内容指向由指针s指向的缓冲区,

char *fgets(char *s, int size, FILE *stream);

定义usercommand数组用于存储字符串,然后封装GetUserCommand()函数用于获取用户命令字符串,

int GetUserCommand(char command[],size_t n)
{char* s = fgets(command, n ,stdin);if(s == NULL) return -1;command[strlen(command)-1] = ZERO;return strlen(command);
}

第三步就是命令行字符串分割,使用‘ ’(空格)作为分隔符,我们想要得到一个数组,这个数组叫做char *argv[],对于这样“ls -l -a”一个字符串,定义指针指向第一个‘l’,再定义另一个指针也指向第一个‘l’,往后遍历,遇到第一个空格把其置为‘\0’,把指向第一个‘l’的指针放到agrv数组的第一个位置,然后指针往后走,指向‘-’,再定义一个指针向后走,遇到第一个空格把其置为‘\0’,把指向第一个‘-’的指针放到agrv数组的第二个位置,依次类推。为了实现这个功能,我们使用strtok函数:

#define NUM 32
char* gArgv[NUM];
void SplitCommand(char command[],size_t n)
{gArgv[0] = strtok(command,SEP);int index = 1;while((gArgv[index++] = strtok(NULL,SEP)));//故意写成=,表示先赋值再判断,分割之后,strtok //会返回NULL,刚好让gArgv最后一个元素是NULL, //并且while判断结束
}

下一步就要执行命令,我们需要创建子进程去执行,在子进程中我们要选择使用哪一个程序替换函数,第一,由于上面分割出来的命令是不带路径的,我要执行的命令全是系统的默认路径,应该让它在环境变量中找,所以选择的函数一定带p,第二,由于我提供的是一个命令数组argv,所以一定要带v,所以,选择execvp函数,这个函数在失败时返回-1,同时会设置错误码errno。

pid_t id = fork();
if(id < 0) Die();
else if(id == 0)
{//childexecvp(gArgv[0],gArgv);exit(errno);
}

但是,上面这个代码只能执行一次,为了能一直执行下去,我们创建一个循环:

int main()
{int quit = 0;while(!quit){//1.我们需要自己输出一个命令行MakeCommandLineAndPrint();//2.获取用户命令字符串char usercommand[SIZE];int n = GetUserCommand(usercommand,sizeof(usercommand));if(n <= 0) return 1;//3.命令行字符串分割SplitCommand(usercommand,sizeof(usercommand));//n.执行命令ExecuteCommand();}return 0;
}

上面我们完成一个粗犷的shell,但是有一点:

我们无法使用cd ..完成路径回退,这是为什么呢?因为我们上面的程序是在子进程中进行的,cd ..是在子进程中进行的,是把子进程的路径进行了回退,但是和父进程无关,父进程没有回退,cd这样的命令应该让父进程进程回退,而不应该让子进程回退,因此,需要对这些内建命令进行单独处理,在执行命令之前,要检查命令是否是内建命令

//3.命令行字符串分割
SplitCommand(usercommand,sizeof(usercommand));
//4.检查命令是否是内建命令
n = CheckBuilding();
if(n) continue; 
void Cd()
{const char* path = gArgv[1];if(path == NULL) path = GetHome();//path一定存在chdir(path);
}

上面代码的意思是,如果“cd”,那么回退到用户家目录,否则把命令行里的路径更改为当前路径。 

int CheckBuilding()
{int yes = 0;const char* enter_cmd = gArgv[0];if(strcmp("cd",enter_cmd) == 0){yes = 1;Cd();}return yes;
}

当时,运行上面代码后,发现命令行的当前路径提示一直不变,只有pwd里的路径才改变,导致这样的原因是没有对环境变量进行更新,需要导入环境变量,将当前的路径放到temp中,然后将cwd导入环境变量中。

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

执行结果虽然正确了,但是我们只想让最后一个路径显示出来(和正常的shell一样),因此,我定义了一个宏函数:

#define SkipPath(p) do{ p += strlen(p)-1; while(*p != '/') p--; }while(0)

这个宏将穿进去的路径字符串指向最后一个/,

最后的效果如上图。

但是还有一个问题,当我们回退到根目录后,命令行不显示路径了,需要做一下特殊处理:

此外,当进程退出时,我们也想用echo看一下退出码,由于echo也是一种内建命令,因此也需要在第四步特殊判断一下:

int CheckBuilding()
{int yes = 0;const char* enter_cmd = gArgv[0];if(strcmp("cd",enter_cmd) == 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;
}

至此,简易的shell完成。

下面附上完整代码:

#include <stdio.h>
#include <errno.h>
#include <unistd.h>
#include <sys/types.h>
#include <stdlib.h>
#include <sys/wait.h>
#include <string.h>#define ZERO '\0'
#define SIZE 512
#define SEP " "
#define NUM 32
#define SkipPath(p) do{ p += strlen(p)-1; while(*p != '/') p--; }while(0)char cwd[SIZE*2];
char* gArgv[NUM];
int lastcode = 0;const char* GetHome()
{const char* home = getenv("HOME");if(home == NULL) return "/";return home;
}const char* GetUserName()
{const char* name = getenv("USER");if(name == NULL) return "None";return name;
}const char* GetHostName()
{const char* hostname = getenv("HOSTNAME");if(hostname == NULL) return "None";return hostname;
}//临时
const char* GetCwd()
{const char* cwd=getenv("PWD");if(cwd == NULL)return "None";return cwd;
}void MakeCommandLineAndPrint()
{char line[SIZE];const char* username = GetUserName();const char* hostname = GetHostName();const char* cwd = GetCwd();SkipPath(cwd);snprintf(line,sizeof(line),"[%s@%s %s]>",username,hostname,strlen(cwd)==1 ? "/":cwd+1);printf("%s",line);fflush(stdout);
}
int GetUserCommand(char command[],size_t n)
{char* s = fgets(command, n ,stdin);if(s == NULL) return -1;command[strlen(command)-1] = ZERO;return strlen(command);
}
void SplitCommand(char command[],size_t n)
{gArgv[0] = strtok(command,SEP);int index = 1;while((gArgv[index++] = strtok(NULL,SEP)));//故意写成=,表示先赋值再判断,分割之后,strtok会返回NULL,刚好让gArgv最后一个元素是NULL,并且while判断结束
}
void Die()
{exit(-1);
}void ExecuteCommand()
{pid_t id = fork();if(id < 0) Die();else if(id == 0){//childexecvp(gArgv[0],gArgv);exit(errno);}else {//fatherint status = 0;pid_t rid = waitpid(id,&status,0);if(rid > 0){lastcode = WEXITSTATUS(status);if(lastcode != 0) printf("%s:%s:%d\n",gArgv[0],strerror(lastcode),lastcode);}}
}
void Cd()
{const char* path = gArgv[1];if(path == NULL) path = GetHome();//path一定存在chdir(path);//刷新环境变量char temp[SIZE*2];getcwd(temp,sizeof(temp));snprintf(cwd,sizeof(cwd),"PWD=%s",temp);putenv(cwd);
}
int CheckBuilding()
{int yes = 0;const char* enter_cmd = gArgv[0];if(strcmp("cd",enter_cmd) == 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;
}int main()
{int quit = 0;while(!quit){//1.我们需要自己输出一个命令行MakeCommandLineAndPrint();//2.获取用户命令字符串char usercommand[SIZE];int n = GetUserCommand(usercommand,sizeof(usercommand));if(n <= 0) return 1;//3.命令行字符串分割SplitCommand(usercommand,sizeof(usercommand));//4.检查命令是否是内建命令n = CheckBuilding();if(n) continue; //5.执行命令ExecuteCommand();}return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Redis从入门到精通(五)Redis实战(二)商户查询缓存

↑↑↑请在文章头部下载测试项目原代码↑↑↑ 文章目录 前言4.2 商户查询缓存4.2.1 缓存介绍4.2.2 查询商户信息的传统做法4.2.2.1 接口文档4.2.2.2 代码实现4.2.2.3 功能测试 4.2.3 查询商户信息添加Redis缓存4.2.3.1 逻辑分析4.2.3.2 代码实现4.2.3.3 功能测试 4.2.3 数据一致…

MySQL基础【语句执行顺序】

一个SQL语句它的执行顺序对于我们思考题意有着很重要的关系 题意就是&#xff1a;找出哪些只逛超市不买单的人&#xff08;买单0元也算哦&#xff0c;可能是使用的是代金券吧&#xff09; 看到此题关键找出两个数据 参观过的人 和 买单的人 他们的差就是白嫖的人&#xff08;支…

H.264 压缩与编解码原理

H.264 压缩与编解码原理 H.264 压缩与编解码原理H.264 简介视频编码的总体思路H.264 压缩技术帧内预测压缩什么是空间冗余&#xff1f;具体预测方法 帧间预测压缩什么是时间冗余&#xff1f;具体预测方法&#xff1a;运动估计 概念&#xff1a;Group of Pictures&#xff08;GO…

LC 96.不同的二叉搜索树

96.不同的二叉搜索树 给你一个整数 n &#xff0c;求恰由 n 个节点组成且节点值从 1 到 n 互不相同的 二叉搜索树 有多少种&#xff1f;返回满足题意的二叉搜索树的种数。 示例 1&#xff1a; 输入&#xff1a; n 3 输出&#xff1a; 5 示例 2&#xff1a; 输入&#xff1a;…

【JavaSE】接口 详解(上)

前言 本篇会讲到Java中接口内容&#xff0c;概念和注意点可能比较多&#xff0c;需要耐心多看几遍&#xff0c;我尽可能的使用经典的例子帮助大家理解~ 欢迎关注个人主页&#xff1a;逸狼 创造不易&#xff0c;可以点点赞吗~ 如有错误&#xff0c;欢迎指出~ 目录 前言 接口 语法…

SQL Server详细安装使用教程

1.安装环境 现阶段基本不用SQL Server数据库了&#xff0c;看到有这样的分析话题&#xff0c;就把多年前的存货发一下&#xff0c;大家也可以讨论看看&#xff0c;思路上希望还有价值。 SQL Server 2008 R2有32位版本和64位版本&#xff0c;32位版本可以安装在Windows XP及以上…

函数式编程(一)

函数式编程总体介绍 函数式编程(functional programming)其实是个很古老的概念&#xff0c;诞生距今快60年啦&#xff01; 最古老的函数式编程语言Lisp 新出现的函数式编程语言&#xff1a;比如Erlang、Scala、clojure等 热门语言&#xff1a;Python、java、JavaScript、C等…

【洛谷 P8655】[蓝桥杯 2017 国 B] 发现环 题解(邻接表+并查集+路径压缩)

[蓝桥杯 2017 国 B] 发现环 题目描述 小明的实验室有 N N N 台电脑&#xff0c;编号 1 ∼ N 1 \sim N 1∼N。原本这 N N N 台电脑之间有 N − 1 N-1 N−1 条数据链接相连&#xff0c;恰好构成一个树形网络。在树形网络上&#xff0c;任意两台电脑之间有唯一的路径相连。 …

拓扑排序--有向无环图中一个节点的所有祖先

题目描述 给你一个正整数 n &#xff0c;它表示一个 有向无环图 中节点的数目&#xff0c;节点编号为 0 到 n - 1 &#xff08;包括两者&#xff09;。 给你一个二维整数数组 edges &#xff0c;其中 edges[i] [fromi, toi] 表示图中一条从 fromi 到 toi 的单向边。 请你返…

OAuth 2.0 的四种方式

RFC 6749 OAuth 2.0 的标准是 RFC 6749 文件。该文件先解释了 OAuth 是什么。 OAuth 引入了一个授权层&#xff0c;用来分离两种不同的角色&#xff1a;客户端和资源所有者。…资源所有者同意以后&#xff0c;资源服务器可以向客户端颁发令牌。客户端通过令牌&#xff0c;去请…

K8S - Deployment 的版本回滚

当前状态 先看deployment rootk8s-master:~# kubectl get deploy -o wide --show-labels NAME READY UP-TO-DATE AVAILABLE AGE CONTAINERS IMAGES …

Xen Server 8 Install

Xen Sevrer 前言 XenServer&#xff08;以前称为 Citrix Hypervisor&#xff09;是业界领先的平台&#xff0c;实现了经济高效的桌面、服务器和云虚拟化基础结构。XenServer 支持任意规模或类型的组织整合计算资源&#xff0c;以及将计算资源转换为虚拟工作负载&#xff0c;从…