详解简单的shell脚本 --- 命令行解释器【Linux后端开发】


首先附上完整代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
//命令行解释器
//shell 运行原理:通过让子进程执行命令,父进程等待&&解析命令

//保存完整的命令行字符串 -- 充当缓冲区
#define NUM 1024
char cmd_line[NUM];

//保存切割之后的字符串
#define SIZE 32
char* g_argv[SIZE];

#define SEP " "

int main()
{
    //0.命令行解释器,一定是一个常驻内存的进程 --- 不退出(死循环)
    while(1)
    {
        //1打印出提示信息 -- [wxq@VM-4-9-centos shell]$ 
        printf("[dwr@VM-1-1-test shell]$ ");
        fflush(stdout);
       
        //2.获取用户的键盘输入[输入的是各种指令和选项"ls -a -l"]
        memset(cmd_line, 0, sizeof cmd_line);
        if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
        {
            continue;
        }
        //输入的回车键设为\0  ls -a -l \n \0
        cmd_line[strlen(cmd_line)-1]  = '\0';

       // printf("echo:%s\n", cmd_line);   //debug
        
        //3.命令行字符串进行解析 "ls -a -l -s"  --->  "la" "-a" "-l" "-s"
        g_argv[0] = strtok(cmd_line, SEP); //第一次调用,传入原始的字符串

        int index = 1;
        while(g_argv[index++] = strtok(NULL, SEP)); //第二次调用,如果还要分割原始字符串,传入NULL
        
        //简单配置ls的颜色
        int i = 1;
        if(strcmp(g_argv[0], "ls") == 0)
        {
            g_argv[i++] = "--color=auto";
        }
       
        //识别别名 - 主要是测试,一般是有接口的
        if(strcmp(g_argv[0], "ll") == 0)
        {
            g_argv[0] = "ls";
            g_argv[i++] = "-l";
            g_argv[i++] = "--color=auto";
        }


        //debug
//        for(index = 0 ; g_argv[index]; index++)
//        {
//            printf("g_argv[%d]: %s\n", index, g_argv[index]);
//        }

        
        //4.TODO 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令
        //内建命令本质就是shell中的一个函数调用
        if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute;
        {
            if(g_argv[1] != NULL)
            {
                chdir(g_argv[1]); //cd  cd ..
            }

            continue;
        
         }

        //5.创建子进程进行程序替换
        pid_t id = fork();

        //child
        if(id == 0)
        {
           execvp(g_argv[0],g_argv);
           exit(-1);
        }

        //father
        int status = 0;
        pid_t ret = waitpid(id, &status, 0); //阻塞等待
        if(ret > 0)
        {
            printf("exit code:%d\n",WEXITSTATUS(status));
        }

    }

    return 0;
}

效果:

命令行解释器  

       
shell 运行原理:通过让子进程执行命令,父进程等待&&解析命令

步骤1. 命令行解释器,一定是一个常驻内存的进程 --- 这意味着它是不退出的(死循环)

代码示例:

#include <stdio.h>
int main()
{
    while(1)
    {
        ;
    }

    return 0;
}

步骤2.打印出提示信息

代码示例:

#include <stdio.h>
int main()
{
    while(1)
    {
       printf("[dwr@VM-1-1-test shell]$\n"); 
    }

    return 0;
}

这样打印出来我们会发现:

所以我们是不能在后面加上 '\n' 的,但是因为有缓冲区的存在,那么应该怎么办呢?

这里需要用到一个函数 --- fflush():刷新缓冲区

代码示例:

#include <stdio.h>
int main()
{
    while(1)
    {
       printf("[dwr@VM-1-1-test shell]$");
       fflush(stdout); 
    }

    return 0;
}

步骤3.获取用户的键盘输入[示例 :  输入的是各种指令和选项"ls -a -l" ]

思路:

①需要一个数组来模拟缓冲区 - - - 提取用户输入的字符

②使用fgets读取用户在键盘上的输入,如果读取失败,continue重新进入循环,重新读取,重新打印。

③使用printf测试一下

代码示例:

#include <stdio.h>
#include <string.h>
#define NUM 1024
char cmd_line[NUM];

int main()
{
    while(1)
    {
       printf("[dwr@VM-1-1-test shell]$");
       fflush(stdout); 
       memset(cmd_line, 0, sizeof cmd_line);
       if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
       {
          continue;
       }
        
       printf("echo:%s\n", cmd_line);

    }

    return 0;
}

但是我们会发现打印出来的结果是:

       这是一个需要注意的小细节的地方,因为在输入的时候,当我们最终输入字符结束的时候,会输入一个“回车”键盘,它会被缓冲区拿到并被识别为“\n”。

示例:输入 ls -l -a    缓冲区读取 ls -l -a \n  

所以这里我们需要把  \n   设置为 \0 作为字符串的结束标志

代码示例:

#include <stdio.h>
#include <string.h>
#define NUM 1024
char cmd_line[NUM];

int main()
{
    while(1)
    {
       printf("[dwr@VM-1-1-test shell]$");
       fflush(stdout); 
       memset(cmd_line, 0, sizeof cmd_line);
       if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
       {
          continue;
       }
       cmd_line[strlen(cmd_line)-1]  = '\0';    
       //printf("echo:%s\n", cmd_line);    //测试辅助

    }

    return 0;
}

输出结果:

步骤4.命令行字符串解析 [ 示例: " ls -a -l  -s" --->  "ls" "-a" "-l" "-s"]

思路:

①可以把空格定为分割符,然后切割为一个一个的子串

②定义一个指针数组保存切割下来的子串

代码示例:

#include <stdio.h>
#include <string.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
char cmd_line[NUM];
char* g_argv[SIZE];

int main()
{
    while(1)
    {
       printf("[dwr@VM-1-1-test shell]$");
       fflush(stdout); 
       memset(cmd_line, 0, sizeof cmd_line);
       if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
       {
          continue;
       }
       cmd_line[strlen(cmd_line)-1]  = '\0';    
       //printf("echo:%s\n", cmd_line);    //测试辅助
       
       g_argv[0] = strtok(cmd_line, SEP);
       int index = 0;
       while(g_argv[index++] = strtok(NULL, SEP));
        
        //测试辅助
       //for(index = 0; g_argv[index]; index++)
            //printf("g_argv[%d]:%s\n", index, g_argv[index]);


    }

    return 0;
}

步骤5.创建子进程进行程序替换

注:进程等待和进程替换后续会更新详细解说

代码示例:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/types.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
char cmd_line[NUM];
char* g_argv[SIZE];

int main()
{
    while(1)
    {
       printf("[dwr@VM-1-1-test shell]$");
       fflush(stdout); 
       memset(cmd_line, 0, sizeof cmd_line);
       if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
       {
          continue;
       }
       cmd_line[strlen(cmd_line)-1]  = '\0';    
       //printf("echo:%s\n", cmd_line);    //测试辅助
       
       g_argv[0] = strtok(cmd_line, SEP);
       int index = 0;
       while(g_argv[index++] = strtok(NULL, SEP));
        
        //测试辅助
       //for(index = 0; g_argv[index]; index++)
            //printf("g_argv[%d]:%s\n", index, g_argv[index]);

                                                                                                                       
        pid_t id = fork();
        //child process
        if(id == 0)
        {
           execvp(g_argv[0],g_argv);
           exit(-1);
        }                                                                                               
                                                                                                          
        //father process                                                                                        
        int status = 0;                                                                                 
        pid_t ret = waitpid(id, &status, 0); //阻塞等待                                                 
        if(ret > 0)                                                                                     
        {                                                                                               
           printf("exit code:%d\n",WEXITSTATUS(status));                                               
        }   
        
    }

    return 0;
}

其实写到这里一些简单的命令就已经可以跑了

示例:

退出自己写的shell脚本是 ctrl + c

步骤6.内置命令

但是上述代码有一些小问题,就是我们自己写的shell脚本它并没有让我们的路径发生变化

示例:

        原因是因为,当前我们自己写的shell,无论我们写的任何指令,都是交给了子进程 , 子进程进行进程替换帮助我们来完成的指令,那么指令就只会影响子进程,而不会影响父进程。所以当我们 cd 回到上级目录的时候,父进程根本没有变化,但是可能子进程所在的路径一直在回到上一层路径。

        那么我们想要的是shell脚本所在的路径发生变化,所以我们想要进行判断命令,如果是所谓cd这样的命令,那么我们不能创建子进程,而是直接交给父进程。

  • 内置命令:让父进程(shell)自己执行的命令。我们叫做内置命令,内建命令
  • 内建命令本质就是shell中的一个函数调用
     

代码示例:

#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/types.h>
#define NUM 1024
#define SIZE 32
#define SEP " "
char cmd_line[NUM];
char* g_argv[SIZE];

int main()
{
    while(1)
    {
       printf("[dwr@VM-1-1-test shell]$");
       fflush(stdout); 
       memset(cmd_line, 0, sizeof cmd_line);
       if(fgets(cmd_line, sizeof cmd_line, stdin) == NULL)
       {
          continue;
       }
       cmd_line[strlen(cmd_line)-1]  = '\0';    
       //printf("echo:%s\n", cmd_line);    //测试辅助
       
       g_argv[0] = strtok(cmd_line, SEP);
       int index = 0;
       while(g_argv[index++] = strtok(NULL, SEP));
        
        //测试辅助
       //for(index = 0; g_argv[index]; index++)
            //printf("g_argv[%d]:%s\n", index, g_argv[index]);

       if(strcmp(g_argv[0], "cd") == 0) //not child execute; father execute;
       {
            if(g_argv[1] != NULL)
            {
              chdir(g_argv[1]); //cd  cd ..
            }
 
           continue;
        
       }
                                                                                                                       
        pid_t id = fork();
        //child process
        if(id == 0)
        {
           execvp(g_argv[0],g_argv);
           exit(-1);
        }                                                                                               
                                                                                                          
        //father process                                                                                        
        int status = 0;                                                                                 
        pid_t ret = waitpid(id, &status, 0); //阻塞等待                                                 
        if(ret > 0)                                                                                     
        {                                                                                               
           printf("exit code:%d\n",WEXITSTATUS(status));                                               
        }   
        
    }

    return 0;
}

 

补充说明:

fflush()

#include<stdio.h>
int main()
{
   int fflush( FILE *stream );
   return 0;
 }

定义:冲洗一个流

头文件:<stdio.h>

注释:如果缓冲区已成功刷新,则Fflush返回0。

 关键字:continue:作用是跳过本次循环continue后面的代码,直接去判断部分,看是否进行下一次循环

memset()

#include<string.h>
int main()
{
   void *memset( void *dest, int c, size_t count );
   return 0;
 }

定义:将缓冲区设置为指定字符 / 可以用来初始化字符串

头文件:<string.h>

注释:memset返回dest的地址

fgets()

#include<stdio.h>
int main()
{
   char *fgets( char *string, int n, FILE *stream );
   return 0;
 }

定义:从流中获取字符串

头文件:<stdio.h>

注释:返回的是string。返回NULL表示错误或文件结束条件

strlen()

#include<string.h>
int main()
{
   size_t strlen( const char *string );
   return 0;
 }

定义:返回的是字符串的长度

头文件:<string.h>

注释:strlen只返回'\0'之前字符串的长度

strtok()

#include<string.h>
int main()
{
   char *strtok( char *strToken, const char *strDelimit );
   return 0;
 }

定义:查找字符串中的下一个标记。(常用于切割字符串)

头文件:<string.h>

注释:

sep参数是个字符串,定义了用作分隔符的字符集合

第一个参数指定一个字符串,它包含了0个或者多个由strDelimit字符串中一个或者多个分隔符分割的标记。

strtok函数找到str中的下一个标记,并将其用 \0 结尾,返回一个指向这个标记的指针。

strtok函数的第一个参数不为 NULL ,函数将找到str中第一个标记,strtok函数将保存它在字符串中的位置。

strtok函数的第一个参数为 NULL ,函数将在同一个字符串中被保存的位置开始,查找下一个标记。

如果字符串中不存在更多的标记,则返回 NULL 指针。

chdir()

#include <unistd.h>
int main()
{
   int chdir(const char *path);
   return 0;
 }

定义:更改工作目录

头文件:#include <unistd.h>

返回值:如果成功,则返回0。如果出现错误,则返回-1,并适当地设置errno。


以上就是完整版简单shell脚本的编写,仅供参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值