【Linux】1w详解如何实现一个简单的shell

07-14 1036阅读

 目录 

实现思路

1. 交互 获取命令行

2. 子串分割 解析命令行

3. 指令的判断 内建命令

4. 普通命令的执行

 补充:vim 文本替换

整体代码

重点思考

1.getenv和putenv是什么意思

2.代码extern char **environ;

3.内建命令是什么

4.lastcode = WEXITSTATUS(status);

5.execvp(_argv[0], _argv);的调用

6._argc&_argv


实现思路

1. 交互 获取命令行

显示提示符和获取用户输入

Shell本质是一个死循环,不断地显示提示符和获取用户输入。

memset 函数

memset 函数用于将一段内存区域设置为指定的值。它的原型是:

void *memset(void *s, int c, size_t n);

参数说明:

  • s:指向要填充的内存区域的指针。

  • c:要设置的值(以无符号字符形式传递,但实际存储在内存中的每个字节的值是该无符号字符的值)。

  • n:要设置的字节数。

    示例用法:

    char command_line[NUM];
    memset(command_line, '\0', sizeof(command_line) * sizeof(char));
    

    这里的代码表示将 command_line 数组的每个字节都设置为 \0(空字符),确保初始化整个数组。

    fgets 函数

    fgets 函数用于从指定的输入流读取字符串。它的原型是:

    char *fgets(char *s, int n, FILE *stream);
    

    参数说明:

    • s:指向存储读取数据的字符数组的指针。

    • n:要读取的最大字符数(包括终止字符 \0)。

    • stream:输入流,通常是 stdin 用于标准输入。

      示例用法:

      fgets(command_line, NUM, stdin);
      

      这行代码表示从标准输入读取最多 NUM-1 个字符(预留一个字符用于终止字符 \0)到 command_line 数组中。

      综合示例

      结合起来,代码片段如下所示:

      char command_line[NUM];
      memset(command_line, '\0', sizeof(command_line) * sizeof(char));
      fgets(command_line, NUM, stdin);
      

      这段代码的作用是:

      1. 使用 memset 函数将 command_line 数组的所有字节都设置为 \0,即初始化数组。

      2. 使用 fgets 函数从标准输入读取最多 NUM-1 个字符并存储在 command_line 数组中。

      这样处理后,command_line 数组会包含从输入读取的字符串,并且如果字符串的长度小于 NUM,数组中剩余的字节会保持为 \0。

      以下是实现这两个步骤的代码:

      #include 
      #include 
      #include 
      #include 
      #define NUM 1024
      char command_line[NUM]; // 用来接收命令行内容
      int main(void) {
          while (1) {
              /* Step1:显示提示符 */
              printf("[用户@主机 当前目录] # ");
              fflush(stdout);
              /* Step2:获取用户输入 */
              memset(command_line, '\0', sizeof(command_line));
              fgets(command_line, NUM, stdin);  // 从键盘获取输入
              command_line[strlen(command_line) - 1] = '\0'; // 消除 '\n'
              printf("%s\n", command_line);
          }
      }
      

      通过上述代码,我们可以实现提示用户输入,并获取用户输入。

      注意点:

      执行发现有空行怎么办

      【Linux】1w详解如何实现一个简单的shell

       我们利用 fgets 函数从键盘上获取,标准输入 stdin,获取到 C 风格的字符串,

      注意默认会添加 \0 ,我们先把获取到的结果 command_line 打印出来看看:

       因为 command_line 里有一个 \n,我们把它替换成 \0 即可:

       command_line[strlen(command_line) - 1] = '\0';  // 消除 '\0'

      2. 子串分割 解析命令行

      获取用户输入后,我们需要将接收到的字符串拆分为命令及其参数。

      将接收到的字符串拆开

      通过 strtok 函数,我们可以将一个字符串按照特定的分隔符打散,依次返回子串:

      #include 
      #include 
      #include 
      #include 
      #define NUM 1024
      #define SEP " "
      #define SIZE 128
      char command_line[NUM];
      char* command_args[SIZE];
      int main(void) {
          while (1) {
              /* 显示提示符和获取用户输入 */
              printf("[用户@主机 当前目录] # ");
              fflush(stdout);
              memset(command_line, '\0', sizeof(command_line));
              fgets(command_line, NUM, stdin);
              command_line[strlen(command_line) - 1] = '\0';
              /* 将接收到的字符串拆开 */
              command_args[0] = strtok(command_line, SEP);
              int idx = 1;
              while ((command_args[idx++] = strtok(NULL, SEP)));
              /* 打印拆分结果 */
              for (int i = 0; i  
      

      通过这段代码,我们可以将输入的命令行字符串拆分成多个子字符串,并打印出来。

      【Linux】1w详解如何实现一个简单的shell

      strtok 函数的原型为:

      char *strtok(char *str, const char *delim);
      

      参数说明:

      • str:要进行分割的字符串,第一次调用时传入要分割的字符串,后续调用时传入 NULL 即可。

      • delim:分隔符,用于指定分割字符串的字符。

        示例用法

        在代码中,使用了 strtok 函数将 command_line 字符串按照 SEP 分隔符进行切割,并将每个子字符串存储在 command_args 数组中。

        command_args[0] = strtok(command_line, SEP);
        int idx = 1;
        

        这里的代码首先将 command_line 字符串按照 SEP 分隔符切割成子字符串,并将第一个子字符串的指针存储在 command_args[0] 中。然后,

        利用循环逐个获取剩余的子字符串,并将它们存储在 command_args 数组中(使用 idx 来索引)。

        3. 指令的判断 内建命令

        为了实现一些特定功能,如路径切换,我们需要在Shell中实现内建命令。

        内建命令:实现路径切换

        #include 
        #include 
        #include 
        #include 
        #define NUM 1024
        #define SEP " "
        #define SIZE 128
        char command_line[NUM];
        char* command_args[SIZE];
        /* Shell 内置函数: 路径跳转 */
        int ChangeDir(const char* new_path) {
            return chdir(new_path);
        }
        int main(void) {
            while (1) {
                /* 显示提示符和获取用户输入 */
                printf("[用户@主机 当前目录] # ");
                fflush(stdout);
                memset(command_line, '\0', sizeof(command_line));
                fgets(command_line, NUM, stdin);
                command_line[strlen(command_line) - 1] = '\0';
                /* 将接收到的字符串拆开 */
                command_args[0] = strtok(command_line, SEP);
                int idx = 1;
                while ((command_args[idx++] = strtok(NULL, SEP)));
                /* 判断并执行内建命令 */
                if (strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL) {
                    ChangeDir(command_args[1]);
                    continue;
                }
                /* 执行普通命令 */
            }
        }
        

        这段代码通过判断输入的命令是否为 cd 来执行路径切换,而无需创建子进程。

        getcwd用于获取当前工作目录(当前目录)的路径。该函数的声明如下:

        char *getcwd(char *buf, size_t size);

        函数参数说明:

        • buf:指向存储当前工作目录路径的缓冲区
        • size:缓冲区的大小

          函数返回值: 如果函数调用成功,则返回指向存储当前工作目录路径的缓冲区的指针;如果函数调用失败,则返回NULL。

          通过调用getcwd函数,可以获取当前程序所在的工作目录路径。


          chdir用于改变当前工作目录(当前目录)的路径。该函数的声明如下:

          int chdir(const char *path);

          函数参数说明:

          • path:要设置为当前工作目录的路径

            函数返回值: 如果函数调用成功,则返回0;如果函数调用失败,则返回-1,并设置errno来指示错误的类型。

            4. 普通命令的执行

            最后,我们实现普通命令的执行,包括创建子进程并执行用户输入的命令。

            创建进程 & 程序替换

            #include 
            #include 
            #include 
            #include 
            #include 
            #include 
             
            #define NUM 1024
            #define SEP " "
            #define SIZE 128
             
            char command_line[NUM];
            char* command_args[SIZE];
             
            int main(void)
            {
                while (1) {
                    /* Step1:显示提示符 */
                    printf("[lvy@我的主机名 当前目录] # ");
                    fflush(stdout);
             
                    /* Step2:获取用户输入 */
                    memset (
                        command_line, 
                        '\0', 
                        sizeof(command_line) * sizeof(char)
                    );
                    fgets(command_line, NUM, stdin);  /* 从键盘获取,标准输入,stdin 
                        获取到 C 风格的字符串,默认添加 '\0' */
                    command_line[strlen(command_line) - 1] = '\0';  // 消除 '\0'
             
                    /* Step3: 将接收到的字符串拆开 - 字符串切分 */
                    command_args[0] = strtok(command_line, SEP);
                    int idx = 1;
                    
                    /* 这里的 = 是故意这么写的,因为 strtok 截取成功返回字符串起始地址
                        截取失败,返回 NULL */
                    while (command_args[idx++] = strtok(NULL, SEP));
             
                    //我们来测试一下看看 
                    // for (int i = 0; i  0) {   // 等待成功
                        printf("等待成功!sig: %d, code: %d\n", status&0x7F, (status>>8)&0xFF);
                    }
                } // end while
             
            }

            通过上述代码,我们可以创建一个进程来执行用户输入的命令,并等待子进程结束。

            【Linux】1w详解如何实现一个简单的shell

            给命令带颜色

            为了增强Shell的用户体验,可以给一些常用命令添加颜色,例如 ls 命令:

            /* 将接收到的字符串拆开 */
            command_args[0] = strtok(command_line, SEP);
            int idx = 1;
            while ((command_args[idx++] = strtok(NULL, SEP)));
            /* 给 ls 命令添加颜色 */
            if (strcmp(command_args[0], "ls") == 0) {
                command_args[idx++] = (char*)"--color=auto";
            }
            

            以上实现了一个简单的Shell,具备了基本的提示符显示、用户输入获取、命令解析、内建命令和普通命令的执行功能。

            内建命令 环境变量

            /* Shell 内置函数: 路径跳转 */
            int ChangeDir(const char* new_path) {
                chdir(new_path);
             
                return 0;  // 调用成功
            }
             
            int main(void) 
            {
                ...
                    /* Step4. TODO 编写后面的逻辑,内建命令 */
                    if (strcmp(command_args[0], "cd") == 0 && command_args[1] != NULL) {
                        ChangeDir(command_args[1]);  // 让调用方进行路径切换
                        continue;
                    }
                ...
            }
            

            保存环境变量的字符串,不能是易变的,所以 strcpy mycommand,实现与argv的分离

            【Linux】1w详解如何实现一个简单的shell

             补充:vim 文本替换

            如何快速将mycmd换为myshell呢

            【Linux】1w详解如何实现一个简单的shell

            通过如下操作

            : %s/mycmd/myshell/g

            【Linux】1w详解如何实现一个简单的shell

            就可以啦

            【Linux】1w详解如何实现一个简单的shell


            细节设置的思考,在最后一部分,让我们先来看一下整体

            整体代码

            #include 
            #include 
            #include 
            #include 
            #include //创建子进程
            #include //这些文件都是什么意思
            #include 
            #include 
            #define LEFT "["
            #define RIGHT "]"
            #define LABLE "#"
            #define DELIM " \t"
            #define LINE_SIZE 1024
            #define ARGC_SIZE 32
            #define EXIT_CODE 44
            int lastcode = 0;
            int quit = 0;
            extern char **environ;
            char commandline[LINE_SIZE];
            char *argv[ARGC_SIZE];//存储切割之后的命令行
            char pwd[LINE_SIZE];
            // 自定义环境变量表
            char myenv[LINE_SIZE];
            // 自定义本地变量表
            const char *getusername()
            {
                return getenv("USER");
            }
            const char *gethostname()
            {
                return getenv("HOSTNAME");
            }
            void getpwd()
            {
                getcwd(pwd, sizeof(pwd));//获取当前工作目录
            }
            void interact(char *cline, int size)//交互
            {
                getpwd();
                printf(LEFT"%s@%s %s"RIGHT""LABLE" ", getusername(), gethostname(), pwd);
                char *s = fgets(cline, size, stdin);输入流进行输入
                assert(s);//断言不为空
                (void)s;//调用s避免报错
                // "abcd\n\0"
                cline[strlen(cline)-1] = '\0';//取消自动换行
            }
            // ls -a -l | wc -l | head 
            int splitstring(char cline[], char *_argv[])
            {
                int i = 0;
                argv[i++] = strtok(cline, DELIM);//区分全局变量和形参_
                while(_argv[i++] = strtok(NULL, DELIM)); // 故意写的=
                //NULL的设置才能实现往后移的切割
                return i - 1;//去除NULL
            }
            void NormalExcute(char *_argv[])
            {
                pid_t id = fork();
                if(id  myfile / ls -a -l >> myfile / cat  "ls -a -l -n\0" -> "ls" "-a" "-l" "-n"
                    // 3. 子串分割的问题,解析命令行
                    int argc = splitstring(commandline, argv);
                    //如何将字串打散呢
                    //strtok需要循环调用
                    //while(argv[i++]=strtok(commandline,DELIM);//故意写的等号
                    if(argc == 0) continue;
                    // 4. 指令的判断 
                    // debug
                    //for(int i = 0; argv[i]; i++) printf("[%d]: %s\n", i, argv[i]);
                    //内键命令,本质就是一个shell内部的一个函数
                    int n = buildCommand(argv, argc);
                    // ls -a -l | wc -l
                    // 4.0 分析输入的命令行字符串,获取有多少个|, 命令打散多个子命令字符串
                    // 4.1 malloc申请空间,pipe先申请多个管道
                    // 4.2 循环创建多个子进程,每一个子进程的重定向情况。最开始. 输出重定向, 1->指定的一个管道的写端 
                    // 中间:输入输出重定向, 0标准输入重定向到上一个管道的读端 1标准输出重定向到下一个管道的写端
                    // 最后一个:输入重定向,将标准输入重定向到最后一个管道的读端
                    // 4.3 分别让不同的子进程执行不同的命令--- exec* --- exec*不会影响该进程曾经打开的文件,不会影响预先设置好的管道重定向
                    // 5. 普通命令的执行
                    if(!n) NormalExcute(argv);//让命令0的时候执行
                }
                return 0;
            }
            

            重点思考

            1.getenv和putenv是什么意思

             getenv函数用于获取指定环境变量的值。它的函数定义如下:

            char *getenv(const char *name);
            • 参数:

                • name:要获取的环境变量的名称。

                • 返回值:

                    • 如果指定的环境变量存在,那么返回一个指向该环境变量值的指针。

                    • 如果指定的环境变量不存在,则返回NULL。

                    以下是一个使用getenv函数的示例:

                    #include 
                    #include 
                    int main() {
                        char *path = getenv("PATH");
                        if (path != NULL) {
                            printf("PATH environment variable: %s\n", path);
                        } else {
                            printf("PATH environment variable not found.\n");
                        }
                        return 0;
                    }

                    【Linux】1w详解如何实现一个简单的shell

                    成功实现对环境变量的调用啦

                    putenv函数

                    putenv函数用于设置环境变量。它的函数定义如下:

                    int putenv(char *string);
                    • 参数:

                        • string:形式为"name=value"的字符串,用于设置具体的环境变量及其值。

                        • 返回值:

                            • 成功时返回0。

                            • 失败时返回非零值。

                            以下是一个使用putenv函数的示例:

                            #include 
                            #include 
                            int main() {
                                char env_str[] = "MY_ENV=hello_world";
                                if (putenv(env_str) == 0) {
                                    printf("Environment variable set successfully.\n");
                                } else {
                                    perror("putenv");
                                    return 1;
                                }
                                
                                char *my_env = getenv("MY_ENV");
                                if (my_env != NULL) {
                                    printf("MY_ENV: %s\n", my_env);
                                } else {
                                    printf("MY_ENV environment variable not found.\n");
                                }
                                
                                return 0;
                            }

                            【Linux】1w详解如何实现一个简单的shell

                            注意事项

                            1. 内存管理:

                              • getenv返回的指针指向的是环境变量的值,不能直接修改此值,否则可能导致未定义行为。

                              • putenv函数参数所指向的字符串在函数调用后仍需存在,因为putenv不会复制这个字符串。因此传递给putenv的字符串应始终位于可修改的全局或堆内存中,而不是局部变量中。

                              1. 线程安全性:

                                • getenv和putenv函数在某些实现中不是线程安全的,特别是当修改同一个环境变量时。建议在多线程环境中使用setenv和unsetenv函数,它们是现代C库中提供的线程安全的替代函数。

                                总结

                                getenv和putenv是C语言中用于获取和设置环境变量的基本函数。通过了解并正确使用它们,可以更好地管理进程环境。

                                2.代码extern char **environ;

                                 extern char **environ; 是C语言中的全局变量声明,用于访问当前进程的环境变量。为了理解这一行代码,我们需要理清以下几个关键概念:

                                环境变量的存储

                                在Unix和类Unix操作系统(如Linux)中,环境变量是一组键值对(例如PATH=/usr/bin),用于向进程传递配置信息。每个环境变量项以字符串的形式存储在一个全局变量数组中。这个数组在进程启动时由操作系统初始化,并且每个程序都可以访问和修改它。

                                环境变量在内存中的表示

                                在内存中,环境变量通常表示为一个字符串数组,每个字符串保存一个环境变量。例如:

                                PATH=/usr/bin
                                HOME=/home/user
                                USER=user
                                ...

                                这些字符串指针存储在一个全局变量数组中,即char **environ。

                                extern关键字

                                extern关键字用于声明一个全局变量,但不定义它。它告诉编译器这个变量是在别处(比如另一个源文件或由操作系统提供)定义的。因此,extern char **environ; 仅仅是一个声明,用来告知编译器这个变量在别处已经定义过,可以在当前文件中使用它。

                                为什么这样写?

                                在标准C库中,environ变量实际上在系统库中已经定义,我们只需要在我们的程序中声明一下即可使用。这种方式使我们能够访问和操作环境变量。

                                这里是extern char **environ;的具体含义:

                                1. 声明:它声明了一个外部变量environ,是一个指向字符指针的指针。

                                2. 外部定义:实际的环境变量数组由操作系统初始化,并定义在某个系统库中。

                                3. 全局访问:通过这个声明,我们可以在任何源文件中访问和操作环境变量。

                                示例

                                下面是一个具体的例子,展示了如何使用environ来访问并打印所有环境变量:

                                #include 
                                // 声明外部环境变量数组
                                extern char **environ;
                                int main(void) {
                                    // 指向环境变量数组的指针
                                    char **env = environ;
                                    // 遍历并打印所有环境变量
                                    while (*env) {
                                        printf("%s\n", *env);
                                        env++;
                                    }
                                    return 0;
                                }

                                就可以成功调用所有环境变量啦

                                【Linux】1w详解如何实现一个简单的shell

                                总结

                                extern char **environ; 这一行代码的作用是声明一个指针数组,用于访问当前进程的环境变量。通过这种方式,我们可以在C程序中方便地读取和操作环境变量。

                                3.内建命令是什么

                                内建命令是指直接内置在操作系统内核中的一些命令,与普通的外部命令(外部程序文件)不同。这些内建命令是直接由shell解释器(如Bash、Zsh等)所处理,而不需要通过外部文件的方式来执行。这些内建命令通常在操作系统的shell环境中被频繁使用,并且执行速度更快,因为它们不需要创建新的进程来执行。

                                在Unix和类Unix操作系统中,通常会有一些内建命令,比如cd、echo、exit等。这些命令不需要单独的可执行文件,而是直接由shell内核提供支持。当用户在shell中输入这些命令时,shell会直接处理它们,而不需要通过搜索系统路径来找到可执行文件。

                                值得一提的是,某些shell也允许用户通过自定义的方式添加新的内建命令,这样用户可以根据自己的需求来扩展shell的内建功能。

                                4.lastcode = WEXITSTATUS(status);

                                在C语言中,WEXITSTATUS(status) 是一个宏,用于从wait或waitpid返回的状态信息中提取子进程的退出状态。这个宏主要用于处理子进程的退出状态信息。

                                具体来说,WEXITSTATUS(status) 用于提取子进程在终止时传递给exit或_exit函数的退出状态。这个宏将状态信息进行适当的位操作,以获取子进程的退出状态值。

                                一般情况下,status 是由wait或waitpid函数返回的子进程状态,其中包含了有关子进程终止的信息,包括退出状态。通过使用WEXITSTATUS(status),可以将状态转换为子进程的退出状态,以便于后续处理和判断子进程的终止情况。

                                具体的用法示例如下:

                                #include 
                                #include 
                                #include 
                                #include 
                                #include 
                                int main() {
                                    pid_t pid;
                                    int status;
                                    int lastcode;
                                    pid = fork();
                                    if (pid  
                                

                                在这个例子中,WEXITSTATUS(status) 会从 status 中提取子进程的退出状态,并将其赋值给 lastcode。然后这个退出状态可以被用来进行一些处理,比如根据不同的退出状态进行不同的操作。

                                【Linux】1w详解如何实现一个简单的shell

                                需要注意的是,使用 WEXITSTATUS(status) 的前提是要确保传入的 status 参数是一个子进程终止的状态,因为该宏只能提取终止进程的退出状态信息。

                                5.execvp(_argv[0], _argv);的调用

                                在代码中,execvp(_argv[0], _argv) 是一个执行函数 execvp 的调用,用于执行磁盘文件上的程序。这个函数会用指定的程序文件(由 _argv[0] 指定)来覆盖当前进程的镜像,并且用 _argv 数组中的参数替换掉原来的程序参数。

                                相对路径执行指令

                                1. 路径搜索:根据 PATH 环境变量,execvp 会在指定路径中查找可执行文件。
                                2. 内存映射:找到可执行文件后,将其映射到当前进程地址空间。
                                3. 替换镜像:用新程序的数据、堆栈、代码段替换当前进程的相应部分。
                                4. 执行:新程序从其入口点开始执行,覆盖原进程的代码。

                                下面是对 execvp 函数调用的解释:

                                • _argv[0] 表示要执行的程序文件的路径或名称。如果是一个程序的名称而没有路径,execvp 会在 $PATH 环境变量指定的路径中搜索这个程序。
                                • _argv 是一个以空指针结尾的字符串数组,用于传递给新程序的命令行参数。数组的第一个元素(_argv[0])通常是被执行的程序的名称,随后的元素是程序的参数。
                                • 当调用 execvp 时,操作系统会加载并执行指定的程序文件,并用 _argv 数组中的参数来替换当前进程的参数。(因为默认会在PATH中查询,就和系统连接上了)
                                • 如果 execvp 调用成功,则当前进程的镜像将被新程序替换,并且新程序开始执行。原来的程序代码和数据都会被新程序的代码和数据取代。
                                • 如果 execvp 调用失败,它会返回-1,并且当前进程的状态不会改变。

                                  在简单的C代码中,execvp 函数通常与 fork 函数一起使用,例如:

                                  #include 
                                  #include 
                                  #include 
                                  int main() {
                                      char *_argv[] = {"ls", "-l", "-a", NULL}; // 要执行的命令及参数组成的数组
                                      execvp(_argv[0], _argv);  // 在新的程序中执行 ls 命令
                                      // 如果执行成功,下面的代码不会被执行
                                      perror("execvp"); // 如果 execvp 失败,打印出错误信息
                                      return 1;
                                  }

                                  【Linux】1w详解如何实现一个简单的shell

                                  需要注意的是,execvp 在执行成功后,原进程的代码和数据将会被新进程替换。这就意味着,如果 execvp 后面还有代码,那么这些代码将不会被执行,因为当前的程序已经不再存在。

                                  实现shell, 一行一行的运行,先判断是否为内建命令

                                  6._argc&_argv

                                  • _argv:是一个字符指针数组,用于存储命令和参数。
                                  • _argc:是整型变量,用于存储命令和参数的数量。
                                  • splitstring 函数将命令行字符串分割成多个子字符串,存储在 _argv 中,并返回子字符串的数量 _argc。
                                  • NormalExcute 函数使用 _argv 数组创建子进程并执行命令。
                                  • buildCommand 函数使用 _argv 和 _argc 处理内建命令。
VPS购买请点击我

文章版权声明:除非注明,否则均为主机测评原创文章,转载或复制请以超链接形式并注明出处。

目录[+]