本篇文章主要是实现一个简易的 shell 程序
目录
[1 内建命令](#1 内建命令)
[2 实现目标](#2 实现目标)
[3 功能实现](#3 功能实现)
[4 总结](#4 总结)
1 内建命令
在 Linux 系统中,大部分命令都不是 shell 程序自己执行的,而是 shell 通过创建子进程的方式来实现的 (下面会进行讲解)。但是有一些命令必须由 shell 程序自身来完成,比如 cd、echo 等命令,这样的、由 shell 自己执行的命令就称为内建命令。
那么为什么会存在内建命令呢?都让子进程通过程序替换执行不可以吗?原因就是这些命令必须由 shell 程序来执行。比如 cd 命令,如果让子进程执行,那么子进程是改变当前工作目录了,但是子进程改变和父进程 shell 有什么关系呢?子进程的路径改变并不会影响父进程啊,而我们使用 cd 命令必须让 shell 改变路径,所以 cd 命令必须由 shell 来执行。
一般能修改 shell 运行环境的命令都是内建命令,比如 export 能修改环境变量,所以 export 就是内建命令,我们可以通过 type 命令来查看是否是内建命令:
bash
type [命令]

这些内建命令在 man 手册中也是找不到的,我们需要 help 命令来查看内建命令:

总之,内建命令是必须存在的,否则 shell 就无法修改运行环境了,也无法通过 cd 来更改路径了。
2 实现目标
我们在这里只是实现一个简易的 shell 命令行解释器,并不是实现 Linux 中完整的 shell,完整的 shell 太复杂了。在这一篇文章中,我们可以通过这一简易的 shell 命令行解释器,我们可以了解到 shell 程序是如何运行的。我们实现完简易的 shell 程序会完成如下目标:
1) 处理普通命令
2) 处理内建命令
3) 了解 shell 的运行原理
4) 理解环境变脸、本地变量、内建命令
5) 熟练使用各种进程相关系统调用
3 功能实现
我这里由于实现的主要是 ubuntu 版本的 Linux,当然如果你是 Centos,实现原理都是一样的,只是命令行提示符等有一定的差别,实现的时候只需要进行相应的调整即可。在简易版 shell 的实现中,我们会创建三个文件,myshell.h、myshell.cc、main.cc,其中 myshell.h 主要包含各种方法的声明,myshell.cc 主要是各种方法的实现,main.cc 是简易 shell 程序的执行逻辑。
shell 程序的运行流程
shell 程序本身是一个打印命令行提示符、获取用户输入字符串、拆解用户输入、检查并处理内建命令、创建子进程执行普通命令的死循环。运行流程如下图:

打印命令行提示符
一个命令行解释器运行起来,我们首先看到的就是一行命令行提示字符串:

这个命令行提示字符串一共包括了 3 部分,其中 ltl 是用户名,VM-24-6 是主机名,~ 是所在的目录名称,所以我们实现简易的 shell 程序必须先打印这个命令行提示字符串。而这些变量是可以通过环境变量来换取的:

所以我们只需要在程序中使用 getenv 来获取环境变量,就可以打出这个命令行提示字符串了。但是在我的云服务上,这个 HOSTNAME 并不是环境变量,而是 bash 的本地变量:

所以在我的主机上 HOSTNAME 是不会被子进程继承的,所以 getenv 是无法获取的,所以我们可以通过 gethostname 来获取环境变量:
cpp
#include <unistd.h>
int gethostname(char *name, size_t len);
功能: 返回当前主机的主机名
返回值: 获取成功返回0,获取失败返回 -1,并且错误码被设置
参数:
name: 获取成功之后,会将字符串写入 name 缓冲区
len: 缓冲区的长度
所以下面我是通过这个系统调用来获取主机名的。
cpp
//Makefile
myshell:main.cc myshell.cc
g++ -o $@ $^ -std=c++11
.PHONY:clean
clean:
rm -f myshell
cpp
//myshell.h
#ifndef __MYSHELL_HPP__
#define __MYSHELL_HPP__
#include <iostream>
#include <string>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
void PrintCommandStr();
#endif
cpp
#include "myshell.h"
static const char* GetPWD()
{
const char* pwd = getenv("PWD");
return pwd;
}
//这里使用输出型参数来获取主机名
static void GetHostName(char* hostname, size_t size)
{
//如果 HOSTNAME 为坏境变量,也可以获取环境变量
//const char* hostname = getenv("HOSTNAME");
//return hostname;
//使用 gethostname 来获取主机名
gethostname(hostname, size);
}
static const char* GetLogName()
{
const char* logname = getenv("LOGNAME");
return logname;
}
void PrintCommandStr()
{
//打印命令行提示字符串
//ltl@VM-24-6-ubuntu:~$
char buffer[64];
GetHostName(buffer, sizeof(buffer));
printf("%s@%s:%s# ", GetLogName(), buffer, GetPWD());
}
cpp
//main.cc
#include "myshell.h"
int main()
{
//1. 打印命令行提示字符串
PrintCommandStr();
return 0;
}

这里有两个问题,第一个就是这个 /home/ltl 是需要打印为 ~,第二个问题就是这个命令行提示字符串只提示了一次,而原 shell 是会不断打印的,所以我们需要设置为死循环:
cpp
//myshell.cc
#include "myshell.h"
static const char* GetPWD()
{
const char* pwd = getenv("PWD");
return pwd;
}
static void GetHostName(char* hostname, size_t size)
{
//如果 HOSTNAME 为坏境变量,也可以获取环境变量
//const char* hostname = getenv("HOSTNAME");
//return hostname;
//使用 gethostname 来获取主机名
gethostname(hostname, size);
}
static const char* GetLogName()
{
const char* logname = getenv("LOGNAME");
return logname;
}
static const char* GetHome()
{
//通过环境变量获取家目录
const char* home = getenv("HOME");
return home;
}
void PrintCommandStr()
{
//打印命令行提示字符串
//ltl@VM-24-6-ubuntu:~$
char buffer[64];
GetHostName(buffer, sizeof(buffer));
const char* pwd = GetPWD();
if (strcmp(pwd, GetHome()) == 0)
pwd = "~";
printf("%s@%s:%s# ", GetLogName(), buffer, pwd);
}
cpp
//main.cc
#include "myshell.h"
int main()
{
while (true)
{
// 1. 打印命令行提示字符串
PrintCommandStr();
//这里暂时使用输入来模拟输入场景
int x = 0;
std::cin >> x;
}
return 0;
}

获取用户输入的命令
其实我们平常输入的命令以及各种选项,其实是构成一个完整的字符串输入的,底层的 shell 拿到这个字符串之后进行解析,进而就变成了可执行程序与选项,然后执行的。比如 "ls -a -l",我们输入这个命令时,其实是作为一整个字符串输入的,然后 bash 进行解析,解析为了 "ls","-a","-l" 这几个字符串,然后进行处理的。所以我们第二步要做的就是将用户输入的命令作为一整个字符串来获取。
我们在 myshell.cc 中设置一个 GetCommandLine 函数来获取用户的输入命令:
cpp
//myshell.h
#ifndef __MYSHELL_HPP__
#define __MYSHELL_HPP__
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
void PrintCommandStr();
bool GetCommandLine(char* buffer, size_t size);
#endif
cpp
//myshell.cc
//之前的代码不变
bool GetCommandLine(char* buffer, size_t size)
{
//获取用户输入字符串
char* result = fgets(buffer, size, stdin);
//判断是否获取成功
if (!result) return false;
//将最后一个 '\n' 设置为 '\0'
buffer[strlen(buffer) - 1] = 0;
//判断是否获取了字符
if (strlen(buffer) == 0) return false;
return true;
}
cpp
//main.cc
#include "myshell.h"
int main()
{
while (true)
{
//1. 打印命令行提示字符串
PrintCommandStr();
//2. 获取用户输入
char command[64];
bool ret = GetCommandLine(command, sizeof(command));
//如果没有获取成功,退出本次循环
if (!ret) continue;
//暂时打印一下获取内容
std::cout << command << std::endl;
}
return 0;
}

分解用户输入的字符串
用户输入的是一整个字符串,如果我们想要进行处理,我们必须先把输入的字符串按照空格拆解为一个一个的字符串,这样后面执行命令时才可以区分命令与选项。
拆解字符串我们可以使用 string 类中的 find 命令,当然在 string.h 头文件中还有一个字符串函数叫做 strtok 用来做字符串拆分。这里我们选择使用 strtok 函数来进行字符串拆分。strtok 函数的使用样例如下:
cpp
#include <iostream>
#include <cstring>
#define sep "$"
int main()
{
char s[] = "3.14$hello$1234";
//第一次调用时会返回第一个分隔后的字符串
char* s1 = strtok(s, sep);
while (s1)
{
std::cout << s1 << std::endl;
//第一个参数传入 nullptr 会从上一次分隔后的字符串再分隔
s1 = strtok(nullptr, sep);
}
return 0;
}
cpp
3.14
hello
1234
使用 strtok 时我们需要注意,虽然 3.14hello1234 只有三段字符串,但是 strtok 是会运行 4 次的,最后一次会返回 NULL,请注意这一点。
这样我们就可以使用 strtok 函数进行字符串拆分了,但是我们需要将拆分后的每个字符串保存起来,以便于后面使用这些字符串,所以我们需要一个全局数组来保存每个字符串和一个全局变量来保存字符串个数:
cpp
//myshell.h
#ifndef __MYSHELL_HPP__
#define __MYSHELL_HPP__
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define sep " "
extern char* gargv[64];
extern int gargc;
void PrintCommandStr();
bool GetCommandLine(char* buffer, size_t size);
void ParseCommandLine(char* buffer);
void Init();
#endif
//myshell.cc
#include "myshell.h"
char* gargv[64]; //解析的命令行参数数组
int gargc; //命令行参数的个数
//其余代码相同
//解析获取的字符串
void ParseCommandLine(char* buffer)
{
//strtok 解析字符串
gargv[gargc++] = strtok(buffer, sep);
while ((gargv[gargc++] = strtok(nullptr, sep))) ;
gargc--;
//让每个字符串中的字符都是一个纯净的字符串,没有空格
for (int i = 0; i < gargc; i++)
{
int index = 0;
while (gargv[i][index] == ' ') ++index;
gargv[i] = gargv[i] + index;
}
}
//每次新循环的时候,需要将 gargv 和 gargc 的数据清楚掉
void Init()
{
//将 gargv 与 gargc 清空
memset(gargv, 0, sizeof(gargv));
gargc = 0;
}
//main.cc
#include "myshell.h"
int main()
{
//shell 是一个死循环
while (true)
{
//初始化需要的缓冲区与数据
Init();
//1. 打印命令行提示字符串
PrintCommandStr();
//2. 获取用户输入
char command[64];
bool ret = GetCommandLine(command, sizeof(command));
//如果没有获取成功,退出本次循环
if (!ret) continue;
//3. 解析用户的输入字符串
ParseCommandLine(command);
//测试打印一下 gargv 数组
for (int i = 0; i < gargc; i++)
{
std::cout << gargv[i] << std::endl;
}
}
return 0;
}

创建子进程运行命令
通过上面的函数,我们可以打印命令行提示字符串,获取用户输入的命令字符串,解析用户输入字符串了,经过上面三步,gargv 里面其实已经存放的是用户输入的命令字符串了,接下来我们只需要执行对应的命令就可以了。
但是执行命令我们不能让 shell 来执行这个命令,因为要想执行命令,我们就必须调用 exec 函数,如果由 shell 来调用 exec 函数,那么当前 shell 进程一旦被替换成功,那么 shell 进程就不存在了,执行结束之后直接退出,也就没法执行命令了。所以 shell 进程必须通过创建子进程的方式来进行程序替换执行命令。
cpp
//myshell.h
#ifndef __MYSHELL_HPP__
#define __MYSHELL_HPP__
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define sep " "
extern char* gargv[64];
extern int gargc;
void PrintCommandStr();
bool GetCommandLine(char* buffer, size_t size);
void ParseCommandLine(char* buffer);
void Init();
bool ExcuteCommand();
#endif
//myshell.cc
//其余代码相同
bool ExcuteCommand()
{
//我们首先创建子进程,让子进程执行对应的命令
pid_t id = fork();
if (id < 0) return false;
if (id == 0)
{
//child
//进行程序替换
//因为有 gargv,所以我们带 v; 同时我们选择使用系统的环境变量,会继承 PATH,所以我们带 p,让其自动搜索路径
execvp(gargv[0], gargv);
//到这里就会替换失败
//这里我们让子进程直接退出
exit(1);
}
//我们需要等待子进程
int status = 0;
int rid = waitpid(id, &status, 0);//这里退出状态暂时设为 nullptr
if (rid <= 0) return false;
return true;
}
//main.cc
#include "myshell.h"
int main()
{
//shell 是一个死循环
while (true)
{
//初始化需要的缓冲区与数据
Init();
//1. 打印命令行提示字符串
PrintCommandStr();
//2. 获取用户输入
char command[64];
bool ret = GetCommandLine(command, sizeof(command));
//如果没有获取成功,退出本次循环
if (!ret) continue;
//3. 解析用户的输入字符串
ParseCommandLine(command);
//测试打印一下 gargv 数组
// for (int i = 0; i < gargc; i++)
// {
// std::cout << gargv[i] << std::endl;
// }
//4. 让子进程执行命令
bool n = ExcuteCommand();
(void)n;
}
return 0;
}

内建命令的执行
在文章开篇我们讲解了什么是内建命令,就是由 shell 自己执行的命令,比如 cd:

我们可以看到我们输入了 cd 命令,但是 shell 当前的工作目录还是家目录,这是因为我们在执行 cd 命令时是子进程去执行了 cd 命令,并不是 shell 程序执行的,所以我们需要让 shell 程序自己来执行内建命令,这里简化一下,我们就实现 cd、env、export 与 echo 这四个内建命令。
在 bash 中,我们可以通过 echo ? 来打印上一次运行程序的退出码,所以当前的 shell 程序也必须保存上一次运行程序的退出码,这里我们选择使用一个全局的 int 变量 lastcode 来保存退出码,当用户输入的是 echo ? 时,我们就将 lastcode 打印出来。
cpp
//myshell.h
#ifndef __MYSHELL_HPP__
#define __MYSHELL_HPP__
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define sep " "
extern char* gargv[64];
extern int gargc;
void PrintCommandStr();
bool GetCommandLine(char* buffer, size_t size);
void ParseCommandLine(char* buffer);
void Init();
void InitEnv();
void AddEnv(const char* env);
bool ExcuteCommand();
bool CheckAndExcuteBuiltCommand();
#endif
//myshell.cc
#include "myshell.h"
char *gargv[64]; // 解析的命令行参数数组
int gargc; // 命令行参数的个数
static int envnum = 64; // 环境变量表的大小
char **genv; // 全局的环境变量表
int lastcode = 0; // 用来保存最后一次执行的退出码
//其余代码相同
void InitEnv()
{
// 这里使用自己的环境变量表,当然也可以使用继承于 bash 的环境变量表
// 初始化环境变量表, 我们从系统的 bash 获取
genv = (char **)malloc(envnum * sizeof(char *));
extern char **environ;
for (int i = 0; environ[i]; i++)
{
genv[i] = environ[i];
}
}
void AddEnv(const char *env)
{
int index = 0;
while (genv[index])
++index;
// 环境变量表不够用进行扩容
if (index + 1 == envnum)
{
genv = (char **)realloc(genv, envnum * 2);
envnum = envnum * 2;
}
// 添加环境变量
genv[index] = (char *)malloc((strlen(env) + 1));
strncpy(genv[index], env, strlen(env) + 1);
// 环境变量表最后以 nullptr 结尾
genv[++index] = nullptr;
}
bool CheckAndExcuteBuiltCommand()
{
// cd、echo、env、export
if (strcmp(gargv[0], "cd") == 0)
{
if (gargc == 2)
{
// 系统调用更改当前目录
chdir(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 1;
return false;
}
return true;
}
else if (strcmp(gargv[0], "export") == 0)
{
if (gargc == 2)
{
AddEnv(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 2;
}
return true;
}
else if (strcmp(gargv[0], "env") == 0)
{
if (gargc == 1)
{
for (int i = 0; genv[i]; i++)
{
std::cout << genv[i] << std::endl;
}
lastcode = 0;
}
else
{
lastcode = 3;
}
return true;
}
else if (strcmp(gargv[0], "echo") == 0)
{
if (gargc == 2)
{
if (gargv[1][0] == '$')
{
if (gargv[1][1] == '?')
{
// echo $?
std::cout << lastcode << std::endl;
lastcode = 0;
}
else
{
// echo $PATH
int flag = 1;
char *envvar = gargv[1];
++envvar;
for (int i = 0; genv[i]; i++)
{
char *content = strstr(genv[i], "=");
size_t len = content - genv[i];
if (strncmp(genv[i], envvar, len) == 0)
{
std::cout << (content + 1) << std::endl;
flag = 0;
break;
}
}
if (flag)
lastcode = 4;
else
lastcode = 0;
}
}
else
{
//echo hello
std::cout << gargv[1] << std::endl;
lastcode = 0;
}
}
else
{
lastcode = 5;
}
return true;
}
return false;
}
//main.cc
#include "myshell.h"
int main()
{
//在循环外设置环境变量
InitEnv();
//shell 是一个死循环
while (true)
{
//初始化需要的缓冲区与数据
Init();
//1. 打印命令行提示字符串
PrintCommandStr();
//2. 获取用户输入
char command[64];
bool ret = GetCommandLine(command, sizeof(command));
//如果没有获取成功,退出本次循环
if (!ret) continue;
//3. 解析用户的输入字符串
ParseCommandLine(command);
//测试打印一下 gargv 数组
// for (int i = 0; i < gargc; i++)
// {
// std::cout << gargv[i] << std::endl;
// }
//4. 检查并执行内建命令
bool m = CheckAndExcuteBuiltCommand();
if(m) continue; // 如果内建命令执行成功,就不用子进程替换执行命令了
//5. 让子进程执行命令
bool n = ExcuteCommand();
(void)n;
}
return 0;
}

虽然当前 shell 程序可以执行内建命令了,但是还存在一个问题:虽然 cd 之后当前 shell 的路径改变了,但是命令行提示符中的路径并没有更新。为什么没有更新呢?因为我们的这个路径是来源于环境变量 PWD,但是 PWD 在我们改变路径之后是不会改变的,需要我们自己来更新 PWD 环境变量:
cpp
//myshell.cc
#include "myshell.h"
char *gargv[64]; // 解析的命令行参数数组
int gargc; // 命令行参数的个数
static int envnum = 64; // 环境变量表的大小
char **genv; // 全局的环境变量表
int lastcode = 0; // 用来保存最后一次执行的退出码
char curpwd[64]; // 存储当前工作路径
static char *GetPWD()
{
char *pwd = getenv("PWD");
return pwd;
}
static void GetHostName(char *hostname, size_t size)
{
// 如果 HOSTNAME 为坏境变量,也可以获取环境变量
// const char* hostname = getenv("HOSTNAME");
// return hostname;
// 使用 gethostname 来获取主机名
gethostname(hostname, size);
}
static const char *GetLogName()
{
const char *logname = getenv("LOGNAME");
return logname;
}
static const char *GetHome()
{
// 通过环境变量获取家目录
const char *home = getenv("HOME");
return home;
}
void PrintCommandStr()
{
// 打印命令行提示字符串
// ltl@VM-24-6-ubuntu:~$
char buffer[64];
GetHostName(buffer, sizeof(buffer));
// 这里需要将路径中家目录改成 ~, 家目录存储在 "HOME" 环境变量下
char *pwd = GetPWD();
std::string newpwd = "~";
if (strlen(pwd) < strlen(GetHome()))
newpwd = pwd;
else
{
char *newpath = pwd + strlen(GetHome());
newpwd += newpath;
}
printf("%s@%s:%s# ", GetLogName(), buffer, newpwd.c_str());
}
//其余代码相同
bool CheckAndExcuteBuiltCommand()
{
// cd、echo、env、export
if (strcmp(gargv[0], "cd") == 0)
{
if (gargc == 2)
{
// 系统调用更改当前目录
chdir(gargv[1]);
getcwd(curpwd, sizeof(curpwd));
// 更新环境变量 PWD
setenv("PWD", curpwd, 1);
lastcode = 0;
}
else
{
lastcode = 1;
return false;
}
return true;
}
else if (strcmp(gargv[0], "export") == 0)
{
if (gargc == 2)
{
AddEnv(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 2;
}
return true;
}
else if (strcmp(gargv[0], "env") == 0)
{
if (gargc == 1)
{
for (int i = 0; genv[i]; i++)
{
std::cout << genv[i] << std::endl;
}
lastcode = 0;
}
else
{
lastcode = 3;
}
return true;
}
else if (strcmp(gargv[0], "echo") == 0)
{
if (gargc == 2)
{
if (gargv[1][0] == '$')
{
if (gargv[1][1] == '?')
{
// echo $?
std::cout << lastcode << std::endl;
lastcode = 0;
}
else
{
// echo $PATH
int flag = 1;
char *envvar = gargv[1];
++envvar;
for (int i = 0; genv[i]; i++)
{
char *content = strstr(genv[i], "=");
size_t len = content - genv[i];
if (strncmp(genv[i], envvar, len) == 0)
{
std::cout << (content + 1) << std::endl;
flag = 0;
break;
}
}
if (flag)
lastcode = 4;
else
lastcode = 0;
}
}
else
{
std::cout << gargv[1] << std::endl;
lastcode = 0;
}
}
else
{
lastcode = 5;
}
return true;
}
return false;
}

经过这个例子,我们也印证了有的环境变量是来源于配置文件,有一些环境变量是实时更新的。
我们在这个简易 shell 程序中实现了 echo 环境变量的功能,如果想要实现 echo 本地变量,只需要和环境变量一样有一个全局的本地变量表就可以了。
简易 shell 程序代码
cpp
//myshell.h
#ifndef __MYSHELL_HPP__
#define __MYSHELL_HPP__
#include <iostream>
#include <string>
#include <cstring>
#include <cstdlib>
#include <cstdio>
#include <unistd.h>
#include <sys/wait.h>
#include <sys/types.h>
#define sep " "
extern char* gargv[64];
extern int gargc;
void PrintCommandStr();
bool GetCommandLine(char* buffer, size_t size);
void ParseCommandLine(char* buffer);
void Init();
void InitEnv();
void AddEnv(const char* env);
bool ExcuteCommand();
bool CheckAndExcuteBuiltCommand();
#endif
//myshell.cc
#include "myshell.h"
char *gargv[64]; // 解析的命令行参数数组
int gargc; // 命令行参数的个数
static int envnum = 64; // 环境变量表的大小
char **genv; // 全局的环境变量表
int lastcode = 0; // 用来保存最后一次执行的退出码
char curpwd[64]; // 存储当前工作路径
static char *GetPWD()
{
char *pwd = getenv("PWD");
return pwd;
}
static void GetHostName(char *hostname, size_t size)
{
// 如果 HOSTNAME 为坏境变量,也可以获取环境变量
// const char* hostname = getenv("HOSTNAME");
// return hostname;
// 使用 gethostname 来获取主机名
gethostname(hostname, size);
}
static const char *GetLogName()
{
const char *logname = getenv("LOGNAME");
return logname;
}
static const char *GetHome()
{
// 通过环境变量获取家目录
const char *home = getenv("HOME");
return home;
}
void PrintCommandStr()
{
// 打印命令行提示字符串
// ltl@VM-24-6-ubuntu:~$
char buffer[64];
GetHostName(buffer, sizeof(buffer));
// 这里需要将路径中家目录改成 ~, 家目录存储在 "HOME" 环境变量下
char *pwd = GetPWD();
std::string newpwd = "~";
if (strlen(pwd) < strlen(GetHome()))
newpwd = pwd;
else
{
char *newpath = pwd + strlen(GetHome());
newpwd += newpath;
}
printf("%s@%s:%s# ", GetLogName(), buffer, newpwd.c_str());
}
bool GetCommandLine(char *buffer, size_t size)
{
// 获取用户输入字符串
char *result = fgets(buffer, size, stdin);
// 判断是否获取成功
if (!result)
return false;
// 将最后一个 '\n' 设置为 '\0'
buffer[strlen(buffer) - 1] = 0;
// 判断是否获取了字符
if (strlen(buffer) == 0)
return false;
return true;
}
void ParseCommandLine(char *buffer)
{
// strtok 解析字符串
gargv[gargc++] = strtok(buffer, sep);
while ((gargv[gargc++] = strtok(nullptr, sep)))
;
gargc--;
// 让每个字符串中的字符都是一个纯净的字符串,没有空格
for (int i = 0; i < gargc; i++)
{
int index = 0;
while (gargv[i][index] == ' ')
++index;
gargv[i] = gargv[i] + index;
}
}
bool ExcuteCommand()
{
// 我们首先创建子进程,让子进程执行对应的命令
pid_t id = fork();
if (id < 0)
return false;
if (id == 0)
{
// child
// 进行程序替换
// 因为有 gargv,所以我们带 v; 同时我们选择使用系统的环境变量,会继承 PATH,所以我们带 p,让其自动搜索路径
execvp(gargv[0], gargv);
// 到这里就会替换失败
// 这里我们让子进程直接退出
exit(1);
}
// 我们需要等待子进程
int status = 0;
int rid = waitpid(id, &status, 0);
// 这里我们需要更新执行结果
lastcode = WEXITSTATUS(status);
if (rid <= 0)
return false;
return true;
}
void Init()
{
// 将 gargv 与 gargc 清空
memset(gargv, 0, sizeof(gargv));
gargc = 0;
}
void InitEnv()
{
// 这里使用自己的环境变量表,当然也可以使用继承于 bash 的环境变量表
// 初始化环境变量表, 我们从系统的 bash 获取
genv = (char **)malloc(envnum * sizeof(char *));
extern char **environ;
for (int i = 0; environ[i]; i++)
{
genv[i] = environ[i];
}
}
void AddEnv(const char *env)
{
int index = 0;
while (genv[index])
++index;
// 环境变量表不够用进行扩容
if (index + 1 == envnum)
{
genv = (char **)realloc(genv, envnum * 2);
envnum = envnum * 2;
}
// 添加环境变量
genv[index] = (char *)malloc((strlen(env) + 1));
strncpy(genv[index], env, strlen(env) + 1);
// 环境变量表最后以 nullptr 结尾
genv[++index] = nullptr;
}
bool CheckAndExcuteBuiltCommand()
{
// cd、echo、env、export
if (strcmp(gargv[0], "cd") == 0)
{
if (gargc == 2)
{
// 系统调用更改当前目录
chdir(gargv[1]);
getcwd(curpwd, sizeof(curpwd));
// 更新环境变量 PWD
setenv("PWD", curpwd, 1);
lastcode = 0;
}
else
{
lastcode = 1;
return false;
}
return true;
}
else if (strcmp(gargv[0], "export") == 0)
{
if (gargc == 2)
{
AddEnv(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 2;
}
return true;
}
else if (strcmp(gargv[0], "env") == 0)
{
if (gargc == 1)
{
for (int i = 0; genv[i]; i++)
{
std::cout << genv[i] << std::endl;
}
lastcode = 0;
}
else
{
lastcode = 3;
}
return true;
}
else if (strcmp(gargv[0], "echo") == 0)
{
if (gargc == 2)
{
if (gargv[1][0] == '$')
{
if (gargv[1][1] == '?')
{
// echo $?
std::cout << lastcode << std::endl;
lastcode = 0;
}
else
{
// echo $PATH
int flag = 1;
char *envvar = gargv[1];
++envvar;
for (int i = 0; genv[i]; i++)
{
char *content = strstr(genv[i], "=");
size_t len = content - genv[i];
if (strncmp(genv[i], envvar, len) == 0)
{
std::cout << (content + 1) << std::endl;
flag = 0;
break;
}
}
if (flag)
lastcode = 4;
else
lastcode = 0;
}
}
else
{
std::cout << gargv[1] << std::endl;
lastcode = 0;
}
}
else
{
lastcode = 5;
}
return true;
}
return false;
}
//main.cc
#include "myshell.h"
int main()
{
//在循环外设置环境变量
InitEnv();
//shell 是一个死循环
while (true)
{
//初始化需要的缓冲区与数据
Init();
//1. 打印命令行提示字符串
PrintCommandStr();
//2. 获取用户输入
char command[64];
bool ret = GetCommandLine(command, sizeof(command));
//如果没有获取成功,退出本次循环
if (!ret) continue;
//3. 解析用户的输入字符串
ParseCommandLine(command);
//测试打印一下 gargv 数组
// for (int i = 0; i < gargc; i++)
// {
// std::cout << gargv[i] << std::endl;
// }
//4. 检查并执行内建命令
bool m = CheckAndExcuteBuiltCommand();
if(m) continue; // 如果内建命令执行成功,就不用子进程替换执行命令了
//5. 让子进程执行命令
bool n = ExcuteCommand();
(void)n;
}
return 0;
}
4 总结
本篇文章我们实现了一个简易的 shell 程序,通过这个简易 shell,我们明白了 shell 程序的运行原理。其实 shell 程序就是一个死循环,只不过在循环开始前会进行初始化环境变量表等一系列的初始化工作,之后就会进入死循环。在死循环中首先打印命令行提示符,然后获取用户输入的字符串(在 shell 中我们数输入的命令其实就是一行字符串),之后 shell 解析字符串得到对应的命令与选项,之后再创建子进程进行程序替换执行命令。但是一些内建命令需要 shell 自己来执行,比如 cd、echo 等。
同时,我们也了解了环境变量的用途,以及熟悉了各种接口的使用。希望大家通过这个简易 shell 程序可以了解 shell 程序的运行原理。