个人主页:C++忠实粉丝
欢迎 点赞👍 收藏✨ 留言✉ 加关注💓本文由 C++忠实粉丝 原创Linux基础-shell的简单实现
收录于专栏[Linux学习]
本专栏旨在分享学习Linux的一点学习笔记,欢迎大家在评论区交流讨论💌
目录
1, 全局变量和常量定义
2. 环境信息获取
2.1 GetUserName
2.2 GetHostName
2.3 GetPwd
2.4 LastDir
3. 命令行提示符
3.1 MakeCommandLine
3.2 PrintCommandLine
4, 命令输入处理
4.1 GetCommandLine
4.2 ParseCommandLine
5. 命令调试
5.1 debug
6. 命令执行
6.1 ExecuteCommand
子进程执行命令:
等待子进程结束:
处理子进程的返回状态:
7. 环境变量管理
7.1 AddEnv
7.2 InitEnv
8. 内建命令处理
8.1 CheckAndExeBuiltCommand
8.1.1 cd命令
8.1.2 export命令
8.1.3 evn命令
8.1.4 echo命令
8.2 为什么使用内建命令
9. 主函数
10. 效果展示:
1, 全局变量和常量定义
功能 : 定义常量和全局变量,包括命令行参数、环境变量、当前工作目录、和最近的命令执行状态代码。
#include <iostream>
#include <cstdio>
#include <cstdlib>
#include <cstring>
#include <string>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
using namespace std;
const int basesize = 1024;
const int argvnum = 64;
const int envnum = 64;
//全局的命令行参数
char *gragv[argvnum];
int gargc = 0;
//全局变量
int lastcode = 0;
//我的系统的环境变量
char *genv[envnum];
//全局的当前shell工作路径
char pwd[basesize];
char pwdenv[basesize];
basesize: 定义了缓冲区的基础大小,用于存储用户输入的命令和路径。
argvnum: 定义最大命令行参数的数量。
envnum: 定义最大环境变量的数量。
gargv: 存储解析后的命令行参数的数组。
gargc: 表示当前解析出的参数数量。
lastcode: 保存最近执行命令的退出状态。
genv: 存储环境变量的数组。
pwd 和 pwdenv: 存储当前工作目录和环境变量PWD的缓冲区。
2. 环境信息获取
功能 : 获取当前用户、主机名和当前工作目录的信息,返回相应的字符串。
2.1 GetUserName
GetUserName: 获取当前用户的用户名,使用getenv从环境变量中获取,若未获取到,则返回"None"。
string GetUserName()
{
string name = getenv("USER");
return name.empty() ? "None" : name;
}
2.2 GetHostName
GetHostName: 获取主机名,类似于获取用户名的方式。
string GetHostName()
{
string hostname = getenv("HOSTNAME");
return hostname.empty() ? "None" : hostname;
}
2.3 GetPwd
GetPwd: 获取当前工作目录,使用getcwd函数获取并更新环境变量PWD。如果获取失败,返回"None"。
string GetPwd()
{
if (nullptr == getcwd(pwd, sizeof(pwd)))
return "None";
snprintf(pwdenv, sizeof(pwdenv), "PWD=%s", pwd);
putenv(pwdenv);
return pwd;
}
补充知识:
snprintf 是一个安全的字符串格式化函数,用于将格式化的数据写入字符串。
pwdenv 是另一个字符数组,用于存储更新后的环境变量字符串。
这里,"PWD=%s" 是格式化字符串,%s 被当前工作目录的值(即 pwd 中的值)替换。最终结果类似于 PWD=/home/user(假设当前工作目录是 /home/user)。
putenv 是一个函数,用于设置环境变量。
通过调用 putenv(pwdenv),将之前构造的环境变量字符串 PWD=/path/to/current/dir 添加到进程的环境中。
注意,使用 putenv 要确保 pwdenv 的内容在后续使用中依然有效,因为 putenv 不会复制传入的字符串。
2.4 LastDir
LastDir: 从当前路径中提取最后一个目录名称,方便在命令行提示符中显示。
string LastDir()
{
string curr = GetPwd();
if (curr == "/" || curr == "None")
return curr;
size_t pos = curr.rfind("/");
if (pos == std::string::npos)
return curr;
return curr.substr(pos + 1);
}
注意 : 这里curr == "/" || curr == "None" 我们都不需要额外处理, 直接返回就行, 因为curr为"/" 表示根目录, 为None时, 我们直接返会空就行
3. 命令行提示符
功能:生成和打印命令行提示符,包含用户、主机和当前目录信息。
3.1 MakeCommandLine
MakeCommandLine: 生成当前命令行提示符的字符串格式,包括用户、主机和当前目录。
string MakeCommandLine()
{
//[XXX@XXXXX myshell]$ 这里就与普通命令行进行区分
char command_line[basesize];
snprintf(command_line, basesize, "[%s@%s %s]#", GetUserName().c_str(), GetHostName().c_str(), LastDir().c_str());
return command_line;
}
3.2 PrintCommandLine
PrintCommandLine: 打印生成的命令行提示符到标准输出。
void PrintCommandLine()
{
printf("%s", MakeCommandLine().c_str());
fflush(stdout);
}
4, 命令输入处理
功能:从标准输入获取用户输入的命令,并解析成命令及其参数。
4.1 GetCommandLine
GetCommandLine: 从标准输入获取用户的命令行输入,使用fgets读取并处理换行符。
bool GetCommandLine(char command_buffer[], int size)
{
//我们认为 : 我们要将用户出入的命令行 -> 当作一个完整的字符串
//"ls -a -1 -n"
char* result = fgets(command_buffer, size, stdin);
if(!result)
{
return false;
}
command_buffer[strlen(command_buffer) - 1] = 0;
if(strlen(command_buffer) == 0) return false;
return true;
}
补充知识:
fgets 是一个标准库函数,用于从标准输入(stdin)读取一行字符,最多读取 size - 1 个字符,并将其存储在 command_buffer 中。
fgets 会在读取到换行符 ('\n') 或 EOF(文件结束符)时停止读取,并会在 command_buffer 的末尾自动添加一个空字符 ('\0')。
如果成功读取,result 将指向 command_buffer;如果失败(例如,输入错误或 EOF),result 将为 nullptr。
注意: strlen(command_buffer) - 1 获取字符串的最后一个字符(换行符)的索引,并将其设为 0,以去掉换行符。
比如:我们输入 ls -a -l -n
fgets 会将缓冲区填充为: command_buffer = "ls -a -l -n\n\0"
经过strlen(command_buffer) - 1处理后 : "ls -a -l -n\0"
4.2 ParseCommandLine
ParseCommandLine: 使用strtok将输入的命令行字符串分割为单独的命令和参数,并存储在gargv数组中。
void ParseCommandLine(char command_buffer[], int len)
{
(void)len;
memset(gargv, 0, sizeof(gargv));
gargc = 0;
//"ls -a -l -n"
const char *sep = " ";
gargv[gargc++] = strtok(command_buffer, sep);
//=是刻意写的
while((bool)(gargv[gargc++] = strtok(nullptr, sep)));
gargc--;
}
补充知识:
strtok 函数会将 command_buffer 字符串中的第一个分隔符(空格)替换为 \0,并返回指向第一个标记的指针,存储到 gargv 数组中。
strtok(nullptr, sep) 意味着继续解析之前传入的字符串。
注意 (bool) 转换是为了确保表达式的结果为布尔值,这样可以明确表示循环的条件。
5. 命令调试
5.1 debug
功能 : 输出当前命令参数的数量和每个参数的值,以帮助开发和调试。
void debug()
{
printf("argc: %d\n", gargc);
for(int i = 0; gargv[i]; i++)
{
printf("argv[%d]: %s\n", i, gargv[i]);
}
}
6. 命令执行
6.1 ExecuteCommand
功能:通过fork()和execvpe()创建子进程执行用户命令,并等待其完成。
bool ExecuteCommand()
{
//让子进程进行执行
pid_t id = fork();
if(id < 0) return false;
if(id == 0)
{
//子进程
//1. 执行命令
execvpe(gargv[0], gargv, genv);
//2. 退出
exit(1);
}
int status = 0;
pid_t rid = waitpid(id, &status, 0);
if(rid > 0)
{
if(WIFEXITED(status))
{
lastcode = WEXITSTATUS(status);
}
else
{
lastcode = 100;
}
return true;
}
return false;
}
子进程执行命令:
execvpe(gargv[0], gargv, genv);:execvpe 用于执行指定的命令。它会用新的程序替换当前进程(子进程)。参数说明:
gargv[0]:命令的名称(通常是命令的路径)。
gargv:一个字符串数组,包含传递给命令的参数。
genv:一个字符串数组,包含环境变量。
如果 execvpe 成功,当前进程(子进程)将被新的程序替换,之后的代码将不会执行。
如果 execvpe 失败(例如命令未找到),子进程将执行 exit(1),返回状态 1,以指示错误。
等待子进程结束:
int status = 0;:用于存储子进程的退出状态。
waitpid(id, &status, 0):在父进程中调用,等待指定的子进程(由 id 指定)结束。结果存储在 status 变量中。
返回值 rid:如果成功,返回已结束子进程的 PID;如果失败,返回 -1。
处理子进程的返回状态:
if(rid > 0):确认 waitpid 调用成功,rid 应该是大于 0 的子进程 PID。
WIFEXITED(status):这个宏检查子进程是否正常退出(通过 exit() 或 return 语句)。
WEXITSTATUS(status):如果子进程正常退出,获取其返回状态并将其存储在 lastcode 中。
如果子进程未正常退出(例如由于信号导致的终止),则将 lastcode 设置为 100,这通常用于指示异常情况。
return true;:表示成功执行命令并处理了其退出状态。
7. 环境变量管理
功能:动态分配内存,将新的环境变量添加到genv数组中。
7.1 AddEnv
void AddEnv(const char* item)
{
int index = 0;
while(genv[index])
{
index++;
}
genv[index] = (char*)malloc(strlen(item) + 1);
strncpy(genv[index], item, strlen(item) + 1);
genv[++index] = nullptr;
}
功能 : 添加环境变量:该函数接受一个字符串 item 作为参数,并将其添加到 genv 环境变量数组中。
逐行分析
int index = 0;:初始化索引为 0,用于遍历 genv 数组。
while(genv[index]):遍历 genv 数组,直到找到第一个空指针,表示可以插入新环境变量的位置。
genv[index] = (char*)malloc(strlen(item) + 1);:
为新的环境变量分配内存。malloc 分配的内存大小为 item 字符串的长度加 1(为字符串结束符 \0 留出空间)。
strncpy(genv[index], item, strlen(item) + 1);:
将 item 的内容复制到 genv[index] 中。使用 strncpy 复制到长度 strlen(item) + 1,确保包含字符串结束符。
genv[++index] = nullptr;:
在数组中下一个位置设置为 nullptr,标志着环境变量的结束。
7.2 InitEnv
void InitEnv()
{
extern char **environ;
int index = 0;
while(environ[index])
{
genv[index] = (char*)malloc(strlen(environ[index] + 1));
strncpy(genv[index], environ[index], strlen(environ[index] + 1));
index++;
}
genv[index] = nullptr;
}
功能 : 初始化环境变量:该函数从系统环境变量中读取所有环境变量并将其存储在 genv 数组中。
逐行分析
extern char **environ;:声明外部变量 environ,这是一个指向环境变量字符串数组的指针
int index = 0;:初始化索引为 0,用于遍历 environ。
while(environ[index]):遍历 environ 数组,直到找到第一个空指针,表示所有环境变量都已读取。
genv[index] = (char*)malloc(strlen(environ[index]) + 1);:
为每个环境变量分配内存。这里 strlen(environ[index]) 计算字符串的长度,并加 1 来包含 \0 结束符。
strncpy(genv[index], environ[index], strlen(environ[index] + 1));:
将 environ[index] 的内容复制到 genv[index] 中。这里同样存在一个错误,应该是 strncpy(genv[index], environ[index], strlen(environ[index]) + 1);。
index++;:增加索引,以便处理下一个环境变量。
genv[index] = nullptr;:在数组最后设置 nullptr,表示环境变量的结束
8. 内建命令处理
8.1 CheckAndExeBuiltCommand
bool CheckAndExeBuiltCommand()
{
if(strcmp(gargv[0], "cd") == 0)
{
//内建命令
if(gargc == 2)
{
chdir(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 1;
}
return true;
}
else if(strcmp(gargv[0], "export") == 0)
{
//export也是内建命令
if(gargc == 2)
{
AddEnv(gargv[1]);
lastcode = 0;
}
else
{
lastcode = 2;
}
return true;
}
else if(strcmp(gargv[0], "env") == 0)
{
for(int i = 0; genv[i]; i++)
{
printf("%d\n", genv[i]);
}
lastcode = 0;
return true;
}
else if(strcmp(gargv[0], "echo") == 0)
{
if(gargc == 2)
{
//echo $?
//echo $PATH
//echo hello
if(gargv[1][0] == '$')
{
if(gargv[1][1] == '?')
{
printf("%d\n", lastcode);
lastcode = 0;
}
else
{
printf("%s\n", gargv[1]);
lastcode = 0;
}
}
else
{
lastcode = 3;
}
return true;
}
return false;
}
}
8.1.1 cd命令
功能:用于更改当前工作目录。
逻辑:
检查参数个数 gargc 是否等于 2(即命令和目录)。
如果参数正确,调用 chdir 函数改变目录,并将 lastcode 设置为 0,表示成功。
如果参数不正确,将 lastcode 设置为 1,表示错误。
chdir() : 将调用进程的当前工作目录更改为path中指定的目录
8.1.2 export命令
功能:用于设置或导出环境变量。
逻辑:
同样检查参数个数 gargc 是否为 2。
如果正确,调用 AddEnv 函数添加环境变量,并将 lastcode 设置为 0。
如果参数不正确,设置 lastcode 为 2。
8.1.3 evn命令
功能:列出所有环境变量。
逻辑:
遍历 genv 数组,打印每个环境变量。
将 lastcode 设置为 0。
8.1.4 echo命令
功能:输出字符串或变量。
逻辑:
检查参数个数是否为 2。
如果是,检查 gargv[1] 的第一个字符是否为 $:
如果是 $?,输出 lastcode 的值。
如果是其他以 $ 开头的字符串,直接输出这个字符串(假设它是环境变量)。
如果没有 $,则将 lastcode 设置为 3。
如果参数个数不正确,返回 false。
8.2 为什么使用内建命令
快速响应:内建命令通常直接在 shell 进程内执行,无需创建新的进程,减少了上下文切换的开销。这使得内建命令的执行速度更快。
基本功能:内建命令提供了一些与 shell 紧密相关的基本功能,如改变目录(cd)、设置环境变量(export)、获取当前环境(env)等,这些操作通常与 shell 的状态直接相关。
环境影响:某些内建命令(如 cd)会影响 shell 的状态(例如当前工作目录),如果用外部程序实现,改变的状态不会反映到 shell 中。
简化用户操作:用户可以直接使用内建命令而无需担心路径问题。例如,cd 命令只需要提供目标路径,而不必寻找相应的外部可执行文件。
状态保持:内建命令如 echo 和 export 能够直接访问 shell 的状态和环境变量,而不需要通过外部程序来管理,这样可以更好地处理如 $?(上一个命令的退出状态)这样的特殊情况。
9. 主函数
功能:主程序循环,负责显示提示符、获取用户命令、解析命令、检查内建命令,并执行命令。
int main()
{
InitEnv();
char command_buffer[basesize];
while(true)
{
//1. 命令行提示符
PrintCommandLine();
//command_buffer -> output
//2. 获取用户命令
if(!GetCommandLine(command_buffer, basesize))
{
continue;
}
//3. 解析命令
ParseCommandLine(command_buffer, strlen(command_buffer));
if(CheckAndExeBuiltCommand())
{
continue;
}
4. 执行命令
ExecuteCommand();
}
return 0;
}