C语言编写Linux的Shell外壳

目录

一、输出命令行

1.1 了解环境变量

1.2 获取用户名、主机名、当前路径

1.3 缓冲区改进MakeCommandLine

二、获取用户命令

2.1 读取函数的选择

2.2 细节优化

2.3 返回值

三、指令和选项分割

3.1 strtok 函数

3.2 分割实现 

四、执行命令

4.1 fork 方法

4.2 进程等待

4.3 进程替换

4.4 程序编写

五、程序优化

5.1 执行次数

5.2 检测命令是否为内建命令

5.3 子进程执行失败

5.4 命令行路径更改

六、完整代码


在学习之前我们要认识到,Shell外壳中的命令行以及我们输入的指令都是字符串!

首先我们要创建两个文件:MyShell.cmakefile ,一个存储我们的 Shell 外壳,一个方便操作

其次,我们看一下 MyShell.c 中需要包含的头文件:

  1 #include <stdio.h>2 #include <unistd.h>     //进程创建接口3 #include <sys/types.h>  //进程等待4 #include <sys/wait.h>   //进程等待     

一、输出命令行


首先我们先来认识一下命令行,其中, Flash 是当前用户名、@ 后紧跟的是当前主机名、主机名空格后紧跟的是当前路径。所以由此我们知道,如果想打印出我们的命令行,我们至少要知道三个信息:1.用户名 2.主机名 3.当前路径

1.1 了解环境变量

这三个信息如何拿去呢?我们的操作系统中有自带的环境变量,在环境变量中存放着我们需要的三个信息。我们如何查我们的环境变量呢?

有三种方法:

1.命令行参数 -> [env] ,当我们在命令行输入指令,系统就会显示出一大串信息(这里有省略),其中就有我们需要的 [USER] [PWD] [HOSTNAME]

2.ENVIRON 等第三方提供的接口,这里不做详细介绍

3.使用C语言提供的接口函数,如 getenv() ,我们这里使用该方式,下面会详细介绍。

1.2 获取用户名、主机名、当前路径

getenv 函数 —— stdlib.h C语言标准库

其中,传参 [const char* name] 表示的是我们需要从环境变量中拿取的变量名称,我们这里需要拿到的就是上述使用命令行参数 [env] 读取到的 [USER] [HOSTNAME] [PWD],它的返回值就是我们需要的一个字符串。

现在我们就可以来编写我们的函数了。

char* GetUserName()
{char* User = getenv("USER");if (!User) return "None";return User;
}
char* GetHostName()
{char* Host = getenv("HOSTNAME");if (!Host) return "None";return Host;
}
char* GetCwd()
{char* PWD = getenv("PWD");if (!PWD) return "None";return PWD;
}
void MakeCommandLine()
{char* UserName = GetUserName();char* HostName = GetHostName();char* Cwd = GetCwd();printf("[%s@%s %s]>\n", UserName, HostName, Cwd);
}
int main()
{MakeCommandLine();return 0;
}

我们还可以继续改进一下,使用缓冲区的概念

1.3 缓冲区改进MakeCommandLine

首先我们先宏定义一个缓冲区的大小,这里我设置为 256 。

#define SIZE 256
char Line[SIZE];//自定义缓冲区

其次我们再来学习一个函数 snprintf

按照参数的顺序,依次是 s : 需要写入的目的地 n : 写入字符的数量 format : 标准格式

使用 snprintf 就可以像我们自定义的缓冲区里写啦!

snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);
printf("%s\n", Line);

成品: 

#define SIZE 256
//只有MakeCommandLine函数变化,只展示该函数
void MakeCommandLine()
{char Line[SIZE];char* UserName = GetUserName();char* HostName = GetHostName();char* Cwd = GetCwd();snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);printf("%s\n", Line);
}

但是还有一个细节,当我们想让程序慢一点结束时,使用 sleep 函数,就会发生神奇的一幕:

为什么没有立刻打印呢?原因是 stdout 的缓冲问题。printf 是行缓冲的,通常情况下,当遇到换行符('\n')时,缓冲区的内容会被送往 stdout 进行输出。然而在某些情况下,如果在调用 sleep 之前没有刷新缓冲区,那么输出可能会延迟直到缓冲区被刷新。

我们的解决方法是使用 fflush 函数!这将刷新(即清空并发送)包含 printf 输出的缓冲区,从而确保立即看到输出:

snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);
printf("%s\n", Line);
fflush(stdout);
sleep(5);

二、获取用户命令

首先,我们在输出命令行时,为了方便阅读,用 printf 输出 Line 时加了 '\n' ,我们现在要保证命令行和命令在同一行,所以现在我们要删除掉 '\n' 。

2.1 读取函数的选择

我们可以继续使用 scanf 函数来读取命令吗?按照我们输入的命令,如 [ls -a -l] ,他们都是以空格为分隔符,显然与我们的 sacnf 发生了冲突,而且无法控制每次输入命令的空格数量,所以我们不能使用 scanf 读取输入。

我们的命令都是用行读取,C语言也提供了这样的函数 fgets 。

其中,返回指向读取到字符开头的指针,传参依次为读取字符后存放的位置,读取字符的长度,读取字符的位置。 

 int GetUserCommand(char Command[], size_t n){                                               char* s = fgets(Command, n, stdin);if (!s) return -1;                                                    }     int main()                                                         {                                                                  //输出命令行                                                     MakeCommandLine();                                               //读取用户命令                                                   char UserCommand[SIZE];                                          GetUserCommand(UserCommand, sizeof(UserCommand));  printf("echo : %s\n", UserCommand); //打印验证一下是否被读取                                            return 0;}

2.2 细节优化

现在又来了一个细节问题:

fgets 按行读取,如果我们想执行就必须按 [回车] ,假设我们输入的是"Hello World",那么其读取到的就是"Hello World\n",再加上我们自己写的 printf 中的 '\n' 就变成了两行,下面我们进一步优化一下,只需要把命令的最后一个字符改成 '\0' 即可:

 int GetUserCommand(char Command[], size_t n){                                               char* s = fgets(Command, n, stdin);if (!s) return -1;Command[strlen(Command) - 1] = '\0';                                                    } 

2.3 返回值

关于函数的返回值,为了与 [return -1] 区分开,也为了更好地执行命令,我们可以返回一下读取到的命令长度,这样当命令 [> 0] 时,我们再继续执行,否则直接退出:

 int GetUserCommand(char Command[], size_t n){                                               char* s = fgets(Command, n, stdin);if (!s) return -1;Command[strlen(Command) - 1] = '\0';    return strlen(Command);                                                } int main()                                                         {                                                                  //输出命令行                                                     MakeCommandLine();                                               //读取用户命令                                                   char UserCommand[SIZE];                                          int n = GetUserCommand(UserCommand, sizeof(UserCommand));  (void)n;//暂时不搞,先强转一下,防止警告                                         return 0;}

三、指令和选项分割

像我们上面使用的 []ls -a -l] ,它们是由指令和选项构成,所以我们如果要执行,肯定也要把读取用户传入的字符串分割成下面的形式, [ls] [-a] [-l] 。

3.1 strtok 函数

那么如何分割呢?这又要使用到C语言中的字符串函数 strtok 。

其中,返回值是分割后的小字符串,传入参数 str 是要分割的字符串, delimiters 是分隔符

如果首次分割是已经传入参数 str ,那么再次调用时把 str 设置为 NULL ,strtok就会默认继续分割上次的字符串。

一说到分隔符,我们这里的分隔符显而易见的就是空格啦!由于我们的分割是把一个字符串分割成若干小字符串,所以我们就可以直接定义一个全局的数组,用来挨个存放这些小字符串:

但是要注意, strtok 函数使用的分隔符都是字符串,我们的空格不能设置为 ' ' ,而应该为 " "

 #define SEP " "    

3.2 分割实现 

void SplitCommand(char Command[], size_t n)
{(void)n;argv[0] = strtok(Command, SEP);int index = 1;while ((argv[index++] = strtok(NULL, SEP)));//strtok如果无法分割,则返回NULL,此时argv最后一个元组直接被赋值为NULL且while循环结束
}

我们还可以通过打印来验证是否正确:

 int i = 0;                                                               for (; argv[i]; i++){printf("%s ", argv[i]);}printf("\n");

四、执行命令

我们之前讲过 Bash 会创建子进程,为了保证安全,都是子进程在执行我们的命令,而我们今天要实现的也是使用子进程来帮助我们执行命令。

4.1 fork 方法

fork 函数并不是我们的C语言函数,而是操作系统提供的,所以我们可以用 man 2 fork 查询

这里不做过多说明,实在不知道可以移步到Linux进程概念(1)-CSDN博客

如果执行的是子进程,那么返回值为0,如果是父进程,会返回子进程的 pid

4.2 进程等待

我们之前说过,如果父进程先于子进程退出,子进程就会变成僵尸进程,为了避免这种影响,父进程可以通过进程等待的方式,回收子进程资源,获取子进程退出信息。

下面我们来介绍几个进程等待的方法:

wait 方法:

    返回值:等待成功返回被等待子进程的 pid ,失败返回 -1

    参数:输出型参数,获取子进程退出状态,不关心则可以设置成为NULL

waitpid 方法:

    返回值:

       当正常返回的时候 waitpid 返回收集到的子进程的进程ID;如果设置了选项 WNOHANG,而调用中 waitpid 发现没有已退出的子进程可收集,则返回 0;如果调用中出错,则返回 -1,这时 errno 会被设置成相应的值以指示错误所在;

    参数:

    pid:
       pid=-1,等待任一个子进程。与wait等效。
       pid>0.等待其进程ID与pid相等的子进程。
    status:
       WIFEXITED: 若为正常终止子进程返回的状态,则为真。(查看进程是否是正常退出)
       WEXITSTATUS: 若WIFEXITED非零,提取子进程退出码。(查看进程的退出码)
    options:
       WNOHANG: 若 pid 指定的子进程没有结束,则 waitpid() 函数返回0,不予以等待。

       若正常结束,则返回该子进程的ID。

4.3 进程替换

我们用fork创建子进程后执行的是和父进程相同的程序(但有可能执行不同的代码分支),子进程往往要调用一种exec函数以执行另一个程序当进程调用一种exec函数时,该进程的用户空间代码和数据完全被新程序替换,从新程序的启动例程开始执行。调用exec并不创建新进程,所以调用exec前后该进程的id并未改变。

有六种以 exec 开头的函数,统称为 exec 函数:

#include <unistd.h>

int execl(const char *path, const char *arg, ...);
int execlp(const char *file, const char *arg, ...);
int execle(const char *path, const char *arg, ...,char *const envp[]);
int execv(const char *path, char *const argv[]);
int execvp(const char *file, char *const argv[]);

这些函数都可以用 man execl 查询:

4.4 程序编写

void ExecuteCommand(char Command[], size_t n)
{pid_t id = fork();if (id == 0)//子进程,执行命令{execvp(argv[0],argv);}else if (id > 0)//父进程{int status = 0;waitpid(id, &status, 0);}else//fork创建失败,直接退出{exit(1);}
}

下面我们来看一下完整代码以及效果图:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>     //进程创建接口
#include <sys/types.h>  //进程等待
#include <sys/wait.h>   //进程等待#define SIZE 256
#define NUM 16
#define SEP " "char* argv[NUM];char* GetUserName()
{char* User = getenv("USER");if (!User) return "None";return User;
}
char* GetHostName()
{char* Host = getenv("HOSTNAME");if (!Host) return "None";return Host;
}
char* GetCwd()
{char* PWD = getenv("PWD");if (!PWD) return "None";return PWD;
}
void MakeCommandLine()
{char Line[SIZE];char* UserName = GetUserName();char* HostName = GetHostName();char* Cwd = GetCwd();snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);printf("%s", Line);fflush(stdout);
}
int GetUserCommand(char Command[], size_t n)
{char* s = fgets(Command, n, stdin);if (!s) return -1;Command[strlen(Command) - 1] = '\0';return strlen(Command);
}
void SplitCommand(char Command[], size_t n)
{(void)n;argv[0] = strtok(Command, SEP);int index = 1;while ((argv[index++] = strtok(NULL, SEP)));//strtok如果无法分割,则返回NULL,此时argv最后一个元组直接被赋值为NULL且while循环结束
}
void ExecuteCommand(char Command[], size_t n)
{pid_t id = fork();if (id == 0)//子进程,执行命令{execvp(argv[0],argv);}else if (id > 0)//父进程{int status = 0;waitpid(id, &status, 0);}else//fork创建失败,直接退出{exit(1);}}
int main()
{//输出命令行MakeCommandLine();//读取用户命令char UserCommand[SIZE];int n = GetUserCommand(UserCommand, sizeof(UserCommand));(void)n;printf("echo : %s\n", UserCommand);//指令和选项分割SplitCommand(UserCommand, sizeof(UserCommand));//执行命令ExecuteCommand(UserCommand, sizeof(UserCommand));return 0;
}

五、程序优化

5.1 执行次数

虽然我们的 Shell 已经完成的有一点雏形了,但是怎么这个外壳只能使用一次呀?我们是不是要让他多执行几次呢?所以我们就要把这几个步骤都放到一个 while 循环中。

int main()
{while(1){//输出命令行MakeCommandLine();//读取用户命令char UserCommand[SIZE];int n = GetUserCommand(UserCommand, sizeof(UserCommand));(void)n;//指令和选项分割SplitCommand(UserCommand, sizeof(UserCommand));//执行命令ExecuteCommand(UserCommand, sizeof(UserCommand));}return 0;
}

5.2 检测命令是否为内建命令

我们在我们的 Shell 中使用 cd 命令,但是我们的命令行无法进入某目录,这是为什么呢?
因为我们上面创建了子进程,我们的 cd 命令是让子进程执行的,和我们真正的 bash 没有关系,我们正确的做法是让父进程执行!

什么是内建命令呢?

下面我们来看看如何检测是否为内建命令。

虽然我们的 [cd] 已经可以使用,但是我们的命令行路径怎么不回退呢?
因为我们还要更改我们的环境变量!

更改环境变量要用到系统调用,需要获取当前路径,并写入环境变量中。

获取当前路径:

man 2 getcwd

写入环境变量:

man snprintf

man putenv

所以我们此时在对我们的 cd 函数做修改:

void ExecuteCd()
{const char* path = argv[1];if (path == NULL) path = GetHome();else chdir(path);char tmp[SIZE];getcwd(tmp, sizeof(tmp));snprintf(cwd, sizeof(cwd), "PWD=%s", tmp);putenv(cwd);
}

当然还有其他内建命令,方法都诸如此类。

5.3 子进程执行失败

我们直接在全局定义一个退出码 int lastcode ,然后在父进程中左对应的修改:

void ExecuteCommand(char Command[], size_t n)
{pid_t id = fork();if (id == 0)//子进程,执行命令{execvp(argv[0],argv);}else if (id > 0)//父进程{int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);if (lastcode != 0) printf("%s:%s:%d\n", argv[0], strerror(lastcode), lastcode);}}else//fork创建失败,直接退出{exit(1);}
}

5.4 命令行路径更改

在我们的 XShell 中提供的 Shell 外壳,其命令行的路径都是相对路径,我们的 Shell 也可以改成这样,如下:

我们这里采用了宏函数,也可以使用正常函数,在使用宏函数是时,若程序是代码块,建议放在 [do while(0)] 中,如下:

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

同时在输出命令行中也调用该函数,如下:

void MakeCommandLine()
{char Line[SIZE];char* UserName = GetUserName();char* HostName = GetHostName();char* Cwd = GetCwd();SkipPath(Cwd);snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);printf("%s", Line);fflush(stdout);
}

六、完整代码

#include <stdio.h>
#include <stdlib.h>
#include <string.h>#include <unistd.h>     //进程创建接口
#include <sys/types.h>  //进程等待
#include <sys/wait.h>   //进程等待#define SIZE 256
#define NUM 16
#define SEP " "
#define SkipPath(p) do{ p += (strlen(p)-1); while(*p != '/') p--; }while(0)char* argv[NUM];
char cwd[SIZE];
int lastcode = 0;char* GetUserName()
{char* User = getenv("USER");if (!User) return "None";return User;
}
char* GetHostName()
{char* Host = getenv("HOSTNAME");if (!Host) return "None";return Host;
}
char* GetCwd()
{char* PWD = getenv("PWD");if (!PWD) return "None";return PWD;
}
void MakeCommandLine()
{char Line[SIZE];char* UserName = GetUserName();char* HostName = GetHostName();char* Cwd = GetCwd();SkipPath(Cwd);snprintf(Line, sizeof(Line), "[%s@%s %s]>", UserName, HostName, Cwd);printf("%s", Line);fflush(stdout);
}
int GetUserCommand(char Command[], size_t n)
{char* s = fgets(Command, n, stdin);if (!s) return -1;Command[strlen(Command) - 1] = '\0';return strlen(Command);
}
void SplitCommand(char Command[], size_t n)
{(void)n;argv[0] = strtok(Command, SEP);int index = 1;while ((argv[index++] = strtok(NULL, SEP)));//strtok如果无法分割,则返回NULL,此时argv最后一个元组直接被赋值为NULL且while循环结束
}
const char* GetHome()
{const char* home = getenv("HOME");if (home == NULL) return "/";return home;
}
void ExecuteCd()
{const char* path = argv[1];if (path == NULL) path = GetHome();else chdir(path);char tmp[SIZE];getcwd(tmp, sizeof(tmp));snprintf(cwd, sizeof(cwd), "PWD=%s", tmp);putenv(cwd);
}
int CheckBuiltin()
{int ch = 0;//默认非内建命令const char* command = argv[0];if (strcmp(command, "cd") == 0){ch = 1;ExecuteCd();}return ch;
}
void ExecuteCommand(char Command[], size_t n)
{pid_t id = fork();if (id == 0)//子进程,执行命令{execvp(argv[0],argv);}else if (id > 0)//父进程{int status = 0;pid_t rid = waitpid(id, &status, 0);if (rid > 0){lastcode = WEXITSTATUS(status);if (lastcode != 0) printf("%s:%s:%d\n", argv[0], strerror(lastcode), lastcode);}}else//fork创建失败,直接退出{exit(1);}}int main()
{while(1){//输出命令行MakeCommandLine();//读取用户命令char UserCommand[SIZE];int n = GetUserCommand(UserCommand, sizeof(UserCommand));(void)n;//指令和选项分割SplitCommand(UserCommand, sizeof(UserCommand));//检查是否为内建命令int ch = CheckBuiltin();if (ch) continue;//执行命令ExecuteCommand(UserCommand, sizeof(UserCommand));}return 0;
}

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

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

相关文章

HarmonyOS 应用开发之通过数据管理服务实现数据共享静默访问

场景介绍 典型跨应用访问数据的用户场景下&#xff0c;数据提供方会存在多次被拉起的情况。 为了降低数据提供方拉起次数&#xff0c;提高访问速度&#xff0c;OpenHarmony提供了一种不拉起数据提供方直接访问数据库的方式&#xff0c;即静默数据访问。 静默数据访问通过数据…

精彩解读:短链接应用全方位探究

title: 精彩解读&#xff1a;短链接应用全方位探究 date: 2024/4/2 17:44:50 updated: 2024/4/2 17:44:50 tags: 短链接定义映射算法原理简洁美化优势工作流程解析安全隐私保护商业营销应用技术趋势发展 1. 短链接的定义和原理 短链接是一种将长网址转换为短网址的服务&#…

内网横向——域渗透之黄金票据复现

kerberos 协议 Kerberos是一种由MIT&#xff08;麻省理工大学&#xff09;提出的一种网络身份验证协议。它旨在通过使用密钥加密技术为客户端/服务器应用程序提供强身份验证。 在Kerberos协议中主要是有三个角色的存在&#xff1a; 访问服务的Client(以下表述为Client 或者用…

redis链表结构和简单动态字符串(SDS)

1.双向链表 redis中的普通链表是双向链表。通过链表节点结构体可知有全驱节点和后继节点。 1.链表节点和链表 //adlist.h typedef struct listNode {struct listNode *prev; //前驱节点struct listNode *next; //后继节点void *value; //节点值 } list…

33-4 XXE漏洞 - xxe判断

判断是否存在 XXE 攻击漏洞&#xff1a; 1&#xff09;有源码情况&#xff1a; 查找相关函数&#xff0c;如 simplexml_load_string()&#xff0c;用于检测 XML 是否会被解析。 2&#xff09;无源码情况&#xff1a; 抓包检查报文是否包含 XML 传参&#xff0c;如果是 XML …

Java项目:86 springboot电影评论网站系统设计与实现

作者主页&#xff1a;源码空间codegym 简介&#xff1a;Java领域优质创作者、Java项目、学习资料、技术互助 文中获取源码 项目介绍 本电影评论网站管理员和用户。 管理员功能有个人中心&#xff0c;用户管理&#xff0c;电影类别管理&#xff0c;电影信息管理&#xff0c;留…

CLIP 图文检索,相似度计算

CLIP 是OpenAI提出的神经网络&#xff0c;它可以从自然语言监督中有效地学习视觉概念。 CLIP 可以应用于任何视觉分类基准&#xff0c;只需提供要识别的视觉类别的名称&#xff0c;类似于 GPT-2 和 GPT-3 的“零样本”功能。 相关paper 用法可以参考github 这里举几个使用CLI…

时间管理系统的设计与实现|Springboot+ Mysql+Java+ B/S结构(可运行源码+数据库+设计文档)大学生

本项目包含可运行源码数据库LW&#xff0c;文末可获取本项目的所有资料。 推荐阅读300套最新项目持续更新中..... 最新ssmjava项目文档视频演示可运行源码分享 最新jspjava项目文档视频演示可运行源码分享 最新Spring Boot项目文档视频演示可运行源码分享 2024年56套包含ja…

JVM 组成

文章目录 概要JVM 是 Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09;JVM 的主要组成部分运行流程&#xff1a;程序计数器堆元空间方法区常量池运行时常量池 概要 JVM 是 Java程序的运行环境&#xff08;java二进制字节码的运行环境&#xff09; 好处&…

在linux下conda 创建虚拟环境失败的解决方法

报错内容一&#xff1a; Solving environment: failed CondaHTTPError: HTTP 000 CONNECTION FAILED for url <https://conda.anaconda.org/conda-forge/noarch/repodata.json> Elapsed: - An HTTP error occurred when trying to retrieve this URL. HTTP errors are…

关系型数据库与非关系型数据库、Redis数据库

相比于其他的内存/缓存数据库&#xff0c;redis可以方便的实现持久化的功能&#xff08;保存至磁盘中&#xff09; 一、关系数据库与非关系型数据库 1.1 关系型数据库 一个结构化的数据库&#xff0c;创建在关系模型基础上一般面向于记录 SQL语句 (标准数据查询语言) 就是一种…

可视化GIS开发:初识地图12大坐标系以及屏幕坐标

hello&#xff0c;我是贝格前端工场&#xff0c;地图在可视化开发中随处可见&#xff0c;地图需要用到坐标来标明位置关系&#xff0c;并映射的屏幕上&#xff0c;我们才能看到&#xff0c;本文先介绍地图中的常用坐标系&#xff0c;欢迎评论区补充&#xff0c;转发和分享。 在…