涉及知识全排列

步骤 1:项目准备
在开始编写代码之前,你需要创建一个新的项目文件夹,并在其中创建一个 .cpp
文件,例如 my_shell.cpp
。同时,确保你已经安装了 C++ 编译器(如 g++
),可以在终端中使用以下命令检查:
g++ --version
步骤 2:包含必要的头文件和定义宏
打开 my_shell.cpp
文件,在文件开头包含所需的头文件,并定义一些宏,这些宏将用于设置命令行大小和提示符格式。
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unordered_map>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
头文件解释:
-
iostream
:用于标准输入输出流,方便进行信息的显示和获取用户输入。 -
cstdio
:提供标准输入输出函数,如printf
和fgets
。 -
cstring
:包含字符串处理函数,像strcpy
和strlen
。 -
cstdlib
:提供通用工具函数,如malloc
和exit
。 -
unistd.h
:包含许多 Unix 系统调用,如fork
、execvp
和chdir
。 -
sys/types.h
:定义了系统数据类型,在系统调用中经常用到。 -
sys/wait.
h
:用于处理进程等待,如waitpid
函数。 -
unordered_map
:C++ 标准库中的哈希表容器,用于存储命令别名映射。
宏定义解释:
-
COMMAND_SIZE
:设置用户输入命令的最大长度为 1024 字节。 -
FORMAT
:定义命令行提示符的格式,会显示用户名、主机名和当前工作目录。
步骤 3:定义全局变量
在头文件和宏定义之后,定义一些全局变量,用于存储命令行参数、环境变量和命令别名等信息。
// 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;
// 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;
// 别名映射表
std::unordered_map<std::string, std::string> alias_list;
// 当前工作目录相关
char cwd[1024];
char cwdenv[2048];
// 上一次退出码
int lastcode = 0;
-
命令行参数:
-
MAXARGC
:最大命令行参数数量为 128。 -
g_argv
:存储命令行参数的指针数组。 -
g_argc
:记录命令行参数的实际数量。
-
-
环境变量:
-
MAX_ENVS
:最大环境变量数量为 100。 -
g_env
:存储环境变量的指针数组。 -
g_env
s
:记录环境变量的实际数量。
-
-
别名映射表 :
alias_list
是一个哈希表,用于存储命令别名和其对应的实际命令。 -
当前工作目录:
-
cwd
:存储当前工作目录的字符数组。 -
cwdenv
:存储PWD
环境变量的字符数组,大小为 2048 字节。
-
-
上一次退出码 :
lastcode
保存上一个执行命令的退出状态码。
步骤 4:编写获取系统信息的函数
接下来,编写几个函数用于获取系统信息,如用户名、主机名、当前工作目录和用户主目录。
// 获取用户名
const char *GetUserName() {
const char *name = getenv("USER");
return name == nullptr ? "None" : name;
}
// 获取主机名
const char *GetHostName() {
const char *hostname = getenv("HOSTNAME");
return hostname == nullptr ? "None" : hostname;
}
// 获取当前工作目录
const char *GetPwd() {
const char *pwd = getcwd(cwd, sizeof(cwd));
if (pwd != nullptr) {
size_t cwd_len = strlen(cwd);
size_t prefix_len = strlen("PWD=");
if (cwd_len + prefix_len + 1 <= sizeof(cwdenv)) {
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv);
}
}
return pwd == nullptr ? "None" : pwd;
}
// 获取用户主目录
const char *GetHome() {
const char *home = getenv("HOME");
return home == nullptr ? "" : home;
}
-
GetUserName
函数 :使用getenv
函数获取USER
环境变量的值,如果获取不到则返回"None"
。 -
GetHostName
函数 :使用getenv
函数获取HOSTNAME
环境变量的值,如果获取不到则返回"None"
。 -
GetPwd
函数:-
使用
getcwd
函数获取当前工作目录并存储到cwd
中。 -
检查
cwdenv
缓冲区是否足够大,如果足够则将PWD
环境变量的值设置为当前工作目录。 -
如果获取不到当前工作目录,则返回
"None"
。
-
-
GetHome
函数 :使用getenv
函数获取HOME
环境变量的值,如果获取不到则返回空字符串。
步骤 5:编写初始化环境变量的函数
编写一个函数用于初始化环境变量,将系统环境变量复制到自定义的环境变量数组中,并添加一个测试用的环境变量。
// 初始化环境变量
void InitEnv() {
extern char **environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
// 从系统环境变量复制
for (int i = 0; environ[i]; ++i) {
g_env[i] = static_cast<char *>(malloc(strlen(environ[i]) + 1));
strcpy(g_env[i], environ[i]);
++g_envs;
}
g_env[g_envs++] = strdup("HAHA=for_test");
g_env[g_envs] = nullptr;
// 设置环境变量
for (int i = 0; g_env[i]; ++i) {
putenv(g_env[i]);
}
environ = g_env;
}
-
InitEnv
函数:-
将
g_env
数组初始化为 0。 -
从系统环境变量
environ
复制环境变量到g_env
中。 -
添加一个测试用的环境变量
"HAHA=for_test"
。 -
使用
putenv
函数将g_env
中的环境变量设置到当前进程。 -
将
environ
指针指向g_env
。
-
步骤 6:编写处理内置命令的函数
编写几个函数用于处理内置命令,如 cd
、echo
、export
和 alias
。
// 处理 cd 命令
bool Cd() {
if (g_argc == 1) {
const char *home = GetHome();
if (home[0] != '\0') {
if (chdir(home) == 0) {
setenv("OLDPWD", getenv("PWD"), 1);
GetPwd();
}
}
} else {
std::string target = g_argv[1];
if (target == "-") {
const char *old_pwd = getenv("OLDPWD");
if (old_pwd != nullptr) {
if (chdir(old_pwd) == 0) {
setenv("OLDPWD", getenv("PWD"), 1);
GetPwd();
}
}
} else if (target == "~") {
const char *home = GetHome();
if (home[0] != '\0') {
if (chdir(home) == 0) {
setenv("OLDPWD", getenv("PWD"), 1);
GetPwd();
}
}
} else {
if (chdir(target.c_str()) == 0) {
setenv("OLDPWD", getenv("PWD"), 1);
GetPwd();
}
}
}
return true;
}
// 处理 echo 命令
void Echo() {
if (g_argc == 2) {
std::string arg = g_argv[1];
if (arg == "$?") {
std::cout << lastcode << std::endl;
lastcode = 0;
} else if (arg[0] == '$') {
std::string env_name = arg.substr(1);
const char *env_value = getenv(env_name.c_str());
if (env_value != nullptr) {
std::cout << env_value << std::endl;
} else {
std::cout << "Environment variable not found." << std::endl;
}
} else {
std::cout << arg << std::endl;
}
}
}
// 检查并执行内置命令
bool CheckAndExecBuiltin() {
std::string cmd = g_argv[0];
if (cmd == "cd") {
return Cd();
} else if (cmd == "echo") {
Echo();
return true;
} else if (cmd == "export") {
if (g_argc == 2) {
char *equal_sign = strchr(g_argv[1], '=');
if (equal_sign != nullptr) {
*equal_sign = '\0';
setenv(g_argv[1], equal_sign + 1, 1);
}
}
return true;
} else if (cmd == "alias") {
if (g_argc == 3) {
alias_list[g_argv[1]] = g_argv[2];
}
return true;
}
return false;
}
-
Cd
函数:-
如果没有参数,则切换到用户主目录。
-
如果参数是
-
,则切换到上一个工作目录。 -
如果参数是
~
,则切换到用户主目录。 -
其他情况,切换到指定目录。
-
每次切换目录后,更新
OLDPWD
环境变量并重新获取当前工作目录。
-
-
Echo
函数:-
如果参数是
$?
,则输出上一次命令的退出状态码并将其重置为 0。 -
如果参数以
$
开头,则输出对应的环境变量值,如果变量不存在则给出提示。 -
其他情况,直接输出参数内容。
-
-
CheckAndExecBuiltin
函数:-
检查命令是否为内置命令(
cd
、echo
、export
、alias
)。 -
如果是
export
命令,则设置新的环境变量。 -
如果是
alias
命令,则将别名和实际命令存储到alias_list
中。
-
步骤 7:编写辅助函数
编写一些辅助函数,用于获取目录名、生成命令行提示符、获取用户输入、解析命令行参数和打印命令行参数。
// 获取目录名
std::string DirName(const char *pwd) {
std::string path = pwd;
if (path == "/") return "/";
size_t pos = path.rfind('/');
if (pos == std::string::npos) return "BUG?";
return path.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));
std::cout << prompt;
std::cout.flush();
}
// 获取用户输入的命令行
bool GetCommandLine(char *out, int size) {
char *input = fgets(out, size, stdin);
if (input == nullptr) return false;
size_t len = strlen(out);
if (len > 0 && out[len - 1] == '\n') {
out[len - 1] = '\0';
}
return len > 0;
}
// 解析命令行参数
bool CommandParse(char *commandline) {
g_argc = 0;
g_argv[g_argc++] = strtok(commandline, " ");
while ((g_argv[g_argc++] = strtok(nullptr, " ")) != nullptr);
--g_argc;
return g_argc > 0;
}
// 打印命令行参数
void PrintArgv() {
for (int i = 0; g_argv[i]; ++i) {
std::cout << "argv[" << i << "]->" << g_argv[i] << std::endl;
}
std::cout << "argc: " << g_argc << std::endl;
}
-
DirName
函数:从路径中提取目录名。 -
MakeCommandLine
函数 :按照FORMAT
格式生成命令行提示符。 -
PrintCommandPrompt
函数:打印生成的命令行提示符并刷新输出缓冲区。 -
GetCommandLine
函数:从标准输入读取用户输入的命令行,去除换行符。 -
CommandParse
函数 :使用strtok
函数将命令行分割成参数,存储到g_argv
中。 -
PrintArgv
函数:打印命令行参数和参数数量。
步骤 8:编写执行外部命令的函数
编写一个函数用于执行外部命令,使用 fork
创建子进程,在子进程中使用 execvp
执行命令。
// 执行外部命令
int Execute() {
pid_t pid = fork();
if (pid == 0) {
if (execvp(g_argv[0], g_argv) == -1) {
perror("execvp");
exit(EXIT_FAILURE);
}
} else if (pid > 0) {
int status;
waitpid(pid, &status, 0);
lastcode = WEXITSTATUS(status);
} else {
perror("fork");
}
return 0;
}
-
Execute
函数:-
使用
fork
函数创建子进程。 -
子进程使用
execvp
函数执行外部命令,如果执行失败则输出错误信息并退出。 -
父进程使用
waitpid
函数等待子进程结束,获取退出状态码并保存到lastcode
中。
-
步骤 9:编写主函数
最后,编写主函数,初始化环境变量,进入无限循环,不断获取用户输入的命令并执行。
// 主函数
int main() {
InitEnv();
while (true) {
PrintCommandPrompt();
char commandline[COMMAND_SIZE];
if (!GetCommandLine(commandline, sizeof(commandline))) {
continue;
}
if (!CommandParse(commandline)) {
continue;
}
if (CheckAndExecBuiltin()) {
continue;
}
Execute();
}
// 释放环境变量内存
for (int i = 0; i < g_envs; ++i) {
free(g_env[i]);
}
return 0;
}
main
函数:
-
调用
InitEnv
函数初始化环境变量。 -
进入无限循环,不断打印命令行提示符。
-
获取用户输入的命令行并进行解析。
-
检查是否为内置命令,如果是则执行并继续循环。
-
如果不是内置命令,则调用
Execute
函数执行外部命令。 -
程序结束前,释放
g_env
数组中分配的内存。
步骤 10:编译和运行程序
终端中,使用 g++
编译器编译 my_shell.cpp
文件:
g++ my_shell.cpp -o my_shell
编译成功后,会生成一个名为 my_shell
的可执行文件。运行该文件:
./my_shell
全代码
#include <iostream>
#include <cstdio>
#include <cstring>
#include <cstdlib>
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <cstring>
#include <unordered_map>
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "
// 下面是shell定义的全局数据
// 1. 命令行参数表
#define MAXARGC 128
char *g_argv[MAXARGC];
int g_argc = 0;
// 2. 环境变量表
#define MAX_ENVS 100
char *g_env[MAX_ENVS];
int g_envs = 0;
// 3. 别名映射表
std::unordered_map<std::string, std::string> alias_list;
// for test
char cwd[1024];
char cwdenv[2048]; // 增大缓冲区,避免snprintf警告
// 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 = getcwd(cwd, sizeof(cwd));
if(pwd != NULL)
{
snprintf(cwdenv, sizeof(cwdenv), "PWD=%s", cwd);
putenv(cwdenv);
}
return pwd == NULL? "None" : pwd;
}
const char *GetHome()
{
const char *home = getenv("HOME");
return home == NULL? "" : home;
}
void InitEnv()
{
extern char **environ;
memset(g_env, 0, sizeof(g_env));
g_envs = 0;
// 本来要从配置文件来
// 1. 获取环境变量
for(int i = 0; environ[i]; i++)
{
// 1.1 申请空间
g_env[i] = (char*)malloc(strlen(environ[i]) + 1);
strcpy(g_env[i], environ[i]);
g_envs++;
}
g_env[g_envs++] = (char*)"HAHA=for_test"; // for_test
g_env[g_envs] = NULL;
// 2. 导成环境变量
for(int i = 0; g_env[i]; i++)
{
putenv(g_env[i]);
}
environ = g_env;
}
// command
bool Cd()
{
// cd argc = 1
if(g_argc == 1)
{
std::string home = GetHome();
if(home.empty()) return true;
chdir(home.c_str());
}
else
{
std::string where = g_argv[1];
// cd - / cd ~
if(where == "-")
{
const char *old_pwd = getenv("OLDPWD");
if (old_pwd)
{
chdir(old_pwd);
setenv("OLDPWD", getcwd(cwd, sizeof(cwd)), 1);
}
}
else if(where == "~")
{
std::string home = GetHome();
if (!home.empty())
{
chdir(home.c_str());
}
}
else
{
chdir(where.c_str());
}
}
return true;
}
void Echo()
{
if(g_argc == 2)
{
// echo "hello world"
// echo $?
// echo $PATH
std::string opt = g_argv[1];
if(opt == "$?")
{
std::cout << lastcode << std::endl;
lastcode = 0;
}
else if(opt[0] == '$')
{
std::string env_name = opt.substr(1);
const char *env_value = getenv(env_name.c_str());
if(env_value)
std::cout << env_value << std::endl;
else
{
std::cout << "Environment variable not found." << std::endl;
}
}
else
{
std::cout << opt << std::endl;
}
}
}
// / /a/b/c
std::string DirName(const char *pwd)
{
#define SLASH "/"
std::string dir = pwd;
if(dir == 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());
//snprintf(cmd_prompt, size, FORMAT, GetUserName(), GetHostName(), GetPwd());
}
void PrintCommandPrompt()
{
char prompt[COMMAND_SIZE];
MakeCommandLine(prompt, sizeof(prompt));
printf("%s", prompt);
fflush(stdout);
}
bool GetCommandLine(char *out, int size)
{
// ls -a -l => "ls -a -l\n" 字符串
char *c = fgets(out, size, stdin);
if(c == NULL) return false;
out[strlen(out) - 1] = 0; // 清理\n
if(strlen(out) == 0) return false;
return true;
}
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
bool CommandParse(char *commandline)
{
#define SEP " "
g_argc = 0;
// 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
g_argv[g_argc++] = strtok(commandline, SEP);
while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
g_argc--;
return g_argc > 0? true:false;
}
void PrintArgv()
{
for(int i = 0; g_argv[i]; i++)
{
printf("argv[%d]->%s\n", i, g_argv[i]);
}
printf("argc: %d\n", g_argc);
}
bool CheckAndExecBuiltin()
{
std::string cmd = g_argv[0];
if(cmd == "cd")
{
Cd();
return true;
}
else if(cmd == "echo")
{
Echo();
return true;
}
else if(cmd == "export")
{
if (g_argc == 2)
{
std::string var = g_argv[1];
char *equal_pos = strchr(var.c_str(), '=');
if (equal_pos)
{
*equal_pos = '\0';
setenv(var.c_str(), equal_pos + 1, 1);
}
}
return true;
}
else if(cmd == "alias")
{
if (g_argc == 3)
{
std::string nickname = g_argv[1];
std::string real_cmd = g_argv[2];
alias_list[nickname] = real_cmd;
}
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()
{
// shell 启动的时候,从系统中获取环境变量
// 我们的环境变量信息应该从父shell统一来
InitEnv();
while(true)
{
// 1. 输出命令行提示符
PrintCommandPrompt();
// 2. 获取用户输入的命令
char commandline[COMMAND_SIZE];
if(!GetCommandLine(commandline, sizeof(commandline)))
continue;
// 3. 命令行分析 "ls -a -l" -> "ls" "-a" "-l"
if(!CommandParse(commandline))
continue;
//PrintArgv();
// 检测别名
// 4. 检测并处理内键命令
if(CheckAndExecBuiltin())
continue;
// 5. 执行命令
Execute();
}
// 释放内存
for (int i = 0; i < g_envs; ++i)
{
free(g_env[i]);
}
return 0;
}