【Linux】解锁Shell脚本编写秘籍,编程高手之路等你开启

润信云 技术支持

1. 打印命令行提示符

image.png

#include<stdlib.h>

char* HostName() //获取主机名
{
    char* ret = getenv("HOSTNAME"); 
    if(ret) return ret;
    else return (char*)"None";
}
 
char* UserName() //获取用户名
{
    char* ret = getenv("USER");
    if(ret) return ret;
    else return (char*)"None";                                                         
}
    
char* CurrentWorkdir() //获取当前工作目录
{
    char* ret = getenv("PWD");
    if(ret) return ret;
    else return (char*)"None";
}

int main()
{
    //打印命令行提示符 
    //读取环境变量的内容 / 调用函数(gethostname获取主机名、getcwd获取当前工作目录)
    printf("[%s@%s %s]#", UserName(), HostName(), CurrentWorkdir());
}

2. 获取用户输入的命令行字符串
1.可以使用scanf读取字符串吗?
答:不能,scanf用于从标准输入(键盘)读取格式化输入,读取字符串时,scanf会在遇到空白字符(空格、换行符\n、制表符\t等)时停止读取,并将读取的字符串存储到字符数组中。

  1. 2.char* fgets(char* str, int size, FILE* stream);

  • 功能:从指定的文件流stream中读取一行(直到遇到换行符\n、文件结束符EOF、已读取了n-1个字符为止),并将读取的字符串(包括换行符,如果有的话)存储在str指向的数组中,并在数组的末尾添加’\0’来标记字符串的结束。


  • 优点:可以限制读取的字符数,防止缓冲区溢出,并且可以读取包含空格的字符串。


  • 缺点:如果读取的行比指定的长度n还要长,则多余的字符会被留在输入缓冲区中。


  • 文件流stream是程序用于输入和输出的基本抽象,stdin为标准输入流(键盘)、stdout为标准输出流(控制台或终端)、stderr为标准错误输出流。

  • image.png

  • #include<stdlib.h>
    
    #define SIZE 1024
    
    int Interative(char* in)
     {
        printf("[%s@%s %s]#", UserName(), HostName(), CurrentWorkdir());                                                      
        fgets(in, SIZE, stdin);  
        in[strlen(in) - 1] = '\0';  //移除换行字符\n
        
        return strlen(in) - 1;  //处理空串""情况
    }
    
    int main()
    {
        char CommandLine[SIZE]; //存储用户输入的命令行字符串
    
        while(1)
        {
            //打印命令行提示符+获取用户输入的命令行字符串                                      
            int n =  Interative(CommandLine);  
            if(n == 0) continue;  //空串,用户继续输入
        }
    
        return 0;
    }


  • 问:为什么用fegets读取一行字符串时,需要单独处理换行字符’\n’?

  • 因为我们在键盘中输入时,会多按一个回车字符’\n’,但对于命令来说,这个字符是无任何作用的,所以对于命令行参数表而言,不能存储带有换行的命令或选项。如:ls回车,实际上就是执行ls命令。

  • 3. 对命令行字符串进行解析(分割)

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

  • 首次调用:它接受两个参数(待分解的字符串、分割符字符串),函数会在str中查找delim中的任意一个字符,一旦找到,就将该字符替换为字符串结束符’\0’,并返回指向当前令牌(分隔符之前的字符串)的指针。


  • 后续调用:在首次调用后,每次调用strtok,应将第一个参数设置为NULL,以便函数从上次分解的位置查找并分解字符串。


  • image.png

  • #include<stdlib.h>
    #include<string.h>
    
    #define SIZE 1024
    
    char* argv[SIZE]; //相当于main函数的命令行参数表,末尾一定要以NULL结尾
    
    void Split(char* in)
    {
        int i = 0;
        argv[i++] = strtok(in, SP);  //首次调用
        while(argv[i++] = strtok(NULL, SP)); //后续调用
    
        if(strcmp("ls", argv[0]) == 0) 
        {
           argv[i-1] = (char*)"--color";   //加上后,ls显示内容,颜色为auto                   
           argv[i] = NULL; //argv以NULL结尾,便于子进程在进程程序替换时参数的传递
        }
    }
    
    int main()
    {
        char CommandLine[SIZE]; 
    
        while(1)
        {
            //1.打印命令行提示符+获取用户输入的命令行字符串                                      
            int n =  Interative(CommandLine);  
            if(n == 0) continue;  //空串,用户继续输入
    
            //2.分割命令行字符串
            Split(CommandLine);
        }
    
        return 0;
    }
  • 4. 处理内建命令

  • 4.1. 内建命令


1.概念:是shell内部直接提供并实现的命令,不要shell创建子进程来调用一个独立的外部程序(exe函数)来执行,而是由shell本身解释并执行。


2.可用性和依赖性:内建命令随shell的安装而自动提供,不需要额外的安装。


3.常见的内建命令:cd、echo、export、pwd、exit等。

4.2. 外部命令

1.概念:由用户自己编写的程序或者从外部引入的程序,它作为独立的程序存在于文件系统中,需要shell调用OS来执行一系列操作(创建子进程、子进程进行程序替换、子进程执行外部命令)。

2.可用性和依赖性:通常需要单独安装,并依赖于特定的OS和环境。

4.3. cd

image.png

问1:为什么会出现bash的当前工作目录,以及提示符中的工作目录,并未发生修改

  1. cd会被当作为外部命令,bash创建子进程,由子进程进行程序替换,执行cd命令,子进程执行完毕,就会退出,父进程无任何影响,所以只改变了子进程的当前工作目录。


  2. int chdir(const char* path);

  • 功能:改变当前工作目录,但不会更新环境变量PWD中的内容。


image.png

int snprintf(char* str,size_t size,const char* format,. . .);

    • 可以将多个变量值合并为单一的、格式化的字符串。


    image.png

    int BuiltinCmd()
    {
        int ret = 0; //用于判断是否为内建命令,不是,则为0、是,则为1
        if(strcmp("cd", argv[0]) == 0)
        {
            ret = 1; 
            char* target = argv[1]; //需要切换的路径
            if(!target) target = getenv("HOME"); //cd,表示切换到家目录中
            
            chdir(target); //改变当前工作目录,但不改变环境变量PWD的内容
            
            char tmp[SIZE]; 
            snprintf(tmp, SIZE, "PWD=%s", target);  //多变一
            putenv(tmp);   //修改或者新增环境变量                                              
     }
    
    return ret;
    }

    image.png

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

    image.png

    #include<stdio.h>
    #include<stdlib.h>
    #include<string.h>
    #include<unistd.h>
    
    #define SIZE 1024
    
    char* argv[SIZE]; //相当于main函数的命令行参数表,末尾一定要以NULL结尾
    
    int BuiltinCmd()
    {
        int ret = 0; //用于判断是否为内建命令,不是,则为0、是,则为1
        if(strcmp("cd", argv[0]) == 0)
        {
            ret = 1; 
            char* target = argv[1]; //需要切换的路径
            if(!target) target = getenv("HOME"); //cd,表示切换到家目录中
            chdir(target); //更改当前工作目录
            
            char tmp[SIZE];  //存储获取到的当前工作目录的绝对路径
            char pwd[SIZE];  //存储更改后的环境变量PWD的内容
            getcwd(tmp, SIZE);  //获取当前工作目录的绝对路径
                                
            snprintf(pwd, SIZE, "PWD=%s", tmp); //多变一,拼接
        
            putenv(pwd); //修改环境变量PWD的内容
        }
    
        return ret;
    }
    
    int main()
    {
        char CommandLine[SIZE]; 
    
        while(1)
        {
            //1.打印命令行提示符+获取用户输入的命令行字符串                                      
            int n =  Interative(CommandLine);  
            if(n == 0) continue;  //空串,用户继续输入
    
            //2.分割命令行字符串
            Split(CommandLine);
    
            //3.处理内建命令
            n = BuiltinCmd();
            if(n) continue;
        }
    
        return 0;
    }

    4.5. export

    int BuiltinCmd()
    {
        int ret = 0; 
        if(strcmp("export", argv[0]) == 0)
        {
            ret = 1;
            if(argv[1])
            {
                 putenv(argv[1]);                                    
            }  
        }
        
        return ret;
    }

    image.png

    问:为什么第一次export新增环境变量成功,但再输入了其他指令,这个新增的环境变量就不见了?

      • 因为argv为字符指针数组,执行完export后,再输入其他指令,则argv中原有的内容就会被其他指令所覆盖。

      char env[SIZE]; //相当于环境变量表,存储新增的环境变量
      
      int BuiltinCmd()
      {
          if(strcmp("export", argv[0]) == 0)
          int ret = 0; 
          {
              ret = 1;
              if(argv[1])
              {
                  strcpy(env, argv[1]); //将新增的环境变量的内容,拷贝到环境变量表中
                  putenv(env);                                 
              }  
          }
          
          return ret;
      }

      4.6. echo

      char env[SIZE]; //相当于环境变量表,存储新增的环境变量
      
      int BuiltinCmd()
      {
          if(strcmp("echo", argv[0]) == 0)
           {                                                       
              ret = 1;
              if(!argv[1])  //echo:打印一个空行
                  printf("\n");
              else if(argv[1][0] != '$') 
                  printf("%s\n", argv[1]); //echo aaaa:打印aaaa
              else 
              {
                  if(argv[1][1] == '?')
                  {
                      printf("%d\n", exit_code); //echo $?:打印退出码
                      exit_code = 0; //便于在执行echo $?时,退出码=0
                  }
                  else 
                      printf("%s\n", getenv(argv[1] + 1)); //echo $USER:打印环境变量的内容
              }
          }
      
          return ret;
      }

      5. 执行命令

      1.Shell脚本编写,bash执行命令时,通常会创建子进程,让子进程执行程序替换。

      1. 2.原因:a. bash是命令行解释器,需要一直执行命令,若让父进程进行替换,执行命令,则父进程就会退出;b. 进程之间具有独立性,子进程进行替换,对父进程无影响。

      5.1. 创建子进程进行程序替换

      #include<stdlib.h>
      #include<string.h>
      #include<sys/types.h>
      #include<sys/wait.h>
      
      #define SIZE 1024
      #define SP " "
      
      char* argv[SIZE];
      
      int exit_code;  //全局变量,存储子进程的退出码,便于echo $?直接输出退出码
      
      void Execute()
      {
          pid_t id = fork(); 
             
          if(id == 0)
          {
              execvp(argv[0], argv); //程序替换,执行命令
              exit(1);  //子进程退出
          }
        
          int status = 0;
          pid_t rid = waitpid(id, &status, 0); //父进程等待,进行回收子进程的资源
          if(rid > 0)
          {
              exit_code = WEXITSTATUS(status); //正常退出,获取子进程的退出码
          }                                                                              
      }
      
      
      int main()
      {
          char CommandLine[SIZE]; 
      
          while(1)
          {
              //1.打印命令行提示符+获取用户输入的命令行字符串                                      
              int n =  Interative(CommandLine);  
              if(n == 0) continue;  //空串,用户继续输入
      
              //2.分割命令行字符串
              Split(CommandLine);
      
              //4.执行命令(创建子进程,进行替换)
              Execute();
          }
      
          return 0;
      }

      6. 重定向

        为什么要宏定义多个整数,定义全局变量redir_type?

        重定向有三种类型:输出重定向>、追加重定向>>、输出重定向。


        我们需要在命令行参数中,查找是否有重定向,如果有重定向,就需要获取重定向的类型,从而在根据重定向的类型执行对应的指令。


        所以我们用宏定义多个整数,来表示重定向的类型; 定义一个全局变量redir_type,用来记录获取到的重定向的类型

        #define NoneRedir -1
        #define StdinRedir 0
        #define StdoutRedir 1
        #define AppenRedir 2
        
        char* filename = NULL;
        int redir_type = -1;
        1. 1.通常来说,重定向由三部分组成:要执行的命令 重定向符号 文件名(如:ls -l > log.txt)。

        • 2.我们需要获取两部分内容:将要执行的命令进行分割,获取重定向文件。

        • char* filename = NULL;

        3.执行流程:首先判断是否包含重定向(checkRedir);再把我们要执行的命令进行分分割(将重定向字符设置为’\0’,这样strtok只能切割要执行的命令);最后让子进程执行重定向操作。

        #define IsSpace(buf, pos) do{while(isspace(buf[pos])) pos      ++;}while(0)

        while(0)目的:可以在使用宏定义的函数处,结尾加上分号(;)可以消除宏和函数之间的这部分差异,让宏看起来更像个函数。

        //用宏定义多个整数,来表示重定向的类型
        #define NoneRedir -1
        #define StdinRedir 0
        #define StdoutRedir 1
        #define AppenRedir 2
        
        char* filename = NULL; //记录文件名
        int redir_type = -1;  //记录重定向类型
        
        //去除命令行参数中的空格(连续空格)
        #define IsSpace(buf, pos) do{while(isspace(buf[pos])) pos++;}while(0)
        
        //检查是否包含重定向符号
        void CheckRedir(char* in)
        {
            filename = NULL;
            redir_type = -1;
            
            int pos = strlen(in) - 1; //从命令行参数字符串后面往前找
            while(pos >= 0)
            {
                if(in[pos] == '>') 
                {
                    if(in[pos - 1] == '>') //追加重定向
                    {
                        redir_type = 2;                          
                        in[pos - 1] = '\0'; //便于将要执行的命令分割开
                        pos++; //当前pos位置不是空格,isspace函数为假,直接返回
                        IsSpace(in, pos);  //跳过空格
                        filename = in + pos; //获取文件名
                        break; //记录完毕,就立即退出
                     }
                     else   //输出重定向
                     {
                         redir_type = 1;
                         in[pos] = '\0';
                         pos++;
                         IsSpace(in, pos);             
                         filename = in + pos;
                         break;
                     }
                }
                else if(in[pos] == '<') //输入重定向
                {
                    redir_type = 0;
                    in[pos] = '\0';
                    pos++;
                    IsSpace(in, pos);
                    filename = in + pos;
                    break;
                }
                else 
                { 
                    pos--;
                }
            }
        }
        
        void Split(char* in)
        {
            CheckRedir(in); //在分割之前,先判断是否包含重定向符号
            
            int i = 0;
            argv[i++] = strtok(in, SP);  //首次调用
            while(argv[i++] = strtok(NULL, SP)); //后续调用
        
            if(strcmp("ls", argv[0]) == 0) 
            {
               argv[i-1] = (char*)"--color";   //加上后,ls显示内容,颜色为auto                   
               argv[i] = NULL; //argv以NULL结尾,便于子进程在进程程序替换时参数的传递
            }
        }
        
        void Execute()
        {
            pid_t id = fork(); 
               
            if(id == 0)  
            {
                //让子进程执行重定向
                int fd = -1;                                     
                if(redir_type == StdinRedir)
                {
                    fd = open(filename, O_RDONLY);
                    dup2(fd, 0);  //改变文件描述符的执行,从而实现输入、输出重定向
                }
                else if(redir_type == StdoutRedir)
                {
                    fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0666);
                    dup2(fd, 1);
                }
                else if(redir_type == AppenRedir)
                {
                    fd = open(filename, O_WRONLY|O_CREAT|O_APPEND      , 0666);
                    dup2(fd, 1);
                }
          
                execvp(argv[0], argv); //程序替换,执行命令
                exit(1);  //子进程退出
            }
          
            int status = 0;
            pid_t rid = waitpid(id, &status, 0); //父进程等待,进行回收子进程的资源
            if(rid > 0)
            {
                exit_code = WEXITSTATUS(status); //正常退出,获取子进程的退出码
            }                                                                              
        }

        7. 总代码

        #include<stdio.h>
        #include<stdlib.h>
        #include<string.h>
        #include<unistd.h>
        #include<sys/types.h>
        #include<sys/wait.h>
        #include<ctype.h>
        #include<sys/stat.h>
        #include<fcntl.h>
        
        #define SIZE 1024
        #define SP " "
        
        char* argv[SIZE]; 
        char env[SIZE]; //相当于环境变量表,存储新增的环境变量
        
        int exit_code;  //全局变量,存储子进程的退出码,便于echo $?直接输出退出码
        
        //以下都和重定向有关
        //用宏定义多个整数,来表示重定向的类型
        #define NoneRedir -1
        #define StdinRedir 0
        #define StdoutRedir 1
        #define AppenRedir 2
        
        char* filename = NULL; //记录文件名
        int redir_type = -1;  //记录重定向类型
        
        //去除命令行参数中的空格(连续空格)
        #define IsSpace(buf, pos) do{while(isspace(buf[pos])) pos++;}while(0)
        
        
        char* HostName() //获取主机名
        {
            char* ret = getenv("HOSTNAME"); 
            if(ret) return ret;
            else return (char*)"None";
        }
         
        char* UserName() //获取用户名
        {
            char* ret = getenv("USER");
            if(ret) return ret;
            else return (char*)"None";                                                         
        }
            
        char* CurrentWorkdir() //获取当前工作目录
        {
            char* ret = getenv("PWD");
            if(ret) return ret;
            else return (char*)"None";
        }
        
        int Interative(char* in)
         {
            printf("[%s@%s %s]#", UserName(), HostName(), CurrentWorkdir());                                                      
            fgets(in, SIZE, stdin);  
            in[strlen(in) - 1] = '\0';  //移除换行字符\n
            
            return strlen(in) - 1;  //处理空串""情况
        }
        
        //检查是否包含重定向符号
        void CheckRedir(char* in)
        {
            filename = NULL;
            redir_type = -1;
            
            int pos = strlen(in) - 1; //从命令行参数字符串后面往前找
            while(pos >= 0)
            {
                if(in[pos] == '>') 
                {
                    if(in[pos - 1] == '>') //追加重定向
                    {
                        redir_type = 2;                          
                        in[pos - 1] = '\0'; //便于将要执行的命令分割开
                        pos++; //当前pos位置不是空格,isspace函数为假,直接返回
                        IsSpace(in, pos);  //跳过空格
                        filename = in + pos; //获取文件名
                        break; //记录完毕,就立即退出
                     }
                     else   //输出重定向
                     {
                         redir_type = 1;
                         in[pos] = '\0';
                         pos++;
                         IsSpace(in, pos);             
                         filename = in + pos;
                         break;
                     }
                }
                else if(in[pos] == '<') //输入重定向
                {
                    redir_type = 0;
                    in[pos] = '\0';
                    pos++;
                    IsSpace(in, pos);
                    filename = in + pos;
                    break;
                }
                else 
                { 
                    pos--;
                }
            }
        }
        
        void Split(char* in)
        {
            CheckRedir(in); //在分割之前,先判断是否包含重定向符号
            
            int i = 0;
            argv[i++] = strtok(in, SP);  //首次调用
            while(argv[i++] = strtok(NULL, SP)); //后续调用
        
            if(strcmp("ls", argv[0]) == 0) 
            {
               argv[i-1] = (char*)"--color";   //加上后,ls显示内容,颜色为auto                   
               argv[i] = NULL; //argv以NULL结尾,便于子进程在进程程序替换时参数的传递
            }
        }
        
        int BuiltinCmd()
        {
            if(strcmp("echo", argv[0]) == 0)
             {                                                       
                ret = 1;
                if(!argv[1])  //echo:打印一个空行
                    printf("\n");
                else if(argv[1][0] != '$') 
                    printf("%s\n", argv[1]); //echo aaaa:打印aaaa
                else 
                {
                    if(argv[1][1] == '?')
                    {
                        printf("%d\n", exit_code); //echo $?:打印退出码
                        exit_code = 0; //便于在执行echo $?时,退出码=0
                    }
                    else 
                        printf("%s\n", getenv(argv[1] + 1)); //echo $USER:打印环境变量的内容
                }
            }
            else if(strcmp("cd", argv[0]) == 0)
            {
                ret = 1; 
                char* target = argv[1]; //需要切换的路径
                if(!target) target = getenv("HOME"); //cd,表示切换到家目录中
                
                chdir(target); //更改当前工作目录
                
                char tmp[SIZE];  //存储获取到的当前工作目录的绝对路径
                char pwd[SIZE];  //存储更改后的环境变量PWD的内容
                getcwd(tmp, SIZE);  //获取当前工作目录的绝对路径
                                    
                snprintf(pwd, SIZE, "PWD=%s", tmp); //多变一,拼接
            
                putenv(pwd); //修改环境变量PWD的内容
            }
            else if(strcmp("export", argv[0]) == 0)
            int ret = 0; 
            {
                ret = 1;
                if(argv[1])
                {
                    strcpy(env, argv[1]); //将新增的环境变量的内容,拷贝到环境变量表中
                    putenv(env);                                 
                }  
            }
        
            return ret;
        }
        
        void Execute()
        {
            pid_t id = fork(); 
               
            if(id == 0)  
            {
                //让子进程执行重定向
                int fd = -1;                                     
                if(redir_type == StdinRedir)
                {
                    fd = open(filename, O_RDONLY);
                    dup2(fd, 0);  //改变文件描述符的执行,从而实现输入、输出重定向
                }
                else if(redir_type == StdoutRedir)
                {
                    fd = open(filename, O_WRONLY|O_CREAT|O_TRUNC, 0666);
                    dup2(fd, 1);
                }
                else if(redir_type == AppenRedir)
                {
                    fd = open(filename, O_WRONLY|O_CREAT|O_APPEND      , 0666);
                    dup2(fd, 1);
                }
          
                execvp(argv[0], argv); //程序替换,执行命令
                exit(1);  //子进程退出
            }
          
            int status = 0;
            pid_t rid = waitpid(id, &status, 0); //父进程等待,进行回收子进程的资源
            if(rid > 0)
            {
                exit_code = WEXITSTATUS(status); //正常退出,获取子进程的退出码
            }
        }
        
        
        
        int main()
        {
            char CommandLine[SIZE]; 
        
            while(1)
            {
                //1.打印命令行提示符+获取用户输入的命令行字符串                                      
                int n =  Interative(CommandLine);  
                if(n == 0) continue;  //空串,用户继续输入
        
                //2.分割命令行字符串
                Split(CommandLine);
        
                //3.处理内建命令
                n = BuiltinCmd();
                if(n) continue;
        
                //4.执行命令(创建子进程,进行替换            
                Execute();
            }
          
            return 0;
        }


        本文链接:https://blog.runxinyun.com/post/100.html 转载需授权!

        分享到:
        版权声明
        网站名称: 润信云资讯网
        本站提供的一切软件、教程和内容信息仅限用于学习和研究目的。
        不得将上述内容用于商业或者非法用途,否则,一切后果请用户自负。
        我们非常重视版权问题,如有侵权请邮件与我们联系处理。敬请谅解!邮件:7104314@qq.com
        网站部分内容来源于网络,版权争议与本站无关。请在下载后的24小时内从您的设备中彻底删除上述内容。
        如无特别声明本文即为原创文章仅代表个人观点,版权归《润信云资讯网》所有,欢迎转载,转载请保留原文链接。
        0 79

        留言0

        评论

        ◎欢迎参与讨论,请在这里发表您的看法、交流您的观点。