Linux:自主Shell命令行解释器

一、目标

  • 理解普通命令
  • 理解内建命令
  • 理解本地变量
  • 理解环境变量
  • 理解shell的运行原理

二、模拟shell

大部分是软件都是死循环,shell的本质也是死循环

1.打印命令行

可以直接调用系统函数,这里是简单的模拟,就不用系统函数了,直接在环境变量里面提取

cpp 复制代码
1 #include <stdio.h>
  2 #include <stdlib.h>
  3 #include <unistd.h>
  4 
  5 const char* GetUserName()
  6 {
  7     char* name = getenv("USER");
  8     if(name == NULL)
  9         return "None";
 10     return name;                                                                                                                                       
 11 }
 12 
 13 const char* GetHostName()
 14 {
 15     char* hostname = getenv("HOSTNAME");
 16     if(hostname == NULL)
 17         return "None";
 18     return hostname;
 19 }
 20 
 21 const char* GetPwd()
 22 {
 23     char* pwd = getenv("PWD");
 24     if(pwd == NULL)
 25         return "None";
 26     return pwd;
 27 }
 28 
 29 void PrintCommanLine()
 30 {
 31     printf("[%s@%s %s]# ",GetUserName(), GetHostName(), GetPwd()); // 用户名 @ 主机名 当前路径11
 32 }
 33 int main()
 34 {
 35     while(1)
 36     {
 37         PrintCommanLine();
 38         sleep(1);
 39     }
 40 
 41 
 42     return 0;
 43 }

运行会发现,什么都没有打印?

因为,在缓冲区里面没有刷新,但是加上换行就不符合预期了,这里需要自己调用刷新缓冲区的函数。

cpp 复制代码
9 void PrintCommanLine()
 30 {
 31     // 打印命令行字符串
 32     printf("[%s@%s %s] ",GetUserName(), GetHostName(), GetPwd()); // 用户名 @ 主机名 当前路径11
 33     fflush(stdout);
 34                                                                                                                                                        
 35 }    

刷新缓冲区可以打印出来,但是一直打印也不符号预期,打印一下之后应该是等待键盘上的输入

2.键盘上输入信息

当然不能使用scanf函数来获取 ,因为有的命令是带有选项的,这里使用fgets函数来获取

**作用:**从指定的文件流中获取一行字符串

**成功:**s的起始地址

**失败:**NULL

cpp 复制代码
 36 int main()
 37 {
 38     char command_line[MAXSIZE] = {0};
 39     while(1)
 40     {
 41         //1.打印命令行字符串
 42         PrintCommanLine();
 43         //2.获取用户输入
 44        if(NULL == fgets(command_line, sizeof(command_line), stdin))
 45            continue;
 46        printf("%s\n", command_line);
 47                                                                                                                                                        
 48         sleep(1);
 49     }
 50 
 51 
 52     return 0;
 53 }

运行会发现多了一个换行符:

运行结果:

cpp 复制代码
[zhangsan@hcss-ecs-f571 shell]$ make
gcc -o myshell myshell.c #-std=c++11
[zhangsan@hcss-ecs-f571 shell]$ ./myshell 
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] ls -a -l
ls -a -l

[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] 

这是因为输入是时候会输入一个换行符,这个换行符也被读进去了。

这样解决:

cpp 复制代码
 43         //2.获取用户输入
 44         if(NULL == fgets(command_line, sizeof(command_line), stdin))
 45            continue;
 46         //2.1用户在输入的时候,至少会按一次回车abcd\n,\n是一个字符
 47         command_line[strlen(command_line)-1] = '\0';
 48         printf("%s\n", command_line);  

运行结果符合预期

封装一下:

cpp 复制代码
 37 int GetCommand(char commandline[], int size)
 38 {
 39 
 40     if(NULL == fgets(commandline, size, stdin))
 41         return 0;
 42     //2.1用户在输入的时候,至少会按一次回车abcd\n,\n是一个字符
 43         commandline[strlen(commandline)-1] = '\0';
 44         return strlen(commandline);
 45 }

 // main函数里面的:
 53         //2.获取用户输入
 54 
 55        if(0 == GetCommand(command_line, sizeof(command_line)))
 56             continue;
 57         printf("%s\n", command_line);    

3.解析字符串

cpp 复制代码
// 解析字符串 "ls -a -l" -> "ls" "-a" "-l" NULL
//           gargv[]下标:    0    1    2    3

关于这个函数的使用,可以看下面这篇文章

C语言常用字符串函数https://blog.csdn.net/2401_88433210/article/details/146296612?spm=1011.2415.3001.10575&sharefrom=mp_manage_link

解析字符串的代码:

cpp 复制代码
 56 int ParseCommand(char commandline[])                    
 57 {                                                       
 58                                                         
 59     // 每次进来清空gargv表                              
 60     gargc = 0;                                          
 61     memset(gargv, 0, sizeof gargv);                     
 62                                                         
 63     // 故意写成= 
 64     // 单个命令也没有错误,while语句里面会加1的 
 65     gargv[0] = strtok(commandline, gsep);
 66     while((gargv[++gargc] = strtok(NULL, gsep)));
 67     printf("gargc:%d\n", gargc);                                                                                                                       
 68     int i = 0;                   
 69     for(;gargv[i]; i++)          
 70     {                                       
 71         printf("gargv[%d]:%s\n",i,gargv[i]);
 72     }                                       
 73     return gargc;                                                                               
 74 }  

4.执行这个命令

当然不可以让bash自己进行程序替换,需要让子进程进行程序替换

cpp 复制代码
 77 int ExecuteCommand()
 78 {
 79     // 不能让bash执行程序替换函数,这里需要创建子进程
 80     pid_t id = fork();
 81     if(id < 0)
 82         return -1;
 83     else if(id == 0)
 84     {
 85         // 子进程   如何执行?
 86         execvp(gargv[0], gargv);
 87         exit(1); // 程序替换失败,退出码设为1
 88     }
 89     else
 90     {
 91         // 父进程等子进程
 92         int status = 0;
 93         pid_t rid = waitpid(id, &status, 0);
 94         if(rid > 0)
 95         {
 96             // 等待成功
 97            // printf("wait child process success!\n");
 98 
 99         }
100     }
101     return 0;                                                                                                                                          
102 }

运行结果:(可以正常运行)

bash 复制代码
[zhangsan@hcss-ecs-f571 shell]$ make
gcc -o myshell myshell.c #-std=c++11
[zhangsan@hcss-ecs-f571 shell]$ ./myshell 
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] ls -a -l
total 36
drwxrwxr-x  3 zhangsan zhangsan  4096 Dec 24 20:13 .
drwxrwxr-x 11 zhangsan zhangsan  4096 Dec 23 20:22 ..
-rw-rw-r--  1 zhangsan zhangsan    80 Dec 23 20:30 Makefile
-rwxrwxr-x  1 zhangsan zhangsan 13408 Dec 24 20:13 myshell
-rw-rw-r--  1 zhangsan zhangsan  2747 Dec 24 20:13 myshell.c
drwxrwxr-x  2 zhangsan zhangsan  4096 Dec 23 22:38 test
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] 

细节1:理解命令

**问题:**自己的shell可以再一次调用自己的shall吗

**答案:**百分之百是可以的

原因:

  1. 系统的bash本身也是一个程序(命令),也是被执行起来的
  2. 当我们在su - 的时候,本质是系统是在当前的bash上再调用(套)一个bash(让root账号重新登陆一次)

bash和shell的关系:

Linux中bash和shell是什么关系

细节2:理解内建命令

$?不能正常执行(下面7解决)

bash 复制代码
[zhangsan@hcss-ecs-f571 shell]$ ./myshell 
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] ls
Makefile  myshell  myshell.c  test
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] $?    
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] echo $?
$?
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] 

cd路径切不过去

bash 复制代码
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] pwd
/home/zhangsan/learn_-linux/shell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] cd ..
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] pwd
/home/zhangsan/learn_-linux/shell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] cd ..
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] pwd
/home/zhangsan/learn_-linux/shell
[zhangsan@hcss-ecs-f571 /home/zhangsan/learn_-linux/shell] 

**原因:**因为当前执行这些命令的是fork后的子进程执行的,不是bash执行的。

这样执行是不对的,需要让bash自己执行这些命令,这些命令就是内建命令

所以在执行命令前需要判断一下这个命令是内建命令还是需要子进程执行的命令

5.处理内建命令

chdir可以改变当前的工作路径(谁调用就该谁的)

bash 复制代码
 // 函数:
 77 // return val:
 78 // 0 :不是内建命令
 79 // 1 :是内建命令 && 执行完毕(没有风险的)
 80 int CheckBuiltinExecute()
 81 {
 82 
 83     if(strcmp(gargv[0],"cd") == 0 )
 84     {
 85         // 内建命令
 86         if(gargc == 2)
 87         {
 88             // 新的目标路径:gargv[1]
 89             chdir(gargv[1]);
 90         }
 91         return 1;
 92     }
 93     return 0;
 94 }
// main函数中:
134         //3.解析字符串"ls -a -l" -> "ls" "-a" "-l"
135         //命令行解释器,需要对用户输入的字符串首先进行解析
136         ParseCommand(command_line);
137         //3.5 判断:这个命令是让父进程bash执行(内建命令),还是让子进程执行
138         if(CheckBuiltinExecute() > 0)
139         {
140             // 是内建命令
141             continue;
142         }
143         //4.子进程执行这个命令    
144         ExecuteCommand();

但是运行会发现命令行的路径没有改变

原因:这里的路径是从环境变量里面拿的,环境变量被更新(操作系统不会自动更改环境变量,是用户级别的数据)需要bash自己来修改

解决方法1:

用系统调用来获取当前路径。

getcwd怎么获取当前工作路径的?

Linux中getcwd是怎么获得当前工作路径的?

cpp 复制代码
 36 const char* GetPwd()
 37 {
 38     //char* pwd = getenv("PWD");
 39     char* pwd = getcwd(cwd, sizeof cwd);
 40     if(pwd == NULL)
 41         return "None";
 42     return pwd;
 43 }

这样是不会修改环境变量里面的PWD,但是运行起来也不符合预期,预期是环境变量是随着当前工作路径而修改的。

解决方法2:

用环境变量来修改

介绍函数:

**snprintf功能:**可以格式化的将内容打印到指定长度的字符串中

cpp 复制代码
121 int CheckBuiltinExecute()
122 {
123 
124     if(strcmp(gargv[0],"cd") == 0 )
125     {
126         // 内建命令
127         if(gargc == 2)
128         {
129             // 新的目标路径:gargv[1]
130             // 1.更改进程内核中的路径
131             chdir(gargv[1]);
132             // 2.更改环境变量
133             char pwd[1024];
134             getcwd(pwd, sizeof(pwd));
135             // 这里避免"PWD="被覆盖(因为putenv不会拷贝字符串,而是直接使用传入指针指向的内存)
136             // 需要开辟新的空间,这里加1是因为字符串需要以"\0"结尾。
137             char* new_pwd = (char*)malloc(strlen("PWD=") + strlen(pwd) + 1);
138             snprintf(new_pwd, strlen("PWD=") + strlen(pwd) + 1, "PWD=%s", pwd);// PWD=/home/zhangsan
139             putenv(new_pwd);// 导出环境变量
140             // 修改自己的环境变量,这里没有覆盖原来的PWD                                                                                                
141             // genv[genvc++] = new_pwd;
142             // genv[genvc] = NULL;
143             lastcode = 0;                  
144         }                                  
145         return 1;                          
146     }                         

关于为什么这里需要开辟新的空间来存:"PWD=":

为什么运行结果没有把PWD=给导入到环境变量中??

如果不开辟环空间来存"PWD=",是这个样子的:

细节:为什么内建命令还有可执行文件

**原因:**命令行有两种执行模式

  1. 交互式(用户输入一条命令一个回车,输出结果)
  2. 批处理:用shell脚本运行命令(把命令写到脚本中,脚本一行一行的执行)

运行shell脚本的时候会在bash上创建子进程,这时候运行内建命令就需要执行对应的程序文件因为内建命令在shell内部的。

6.处理命令行的格式问题

这里用c++string的rfind来查找最后一个"/"的位置,使用substr来进行截取

直接实现成函数:

cpp 复制代码
 21 static std::string rfindDir(const std::string& p)
 22 {
 23     if(p == "/") 
 24         return p;
 25     const std::string psep = "/";
 26     auto pos = p.rfind(psep);
 27     if(pos == std::string::npos)
 28         return std::string();
 29     return p.substr(pos + 1);
 30 }

在打印函数里面调用即可

cpp 复制代码
 57 void PrintCommanLine()
 58 {
 59     printf("[%s@%s %s]# ",GetUserName(), GetHostName(),rfindDir(GetPwd()).c_str()); // 用户名 @ 主机名 当前路径
 60     fflush(stdout);
 61 
 62 }

成功解决:

7.解决**$?不能正常执行**

定义一个全局变量退出码:lastcode

  • 在父进程成功等待后,接收一下退出码
  • 在内建命令运行完后,也设置一下退出码

可以保证lastcode是最后一个命令执行时的退出码!

内建命令函数:(添加一个内建命令并且设置lastcode)

cpp 复制代码
99 int CheckBuiltinExecute()
100 {
101 
102     if(strcmp(gargv[0],"cd") == 0 )
103     {
104         // 内建命令
105         if(gargc == 2)
106         {
107             // 新的目标路径:gargv[1]
108             // 1.更改进程内核中的路径
109             chdir(gargv[1]);
110             // 2.更改环境变量
111             char pwd[1024];
112             getcwd(pwd, sizeof(pwd));
113             snprintf(cwd, sizeof(cwd), "PWD=%s", pwd);// PWD=/home/zhangsan 
114             putenv(cwd);// 导出环境变量
115             lastcode = 0;
116         }
117         return 1;
118     }
119     else if(strcmp(gargv[0], "echo") == 0)
120     {
121         if(gargc == 2)
122         {
123             if(gargv[1][0] == '$')
124             {
125                 // $?    ? -> 看做一个变量名
126                 if(strcmp(gargv[1] + 1, "?") == 0)
127                 {
128                     printf("lastcode:%d\n", lastcode);
129                 }
130                 lastcode = 0;
131             }                                                                                                                                          
132         }
133         return 1;
134     }
135     return 0;
136 }

在父进程的代码:(接收退出码,设置lastcode)

cpp 复制代码
137 int ExecuteCommand()
138 {
139     // 不能让bash执行程序替换函数,这里需要创建子进程
140     pid_t id = fork();
141     if(id < 0)
142         return -1;
143     else if(id == 0)
144     {
145         // 子进程   如何执行?
146         execvp(gargv[0], gargv);
147         exit(1); // 程序替换失败,退出码设为1
148     }
149     else 
150     {
151         // 父进程等子进程
152         int status = 0;
153         pid_t rid = waitpid(id, &status, 0);
154         if(rid > 0)
155         {
156             // 等待成功
157             // printf("wait child process success!\n");
158             lastcode = WEXITSTATUS(status); 
159         }
160     }
161     return 0;
162 }

运行结果:

bash 复制代码
[zhangsan@hcss-ecs-f571 shell]$ ./myshell 
[zhangsan@hcss-ecs-f571 shell]# ls faf
ls: cannot access faf: No such file or directory
[zhangsan@hcss-ecs-f571 shell]# echo $?
lastcode:2
[zhangsan@hcss-ecs-f571 shell]# echo $?
lastcode:0
[zhangsan@hcss-ecs-f571 shell]# 

8.手动加载环境变量

cpp 复制代码
 19 // 环境变量表                                                                                                                         
 20 char* genv[MAXSIZE];                                                                                                                  
 21 int genvc = 0;  

 31 void LoadEnv()
 32 {
 33     // 正常情况下,环境变量表内部是从环境变量里来的
 34     // 这里从父进程中取
 35     extern char **environ;
 36 
 37     for(;environ[genvc]; genvc++)
 38     {
 39          genv[genvc] = (char*)malloc(sizeof(char)*4096);
 40         // genv[genvc] = new char[4096];                                                                                                               
 41         strcpy(genv[genvc], environ[genvc]);// 这里必须拷贝过来,不能指向environ里面的地址处
 42     }
 43     genv[genvc] = NULL;
 44 
 45 }
 
相关推荐
wdfk_prog2 小时前
[Linux]学习笔记系列 -- [fs]pipe
linux·笔记·学习
white-persist2 小时前
【内网运维】Netstat与Wireshark:内网运维溯源实战解析
运维·网络·数据结构·测试工具·算法·网络安全·wireshark
oMcLin2 小时前
Debian 9 内核升级后出现硬件驱动不兼容问题:如何回滚内核与修复驱动
运维·debian
oMcLin2 小时前
Ubuntu 22.04 系统中不明原因的磁盘 I/O 高负载:如何利用 iotop 和 systemd 排查优化
linux·运维·ubuntu
是阿威啊2 小时前
【用户行为归因分析项目】- 【企业级项目开发第二站】项目通用代码开发
大数据·服务器·数据仓库·hive·hadoop
testpassportcn2 小时前
微軟 DP-600 認證介紹|Microsoft Fabric Analytics Engineer Associate 完整解析與考試攻略
运维·fabric
释怀不想释怀2 小时前
打包部署(vue前端)(Nginx)
运维·nginx
fengyehongWorld2 小时前
Linux systemd 与 systemctl 命令
linux·运维·服务器
公众号:ITIL之家2 小时前
服务价值体系重构:在变化中寻找不变的运维本质
java·运维·开发语言·数据库·重构