简单的 shell 程序

整体思路

一个简单的 shell 程序的工作流程如下:

  1. 初始化环境:在启动时从系统获取环境变量。
  2. 循环等待用户输入:不断输出命令行提示符,等待用户输入命令。
  3. 解析命令:把用户输入的命令解析成可执行的格式。
  4. 执行命令:判断是内置命令还是外部命令,然后执行相应操作。

 

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_test
    g_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; // 清理\n
    if(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_test
    g_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; // 清理\n
    if(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,表示程序正常结束。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值