简单的 shell 程序
整体思路
一个简单的 shell 程序的工作流程如下:
- 初始化环境:在启动时从系统获取环境变量。
- 循环等待用户输入:不断输出命令行提示符,等待用户输入命令。
- 解析命令:把用户输入的命令解析成可执行的格式。
- 执行命令:判断是内置命令还是外部命令,然后执行相应操作。
1. 定义全局变量
在编写一个简单的 shell 程序时,我们需要一些全局变量来存储和管理不同类型的数据。以下是详细解释:
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "// 命令行参数表
#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;// 最后一次命令的退出码
int lastcode = 0;
COMMAND_SIZE
:这是一个宏定义,用来规定用户输入命令行的最大长度。在读取用户输入时,我们可以使用这个值来确保不会超出缓冲区的范围,避免缓冲区溢出问题。FORMAT
:同样是宏定义,它规定了命令行提示符的格式。%s
是格式化字符串中的占位符,分别用于插入用户名、主机名和当前工作目录。MAXARGC
:定义了命令行参数的最大数量。g_argv
是一个字符指针数组,用于存储解析后的命令行参数。g_argc
则记录当前命令行参数的实际数量,初始化为 0。MAX_ENVS
:规定了环境变量表的最大容量。g_env
是一个字符指针数组,用于存储环境变量的字符串。g_envs
记录当前环境变量的实际数量,初始化为 0。alias_list
:这是一个std::unordered_map
,用于存储命令别名和实际命令的映射关系。用户可以通过alias
命令为常用命令设置别名,方便使用。lastcode
:用于存储上一次执行命令的退出码。退出码可以表示命令执行的结果,例如 0 通常表示成功,非 0 表示有错误发生。
2. 实现获取用户信息的函数
这些函数用于获取一些系统信息,用于显示在命令行提示符中或在某些操作中使用。
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;
}
GetUserName()
:使用getenv
函数从环境变量中获取当前用户的用户名。如果获取失败(返回NULL
),则返回"None"
。GetHostName()
:同样使用getenv
函数获取主机名。若获取失败,返回"None"
。GetPwd()
:getcwd
函数用于获取当前工作目录的路径,并将其存储在cwd
数组中。如果获取成功,使用snprintf
函数将路径格式化为PWD=路径
的形式,存储在cwdenv
中,然后使用putenv
函数将其设置为环境变量。最后返回当前工作目录的路径,如果获取失败则返回"None"
。GetHome()
:使用getenv
函数获取用户的主目录路径。如果获取失败,返回空字符串。
3. 初始化环境变量
在 shell 启动时,需要从系统中获取环境变量,并将其存储到自定义的环境变量表中。
c++
void InitEnv()
{extern char **environ;memset(g_env, 0, sizeof(g_env));g_envs = 0;// 获取环境变量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_testg_env[g_envs] = NULL;// 导入环境变量for(int i = 0; g_env[i]; i++){putenv(g_env[i]);}environ = g_env;
}
extern char **environ;
:environ
是一个全局变量,指向系统的环境变量数组。通过extern
声明,我们可以在当前函数中使用它。memset(g_env, 0, sizeof(g_env));
:将g_env
数组初始化为全 0,确保每个元素都为空指针。g_envs = 0;
:将环境变量的实际数量初始化为 0。- 获取环境变量:使用
for
循环遍历系统的环境变量数组environ
,为每个环境变量分配内存空间,并将其内容复制到g_env
数组中。同时,增加g_envs
的值来记录环境变量的数量。 g_env[g_envs++] = (char*)"HAHA=for_test";
:添加一个测试用的环境变量"HAHA=for_test"
。g_env[g_envs] = NULL;
:确保环境变量数组以NULL
结尾,这是符合环境变量数组的标准格式。- 导入环境变量:使用
putenv
函数将g_env
数组中的环境变量设置到系统中。最后,将environ
指向g_env
,使得后续的操作都使用自定义的环境变量表。
4. 实现内置命令处理函数
内置命令是 shell 本身提供的命令,不需要创建新的进程来执行。以下是几个常见内置命令的处理函数。
Cd
命令
c++
bool Cd()
{if(g_argc == 1){std::string home = GetHome();if(home.empty()) return true;chdir(home.c_str());}else{std::string where = g_argv[1];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;
}
g_argc == 1
:如果cd
命令后面没有参数,使用GetHome()
函数获取用户的主目录,并使用chdir
函数将当前工作目录切换到主目录。where == "-"
:如果参数是-
,表示切换到上一个工作目录。使用getenv
函数获取OLDPWD
环境变量的值,然后使用chdir
函数切换到该目录。同时,使用setenv
函数更新OLDPWD
环境变量为当前工作目录。where == "~"
:如果参数是~
,表示切换到用户的主目录。使用GetHome()
函数获取主目录路径,然后使用chdir
函数进行切换。- 其他情况:直接使用
chdir
函数将当前工作目录切换到指定的路径。
Echo
命令
c++
void Echo()
{if(g_argc == 2){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;}}
}
opt == "$?"
:如果参数是$?
,表示输出上一次命令的退出码。将lastcode
的值输出到控制台,并将lastcode
重置为 0。opt[0] == '$'
:如果参数以$
开头,表示输出对应的环境变量的值。使用substr
函数截取$
后面的部分作为环境变量名,然后使用getenv
函数获取该环境变量的值。如果找到则输出,否则输出错误信息。- 其他情况:直接将参数输出到控制台。
5. 实现命令行处理函数
这些函数用于处理命令行的显示、输入和解析。
生成并输出命令行提示符
c++
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);
}
MakeCommandLine
:使用snprintf
函数将用户名、主机名和当前工作目录的简短名称(通过DirName
函数获取)按照FORMAT
格式填充到cmd_prompt
数组中。PrintCommandPrompt
:创建一个prompt
数组,调用MakeCommandLine
函数生成命令行提示符,然后使用printf
函数输出到控制台。最后使用fflush(stdout)
强制刷新输出缓冲区,确保提示符立即显示。
获取用户输入的命令
c++
bool GetCommandLine(char *out, int size)
{char *c = fgets(out, size, stdin);if(c == NULL) return false;out[strlen(out) - 1] = 0; // 清理\nif(strlen(out) == 0) return false;return true;
}
- 使用
fgets
函数从标准输入(键盘)读取一行命令,存储到out
数组中。 - 如果
fgets
返回NULL
,表示读取失败,返回false
。 out[strlen(out) - 1] = 0;
:fgets
会将换行符\n
也读取到字符串中,这里将其替换为字符串结束符\0
,去除换行符。- 如果读取的字符串长度为 0,说明用户没有输入有效内容,返回
false
。 - 否则返回
true
,表示成功获取到命令。
解析命令行
c++
bool CommandParse(char *commandline)
{
#define SEP " "g_argc = 0;g_argv[g_argc++] = strtok(commandline, SEP);while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));g_argc--;return g_argc > 0? true:false;
}
SEP
是一个宏定义,表示命令行参数的分隔符,这里使用空格。g_argc = 0;
:将命令行参数的实际数量初始化为 0。g_argv[g_argc++] = strtok(commandline, SEP);
:使用strtok
函数将commandline
字符串按照分隔符SEP
进行分割,获取第一个参数,并存储到g_argv
数组中。同时增加g_argc
的值。while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
:使用strtok
函数继续分割剩余的字符串,直到没有更多的参数为止。每次分割得到的参数都存储到g_argv
数组中,并增加g_argc
的值。g_argc--;
:由于最后一次分割会得到一个NULL
指针,所以需要将g_argc
的值减 1。- 如果
g_argc
大于 0,表示解析到了有效的命令行参数,返回true
;否则返回false
。
6. 检测并执行内置命令
c++
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;
}
std::string cmd = g_argv[0];
:获取命令行的第一个参数,即命令名。cmd == "cd"
:如果命令是cd
,调用Cd
函数执行cd
命令,并返回true
。cmd == "echo"
:如果命令是echo
,调用Echo
函数执行echo
命令,并返回true
。cmd == "export"
:如果命令是export
,并且参数数量为 2,使用strchr
函数查找参数中=
的位置。如果找到,将=
替换为字符串结束符\0
,然后使用setenv
函数设置环境变量。最后返回true
。cmd == "alias"
:如果命令是alias
,并且参数数量为 3,将第一个参数作为别名,第二个参数作为实际命令,存储到alias_list
中。最后返回true
。- 如果以上条件都不满足,返回
false
,表示该命令不是内置命令。
7. 执行外部命令
c++
int Execute()
{pid_t id = fork();if(id == 0){// 子进程execvp(g_argv[0], g_argv);exit(1);}int status = 0;// 父进程pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}
pid_t id = fork();
:使用fork
函数创建一个新的进程。fork
函数会返回两次,在父进程中返回子进程的进程 ID,在子进程中返回 0。- 子进程:如果
id == 0
,表示当前是子进程。使用execvp
函数执行命令行指定的外部命令。execvp
函数会用新的程序替换当前进程的映像,如果执行失败,会返回 -1。为了避免子进程继续执行后面的代码,使用exit(1)
终止子进程。 - 父进程:在父进程中,使用
waitpid
函数等待子进程结束。waitpid
函数会阻塞父进程,直到指定的子进程结束。status
参数用于存储子进程的退出状态。 lastcode = WEXITSTATUS(status);
:使用WEXITSTATUS
宏从status
中提取子进程的退出码,并将其存储到lastcode
中。- 最后返回 0,表示执行成功。
8. 主函数
c++
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;
}
1. 定义全局变量
在编写一个简单的 shell 程序时,我们需要一些全局变量来存储和管理不同类型的数据。以下是详细解释:
c++
#define COMMAND_SIZE 1024
#define FORMAT "[%s@%s %s]# "// 命令行参数表
#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;// 最后一次命令的退出码
int lastcode = 0;
COMMAND_SIZE
:这是一个宏定义,用来规定用户输入命令行的最大长度。在读取用户输入时,我们可以使用这个值来确保不会超出缓冲区的范围,避免缓冲区溢出问题。FORMAT
:同样是宏定义,它规定了命令行提示符的格式。%s
是格式化字符串中的占位符,分别用于插入用户名、主机名和当前工作目录。MAXARGC
:定义了命令行参数的最大数量。g_argv
是一个字符指针数组,用于存储解析后的命令行参数。g_argc
则记录当前命令行参数的实际数量,初始化为 0。MAX_ENVS
:规定了环境变量表的最大容量。g_env
是一个字符指针数组,用于存储环境变量的字符串。g_envs
记录当前环境变量的实际数量,初始化为 0。alias_list
:这是一个std::unordered_map
,用于存储命令别名和实际命令的映射关系。用户可以通过alias
命令为常用命令设置别名,方便使用。lastcode
:用于存储上一次执行命令的退出码。退出码可以表示命令执行的结果,例如 0 通常表示成功,非 0 表示有错误发生。
2. 实现获取用户信息的函数
这些函数用于获取一些系统信息,用于显示在命令行提示符中或在某些操作中使用
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;
}
GetUserName()
:使用getenv
函数从环境变量中获取当前用户的用户名。如果获取失败(返回NULL
),则返回"None"
。GetHostName()
:同样使用getenv
函数获取主机名。若获取失败,返回"None"
。GetPwd()
:getcwd
函数用于获取当前工作目录的路径,并将其存储在cwd
数组中。如果获取成功,使用snprintf
函数将路径格式化为PWD=路径
的形式,存储在cwdenv
中,然后使用putenv
函数将其设置为环境变量。最后返回当前工作目录的路径,如果获取失败则返回"None"
。GetHome()
:使用getenv
函数获取用户的主目录路径。如果获取失败,返回空字符串。
3. 初始化环境变量
在 shell 启动时,需要从系统中获取环境变量,并将其存储到自定义的环境变量表中。
void InitEnv()
{extern char **environ;memset(g_env, 0, sizeof(g_env));g_envs = 0;// 获取环境变量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_testg_env[g_envs] = NULL;// 导入环境变量for(int i = 0; g_env[i]; i++){putenv(g_env[i]);}environ = g_env;
}
extern char **environ;
:environ
是一个全局变量,指向系统的环境变量数组。通过extern
声明,我们可以在当前函数中使用它。memset(g_env, 0, sizeof(g_env));
:将g_env
数组初始化为全 0,确保每个元素都为空指针。g_envs = 0;
:将环境变量的实际数量初始化为 0。- 获取环境变量:使用
for
循环遍历系统的环境变量数组environ
,为每个环境变量分配内存空间,并将其内容复制到g_env
数组中。同时,增加g_envs
的值来记录环境变量的数量。 g_env[g_envs++] = (char*)"HAHA=for_test";
:添加一个测试用的环境变量"HAHA=for_test"
。g_env[g_envs] = NULL;
:确保环境变量数组以NULL
结尾,这是符合环境变量数组的标准格式。- 导入环境变量:使用
putenv
函数将g_env
数组中的环境变量设置到系统中。最后,将environ
指向g_env
,使得后续的操作都使用自定义的环境变量表。
4. 实现内置命令处理函数
内置命令是 shell 本身提供的命令,不需要创建新的进程来执行。以下是几个常见内置命令的处理函数。
Cd
命令
bool Cd()
{if(g_argc == 1){std::string home = GetHome();if(home.empty()) return true;chdir(home.c_str());}else{std::string where = g_argv[1];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;
}
g_argc == 1
:如果cd
命令后面没有参数,使用GetHome()
函数获取用户的主目录,并使用chdir
函数将当前工作目录切换到主目录。where == "-"
:如果参数是-
,表示切换到上一个工作目录。使用getenv
函数获取OLDPWD
环境变量的值,然后使用chdir
函数切换到该目录。同时,使用setenv
函数更新OLDPWD
环境变量为当前工作目录。where == "~"
:如果参数是~
,表示切换到用户的主目录。使用GetHome()
函数获取主目录路径,然后使用chdir
函数进行切换。- 其他情况:直接使用
chdir
函数将当前工作目录切换到指定的路径。
Echo
命令
void Echo()
{if(g_argc == 2){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;}}
}
opt == "$?"
:如果参数是$?
,表示输出上一次命令的退出码。将lastcode
的值输出到控制台,并将lastcode
重置为 0。opt[0] == '$'
:如果参数以$
开头,表示输出对应的环境变量的值。使用substr
函数截取$
后面的部分作为环境变量名,然后使用getenv
函数获取该环境变量的值。如果找到则输出,否则输出错误信息。- 其他情况:直接将参数输出到控制台。
5. 实现命令行处理函数
这些函数用于处理命令行的显示、输入和解析。
生成并输出命令行提示符
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);
}
MakeCommandLine
:使用snprintf
函数将用户名、主机名和当前工作目录的简短名称(通过DirName
函数获取)按照FORMAT
格式填充到cmd_prompt
数组中。PrintCommandPrompt
:创建一个prompt
数组,调用MakeCommandLine
函数生成命令行提示符,然后使用printf
函数输出到控制台。最后使用fflush(stdout)
强制刷新输出缓冲区,确保提示符立即显示。
获取用户输入的命令
bool GetCommandLine(char *out, int size)
{char *c = fgets(out, size, stdin);if(c == NULL) return false;out[strlen(out) - 1] = 0; // 清理\nif(strlen(out) == 0) return false;return true;
}
- 使用
fgets
函数从标准输入(键盘)读取一行命令,存储到out
数组中。 - 如果
fgets
返回NULL
,表示读取失败,返回false
。 out[strlen(out) - 1] = 0;
:fgets
会将换行符\n
也读取到字符串中,这里将其替换为字符串结束符\0
,去除换行符。- 如果读取的字符串长度为 0,说明用户没有输入有效内容,返回
false
。 - 否则返回
true
,表示成功获取到命令。
解析命令行
bool CommandParse(char *commandline)
{
#define SEP " "g_argc = 0;g_argv[g_argc++] = strtok(commandline, SEP);while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));g_argc--;return g_argc > 0? true:false;
}
SEP
是一个宏定义,表示命令行参数的分隔符,这里使用空格。g_argc = 0;
:将命令行参数的实际数量初始化为 0。g_argv[g_argc++] = strtok(commandline, SEP);
:使用strtok
函数将commandline
字符串按照分隔符SEP
进行分割,获取第一个参数,并存储到g_argv
数组中。同时增加g_argc
的值。while((bool)(g_argv[g_argc++] = strtok(nullptr, SEP)));
:使用strtok
函数继续分割剩余的字符串,直到没有更多的参数为止。每次分割得到的参数都存储到g_argv
数组中,并增加g_argc
的值。g_argc--;
:由于最后一次分割会得到一个NULL
指针,所以需要将g_argc
的值减 1。- 如果
g_argc
大于 0,表示解析到了有效的命令行参数,返回true
;否则返回false
。
6. 检测并执行内置命令
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;
}
std::string cmd = g_argv[0];
:获取命令行的第一个参数,即命令名。cmd == "cd"
:如果命令是cd
,调用Cd
函数执行cd
命令,并返回true
。cmd == "echo"
:如果命令是echo
,调用Echo
函数执行echo
命令,并返回true
。cmd == "export"
:如果命令是export
,并且参数数量为 2,使用strchr
函数查找参数中=
的位置。如果找到,将=
替换为字符串结束符\0
,然后使用setenv
函数设置环境变量。最后返回true
。cmd == "alias"
:如果命令是alias
,并且参数数量为 3,将第一个参数作为别名,第二个参数作为实际命令,存储到alias_list
中。最后返回true
。- 如果以上条件都不满足,返回
false
,表示该命令不是内置命令。
7. 执行外部命令
int Execute()
{pid_t id = fork();if(id == 0){// 子进程execvp(g_argv[0], g_argv);exit(1);}int status = 0;// 父进程pid_t rid = waitpid(id, &status, 0);if(rid > 0){lastcode = WEXITSTATUS(status);}return 0;
}
pid_t id = fork();
:使用fork
函数创建一个新的进程。fork
函数会返回两次,在父进程中返回子进程的进程 ID,在子进程中返回 0。- 子进程:如果
id == 0
,表示当前是子进程。使用execvp
函数执行命令行指定的外部命令。execvp
函数会用新的程序替换当前进程的映像,如果执行失败,会返回 -1。为了避免子进程继续执行后面的代码,使用exit(1)
终止子进程。 - 父进程:在父进程中,使用
waitpid
函数等待子进程结束。waitpid
函数会阻塞父进程,直到指定的子进程结束。status
参数用于存储子进程的退出状态。 lastcode = WEXITSTATUS(status);
:使用WEXITSTATUS
宏从status
中提取子进程的退出码,并将其存储到lastcode
中。- 最后返回 0,表示执行成功。
8. 主函数
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;
}
- 初始化环境变量:调用
InitEnv
函数初始化环境变量。 - 无限循环:使用
while(true)
进入一个无限循环,不断等待用户输入命令。 - 输出命令行提示符:调用
PrintCommandPrompt
函数输出命令行提示符。 - 获取用户输入的命令:创建一个
commandline
数组,调用GetCommandLine
函数获取用户输入的命令。如果获取失败,跳过本次循环,继续等待下一次输入。 - 解析命令行:调用
CommandParse
函数解析命令行。如果解析失败,跳过本次循环,继续等待下一次输入。 - 检测并执行内置命令:调用
CheckAndExecBuiltin
函数检测命令是否为内置命令。如果是,执行相应的内置命令,并跳过本次循环,继续等待下一次输入。 - 执行外部命令:如果不是内置命令,调用
Execute
函数执行外部命令。 - 释放内存:在程序结束前,使用
free
函数释放g_env
数组中分配的内存,避免内存泄漏。 - 最后返回 0,表示程序正常结束。
相关文章:
简单的 shell 程序
整体思路 一个简单的 shell 程序的工作流程如下: 初始化环境:在启动时从系统获取环境变量。循环等待用户输入:不断输出命令行提示符,等待用户输入命令。解析命令:把用户输入的命令解析成可执行的格式。执行命令&…...
MIT XV6 - 1.1 Lab: Xv6 and Unix utilities - sleep
接上文 MIT XV6 - 1. 环境准备 实验正文 打开实验链接 Lab: Xv6 and Unix utilities ,可以看到他一共有6个小实验(头大,让我们争取日拱一卒吧…): Boot xv6 (easy)sleep (easy)pingpong (easy)primes (moderate)/(hard)find (moderate)xargs (moderate…...
测地型GNSS接收机_毫米高精度精准定位
在建筑安全、地灾监测、大坝桥梁隧道监测、工程测绘、国土规划等专业领域,传统工具效率低、精度差、环境适应性弱的痛点长期存在。计讯物联测地型GNSS接收机以毫米级精度、全场景适应、智能互联为核心优势,重新定义高精度空间数据采集标准&#…...
React19源码系列之 root.render过程
在创建react项目的时候,入口文件总是有这样一行代码 root.render(<App />) 所以 root.render() 执行是怎样的? 下面就来看看。 之前的文章就提及,root是一个 ReactDOMRoot 对象,其原型链上有 render 和 unmount 方法。 ReactDOMHy…...
0804标星_复制_删除-网络ajax请求2-react-仿低代码平台项目
文章目录 1 标星2 复制3 假删除4 恢复5 彻底删除结语 1 标星 操作:标星和取消标星,有2种状态的布尔值。通过更新问卷功能实现。 后端quetion.js添加接口 {// 更新问卷信息url: /api/question/:id,method: patch,response() {return {errno: 0}}},前端q…...
二叉树遍历(C语言版)
前序遍历创建树,中序遍历把创建出来的二叉树的结点打印出来 题目链接:牛客网-二叉树遍历 前序遍历创建树的思想: 把每个结点看作是子树的根节点,以根左右的顺序创建一整棵二叉树 1.空 返回空 2.非空 先是malloc一个结点ÿ…...
OceanBase数据库-学习笔记2-C#/C++程序如何访问
MySQL模式下,程序(C#)连接数据库操作demo SqlSugar public class MainModel {private static readonly ConnectionConfig connectionConfig new ConnectionConfig(){ConnectionString "serverxxx.xxx.xxx.xxx;port2881;user idroot;…...
Python中的Walrus运算符分析
Python中的Walrus运算符(:)是Python 3.8引入的一个新特性,允许在表达式中同时赋值和返回值。它的核心作用是减少重复计算,提升代码简洁性。以下是其适用的典型场景及示例: 1. 在循环中避免重复计算 当循环条件需要多次…...
【深度好文】4、Milvus 存储设计深度解析
引言 作为一款主流的云原生向量数据库,Milvus 通过其独特的存储架构设计来保证高效的查询性能。本文将深入剖析 Milvus 的核心存储机制,特别是其最小存储单元 Segment 的完整生命周期,包括数据写入、持久化、合并以及索引构建等关键环节。 …...
航顺 芯片 开发记录 (一) 2025年4月27日19:23:32
芯片型号: HK32F030MF4P6 第一步:创建工程目录 inc :头文件目录 MDK-ARM : 工程根目录 (新建工程选择该目录) src :相关资源存放位置 官方函数库相关内容 官方函数库大致结构图 ├─HK32F030MLib ├─CMSIS │ ├─CM0 │ │ └─Core │ │ arm_common_table…...
Java 设计模式
Java后端常用设计模式总览表 模式核心思想Spring / Spring Boot应用手写实现核心单例模式 (Singleton)一个类只有一个实例,提供全局访问点Spring容器中的默认Bean都是单例管理volatile synchronized 双重检查锁定,懒加载单例工厂模式 (Factory)统一管理…...
Milvus如何实现关键词过滤和向量检索的混合检索
Milvus 可以实现关键词过滤和向量检索的混合检索,具体来说,可以结合向量搜索与其他属性字段(如关键词、类别标签等)进行联合查询。这样,在检索时不仅考虑向量的相似度,还能根据特定的关键词或标签等条件对数据进行筛选,从而提高检索的精度和灵活性。 1. 理解混合检索的…...
基于Qt5的蓝牙打印开发实战:从扫描到小票打印的全流程
文章目录 前言一、应用案例演示二、开发环境搭建2.1 硬件准备2.2 软件配置 三、蓝牙通信原理剖析3.1 实现原理3.2 通信流程3.3 流程详解3.4 关键技术点 四、Qt蓝牙核心类深度解析4.1 QBluetoothDeviceDiscoveryAgent4.2 QBluetoothDeviceInfo4.3 QBluetoothSocket 五、功能实现…...
Linux日志处理命令多管道实战应用
全文目录 1 日志处理1.1 实时日志分析1.1.1 nginx日志配置1.1.2 nginx日志示例1.1.3 日志分析示例 1.2 多文件合并分析1.3 时间范围日志提取 2 问题追查2.1 进程级问题定位2.2 网络连接排查2.3 硬件故障追踪 3 数据统计3.1 磁盘空间预警3.2 进程资源消耗排名3.3 HTTP状态码统计…...
Node.js CSRF 保护指南:示例及启用方法
解释 CSRF 跨站请求伪造 (CSRF/XSRF) 是一种利用用户权限劫持会话的攻击。这种攻击策略允许攻击者通过诱骗用户以攻击者的名义提交恶意请求,从而绕过我们的安全措施。 CSRF 攻击之所以可能发生,是因为两个原因。首先,CSRF 攻击利用了用户无法辨别看似合法的 HTML 元素是否…...
线性代数—向量与矩阵的范数(Norm)
参考链接: 范数(Norm)——定义、原理、分类、作用与应用 - 知乎 带你秒懂向量与矩阵的范数(Norm)_矩阵norm-CSDN博客 什么是范数(norm)?以及L1,L2范数的简单介绍_l1 norm-CSDN博客 范数(Norm…...
微服务基础-Ribbon
1. Ribbon简介: 客户端的负载均衡: 2....
移除生产环境所有console.log
大多数团队都会要求不能在生产环境输出业务侧的内容,但是往往业务开发人员会有疏漏,所以需要在工程化环境中,整体来管理console.log。我最近也是接到这样一个需求,整理了一下实现方案。 不同团队,不同场景,…...
数字人接大模型第二步:实时语音同步
接上例第一步,还是dh_live项目,增加了一个完整的实时对话样例,包含vad-asr-llm-tts-数字人全流程,以弥补之前的只有固定的问答的不足。 VAD(Voice Activity Detection,语音活动检测)VAD用于检测用户是否正在说话,从而触发后续的语音处理流程。 ASR(Automatic Speech R…...
Tomcat的安装与配置
Tomcat Tomcat是一个Java圈子中广泛使用的HTTP服务器. 后续学习Severlet内容,就是依赖Tomcat. Java程序员,要想写个网站出来,绕不开Tomcat. 我们这里使用Tomcat8 在bin目录下,这两个文件尤为重要,需要说明的是,Tomcat是那Java写的,所以在运行时需要jdk. bat后缀:是Window…...
Spring AI Alibaba - MCP连接 MySQL
先看效果 直接问他数据库有什么表。 大模型调用MySQL进行查询 搭建项目 添加依赖 创建项目后新添加Maven 依赖: <dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId> …...
Spring Cloud Stream喂饭级教程【搜集全网资料整理】
文章较长,建议收藏关注,随时查看 Spring Cloud Stream 简介 Spring Cloud Stream 是 Spring 提供的一个框架,用于构建与共享消息系统相连接的高度可伸缩的事件驱动微服务,它建立在 Spring 已有的成熟组件和最佳实践之上ÿ…...
prometheus手动添加k8s集群外的node-exporter监控
1、部署node-exporter 1)helm方式部署 rootiZj6c72dzbei17o2cuksmeZ:~# helm repo add prometheus-community https://prometheus-community.github.io/helm-charts "prometheus-community" has been added to your repositories rootiZj6c72dzbei17o2cu…...
Linux(Centos版本)中安装Docker
文章目录 Linux(Centos版本)中安装Docker整体流程 Linux(Centos版本)中安装Docker整体流程 进入root权限进行安装: 下面开始安装Docker: 1、安装docker的yum管理工具:记得将yum仓库更改为国内的镜像源&…...
C语言-- 深入理解指针(4)
C语言-- 深入理解指针(4) 一、回调函数二、冒泡排序三、qsort函数3.1 使用qsort函数排序整型数据3.2 使用qsort函数排序double数据3.3 使用qsort来排序结构体数据 四、模仿qsort库函数实现通用的冒泡排序4.1 通用冒泡排序函数排序整型数据4.2 通用冒泡排…...
牟乃夏《ArcGIS Engine地理信息系统开发教程》学习笔记3-地图基本操作与实战案例
目录 一、开发环境与框架搭建 二、地图数据加载与文档管理 1. 加载地图文档(MXD) 2. 动态添加数据源 三、地图浏览与交互操作 1. 基础导航功能 2. 书签管理 3. 量测功能 四、要素选择与属性查询 1. 属性查询 2. 空间查询 五、视图同步与鹰眼…...
Spark Streaming实时数据处理实战:从DStream基础到自定义数据源集成
park-Streaming概述 Spark-Streaming是什么 Spark Streaming 用于流式数据的处理。Spark Streaming 支持的数据输入源很多,例如:Kafka、Flume、Twitter等,以及和简单的 TCP 套接字等等。数据输入后可以用 Spark 的高度抽象原语如:…...
微软GraphRAG的安装和在RAG中的使用体会
文章目录 0. 简介(1)**技术原理**(2)**优势**(3)**开源与演进** 1. 下载graphrag.git2.安装 poetry3.初始化项目:建立cases目录4. 修改.env5.修改settings.yaml,将两处 api_base改成中转站地址:…...
Python学习记录7——集合set()的使用指南
文章目录 引言一、集合特性二、创建方式三、元素操作1、添加元素(1)add(element)(2)update(iterables) 2、删除元素(1)remove(element)(2)discard(element)(3)…...
apkpure 谷歌插件 下载的apk包
谷歌插件市场搜索 apkpure 然后直接搜索下载就行了 想看apk包中的静态资源,直接改apk 为zip后缀解压就行了 apple的ipa包也是相同的道理...
Android四大核心组件
目录 一、为什么需要四大组件? 二、Activity:看得见的界面 核心功能 生命周期图解 代码示例 三、Service:看不见的劳动者 两大类型 生命周期对比 注意陷阱 四、BroadcastReceiver:消息传递专员 两种注册方式 广播类型 …...
WSL2里手动安装Docker 遇坑
在 WSL2 里手动安装 Docker Engine 时遇坑:systemctl 和 service 命令在默认的 WSL2 Ubuntu 中 无法使用,因为 WSL2 没有 systemd。怎么办? 自己操作让 Docker Engine(dockerd)直接跑起来,挂到 /var/run/do…...
【ROS2】ROS开发环境配置——vscode和git
古月21讲-ROS2/1.系统架构/1.5_ROS2开发环境配置/ ROS机器人开发肯定离不开代码编写,课程中会给大家提供大量示例源码,这些代码如何查看、编写、编译 安Linux中安装装git sudo apt install git下载教程源码 《ROS2入门21讲》课程源码的下载方式&#x…...
django.db.models.query_utils.DeferredAttribute object
在 Django 中,当你看到 django.db.models.query_utils.DeferredAttribute 对象时,通常是因为你在查询时使用了 only() 或 defer() 方法来延迟加载某些字段。这两个方法允许你控制数据库查询中的字段加载方式,从而优化查询性能。 only() 方法…...
Linux内核中的编译时安全防护:以网络协议栈控制块校验为例
引言:内存安全的无声守卫者 在操作系统内核开发中,内存溢出引发的错误往往具有极高的隐蔽性和破坏性。Linux内核作为承载全球数十亿设备的基石,其网络协议栈的设计尤其注重内存安全性。本文通过分析一段看似简单的内核代码,揭示Linux如何通过编译时静态检查(Compile-Time…...
第11章 安全网络架构和组件(一)
11.1 OSI 模型 协议可通过网络在计算机之间进行通信。 协议是一组规则和限制,用于定义数据如何通过网络介质(如双绞线、无线传输等)进行传输。 国际标准化组织(ISO)在20世纪70年代晚期开发了开放系统互连(OSI)参考模型。 11.1.1 OSI模型的…...
Git常用命令简明教程
本教程整合并优化了Git核心命令,涵盖初始化、配置、文件操作、分支管理、远程仓库操作及常见场景,适合快速入门和日常参考。命令按使用流程分组,简洁明了,包含注意事项和最佳实践。 1. 初始化与配置 初始化Git仓库并设置基本配置…...
在 Ubuntu 24.04 系统上安装和管理 Nginx
1、安装Nginx 在Ubuntu 24.04系统上安装Nginx,可以按照下面的步骤进行: 1.1、 更新系统软件包列表 在安装新软件之前,需要先更新系统的软件包列表,确保获取到最新的软件包信息。打开终端,执行以下命令: …...
数据结构——二叉树和堆(万字,最详细)
目录 1.树 1.1 树的概念与结构 1.2 树相关的术语 1.3 树的表示法 2.二叉树 2.1 概念与结构 2.2 特殊的二叉树 2.2.1 满二叉树 2.2.2 完全二叉树 2.3 二叉树存储结构 2.3.1 顺序结构 2.3.2 实现顺序结构二叉树 2.3.2.1 堆的概念与结构 2.3.2. 2 堆的插入与删除数据…...
IdeaVim 配置与使用指南
一、什么是 IdeaVim? IdeaVim 是 JetBrains 系列 IDE(如 IntelliJ IDEA, WebStorm, PyCharm 等)中的一个插件,让你在 IDE 里使用 Vim 的按键习惯,大大提升效率。 安装方法: 在 IDE 中打开 设置(Settings) →…...
前端浏览器窗口交互完全指南:从基础操作到高级控制
浏览器窗口交互是前端开发中构建复杂Web应用的核心能力,本文深入探讨23种关键交互技术,涵盖从传统API到最新的W3C提案,助您掌握跨窗口、跨标签页的完整控制方案。 一、基础窗口操作体系 1.1 窗口创建与控制 // 新窗口创建(现代浏…...
考研系列-计算机组成原理第五章、中央处理器
一、CPU的功能及结构 1.运算器的基本结构 2.控制器结构...
python+flask+flask-sockerio,部署后sockerio通信异常
前言 用python开发了一个flask web服务,前端用html,前后端通过socketio通信,开发环境,windowsminicondavscode,开发完成后本地运行没有问题,然后就开始部署,噩梦就开始了。 问题描述 程序是部…...
深度解析:TextRenderManager——Cocos Creator艺术字体渲染核心类
一、类概述 TextRenderManager 是 Cocos Creator 中实现动态艺术字体渲染的核心单例类。它通过整合资源加载、缓存管理、异步队列和自动布局等功能,支持普通字符模式和图集模式两种渲染方案,适用于游戏中的动态文本(如聊天内容、排行榜&…...
同样开源的自动化工作流工具n8n和Dify对比
n8n和Dify作为两大主流工具,分别专注于通用自动化和AI应用开发领域,选择哪个更“好用”需结合具体需求、团队能力及业务场景综合判断。以下是核心维度的对比分析: 一、核心定位与适用场景 维度n8nDify核心定位开源全场景自动化工具ÿ…...
设计模式每日硬核训练 Day 16:责任链模式(Chain of Responsibility Pattern)完整讲解与实战应用
🔄 回顾 Day 15:享元模式小结 在 Day 15 中,我们学习了享元模式(Flyweight Pattern): 通过共享对象,分离内部状态与外部状态,大量减少内存开销。适用于字符渲染、游戏场景、图标缓…...
基于边缘人工智能的AI无人机-更高效更安全的飞行任务执行
基于边缘人工智能的AI无人机-更高效更安全的飞行任务执行 人工智能有可能改变人们的生活和工作方式。人工智能和无人机是近年来发展迅速的两项技术。当这两种技术结合在一起时,它们会创造出许多以前不可能的应用。基于人工智能的无人机旨在独立执行任务,…...
30、不是说字符串是不可变的吗,string s=“abc“;s=“123“不就是变了吗?
一、核心概念澄清:不可变性的真实含义 1、不可变性的定义 字符串不可变性指对象内容不可修改,而非变量不可修改。 类比: 不可变字符串 装在密封信封里的信纸(内容不可更改)变量赋值 更换信封的指向(从…...
线上查询车辆出险记录:快速掌握事故情况!
在如今汽车成为人们日常不可或缺的交通工具之际,车辆出险记录成为了许多车主关注的焦点之一。为了帮助车主们快速了解车辆出险、理赔、事故记录,现在有了一种便捷的方式,那就是通过API接口在线查询。本文将介绍如何利用API接口,通…...
Python爬虫课程实验指导书
1.1Requests类库的认知 1.1.1 认识请求类库 Requests是用Python语言编写,基于,采用Apache2 Licensed开源协议的。它比urllib更加方便,可以节约我们大量的工作,完全满足HTTP测试需求。urllibHTTP库 Requests官网地址:ht…...