【Linux】命令行解释器脚本编写

【Linux】命令行解释器脚本编写_第1张图片

樊梓慕:个人主页

 个人专栏:《C语言》《数据结构》《蓝桥杯试题》《LeetCode刷题笔记》《实训项目》《C++》《Linux》《算法》

每一个不曾起舞的日子,都是对生命的辜负


目录

前言

1.简单了解命令行解释器

2.为什么要手写一个命令行解释器?

3.命令行解释器脚本编写

3.1打印提示符

3.2获取用户输入

3.3解析用户输入(分割字符串)

3.4创建子进程进行程序替换

3.5内建命令的处理

3.5.1什么是内建命令?

3.5.2『 cd』

 3.5.3『 export』

3.5.4『 echo』

3.6重定向

4.完整代码


前言

综合前面所学,我们今天来写一个经典的shell脚本,『 命令行解释器』。


欢迎大家收藏以便未来做题时可以快速找到思路,巧妙的方法可以事半功倍。

=========================================================================

GITEE相关代码:樊飞 (fanfei_c) - Gitee.com

=========================================================================


1.简单了解命令行解释器

学习linux,我们最先接触到的就是命令行解释器,与windows这种注重用户体验以及简单易操作的操作系统不同,linux并没有设计出像windows一样的美观的图形化界面。

第一次打开linux系统,你看到的只有这一行孤零零的字符串,而这行字符串就是我们与linux系统进行交互的重要工具,它被称为命令行解释器。

我们可以通过不同的指令与linux系统进行交互,比如这样:

【Linux】命令行解释器脚本编写_第2张图片

它是与计算机进行交互的一种文本界面,相比于图形用户界面,命令行界面更加灵活和高效。 

这样的软件程序我们常常称其为Shell


2.为什么要手写一个命令行解释器?

鉴于之前对于『 进程周边』的学习,包括进程创建,进程终止,进程等待,进程程序替换等,并且有关『 重定向』我们也有了一定的了解。

为了更好的『 理解与掌握』,我们需要搭建一个适合的『 应用场景』用来实践,通过自己手写一个简单的命令行解释器,我们可以『 更好地理解』这些概念。


3.命令行解释器脚本编写

3.1打印提示符

const char* HostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "None";
}

const char* UserName()
{
    char *hostname = getenv("USER");
    if(hostname) return hostname;
    else return "None";
}

const char *CurrentWorkDir()
{
    char *hostname = getenv("PWD");
    if(hostname) return hostname;
    else return "None";
}

int main()
{
    // 输出提示符并获取用户输入的命令字符串"ls -a -l"
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
}

3.2获取用户输入

因为用户输入中可能会有空格存在,所以获取用户输入我们采用fgets函数。

  • 第一个参数是用于读取文本的字符数组的指针。
  • 第二个参数是最大读取的字符数(包括换行符和空字符)。
  • 第三个参数是要读取的文件流,这里我们传入标准输入stdin即可。
#define SIZE 1024

int main()
{
    char commandline[SIZE];//声明用户输入的字符串
    fgets(commandline, SIZE, stdin);//获取输入
    commandline[strlen(commandline)-1] = 0; //清除最后的\n
    return 0;
}

如果用户直接回车,传入一个\n怎么办?

所以我们可以这样设计:

#define SIZE 1024

int Interactive(char out[], int size)
{
    // 输出提示符并获取用户输入的命令字符串"ls -a -l"
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
    fgets(out, size, stdin);
    out[strlen(out)-1] = 0; 
    return strlen(out);
}

int main()
{
    while(1)//命令行解释器的本质就是一直运行死循环获取指令
    {
        char commandline[SIZE];
        // 1. 打印命令行提示符,获取用户输入的命令字符串
        int n = Interactive(commandline, SIZE);
        if(n == 0) continue;
    }
}

我们将打印提示符与获取输入封装为一个函数,然后检测如果为空串则跳过本次循环。


3.3解析用户输入(分割字符串)

在获取用户输入后,我们要获取指令,然后根据参数执行具体操作。

首先我们对字符串进行分割,需要用到strtok函数:

  • 第一个参数是要进行分割的字符串,
  • 第二个参数为用于指定分割子字符串的分隔符字符串。
  • 返回值为子字符串的指针

strtok的函数原型为char *strtok(char *s, char *delim),功能为“Parse S into tokens separated by characters in DELIM.If S is NULL, the saved pointer in SAVE_PTR is used as the next starting point. ” 翻译成汉语就是:作用于字符串s,以包含在delim中的字符为分界符,将s切分成一个个子串;如果,s为空值NULL,则函数保存的指针SAVE_PTR在下一次调用中将作为起始位置。

根据返回值,所以我们可以这样设计:

#define SEP " "
char *argv[MAX_ARGC];

void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in, SEP); // "ls -a -l"
    while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
    if(strcmp(argv[0], "ls") ==0)//如果是ls指令
    {
        argv[i-1] = (char*)"--color";//加上后,颜色为auto
        argv[i] = NULL;
    }
}

3.4创建子进程进行程序替换

Linux操作系统中,命令行解释器为bash,bash执行命令往往创建一个子进程再进程程序替换为指定的进程去执行,这样做的好处就是确保bash的稳定运行,由于进程的独立性,当出现错误时,只有子进程会出问题,而bash进程不会受到任何影响。

主进程创建子进程,并使用execvp函数进行进程程序替换,最后父进程回收子进程的资源。

为了获取子进程的退出信息,定义一个全局变量lastCode。

int lashcode = 0;
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 == id) lastcode = WEXITSTATUS(status); 
}

有关进程程序替换函数『 execvp』详解请见:【Linux】进程周边007之进程控制-CSDN博客


3.5内建命令的处理

3.5.1什么是内建命令?

所谓内建命令,就是由 Bash 自身提供的命令,而不是文件系统中的某个可执行文件。

可以使用 type 来确定一个命令是否是内建命令。

通常来说,内建命令会比外部命令执行得更快,执行『 外部命令』时不但会触发磁盘 I/O,还需要 『 fork 』出一个单独的进程来执行,执行完成后再退出

而执行内建命令相当于调用当前 Shell 进程的一个函数。

常见的内建命令有cd、export和echo。

3.5.2『 cd』

我们的shell还没有对内建命令进行单独处理,所以此时cd也被默认为外部命令执行。

让我们来看看现象:

【Linux】命令行解释器脚本编写_第3张图片

这是为什么呢?

因为cd此时默认为外部命令,所以执行cd会创建子进程,子进程的当前工作路径被修改了,然后子进程子会被父进程回收,但是父进程myshell的工作路径并没有修改,这也就是为什么cd前后当前工作目录没有被修改的原因。

所以如果我们要改变父进程的工作路径,不能创建子进程!在父进程中对工作路径进行修改,用的是chdir函数,但是chdir并不会修改环境变量,如果不修改可能会造成如下图问题:

实际改了,但是提示符处并没有修改。

因为我们打印提示符那块打印当前工作目录用的是环境变量的获取,所以这里我们最好也将环境变量PWD一并修改。

解决方案如下:

void BuildinCmd()
{
    if(strcmp("cd", argv[0]) == 0)
    {
        char *target = argv[1]; //cd XXX or cd
        if(!target) target = Home();
        chdir(target);//修改当前工作目录
        char temp[1024];
        getcwd(temp, 1024);//获取当前工作目录
        snprintf(pwd, SIZE, "PWD=%s", temp);
        putenv(pwd);//修改环境变量
    }
}


 3.5.3『 export』

void BuildinCmd()
{
    if(strcmp("export", argv[0]) == 0)
    {
        if(argv[1])
        {
            strcpy(env, argv[1]);
            putenv(env);
        }
    }
}

3.5.4『 echo』

void BuildinCmd()
{
    if(strcmp("echo", argv[0]) == 0)
    {
        if(argv[1] == NULL) {
            printf("\n");
        }
        else{
            if(argv[1][0] == '$')
            {
                if(argv[1][1] == '?')//打印进程退出码
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
                else{
                    char *e = getenv(argv[1]+1);//打印环境变量
                    if(e) printf("%s\n", e);
                }
            }
            else{
                printf("%s\n", argv[1]);
            }
        }
    }
}

当检测到是内建命令时,我们执行以上逻辑,不走执行外部命令的逻辑,所以我们将内建命令的处理进行封装,然后定义一个ret返回值,在执行外部命令之前先进行检测是否为内建命令,如果是返回1否则返回0,根据返回值判断是否跳过执行外部命令的函数:

#include 
#include 
#include 
#include 
#include 
#include 


#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
    
char *argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE]; 
int lastcode = 0;

const char* HostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "None";
}

const char* UserName()
{
    char *hostname = getenv("USER");
    if(hostname) return hostname;
    else return "None";
}

const char *CurrentWorkDir()
{
    char *hostname = getenv("PWD");
    if(hostname) return hostname;
    else return "None";
}

char *Home()
{
    return getenv("HOME");
}

int Interactive(char out[], int size)
{
    // 输出提示符并获取用户输入的命令字符串"ls -a -l"
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
    fgets(out, size, stdin);
    out[strlen(out)-1] = 0;
    return strlen(out);
}

void Split(char in[])
{
    int i = 0;
    argv[i++] = strtok(in, SEP); // "ls -a -l"
    while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
    if(strcmp(argv[0], "ls") ==0)//如果是ls命令
    {
        argv[i-1] = (char*)"--color";//加上后,颜色为auto
        argv[i] = NULL;
    }
}

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 == id) lastcode = WEXITSTATUS(status); 
    //printf("run done, rid: %d\n", rid);
}

int BuildinCmd()
{
    int ret = 0;
    // 1. 检测是否是内建命令, 是 1, 否 0
    if(strcmp("cd", argv[0]) == 0)
    {
        // 2. 执行
        ret = 1;
        char *target = argv[1]; //cd XXX or cd
        if(!target) target = Home();
        chdir(target);//修改当前工作目录
        char temp[1024];
        getcwd(temp, 1024);//获取当前工作目录
        snprintf(pwd, SIZE, "PWD=%s", temp);
        putenv(pwd);//修改环境变量
    }
    else if(strcmp("export", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1])
        {
            strcpy(env, argv[1]);
            putenv(env);
        }
    }
    else if(strcmp("echo", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1] == NULL) {
            printf("\n");
        }
        else{
            if(argv[1][0] == '$')
            {
                if(argv[1][1] == '?')//打印进程退出码
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
                else{
                    char *e = getenv(argv[1]+1);//打印环境变量
                    if(e) printf("%s\n", e);
                }
            }
            else{
                printf("%s\n", argv[1]);
            }
        }
    }
    return ret;
}
int main()
{
    while(1)
    {
        char commandline[SIZE];
        // 1. 打印命令行提示符,获取用户输入的命令字符串
        int n = Interactive(commandline, SIZE);
        if(n == 0) continue;
        // 2. 对命令行字符串进行切割
        Split(commandline);
        // 3. 处理内建命令
        n = BuildinCmd();
        if(n) continue;
        // 4. 执行这个命令
        Execute();
    }
    return 0;
}

3.6重定向

重定向符号有三种情况:

  • 输出重定向 >
  • 追加重定向 >>
  • 输入重定向 <

一般重定向指令由三部分内容构成:

ls -a > log.txt

左面为指令及参数,中间为重定向符号,最后为文件名。

(1)我们需要获取的是重定向的类型,这里我们可以宏定义为多个整型值,比如:

#define NoneRedir  -1 //无重定向
#define StdinRedir  0 //输入重定向
#define StdoutRedir 1 //输出重定向
#define AppendRedir 2 //追加重定向

然后我们在定义一个全局变量,通过改变改变量的值,获取当前指令是那种类型的重定向:

int redir_type = NoneRedir; //初始化为无重定向

 (2)获取文件名,定义一个全局变量:

char *filename = NULL;

(3)记得重定向符号与文件名之间可能会存在空格,我们可以设计一个宏用来跳过空格:

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

 while(0)的目的是可以在语句中结尾加;,可以消除宏与函数之间的这部分差异,让该宏看起来像个函数。

以上操作我们放在分割字符串之前进行, 然后在执行外部命令的函数中,程序替换之前,判断是否需要重定向,然后利用dup2函数替换对应的输入或输出。

思路如上,开始模拟实现:

#define NoneRedir  -1
#define StdinRedir  0
#define StdoutRedir 1
#define AppendRedir 2

#define STREND '\0'
#define SEP " "

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

int redir_type = NoneRedir;
char *filename = NULL;

void CheckRedir(char in[])
{
    // ls -a -l
    // ls -a -l > log.txt
    // ls -a -l >> log.txt
    // cat < log.txt
    redir_type = NoneRedir; //初始化为无重定向
    filename = NULL;
    int pos = strlen(in) - 1;
    while( pos >= 0 )
    {
        if(in[pos] == '>')
        {
            if(in[pos-1] == '>') //如果是追加重定向
            {
                redir_type = AppendRedir; //设置为追加重定向
                in[pos-1] = STREND; //该位置设置为\0,方便后续切割字符串
                pos++;
                IgnSpace(in, pos); //跳过空格
                filename = in+pos; //获取文件名
                break;
            }
            else //否则为输出重定向
            {
                redir_type = StdoutRedir; //设置为输出重定向
                in[pos++] = STREND; //该位置设置为\0,方便后续切割字符串
                IgnSpace(in, pos); //跳过空格
                filename = in+pos; //获取文件名
                break;
            }
        }
        else if(in[pos] == '<') //如果是输入重定向
        {
            redir_type = StdinRedir; //设置为输入重定向
            in[pos++] = STREND; //该位置设置为\0,方便后续切割字符串
            IgnSpace(in, pos); //跳过空格
            filename = in+pos; //获取文件名
            break;
        }
        else
        {
            pos--;
        }
    }
}

void Split(char in[])
{
    CheckRedir(in);//分割字符串之前进行判断
    int i = 0;
    argv[i++] = strtok(in, SEP); // "ls -a -l"
    while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
    if(strcmp(argv[0], "ls") ==0)
    {
        argv[i-1] = (char*)"--color";
        argv[i] = 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_CREAT | O_WRONLY | O_TRUNC);
            dup2(fd, 1);
        }
        else if(redir_type == AppendRedir)
        {
            fd = open(filename, O_CREAT | O_WRONLY | O_APPEND);
            dup2(fd, 1);
        }
        else
        {
            // do nothing
        }

        // 让子进程执行命名
        execvp(argv[0], argv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id) lastcode = WEXITSTATUS(status); 
}

4.完整代码

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

#define SIZE 1024
#define MAX_ARGC 64
#define SEP " "
#define STREND '\0'
    
char *argv[MAX_ARGC];
char pwd[SIZE];
char env[SIZE]; 
int lastcode = 0;

#define NoneRedir  -1
#define StdinRedir  0
#define StdoutRedir 1
#define AppendRedir 2

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

int redir_type = NoneRedir;
char *filename = NULL;

const char* HostName()
{
    char *hostname = getenv("HOSTNAME");
    if(hostname) return hostname;
    else return "None";
}

const char* UserName()
{
    char *hostname = getenv("USER");
    if(hostname) return hostname;
    else return "None";
}

const char *CurrentWorkDir()
{
    char *hostname = getenv("PWD");
    if(hostname) return hostname;
    else return "None";
}

char *Home()
{
    return getenv("HOME");
}

int Interactive(char out[], int size)
{
    // 输出提示符并获取用户输入的命令字符串"ls -a -l"
    printf("[%s@%s %s]$ ", UserName(), HostName(), CurrentWorkDir());
    fgets(out, size, stdin);
    out[strlen(out)-1] = 0;
    return strlen(out);
}

void CheckRedir(char in[])
{
    // ls -a -l
    // ls -a -l > log.txt
    // ls -a -l >> log.txt
    // cat < log.txt
    redir_type = NoneRedir; //初始化为无重定向
    filename = NULL;
    int pos = strlen(in) - 1;
    while( pos >= 0 )
    {
        if(in[pos] == '>')
        {
            if(in[pos-1] == '>') //如果是追加重定向
            {
                redir_type = AppendRedir; //设置为追加重定向
                in[pos-1] = STREND; //该位置设置为\0,方便后续切割字符串
                pos++;
                IgnSpace(in, pos); //跳过空格
                filename = in+pos; //获取文件名
                break;
            }
            else //否则为输出重定向
            {
                redir_type = StdoutRedir; //设置为输出重定向
                in[pos++] = STREND; //该位置设置为\0,方便后续切割字符串
                IgnSpace(in, pos); //跳过空格
                filename = in+pos; //获取文件名
                break;
            }
        }
        else if(in[pos] == '<') //如果是输入重定向
        {
            redir_type = StdinRedir; //设置为输入重定向
            in[pos++] = STREND; //该位置设置为\0,方便后续切割字符串
            IgnSpace(in, pos); //跳过空格
            filename = in+pos; //获取文件名
            break;
        }
        else
        {
            pos--;
        }
    }
}

void Split(char in[])
{
    CheckRedir(in);//分割字符串之前进行判断
    int i = 0;
    argv[i++] = strtok(in, SEP); // "ls -a -l"
    while(argv[i++] = strtok(NULL, SEP)); // 故意将== 写成 =
    if(strcmp(argv[0], "ls") ==0)
    {
        argv[i-1] = (char*)"--color";
        argv[i] = NULL;
    }
}

int BuildinCmd()
{
    int ret = 0;
    // 1. 检测是否是内建命令, 是 1, 否 0
    if(strcmp("cd", argv[0]) == 0)
    {
        // 2. 执行
        ret = 1;
        char *target = argv[1]; //cd XXX or cd
        if(!target) target = Home();
        chdir(target);//修改当前工作目录
        char temp[1024];
        getcwd(temp, 1024);//获取当前工作目录
        snprintf(pwd, SIZE, "PWD=%s", temp);
        putenv(pwd);//修改环境变量
    }
    else if(strcmp("export", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1])
        {
            strcpy(env, argv[1]);
            putenv(env);
        }
    }
    else if(strcmp("echo", argv[0]) == 0)
    {
        ret = 1;
        if(argv[1] == NULL) {
            printf("\n");
        }
        else{
            if(argv[1][0] == '$')
            {
                if(argv[1][1] == '?')//打印进程退出码
                {
                    printf("%d\n", lastcode);
                    lastcode = 0;
                }
                else{
                    char *e = getenv(argv[1]+1);//打印环境变量
                    if(e) printf("%s\n", e);
                }
            }
            else{
                printf("%s\n", argv[1]);
            }
        }
    }
    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_CREAT | O_WRONLY | O_TRUNC);
            dup2(fd, 1);
        }
        else if(redir_type == AppendRedir)
        {
            fd = open(filename, O_CREAT | O_WRONLY | O_APPEND);
            dup2(fd, 1);
        }
        else
        {
            // do nothing
        }

        // 让子进程执行命名
        execvp(argv[0], argv);
        exit(1);
    }
    int status = 0;
    pid_t rid = waitpid(id, &status, 0);
    if(rid == id) lastcode = WEXITSTATUS(status); 
}

int main()
{
    while(1)
    {
        char commandline[SIZE];
        // 1. 打印命令行提示符,获取用户输入的命令字符串
        int n = Interactive(commandline, SIZE);
        if(n == 0) continue;
        // 2. 对命令行字符串进行切割
        Split(commandline);
        // 3. 处理内建命令
        n = BuildinCmd();
        if(n) continue;
        // 4. 执行这个命令
        Execute();
    }
    return 0;
}

=========================================================================

如果你对该系列文章有兴趣的话,欢迎持续关注博主动态,博主会持续输出优质内容

博主很需要大家的支持,你的支持是我创作的不竭动力

~ 点赞收藏+关注 ~

=========================================================================

你可能感兴趣的:(Linux,linux,运维,服务器)