这里先补充一个vim在命令模式下的命令:
%s/src/dest/g
可将字段进行替换。
一.手搓一个命令行
1.命令行细解
首先我们要明晰命令行是干嘛的:它由命令行提示符+命令 组成,是人机交互的一种方式 ,在命令行的"#"或"$"后面(命令行提示符后面)可以输入一行文本(命令),然后交由命令行解释器shell(bash),bash对命令进行翻译以后,翻译过的命令交由内核处理。
关于shell相关的知识点,可以看笔者Linux基本指令及知识点(二)的末尾。
这是centos版的命令行:

用户名+主机名+目录这三者组成的字符串 是命令行提示符。
2.设计myshell的命令行提示符
初版:
#include<stdio.h>
2 int main()
3 {
4 printf("laotian@VM-0-15-ubuntu:~/linux_storage/myshell$ ");
5 return 0;
6 }

但是这个版怎么那么low啊,打的东西都是固定死的,我们需要去增加获取用户名,主机名,以及当前路径的函数。
再版:
①.获取用户名
这里用到一个函数:getenv 去获取环境变量USER里的用户名。

②.获取主机名
同样使用getenv,只是把环境变量换成HOSTNAME就行了。
③.获取当前路径
用getenv获取环境变量PWD就行。
④.代码
#include<stdio.h>
2 #include<stdlib.h>
3 #include<string.h>
4 const char* GetUserName()
5 {//获得用户名
6 const char* name = getenv("USER");
7 return name == NULL ? "None" : name;
8 }
9 const char* GetHostName()
10 {
11 const char* hostname = getenv("HOSTNAME");
12 return hostname == NULL ? "None" : hostname;
13 }
14 const char* GetPwd()
15 {
16 const char* pwd = getenv("PWD");
17 return pwd == NULL ? "None" : pwd;
18 }
19 int main()
20 {
21 printf("%s@%s %s$ ",GetUserName(),GetHostName(),GetPwd());
22 return 0;
23 }
3.命令行提示符再设计+获取用户输入信息
输入命令之前,命令行会卡住,等待用户输入,这该怎么实现呢?
由于解决方案跟c++有关,这里先用mv指令将myshell.c换成myshell.cc,别忘了把makefile里的.c也换成.cc。再改一下.cc里包含的头文件,使得c和c++风格的代码都可以跑。


输入命令,在shell看来,就是输入一串字符串。既然是输入字符串,我们可以借用stdin输入流和fgets函数。
用fgets,可以从指定文件中读取数据,并存到指定容器中;stdin是个FILE*类型的指针,绑定了键盘,就是读取键盘输入。
所以能这么设计:

由于以上方式实现的命令行提示符,乱成一锅粥,一点也不美观,所以我们重新制作一个命令行:
①.首先来做命令行提示符。
MakeCommandLine()
我们单独封装一个MakeCommandLine:
这里会用到一个函数------snprintf,该函数可以格式化输出字符串,并将结果写入 指定的字符数组里:
int snprintf(char *str, size_t size, const char *format, ...);

代码:
void MakeCommandLine(char cmd_prompt[],int size)
23 {
24 snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),GetPwd());
25 }
封装该接口能够将命令行提示符这个字符串写入字符数组里 ,然后将该字符串传给打印函数,即可实现对命令行提示符的打印。
PrintCommandPrompt()
打印装有命令行提示符的字符数组。
代码:
void PrintCommandPrompt()
27 {//打印命令行提示符
28 char prompt[COMMAND_SIZE];//定义命令行提示符
29 MakeCommandLine(prompt,sizeof(prompt));//制作命令行提示符
30 printf("%s",prompt);
31 fflush(stdout);
32 }
②.实现获取用户输入信息,以及shell退出前一直打印命令行提示符
获取用户输入信息:
bool GetCommandLine(char* out,int size)
34 {
35
36 char commandline[COMMAND_SIZE];
37 char* c = fgets(out,size,stdin);
38 if(c == NULL) return false;//fgets导入失败,返回false。
39 out[strlen(commandline) - 1] = 0;//清理\n,防止出现一个空行。
40 if(strlen(out) == 0)return false;//只输入一个回车,也要返回false
41 return true;
42 }
循环打印命令行提示符:

shell本质是个死循环,一直在打印提示符,获取用户输入信息,直到shell的进程退出才结束循环!
4.命令行分析(拆解)
由于执行指令需要先创建子进程,再使用exec系列函数作程序替换, 而将命令行参数(例如-l-a)传给execa系列函数时,不能整个字符串的传,需要将字符串切割成一个个的命令行参数的子串,才能传参。

那么如何快速找到每一个元素呢(子串)?
定义两个全局变量g_argv和g_argc,很熟悉,就是main函数的两个形参。
然后封装分析函数:CommandParse():
这里调用一个切割字符串的函数:**STRTOK,**将命令行commandline进行切割:

每切割下一个子串,就把子串放入g_argv。
bool CommandParse(char* commandline)
48 {
49 #define SEP " "
50 g_argc = 0;
51 g_argv[g_argc++] = strtok(commandline,SEP);//首次切割需要传字符串首地址。
✹ 52 while(g_argv[g_argc++] = strtok(nullptr,SEP));//后续调用传NULL。
53 g_argc--;
54 return true;
55 }
因为argv的结尾有个null,所以argc++会多算一个null。所以要作调整,等for循环结束,argc--。
二.命令的处理
1.执行外部命令
执行命令时,需要先fork()一个子进程,然后用exec进行程序替换。
pid_t id =fork();
85 if(id == 0)
86 {//child
87 //程序替换
88 execvp(g_argv[0],g_argv);
89 exit(1);
90 }
91 //father
92 pid_t rid = waitpid(id,nullptr,0);
93 (void)rid;//执行一下,防止报警告
2.路径切割
由于命令行提示符只有当前目录,所以我们还要做路径的切割。
此处用cpp编写一个接口:
std::string DirName(const char* pwd)
32 {
33 #define slash "/"
34 std::string dir = pwd;
✹ 35 if(pwd == slash) return slash;
36 auto pos = dir.rfind(slash);
37 if(pos == std::string::npos)return"BUG?";
38 return dir.substr(pos + 1);
39 }
rfind:从字符串末尾往前搜索,找到指定字符,就返回其所在的下标。
npos:

3.检测并处理内建命令
有些命令需要bash亲自去执行,那么用上面execvp,让子进程去执行就走不通了。譬如执行内建命令pwd,pwd的功能是显示**bash内容所在的目录的路径,**如果直接将pwd交给子进程,那么打出来的目录,就是子进程代码和数据所在的目录的路径了,就是不正确的。
这里设计一个CheckAndExecuteBuiltin() 接口:
在其内部,还要设计一个寻找家目录的接口,以应对cd命令没有命令行参数,直接跳转家目录。
const char* GetHome()
32 {//找到家目录
33 const char* home = getenv("HOME");
34 return home == NULL ? NULL : home;
35 }
那么如何更改bash所在目录的路径呢?
答案是chdir函数。

bool CheckAndExecBuiltin()
85 {
86 std::string cmd = g_argv[0];
87 if(cmd == "cd")
88 {
89 if(g_argc == 1)
90 {//没有命令行参数,就跳转到家目录下。
91 std::string home = GetHome();
92 if(home.empty())return true;//获取家目录路径失败,就返回。
93 chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
94 }
95 else
96 {//有命令行参数
97 std::string where = g_argv[1];
98 //cd -/cd ~
99 if(where == "-")
100 {
101 std::string home = GetHome();
102 if(home.empty())return true;//获取家目录路径失败,就返回。
103 chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
105 else if(where == "~")
106 {
107 std::string home = GetHome();
108 if(home.empty())return true;//获取家目录路径失败,就返回。
109 chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
110 }
111 else
112 {
113 chdir(where.c_str());
114 return true;
115 }
116 }
117 return true;
118 }
119 return false;
120 }
可以看出cd的底层其实就是调用chdir,去变更bash所在的目录的路径。
4.重构GetPwd()
cd以后,我们发现了一个问题,命令行提示符上的当前bash的目录的路径不是实时变化的,这是什么原因呢?该如何解决呢?
cd以后,当前bash进程的路径发生变化,但环境变量PWD没有更新 ,导致GetPwd函数通过环境变量获取到的bash所在目录的路径也是没有更新过的老路径 ,所以上面GetPwd通过环境变量得路径的方式是不行的,需要重新设计,以便获得实时的路径。
如何获得实时的路径呢?
答案是getcwd()函数:

这里定义了一个全局变量。

代码:
const char* pwd = getcwd(cwd,sizeof(cwd));
同时还要更新环境变量:
用到putenv :将环境变量(字符串)导给当前进程(bash),但是有格式的要求:

先使用snprintf把格式化的环境变量的字符串导入全局变量cwdenv里,再把cwdenc传给getenv:
const char* GetPwd()
30 {
31 //const char* pwd = getenv("PWD");
32 const char* pwd = getcwd(cwd,sizeof(cwd));
33 //更新环境变量
34 if(pwd != NULL)
35 {
36 snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",pwd);//将格式化的字符串导入cwdenv
37 getenv(cwdenv);//将swdenv导给当前bash进程,以实现环境变量的更新。
38 }
39 return pwd == NULL ? "None" : pwd;
40 }
pwd指令不也是实时显示当前bash的路径吗?所以还能输出一个结论------pwd的底层也是调用了getcwd()函数!
三.最终版的shell
#include <cerrno>
#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<unistd.h>
#include<sys/types.h>
#include<wait.h>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]"//宏定义一个版式
#define MAXARGC 128
//定义shell的全局变量
char* g_argv[MAXARGC];
int g_argc = 0;//g_是global,全局的意思。
//for test
char cwd[1024];
char cwdenv[1024];
//last exit code最近一个程序的退出码
int lastcode = 0;
const char* GetUserName()
{//获得用户名
const char* name = getenv("USER");
return name == NULL ? "None" : name;
}
const char* GetHostName()
{
const char* hostname = getenv("HOSTNAME");
return hostname == NULL ? "None" : hostname;
}
const char* GetPwd()
{
//const char* pwd = getenv("PWD");
const char* pwd = getcwd(cwd,sizeof(cwd));
//更新环境变量
if(pwd != NULL)
{
snprintf(cwdenv,sizeof(cwdenv),"PWD=%s",pwd);//将格式化的字符串导入cwdenv
getenv(cwdenv);//将swdenv导给当前bash进程,以实现环境变量的更新。
}
return pwd == NULL ? "None" : pwd;
}
const char* GetHome()
{//找到家目录
const char* home = getenv("HOME");
return home == NULL ? NULL : home;
}
std::string DirName(const char* pwd)
{
#define slash "/"
std::string dir = pwd;
if(pwd == slash) return slash;
auto pos = dir.rfind(slash);
if(pos == std::string::npos)return"BUG?";
return dir.substr(pos + 1);
}
void MakeCommandLine(char cmd_prompt[],int size)
{
snprintf(cmd_prompt,size,FORMAT,GetUserName(),GetHostName(),DirName(GetPwd()).c_str());
}
void PrintCommandPrompt()
{//打印命令行提示符
char prompt[COMMAND_SIZE];//定义命令行提示符
MakeCommandLine(prompt,sizeof(prompt));//制作命令行提示符
printf("%s",prompt);
fflush(stdout);
}
bool GetCommandLine(char* out,int size)
{
char* c = fgets(out,size,stdin);
if(c == NULL) return false;//fgets导入失败,返回false。
out[strlen(out) - 1] = 0;//清理\n,防止出现一个空行。
if(strlen(out) == 0)return false;//只输入一个回车,也要返回false
return true;
}
bool CommandParse(char* commandline)
{
#define SEP " "
g_argc = 0;
g_argv[g_argc++] = strtok(commandline,SEP);//首次切割需要传字符串首地址。
while(g_argv[g_argc++] = strtok(nullptr,SEP));//后续调用传NULL。
g_argc--;
return g_argc < 0 ? false : true;
}
void PrintArgv()
{
for(int i = 0;g_argv[i];i++)
{//argv以null为结尾,所以可以这样设计循环条件
printf("argv[%d]->%s\n",i,g_argv[i]);
}
printf("argc:%d",g_argc);
printf("\n");
}
bool CheckAndExecBuiltin()
{
std::string cmd = g_argv[0];
if(cmd == "cd")
{
if(g_argc == 1)
{//没有命令行参数,就跳转到家目录下。
std::string home = GetHome();
if(home.empty())return true;//获取家目录路径失败,就返回。
chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
}
else
{//有命令行参数
std::string where = g_argv[1];
//cd -/cd ~
if(where == "-")
{
std::string home = GetHome();
if(home.empty())return true;//获取家目录路径失败,就返回。
chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
}
else if(where == "~")
{
std::string home = GetHome();
if(home.empty())return true;//获取家目录路径失败,就返回。
chdir(home.c_str());//获取成功,就把当前bash的路径切换到家目录路径,由于函数是c语言风格的,实参是cpp风格的,需要.c_str转换一下。
}
else
{
chdir(where.c_str());
return true;
}
} return true;
}
else if(cmd == "echo")
{
if(g_argc == 2)
{
std::string opt = g_argv[1];
if(opt == "$?")
{
//显示上个退出进程的退出码。
std::cout << lastcode << std::endl;
lastcode = 0;//需要清零
return true;
}
if(opt[0] == '$')
{//echo +$环境变量,可以查环境变量
std::string env_name = opt.substr(1);//下标1后面的就是环境变量。
const char* env_value = getenv(env_name.c_str());//获得对应环境变量的内容
if(env_value)
{
std:: cout << env_value << std::endl;
}
}
}
return true;
}
return false;
}
int Execute()
{//执行程序
pid_t id =fork();
if(id == 0)
{//child
//程序替换
execvp(g_argv[0],g_argv);
exit(1);
}
int status = 0;
//father
pid_t rid = waitpid(id,&status,0);
if(rid > 0)
{//状态码
lastcode = WEXITSTATUS(status);//用宏获取状态码
}
return 0;
}
int main()
{
while(true)
{
//1.输出命令行提示符
PrintCommandPrompt();
//2.获取用户输入信息
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline,sizeof(commandline)))
{//获取用户输入信息失败,依旧继续跑。
continue;
}
//3.命令行解析
if(!CommandParse(commandline))
continue;
//3.5检测并处理内建命令
if(CheckAndExecBuiltin())
continue;//Check函数判断并处理了内建命令,就开启下一轮循环,不需要继续执行普通命令了。
//4.执行命令
Execute();
}
return 0;
}