[Linux] 教你实现一个简单的、属于自己的Shell
简易Shell实现
简易shell功能
-
首先要知道, shell应该是一个死循环的程序.
为什么?因为shell是可以
循环从命令行接收用户输入的内容
的 -
其次, shell 需要一个设置一个提示符. 类似这样的东西:
-
第三, 我们使用shell是需要执行命令的, 且这些命令需要在环境变量PATH下
这些命令大多都是需要由我们的shell创建子进程来执行的
-
第四, shell需要可以
等待回收
创建的子进程 -
第五, 需要实现一些内建命令:比如
export
等这些命令 是不需要创建子进程来执行的
实现shell
1. 循环接收用户命令
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, '\0', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_V, SIZE, stdin);
printf("%s", argV); // 测试进程是否接收了输入内容
}
return 0;
}
2. 创建子进程执行命令
exevp()
- 首先是因为, 我们接收了命令行输入的程序及选项字符串, 将字符串根据空格分割开 就是一个命令和选项的数组
- 带
p
字的接口, 会默认从环境变量PATH的路径下搜索, 不需要在添加程序的路径
使用strtok()可以将指定字符串按照传入的分割符分开
strtok
:第一个参数
str
, 传入需要分割的字符串第二个参数
delimiters
, 传入分割符此函数的返回规则为, 如果分割出了字符串, 则返回此字符串的指针; 否则 返回空指针
且, 第一次调用此函数之后, 若原字符串中还存在可以分割的字符串,
可以直接在strtok()的第一个参数传入空指针以再次调用此函数从上次分割出的字符串之后继续分割
'\0'
, 因为接收到最后一个字符是'\n'
command_V[strlen(command_V)-1] = '\0'
, 既可以修改'\n'
为 '\0'
分割字符串
// 分割命令行
command_argV[0] = strtok(command_S, " ");
int index = 1;
while(command_argV[index++] = strtok(NULL, " "));
// strtok分割不到字符串时, 会返回空指针, 刚好可以作为数组的最后一个元素 及 循环结束的条件
命令名 选项1 选项2 选项3 ...
" "
, 第一次执行 strtok(command_S, " ")
分割出命令名, 会返回命令名字符串strtok(NULL, " ")
strtok会自动从命令名之后再次分割
#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, '\0', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = '\0'; // 修改'\n' 为 '\0'
// 分割命令行
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. 回收子进程
博主的进程等待相关文章:
#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, '\0', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = '\0'; // 修改'\n' 为 '\0'
// 分割命令行
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: %d\n", WEXITSTATUS(status), WTERMSIG(status));
}
}
return 0;
}
4. 优化不足
#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, '\0', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = '\0'; // 修改'\n' 为 '\0'
// 分割命令行
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: %d\n", WEXITSTATUS(status), WTERMSIG(status));
}
}
return 0;
}
5. 自建命令添加
其实并不是没有作用, 而是
cd进程当前运行的路径没有改变
在介绍Linux进程时提到过, 进程存在一个当前路径, 表示进程当前运行的路径, 在/proc目录下的进程目录下可以看到
举个例子:
当我在/home/July 路径下执行 /home/July/procTest/a.out 程序时, 创建出来的进程运行的当前路径是什么呢?
可以看到, 在 /home/July 路径下执行 /home/July/procTest/a.out 程序时, 创建出的进程的当前运行的路径其实时 /home/July
同样的道理, 我们执行cd总是在用户当前所处的路径下, 那么cd执行之后的当前路径也就是用户执行cd时所在的路径, 并不会发生改变
所以 要实现cd的功能, 就需要
内建命令实现修改进程当前运行的路径
#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, '\0', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = '\0'; // 修改'\n' 为 '\0'
// 分割命令行
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代码
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, '\0', SIZE);
// 首先是用户提示符:
printf("[七月July@MyBlog 当前目录]# ");
fgets(command_S, SIZE, stdin);
command_S[strlen(command_S) - 1] = '\0'; // 修改'\n' 为 '\0'
// 分割命令行
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;
}
作者: 哈米d1ch 发表日期:2023 年 3 月 11 日