首先附上完整代码
#include <stdio.h> #include <stdlib.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> //命令行解释器 //shell 运行原理:通过让子进程执行命令,父进程等待&&解析命令 //保存完整的命令行字符串 -- 充当缓冲区 #define NUM 1024 char cmd_line[NUM]; //保存切割之后的字符串 #define SIZE 32 char* g_argv[SIZE]; #define SEP " " int main() { //0.命令行解释器,一定是一个常驻内存的进程 --- 不退出(死循环) while(1) { //1打印出提示信息 -- [wxq@VM-4-9-centos shell]$ printf("[dwr@VM-1-1-test shell]$ "); fflush(stdout); //2.获取用户的键盘输入[输入的是各种指令和选项"ls -a -l"] memset(cmd_line, 0, sizeof cmd_line); if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } //输入的回车键设为\0 ls -a -l \n \0 cmd_line[strlen(cmd_line)-1] = '\0'; // printf("echo:%s\n", cmd_line); //debug //3.命令行字符串进行解析 "ls -a -l -s" ---> "la" "-a" "-l" "-s" g_argv[0] = strtok(cmd_line, SEP); //第一次调用,传入原始的字符串 int index = 1; while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要分割原始字符串,传入NULL //简单配置ls的颜色 int i = 1; if(strcmp(g_argv[0], "ls") == 0) { g_argv[i++] = "--color=auto"; } //识别别名 - 主要是测试,一般是有接口的 if(strcmp(g_argv[0], "ll") == 0) { g_argv[0] = "ls"; g_argv[i++] = "-l"; g_argv[i++] = "--color=auto"; } //debug // for(index = 0 ; g_argv[index]; index++) // { // printf("g_argv[%d]: %s\n", index, g_argv[index]); // } //4.TODO 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令 //内建命令本质就是shell中的一个函数调用 if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute; { if(g_argv[1] != NULL) { chdir(g_argv[1]); //cd cd .. } continue; } //5.创建子进程进行程序替换 pid_t id = fork(); //child if(id == 0) { execvp(g_argv[0],g_argv); exit(-1); } //father int status = 0; pid_t ret = waitpid(id, &status, 0); //阻塞等待 if(ret > 0) { printf("exit code:%d\n",WEXITSTATUS(status)); } } return 0; }
效果:
命令行解释器
shell 运行原理:通过让子进程执行命令,父进程等待&&解析命令
步骤1. 命令行解释器,一定是一个常驻内存的进程 --- 这意味着它是不退出的(死循环)
代码示例:
#include <stdio.h> int main() { while(1) { ; } return 0; }
步骤2.打印出提示信息
代码示例:
#include <stdio.h> int main() { while(1) { printf("[dwr@VM-1-1-test shell]$\n"); } return 0; }
这样打印出来我们会发现:
所以我们是不能在后面加上 '\n' 的,但是因为有缓冲区的存在,那么应该怎么办呢?
这里需要用到一个函数 --- fflush():刷新缓冲区
代码示例:
#include <stdio.h> int main() { while(1) { printf("[dwr@VM-1-1-test shell]$"); fflush(stdout); } return 0; }
步骤3.获取用户的键盘输入[示例 : 输入的是各种指令和选项"ls -a -l" ]
思路:
①需要一个数组来模拟缓冲区 - - - 提取用户输入的字符
②使用fgets读取用户在键盘上的输入,如果读取失败,continue重新进入循环,重新读取,重新打印。
③使用printf测试一下
代码示例:
#include <stdio.h> #include <string.h> #define NUM 1024 char cmd_line[NUM]; int main() { while(1) { printf("[dwr@VM-1-1-test shell]$"); fflush(stdout); memset(cmd_line, 0, sizeof cmd_line); if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } printf("echo:%s\n", cmd_line); } return 0; }
但是我们会发现打印出来的结果是:
这是一个需要注意的小细节的地方,因为在输入的时候,当我们最终输入字符结束的时候,会输入一个“回车”键盘,它会被缓冲区拿到并被识别为“\n”。
示例:输入 ls -l -a 缓冲区读取 ls -l -a \n
所以这里我们需要把 \n 设置为 \0 作为字符串的结束标志
代码示例:
#include <stdio.h> #include <string.h> #define NUM 1024 char cmd_line[NUM]; int main() { while(1) { printf("[dwr@VM-1-1-test shell]$"); fflush(stdout); memset(cmd_line, 0, sizeof cmd_line); if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } cmd_line[strlen(cmd_line)-1] = '\0'; //printf("echo:%s\n", cmd_line); //测试辅助 } return 0; }
输出结果:
步骤4.命令行字符串解析 [ 示例: " ls -a -l -s" ---> "ls" "-a" "-l" "-s"]
思路:
①可以把空格定为分割符,然后切割为一个一个的子串
②定义一个指针数组保存切割下来的子串
代码示例:
#include <stdio.h> #include <string.h> #define NUM 1024 #define SIZE 32 #define SEP " " char cmd_line[NUM]; char* g_argv[SIZE]; int main() { while(1) { printf("[dwr@VM-1-1-test shell]$"); fflush(stdout); memset(cmd_line, 0, sizeof cmd_line); if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } cmd_line[strlen(cmd_line)-1] = '\0'; //printf("echo:%s\n", cmd_line); //测试辅助 g_argv[0] = strtok(cmd_line, SEP); int index = 0; while(g_argv[index++] = strtok(NULL, SEP)); //测试辅助 //for(index = 0; g_argv[index]; index++) //printf("g_argv[%d]:%s\n", index, g_argv[index]); } return 0; }
步骤5.创建子进程进行程序替换
注:进程等待和进程替换后续会更新详细解说
代码示例:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/types.h> #define NUM 1024 #define SIZE 32 #define SEP " " char cmd_line[NUM]; char* g_argv[SIZE]; int main() { while(1) { printf("[dwr@VM-1-1-test shell]$"); fflush(stdout); memset(cmd_line, 0, sizeof cmd_line); if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } cmd_line[strlen(cmd_line)-1] = '\0'; //printf("echo:%s\n", cmd_line); //测试辅助 g_argv[0] = strtok(cmd_line, SEP); int index = 0; while(g_argv[index++] = strtok(NULL, SEP)); //测试辅助 //for(index = 0; g_argv[index]; index++) //printf("g_argv[%d]:%s\n", index, g_argv[index]); pid_t id = fork(); //child process if(id == 0) { execvp(g_argv[0],g_argv); exit(-1); } //father process int status = 0; pid_t ret = waitpid(id, &status, 0); //阻塞等待 if(ret > 0) { printf("exit code:%d\n",WEXITSTATUS(status)); } } return 0; }
其实写到这里一些简单的命令就已经可以跑了
示例:
退出自己写的shell脚本是 ctrl + c
步骤6.内置命令
但是上述代码有一些小问题,就是我们自己写的shell脚本它并没有让我们的路径发生变化
示例:
原因是因为,当前我们自己写的shell,无论我们写的任何指令,都是交给了子进程 , 子进程进行进程替换帮助我们来完成的指令,那么指令就只会影响子进程,而不会影响父进程。所以当我们 cd 回到上级目录的时候,父进程根本没有变化,但是可能子进程所在的路径一直在回到上一层路径。
那么我们想要的是shell脚本所在的路径发生变化,所以我们想要进行判断命令,如果是所谓cd这样的命令,那么我们不能创建子进程,而是直接交给父进程。
- 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令
- 内建命令本质就是shell中的一个函数调用
代码示例:
#include <stdio.h> #include <string.h> #include <unistd.h> #include <stdlib.h> #include <sys/types.h> #include <sys/types.h> #define NUM 1024 #define SIZE 32 #define SEP " " char cmd_line[NUM]; char* g_argv[SIZE]; int main() { while(1) { printf("[dwr@VM-1-1-test shell]$"); fflush(stdout); memset(cmd_line, 0, sizeof cmd_line); if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL) { continue; } cmd_line[strlen(cmd_line)-1] = '\0'; //printf("echo:%s\n", cmd_line); //测试辅助 g_argv[0] = strtok(cmd_line, SEP); int index = 0; while(g_argv[index++] = strtok(NULL, SEP)); //测试辅助 //for(index = 0; g_argv[index]; index++) //printf("g_argv[%d]:%s\n", index, g_argv[index]); if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute; { if(g_argv[1] != NULL) { chdir(g_argv[1]); //cd cd .. } continue; } pid_t id = fork(); //child process if(id == 0) { execvp(g_argv[0],g_argv); exit(-1); } //father process int status = 0; pid_t ret = waitpid(id, &status, 0); //阻塞等待 if(ret > 0) { printf("exit code:%d\n",WEXITSTATUS(status)); } } return 0; }
补充说明:
fflush()
#include<stdio.h> int main() { int fflush( FILE *stream ); return 0; }
定义:冲洗一个流
头文件:<stdio.h>
注释:如果缓冲区已成功刷新,则Fflush返回0。
关键字:continue:作用是跳过本次循环continue后面的代码,直接去判断部分,看是否进行下一次循环
memset()
#include<string.h> int main() { void *memset( void *dest, int c, size_t count ); return 0; }
定义:将缓冲区设置为指定字符 / 可以用来初始化字符串
头文件:<string.h>
注释:memset返回dest的地址
fgets()
#include<stdio.h> int main() { char *fgets( char *string, int n, FILE *stream ); return 0; }
定义:从流中获取字符串
头文件:<stdio.h>
注释:返回的是string。返回NULL表示错误或文件结束条件
strlen()
#include<string.h> int main() { size_t strlen( const char *string ); return 0; }
定义:返回的是字符串的长度
头文件:<string.h>
注释:strlen只返回'\0'之前字符串的长度
strtok()
#include<string.h> int main() { char *strtok( char *strToken, const char *strDelimit ); return 0; }
定义:查找字符串中的下一个标记。(常用于切割字符串)
头文件:<string.h>
注释:
sep参数是个字符串,定义了用作分隔符的字符集合
第一个参数指定一个字符串,它包含了0个或者多个由strDelimit字符串中一个或者多个分隔符分割的标记。
strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。
strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。
strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。
如果字符串中不存在更多的标记,则返回 NULL 指针。
chdir()
#include <unistd.h> int main() { int chdir(const char *path); return 0; }
定义:更改工作目录
头文件:#include <unistd.h>
返回值:如果成功,则返回0。如果出现错误,则返回-1,并适当地设置errno。
以上就是完整版简单shell脚本的编写,仅供参考