简易Shell实现
我们在Linux中使用的shell, 一般有两个 bash 和 zsh.
我们可以通过shell, 执行各种命令. 而本篇文章的主要内容, 就是实现一个简易的shell
简易shell功能
在实现shell之前, 肯定要明白简易的shell需要实现什么功能
实现shell
1. 循环接收用户命令
执行:
解决这个无限循环的打印, 只需要在printf之后设置一个接收输入内容地函数即可, 这里我们使用 fgets()
:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
int main() {
while(1) {
char command_V[SIZE];
memset(command_V, '', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_V, SIZE, stdin);
printf("%s", argV); // 测试进程是否接收了输入内容
}
return 0;
}
2. 创建子进程执行命令
shell 中的大多数命令都是通过创建子进来执行的.
可以通过fork()创建子进程, 然后进程替换实现命令的执行
实现fork()子进程替换为命令子进程, 最佳的进程替换的接口是:exevp()
C语言中, 关于字符串的函数中, 有一个strtok()函数是用来分割字符串的, 使用strtok()可以将指定字符串按照传入的分割符分开
此函数的返回规则为, 如果分割出了字符串, 则返回此字符串的指针; 否则 返回空指针
且, 第一次调用此函数之后, 若原字符串中还存在可以分割的字符串,
可以直接在strtok()的第一个参数传入空指针以再次调用此函数从上次分割出的字符串之后继续分割
但是在分割字符串之前, 需要将接收到的字符串的最后一个有效字符设置为''
, 因为接收到最后一个字符是'n'
command_V[strlen(command_V)-1] = ''
, 既可以修改'n'
为 ''
分割字符串
// 分割命令行
command_argV[0] = strtok(command_S, " ");
int index = 1;
while(command_argV[index++] = strtok(NULL, " "));
// strtok分割不到字符串时, 会返回空指针, 刚好可以作为数组的最后一个元素 及 循环结束的条件
command_argV是一个指针数组, 每个元素存储一个字符串. 此指针数组的作用是, 进程替换时传入execvp()接口
所以应该按照要求存储数组, 即 第0元素存储命令名, 之后每个元素存储一个选项
而我们从命令行接收到正确的命令的字符串的格式应该是:命令名 选项1 选项2 选项3 ...
所以 strtok() 传入的分割符应该是 " "
, 第一次执行 strtok(command_S, " ")
分割出命令名, 会返回命令名字符串
此后, 可以再次使用strtok(NULL, " ")
strtok会自动从命令名之后再次分割
command_argV[0] 设置为 命令名之后, 从 command_arg[1] 开始 将每一个选项存入其中
分割存储之后的 command_argV 内容可以展示一下:
将接收到的字符串分割存储到字符指针数组中之后, 就可以创建子进程并进程替换了
创建子进程就非常简单了:
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
char *command_argV[SIZE];
int main() {
while(1) {
char command_S[SIZE];
memset(command_S, '', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = ''; // 修改'n' 为 ''
// 分割命令行
command_argV[0] = strtok(command_S, " ");
int index = 1;
while(command_argV[index++] = strtok(NULL, " "));
// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件
// 创建子进程, 并进程替换
pid_t id = fork();
if(id == 0) {
//进程替换
execvp(command_argV[0], command_argV);
exit(-1); // 替换失败则 退出码-1
}
}
return 0;
}
但是 执行结果好像有些奇怪
3. 回收子进程
我们使用waitpid()来等待子进程. fork()获取的子进程pid, 刚好可以指定回收子进程
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
char *command_argV[SIZE];
int main() {
while(1) {
char command_S[SIZE];
memset(command_S, '', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = ''; // 修改'n' 为 ''
// 分割命令行
command_argV[0] = strtok(command_S, " ");
int index = 1;
while(command_argV[index++] = strtok(NULL, " "));
// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件
// 创建子进程, 并进程替换
pid_t id = fork();
if(id == 0) {
//进程替换
execvp(command_argV[0], command_argV);
exit(-1); // 替换失败则 退出码-1
}
// 父进程回收子进程
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0) {
printf("父进程成功回收子进程, exit_code: %d, exit_sig: %dn", WEXITSTATUS(status), WTERMSIG(status));
}
}
return 0;
}
此时, 再执行代码:
可以看到, 命令就可以正常执行了.
4. 优化不足
我们的myShell已经可以正常执行大部分的命令了, 但是还存在一些不足:
导致这些不足的原因是什么?怎么优化这些不足呢?
可以发现, 这两个命令真正执行的并不是简单的原命令, 那么我们也可以在myShell中做出优化
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
char *command_argV[SIZE];
int main() {
while(1) {
char command_S[SIZE];
memset(command_S, '', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = ''; // 修改'n' 为 ''
// 分割命令行
command_argV[0] = strtok(command_S, " ");
int index = 1;
// ls 色彩优化
if(strcmp(command_argV[0], "ls") == 0) {
command_argV[index++] = "--color=auto"; // 若执行ls命令, 则在ls命令后携带一个--color=auto选项
}
// ll 命令优化
if(strcmp(command_argV[0], "ll") == 0) {
command_argV[0] = "ls";
command_argV[index++] = "-l";
command_argV[index++] = "--color=auto";
}
while(command_argV[index++] = strtok(NULL, " "));
// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件
// 创建子进程, 并进程替换
pid_t id = fork();
if(id == 0) {
//进程替换
execvp(command_argV[0], command_argV);
exit(-1); // 替换失败则 退出码-1
}
// 父进程回收子进程
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret > 0) {
printf("父进程成功回收子进程, exit_code: %d, exit_sig: %dn", WEXITSTATUS(status), WTERMSIG(status));
}
}
return 0;
}
此时, ll 和 ls 就可以更加完善的执行:
5. 自建命令添加
我们可以通过自己的实现的简易的EasyShell, 来执行大多数的命令
但是 有一些命令是无法执行的:
为什么 cd 和 export 明明都可以执行, 但是却没有作用呢?
因为, cd 和 export 命令实际上都是shell的内建命令, PATH环境变量路径下存在的程序其实也没有实际功能的:
可以看到, 执行 /usr/bin 路径下的cd程序, 也是没有作用的
在介绍Linux进程时提到过, 进程存在一个当前路径, 表示进程当前运行的路径, 在/proc目录下的进程目录下可以看到
举个例子:
当我在/home/July 路径下执行 /home/July/procTest/a.out 程序时, 创建出来的进程运行的当前路径是什么呢?
可以看到, 在 /home/July 路径下执行 /home/July/procTest/a.out 程序时, 创建出的进程的当前运行的路径其实时 /home/July
同样的道理, 我们执行cd总是在用户当前所处的路径下, 那么cd执行之后的当前路径也就是用户执行cd时所在的路径, 并不会发生改变
所以 要实现cd的功能, 就需要
内建命令实现修改进程当前运行的路径
这些命令并不是shell通过创建子进程的方式执行的, 而是shell自己在内部执行的, 此类的命令被称为内建命令:
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
int putEnvInmyShell(char *put_Env) {
putenv(put_Env);
return 0;
}
int changeDir(const char* new_path) {
chdir(new_path); // 系统调用
return 0;
}
char *command_argV[SIZE];
char copy_env[SIZE];
int main() {
while(1) {
char command_S[SIZE];
memset(command_S, '', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = ''; // 修改'n' 为 ''
// 分割命令行
command_argV[0] = strtok(command_S, " ");
int index = 1;
if(strcmp(command_argV[0], "ls") == 0) {
command_argV[index++] = "--color=auto"; // 若执行ls命令, 则在ls命令后携带一个--color=auto选项
}
if(strcmp(command_argV[0], "ll") == 0) {
command_argV[0] = "ls";
command_argV[index++] = "-l";
command_argV[index++] = "--color=auto";
}
while(command_argV[index++] = strtok(NULL, " "));
// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件
// 内建命令
if(strcmp(command_argV[0], "cd") == 0 && command_argV[1] != NULL) {
// 使用cd命令时, command_argV[1]位置应该是需要进入的路径
changeDir(command_argV[1]);
continue; // 非子进程命令, 不用执行下面的代码, 所以直接进入下个循环
}
if(strcmp(command_argV[0], "export") == 0 && command_argV[1] != NULL) {
// 我们接收的命令, 都在command_S 字符串中, 此字符串每次循环都会被清除
// 所以不能直接将 command_argV[1] putenv到环境变量中, 因为指向的同一块地址
// 所以 需要先拷贝一份
strcpy(copy_env, command_argV[1]);
putEnvInmyShell(copy_env);
continue;
}
// 创建子进程, 并进程替换
pid_t id = fork();
if(id == 0) {
//进程替换
execvp(command_argV[0], command_argV);
exit(-1); // 替换失败则 退出码-1
}
// 父进程回收子进程
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret < 0) {
exit(-1);
}
}
return 0;
}
简易shell代码
完成了两个内建命令的代码之后, 简易shell可以说是基本实现了
实现这个简易的shell, 只是为了加深对进程、环境变量、进程等待、进程替换等进程相关知识的理解
比起真正的一个完善的shell, 差的还有十万八千里.
我们一般使用的bash, 除了执行命令的功能, 至少还有:backspace删除
、历史命令
、Tab补全
等非常方便的功能, 这些功能都没有在本篇文章中实现, 有兴趣的话可以查找资料实现一下
#include <unistd.h>
#include <stdio.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <stdlib.h>
#define SIZE 128
int putEnvInmyShell(char *put_Env) {
putenv(put_Env);
return 0;
}
int changeDir(const char* new_path) {
chdir(new_path); // 系统调用
return 0;
}
char *command_argV[SIZE];
char copy_env[SIZE];
int main() {
while(1) {
char command_S[SIZE];
memset(command_S, '', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = ''; // 修改'n' 为 ''
// 分割命令行
command_argV[0] = strtok(command_S, " ");
int index = 1;
if(strcmp(command_argV[0], "ls") == 0) {
command_argV[index++] = "--color=auto"; // 若执行ls命令, 则在ls命令后携带一个--color=auto选项
}
if(strcmp(command_argV[0], "ll") == 0) {
command_argV[0] = "ls";
command_argV[index++] = "-l";
command_argV[index++] = "--color=auto";
}
while(command_argV[index++] = strtok(NULL, " "));
// strtok分割不到字符串时, 会返回空指针, 刚好可以作为循环结束的条件
// 内建命令
if(strcmp(command_argV[0], "cd") == 0 && command_argV[1] != NULL) {
// 使用cd命令时, command_argV[1]位置应该是需要进入的路径
changeDir(command_argV[1]);
continue; // 非子进程命令, 不用执行下面的代码, 所以直接进入下个循环
}
if(strcmp(command_argV[0], "export") == 0 && command_argV[1] != NULL) {
// 我们接收的命令, 都在command_S 字符串中, 此字符串每次循环都会被清除
// 所以不能直接将 command_argV[1] putenv到环境变量中, 因为指向的同一块地址
// 所以 需要先拷贝一份
strcpy(copy_env, command_argV[1]);
putEnvInmyShell(copy_env);
continue;
}
// 创建子进程, 并进程替换
pid_t id = fork();
if(id == 0) {
//进程替换
execvp(command_argV[0], command_argV);
exit(-1); // 替换失败则 退出码-1
}
// 父进程回收子进程
int status = 0;
pid_t ret = waitpid(id, &status, 0);
if(ret < 0) {
exit(-1);
}
}
return 0;
}
原文地址:https://blog.csdn.net/dxyt2002/article/details/129800496
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:http://www.7code.cn/show_10635.html
如若内容造成侵权/违法违规/事实不符,请联系代码007邮箱:suwngjj01@126.com进行投诉反馈,一经查实,立即删除!