[Linux]编写一个极简版的shell-version1
文章目录
- [Linux]编写一个极简版的shell-version1
- 命令行提示符打印
- 接收命令行参数
- 将命令行参数进行解释
- 执行用户命令
- 完整代码
本文能够帮助Linux系统学习者通过代码的角度更好地理解命令行解释器的实现原理。
命令行提示符打印
Linux操作系统运行时,就需要shell进程进行命令行解释,然后让系统完成对应的命令,因此打印命令行提示符时要采用死循环打印的方式,具体的代码逻辑如下:
int main()
{while(1){printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));fflush(stdout);//sleep(200); -- 当前阶段用于演示效果}
- 为了和系统的shell进行区分因此打印两个$符号
- 命令行提示符打印的信息都是通过环境变量得来的,因此只需要调用系统接口获取相应的环境变量即可
USER
环境变量 – 当前使用的用户名HOSTNAME
环境变量 – 当前的主机名称PWD
环境变量 – 当前用户所处绝对路径
- 由于显示器采用的是行缓冲的策略,因此需要手动刷新缓冲区才能让命令行提示符显式到屏幕上
由于环境变量PWD是当前用户所处的绝对路径,因此需要编写一个函数getpath
来获取当前所处的目录名称,具体的代码逻辑如下:
const char* getpath(char* path)
{int length = strlen(path);if(length == 1) return "/"; //根目录int i = length - 1;while((path[i] != '/')) i--;return path+i+1;
}
- 如果绝对路径的字符串长度为1,表示所处为根目录,返回
"/"
- 其余路径下都要将绝对路径里当前目录前的所有路径分割掉,包括路径分隔符,譬如当前记录绝对路径环境变量为PWD=/home/qxm/linux-warehouse/review/mybash/version1,
getpath
的返回值为verson1字符串的首地址
效果演示:
接收命令行参数
接受命令行参数只需要设置一个字符串数组,将用户的输入接收即可,具体的代码逻辑如下:
#define MAX 1024int main()
{while(1){char commandstr[MAX] = { 0 }; //接收命令行参数printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));fflush(stdout);char* s = fgets(commandstr, sizeof(commandstr), stdin);commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'//printf("%s\n", commandstr); -- 当前阶段用于演示效果}return 0;
}
- 由于用户输入时会按下回车也就是会在输入时在末尾出现一个
'\n'
,命令行参数使用时需要将其去除掉,譬如用户输入ls -a -l\n,命令行参数只需要ls -a -l
效果演示:
将命令行参数进行解释
获取用户输入的命令行参数后,需要将命令行参数根据输入的空格将字符串分割为多个字串,譬如用户输入ls -a -l,要分割为ls, -a, -l,具体的代码逻辑如下:
#define MAX 1024
#define ARGC 64
#define SEP " "int split(char commandstr[], char* argv[])//命令行参数解释
{argv[0] = strtok(commandstr, SEP);if (argv[0] == NULL) return -1; //字符串为空int i = 1;while(1){argv[i] = strtok(NULL, SEP);if(argv[i] == NULL) break;i++;}return 0;
}void debugPrint(char* argv[])//--当前阶段用于演示效果
{int i = 0;for (i = 0; argv[i] != NULL; i++){printf("%s\n", argv[i]);}
}int main()
{while(1){char commandstr[MAX] = { 0 }; //接收命令行参数char* argv[ARGC] = { NULL }; //存储命令行参数printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));fflush(stdout);char* s = fgets(commandstr, sizeof(commandstr), stdin);commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'int n = split(commandstr, argv);if(n != 0) continue; //用户输入空串//debugPrint(argv); -- 当前阶段用于演示效果}return 0;
}
- 调用C语言库函数
strtok
获取分割空格字符后字串的首地址,并且将空格置为'\0'
,并将字串的首地址存储起来
效果演示:
执行用户命令
创建子进程,进程进程程序替换,让子进程完成用户所输入的命令,具体的代码逻辑如下:
#define MAX 1024
#define ARGC 64
#define SEP " "const char* getpath(char* path)
{int length = strlen(path);if(length == 1) return "/"; //根目录int i = length - 1;while((path[i] != '/')) i--;return path+i+1;
}int split(char commandstr[], char* argv[])
{argv[0] = strtok(commandstr, SEP);if (argv[0] == NULL) return -1; //字符串为空int i = 1;while(1){argv[i] = strtok(NULL, SEP);if(argv[i] == NULL) break;i++;}return 0;
}int main()
{while(1){char commandstr[MAX] = { 0 }; //接收命令行参数char* argv[ARGC] = { NULL }; //存储命令行参数printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));fflush(stdout);char* s = fgets(commandstr, sizeof(commandstr), stdin);commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'int n = split(commandstr, argv);if(n != 0) continue; //用户输入空串pid_t id = fork();//创建子进程完成命令执行if (id == 0){//子进程execvp(argv[0], argv); //进程程序替换exit(0);}int status = 0;waitpid(id, &status, 0);//回收子进程}return 0;
}
- 由于实现的shell用于执行系统命令,并且获取了记录命令行参数的数组,因此采用
execvp
进程程序替换
效果演示:
完整代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>#define MAX 1024
#define ARGC 64
#define SEP " "const char* getpath(char* path)
{int length = strlen(path);if(length == 1) return "/"; //根目录int i = length - 1;while((path[i] != '/')) i--;return path+i+1;
}int split(char commandstr[], char* argv[])
{assert(commandstr);assert(argv);argv[0] = strtok(commandstr, SEP);if (argv[0] == NULL) return -1; //字符串为空int i = 1;while(1){argv[i] = strtok(NULL, SEP);if(argv[i] == NULL) break;i++;}return 0;
}int main()
{while(1){char commandstr[MAX] = { 0 }; //接收命令行参数char* argv[ARGC] = { NULL }; //存储命令行参数printf("[%s@%s %s]$$ ", getenv("USER"), getenv("HOSTNAME"), getpath(getenv("PWD")));fflush(stdout);char* s = fgets(commandstr, sizeof(commandstr), stdin);assert(s); //对fgets函数的结果断言(void)s;//保证在release方式发布的时候,因为去掉assert了,所以s就没有被使用,而带来的编译告警, 什么都没做,但是充当一次使用commandstr[strlen(commandstr) - 1] = '\0'; //去除输入时末尾的'\n'int n = split(commandstr, argv);if(n != 0) continue; //用户输入空串pid_t id = fork();assert(id >= 0);(void)id;if (id == 0){//子进程execvp(argv[0], argv); exit(0);} int status = 0;waitpid(id, &status, 0);}return 0;
}
);if(n != 0) continue; //用户输入空串pid_t id = fork();assert(id >= 0);(void)id;if (id == 0){//子进程execvp(argv[0], argv); exit(0);} int status = 0;waitpid(id, &status, 0);}return 0;
}
说明: 该版本shell只能够执行系统命令,并且不能够支持所有系统命令比如cd命令。