引言:本篇文章主要是简单实现一个shell命令行解释器,可以支持基础常见的linux的命令,支持内建命名echo、cd,同时支持重定向的操作!
一、代码剖析
1. 头文件引入:
因代码是在linux下实现,引入的大多头文件是Linux的系统调用,建议在linux环境下使用。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <assert.h>
#include <string.h>
#include <errno.h>
这些头文件包含了一些需要使用的库函数和系统调用。
2. 定义一些常量和全局变量:
这些常量和变量用于存储命令行输入、命令参数、重定向类型和重定向文件等信息。
#define OPTION 64
#define NUM 1024
//定义重定向类型
#define NONE_REDIR 0
#define INPUT_REDIR 1
#define OUTPUT_REDIR 2
#define APPEND_REDIR 3
//定义个宏函数,用于将字符串指针移动到无空格的第一个字符上
#define trimSpace(start) do{\
while(*start == ' '){\
start++;}\
}while(0)
//保存键盘上输入的命令
char lineComend[NUM];
char* myargv[OPTION];//定义一个char*的指针数组,用于存放参数
int lastCode = 0;//父进程记录上次子进程执行结果
int lastSig = 0;
//记录是否是重定向和类型
int redirType = NONE_REDIR;
char *redirFile = NULL;//记录重定向文件名
3. 定义一个辅助函数find_Redirect:
这个函数用于查找命令行参数中的重定向符号(>和<)并解析相关的重定向文件和类型。
void find_Redirect(char* argv){
char* start = NULL;
for(int i = 0;i<(int)strlen(argv);i++){
if(argv[i] == '>'||argv[i]=='<'){
if(argv[i]=='<'){
//cat < log.txt
argv[i] = '\0';
//去除重定向后面的空格,只保留文件名部分,存放到全局变量redirFile中
start = argv+i+1;
trimSpace(start);
redirFile = start;
redirType = INPUT_REDIR;
}else{
if(argv[i+1] == '>'){
//cat xiaomi >> log.txt
argv[i] = '\0';
start = argv+i+2;
trimSpace(start);
redirFile = start;
redirType = APPEND_REDIR;
}else{
argv[i] = '\0';
start = argv+i+1;
trimSpace(start);
redirFile = start;
redirType = OUTPUT_REDIR;
}
}
}
}
}
4. 主函数
- 主函数包含一个无限循环,在每次循环中等待用户输入命令,并处理命令执行、重定向和内置命令等逻辑。
- 在循环的开始,通过
printf
输出命令提示符,然后使用fgets
读取用户输入的命令行,并处理去除结尾的换行符。- 接下来,调用
find_Redirect
函数来查找重定向符号,并解析重定向文件和类型。- 然后,使用
strtok
函数对输入命令进行切割,将切割后的命令参数存储到myargv
数组中。同时,处理一些特殊的内置命令,如cd
和echo
。- 对于
echo
命令,根据重定向类型进行输出重定向。- 如果不是内置命令,则创建子进程,并在子进程中执行命令。根据重定向类型,进行输入输出重定向。
- 最后,使用
waitpid
等待子进程执行完毕,并获取子进程的退出状态码和信号。
int main(){
//一开始先初始化重定向文件和类型
while(1){
redirFile = NULL;
redirType = NONE_REDIR;
printf("[suhh@ziqiang address]$ ");
fflush(stdout);
//用户输入
assert(fgets(lineComend,sizeof(lineComend)-1,stdin)!=NULL);
//清除数组最后一个\n
lineComend[strlen(lineComend)-1] = '\0';
//ls -a -l
//切割字符串 靠' '
find_Redirect(lineComend);
myargv[0] = strtok(lineComend," ");
// char * echo_str = NULL;
if(strcmp(myargv[0],"echo")==0){
myargv[1] = myargv[0] + strlen(myargv[0]) + 1;//取出剩余字串
goto echo_start;
}
//strtok 会把剩余的放到静态变量中,下次调用只用穿null
int i = 1;
//myargv[end]=NULL
if(myargv[0]!=NULL&&(strcmp(myargv[0],"ls")==0||strcmp(myargv[0],"ll")==0)){
myargv[i++] = (char*)"--color=auto";
if(strcmp(myargv[0],"ll")==0) {
myargv[i++] = (char*)"-l";
myargv[0] = (char*)"ls";
}
//让ls命令默认加上颜色
}
while(myargv[i++] = strtok(NULL," ")){}
if(strcmp(myargv[0],"cd")==0){
//如果是cd命令,不能用子进程执行,如果用子进程执行不会影响父进程,达不到效果
//这时这就是内建命令
if(myargv[1]!=NULL)chdir(myargv[1]);
continue;
}
echo_start:
if(myargv[0]!=NULL && strcmp(myargv[0],"echo")==0){
//这也是内建命令
//判断重定向类型
int fd = 0;
if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
int flags = O_WRONLY | O_CREAT;
if(redirType == APPEND_REDIR) flags = flags | O_APPEND;
else flags = flags | O_TRUNC;
fd = open(redirFile,flags,0666);
//先把标准输出流备份一下
dup2(1,5);
//重定向
dup2(fd,1);
}
if(myargv[1]!=NULL&&strcmp(myargv[1],"$?")==0){
printf("%d,%d\n",lastCode,lastSig);
}else{
if(myargv[1]!=NULL){
//如果用户输入 echo "hello" 去除“”
if(myargv[1][0] =='"'){
myargv[1] = (char*)&myargv[1][1];
}
int str_len = strlen(myargv[1]);
if((myargv[1][str_len-1] == '"')||(myargv[1][str_len -2] == '"')){
if(myargv[1][str_len-1] == '"') str_len --;
else str_len -= 2;
}
for(int i = 0 ;i<str_len;i++){
printf("%c",myargv[1][i]);
}
printf("\n");
}
}
if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
//恢复标准输出流
close(fd);
dup2(5,1);
}
continue;
}
pid_t it = fork();
assert(it>=0);
if(it == 0){
//判断重定向
//cat < log.txt
if(redirType == INPUT_REDIR){
int rfd = open(redirFile,O_RDONLY);
if(rfd == -1){
perror("错误:open");
exit(errno);
}
dup2(rfd,0);
}else if((redirType == OUTPUT_REDIR)||(redirType == APPEND_REDIR)){
int flags = O_WRONLY | O_CREAT;
if(redirType == OUTPUT_REDIR) flags |= O_TRUNC;
else flags |= O_APPEND;
int wfd = open(redirFile,flags,0666);
assert(wfd>=0);
(void)wfd;
dup2(wfd,1);
}
//子进程执行进程程序替换
execvp(myargv[0],myargv);
exit(1);
}
int status = 0;
int wp = waitpid(it,&status,0);//阻塞等待
assert(wp>=0);
(void)wp;
lastCode = (status>>8)&0xFF;
lastSig = status&0x7F;
printf("exit_code:%d,exit_signal:%d\n",lastCode,lastSig);
}
return 0;
}
二、 涉及知识点
1. fork()函数
fork函数属于系统调用,用于创建子进程,返回子进程pid给父进程,返回0给子进程.
- fork之后会有两个进程分别执行后续的代码(其实在fork函数体内,父子进程已经产生,所以有两个返回值)
- 子进程拥有自己的PCB和虚拟地址,这些都是和父进程一样的,也就是说,父进程的虚拟地址的内容拷贝了一份给子进程。
- 因为子进程拥有和父进程一样的虚拟地址,体现在代码上,就是子进程也可以”共有“父进程定义的变量等,但如果子进程要改变它的值,就会发生写时拷贝,操作系统会在物理内存中重新开辟一段空间,再通过页表,重新映射到虚拟地址处。换句话说,子进程以为访问的虚拟地址是父进程一起共用的,实际上是另一个物理地址。
2. 进程程序替换
将指定进程加载到内存中,替换原本进程的代码段和数据段,让这个进程以为在执行自己的代码,其实是在执行别人的代码。
在C语言中提供了丰富的函数用于程序替换
函数原型: int execl(const char *path, const char *arg, ...);
举例:execl("/user/bin/ls","ls",NULL);
解释:第一个参数是填要替换的程序在哪,路径
第二个参数是要填这个程序要怎么执行
第三个参数是要填这个程序要带什么参数,要以NULL结尾
错误返回-1
函数原型:int execlp(const char *file, const char *arg, ...);
举例:execlp("ls","ls",NULL);
解释:在上一个函数原型的基础上了一个p,代表path,此时无需传入程序地址,只需告诉程序叫什么,会自动在环境变量中进行可执行程序的查找。
第一个参数填程序名
第二个参数是要填这个程序要怎么执行
第三个参数是要填这个程序要带什么参数,要以NULL结尾
错误返回-1
函数原型:int execv(const char *path, char *const argv[])
举例:char* argv[] ={"ls","-a","-l","--color = auto",NULL};
execv("/user/bin/ls",argv);
解释:这次加了一个v,可以将所有可执行参数放到一个字符串指针数组中,统一传递。
第一个参数是填要替换的程序在哪,路径
第二个参数是要填字符串指针数组
错误返回-1
函数原型:int execvp(const char *file, char *const argv[]);
举例:execvp("ls",argv);
解释:这个加了v和p,拥有了前两个的特性。本文中的代码就是用的这个
第一个参数填程序名
第二个参数是要填字符串指针数组
错误返回-1