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

目录

  • [1. 打印命令行提示符](#1. 打印命令行提示符)
  • [2. 获取用户输入的命令行字符串](#2. 获取用户输入的命令行字符串)
  • [3. 对命令行字符串进行解析(分割)](#3. 对命令行字符串进行解析(分割))
  • [4. 处理内建命令](#4. 处理内建命令)
    • [4.1. 内建命令</h3>](#4.1. 内建命令)
    • [4.2. 外部命令](#4.2. 外部命令)
    • [4.3. cd](#4.3. cd)
    • [4.5. export](#4.5. export)
    • [4.6. echo](#4.6. echo)
  • [5. 执行命令](#5. 执行命令)
    • [5.1. 创建子进程进行程序替换](#5.1. 创建子进程进行程序替换)
  • [6. 重定向</h2>](#6. 重定向)
  • [7. 总代码](#7. 总代码)

1. 打印命令行提示符

c 复制代码
#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. char* fgets(char* str, int size, FILE* stream);
  • 功能:从指定的文件流stream中读取一行(直到遇到换行符\n、文件结束符EOF、已读取了n-1个字符为止),并将读取的字符串(包括换行符,如果有的话)存储在str指向的数组中,并在数组的末尾添加'\0'来标记字符串的结束。

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

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

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

c 复制代码
#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,以便函数从上次分解的位置查找并分解字符串。

c 复制代码
#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

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

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

int chdir(const char* path);

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

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

  • 可以将多个变量值合并为单一的、格式化的字符串。
c 复制代码
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;
}

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

c 复制代码
#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

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

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

  • 因为argv为字符指针数组,执行完export后,再输入其他指令,则argv中原有的内容就会被其他指令所覆盖。
c 复制代码
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

c 复制代码
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. 原因:a. bash是命令行解释器,需要一直执行命令,若让父进程进行替换,执行命令,则父进程就会退出;b. 进程之间具有独立性,子进程进行替换,对父进程无影响。

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

c 复制代码
#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. 重定向

  1. 为什么要宏定义多个整数,定义全局变量redir_type?
  • 重定向有三种类型:输出重定向>、追加重定向>>、输出重定向。

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

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

c 复制代码
#define NoneRedir -1
#define StdinRedir 0
#define StdoutRedir 1
#define AppenRedir 2

char* filename = NULL;
int redir_type = -1;
  1. 通常来说,重定向由三部分组成:要执行的命令 重定向符号 文件名(如:ls -l > log.txt)。
  • 我们需要获取两部分内容:将要执行的命令进行分割,获取重定向文件。
c 复制代码
char* filename = NULL;
  1. 执行流程:首先判断是否包含重定向(checkRedir);再把我们要执行的命令进行分分割(将重定向字符设置为'\0',这样strtok只能切割要执行的命令);最后让子进程执行重定向操作。
c 复制代码
#define IsSpace(buf, pos) do{while(isspace(buf[pos])) pos      ++;}while(0)

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

c 复制代码
//用宏定义多个整数,来表示重定向的类型
#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. 总代码

c 复制代码
#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;
}
相关推荐
搬码临时工1 分钟前
小企业如何搭建本地私有云服务器,并设置内部网络地址提供互联网访问
运维·服务器
oioihoii9 分钟前
C++11 forward_list 从基础到精通:原理、实践与性能优化
c++·性能优化·list
m0_6873998417 分钟前
写一个Ununtu C++ 程序,调用ffmpeg API, 来判断一个数字电影的视频文件mxf 是不是Jpeg2000?
开发语言·c++·ffmpeg
Natsume171021 分钟前
嵌入式开发:GPIO、UART、SPI、I2C 驱动开发详解与实战案例
c语言·驱动开发·stm32·嵌入式硬件·mcu·架构·github
old-six-programmer21 分钟前
NAT 类型及 P2P 穿透
服务器·网络协议·webrtc·p2p·nat
tan77º37 分钟前
【Linux网络编程】网络基础
linux·服务器·网络
风口上的吱吱鼠38 分钟前
Armbian 25.5.1 Noble Gnome 开启远程桌面功能
服务器·ubuntu·armbian
shaun20011 小时前
华为c编程规范
c语言
18你磊哥1 小时前
Windows 本地安装部署 Apache Druid
运维·debian
MeshddY1 小时前
(超详细)数据库项目初体验:使用C语言连接数据库完成短地址服务(本地运行版)
c语言·数据库·单片机