用C编写一个简单的Shell应用

这是一个用于练手的使用C编写的shell程序,当然它只包含非常简单的功能。通常情况下,一个shell程序相对复杂,例如很多配置项以改变shell的现实效果或者部分行为。

Shell的基本生命周期

从shell开始运行,到结束退出,它主要执行三个工作内容:

  1. 初始化:这部分用于读取并执行配置文件。当然本项目中并不包含此部分内容。
  2. 解释:从stdin中读取命令并执行他们。此处可以是交互式shell,也可以从文件读取。
  3. 终止:执行命令后,shell将关闭所有命令,并释放所有内存,最后终止程序。

下面来看一下程序的main函数内容:

int main(int argc, const char * argv[]) {
    lsh_loop();
    return EXIT_SUCCESS;
}

函数lsh_loop()将循环执行,解析命令。下面来看一下此函数的实现。

Shell的循环

shell在一个循环过程中会执行三个步骤:

  1. 读取:从标准输入中读取命令。
  2. 解析:将命令字符串分割成为程序和参数。
  3. 执行:运行已经解析的命令。

代码实现如下:

void lsh_loop(void){
    char *line;
    char **args;
    int status;
    
    do{
        printf("> ");
        line = lsh_read_line();
        args = lsh_split_line(line);
        status = lsh_execute(args);

        free(line);
        free(args);
    }while(status);
}

这里涉及到另外三个函数,下面我们来逐一详解。在这之前我们需要注意,指针变量lineargs这运行时使用动态内存分配,在命令执行后,应该使用free将其内存释放。

读取命令

从标准输入读取命令,可以按行进行读取,我们定义一个lsh_read_line()来实现此功能。我们压力定义一个宏,来表示每次读取的行所占用的内存长度。

#define LSH_RL_BUFSIZE 1024
char *lsh_read_line(void){
    int bufsize = LSH_RL_BUFSIZE;
    int position = 0;
    char *buffer = malloc(sizeof(char) *bufsize);
    int c;
    
    if(!buffer){
        fprintf(stderr, "lsh: allocation error\n");
        exit(EXIT_FAILURE);
    }
    
    while (1) {
        c = getchar();
        
        if( c==EOF || c== '\n'){
            buffer[position] = '\0';
            return buffer;
        }else{
            buffer[position] = c;
        }
        position++;
        
        if(position>=bufsize){
            bufsize += LSH_RL_BUFSIZE;
            buffer = realloc(buffer, bufsize);
            if(!buffer){
                fprintf(stderr, "lsh: allocation error\n");
                exit(EXIT_FAILURE);
            }
        }
    }
}

值得注意的是,通过malloc或者realloc申请或者重新申请内存空间后,一定要对结果进行判定,以防止内存分配失败后程序崩溃。

当然,上面的实现未使用新版本中stdio.h中的getline()函数,如果使用,则代码可以如下实现:

char *lsh_read_line(void)
{
  char *line = NULL;
  ssize_t bufsize = 0;

  if (getline(&line, &bufsize, stdin) == -1){
    if (feof(stdin)) {
      exit(EXIT_SUCCESS);
    } else  {
      perror("readline");
      exit(EXIT_FAILURE);
    }
  }

  return line;
}

解析命令

现在我们已经读取了一行的输入,这是一个完整的C-Style字符串,我们要解析它,按照空格将它们切割,得到命令和对应的参数。为了简化我们的工作,此处使用string.h中的strtok函数实现。

#define LSH_TOK_BUFSIZE 64
#define LSH_TOK_DELIM " \t\r\n\a"

char **lsh_split_line(char *line){
    int bufsize = LSH_TOK_BUFSIZE, position = 0;
    char **tokens = malloc(bufsize*sizeof(char*));
    char *token;
    
    if(!tokens){
        fprintf(stderr, "lsh: allocation error\n");
        exit(EXIT_FAILURE);
    }
    
    token = strtok(line, LSH_TOK_DELIM);
    while(token!=NULL){
        tokens[position] = token;
        position++;
        
        if(position >= bufsize){
            bufsize += LSH_TOK_BUFSIZE;
            tokens = realloc(tokens, bufsize*sizeof(char *));
            if(!tokens){
                fprintf(stderr, "lsh: allocation error\n");
                exit(EXIT_FAILURE);
            }
        }
        token = strtok(NULL,LSH_TOK_DELIM);
    }
    tokens[position] = NULL;
    return tokens;
}

我们将切割后的每一个元素称之为token,将字符串切割完成后,我们就进入到了执行阶段。

启动进程

如果你了解linux系统进程相关知识,此处应该知道,当前的shell程序将作为父进程,我们需要启动一个新的进程作为shell的子进程。并在它执行完成后得到它的执行结果。

在子进程执行时,我们要保持shell处于等待状态。以确保不会发生其他问题。

int lsh_launch(char **args){
    pid_t pid, wpid;
    int status;
    pid = fork();
    if(pid==0){
        if(execvp(args[0], args)==-1){
            perror("lsh");
        }
        exit(EXIT_FAILURE);
    }else if(pid<0){
        perror("lsh");
    }else{
        do{
            wpid = waitpid(pid,&status, WUNTRACED);
        }while(!WIFEXITED(status) && !WIFSIGNALED(status));
    }
    return 1;
}

当我们fork出一个进程后,先判定它的pid是否合法。如果不合法,则告知用户。如果正常,则调用execvp执行命令,同时等待进程结束。

shell内置命令

完成以上工作,我们还希望提供意见默认的命令,比如更改目录,可以使用chdir()函数,退出shell这样的功能。下面来看一下三个内置命令的实现:

int lsh_cd(char **args);
int lsh_help(char **args);
int lsh_exit(char **args);

char *builtin_str[] = {
    "cd",
    "help",
    "exit"
};

int (*builtin_func[])(char **) = {
    &lsh_cd,
    &lsh_help,
    &lsh_exit,
};

int lsh_num_builtins(){
    return sizeof(builtin_str) / sizeof(char *);
}

int lsh_cd(char **args){
    if(args[1]==NULL){
        fprintf(stderr, "lsh: expected argument to \"cd\"\n");
    }else{
        if(chdir(args[1])!=0){
            perror("lsh");
        }
    }
    return 1;
}

int lsh_help(char **args){
    int i ;
    printf("LSH\n");
    printf("Type program names and arguments, and hit enter.\n");
    printf("The following are built in:\n");

    for (i = 0; i < lsh_num_builtins(); i++) {
      printf("  %s\n", builtin_str[i]);
    }

      printf("Use the man command for information on other programs.\n");
      return 1;
}

int lsh_exit(char **args)
{
  return 0;
}

此处使用指针函数,将三个命令包裹到一个数组中,方便后续逻辑操作。

处理内置命令和其他命令

当开始执行命令是,首先需要判定当前命令是否合法。然后确认当前命令是否属于内置命令,如果不是,则调用lsh_launch函数进启动子进程执行。

int lsh_execute(char **args){
    int i ;
    if(args[0]==NULL){
        return 1;
    }
    for(i=0;i<lsh_num_builtins();i++){
        if(strcmp(args[0], builtin_str[i])==0){
            return (*builtin_func[i])(args);
        }
    }
    return lsh_launch(args);
}

编译并测试

完成了上面所有的代码,你可以执行

gcc main.c
./main

如果你的代码没有任何错误的话,可以尝试输入命令查看效果。

完整代码如下:

//
//  Write a Shell in C
//
//  Created by mebius on 2020/12/13.
//

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>

#define LSH_RL_BUFSIZE 1024
#define LSH_TOK_BUFSIZE 64
#define LSH_TOK_DELIM " \t\r\n\a"

int lsh_cd(char **args);
int lsh_help(char **args);
int lsh_exit(char **args);


char *lsh_read_line(void){
    int bufsize = LSH_RL_BUFSIZE;
    int position = 0;
    char *buffer = malloc(sizeof(char) *bufsize);
    int c;
    
    if(!buffer){
        fprintf(stderr, "lsh: allocation error\n");
        exit(EXIT_FAILURE);
    }
    
    while (1) {
        c = getchar();
        
        if( c==EOF || c== '\n'){
            buffer[position] = '\0';
            return buffer;
        }else{
            buffer[position] = c;
        }
        position++;
        
        if(position>=bufsize){
            bufsize += LSH_RL_BUFSIZE;
            buffer = realloc(buffer, bufsize);
            if(!buffer){
                fprintf(stderr, "lsh: allocation error\n");
                exit(EXIT_FAILURE);
            }
        }
    }
}

char **lsh_split_line(char *line){
    int bufsize = LSH_TOK_BUFSIZE, position = 0;
    char **tokens = malloc(bufsize*sizeof(char*));
    char *token;
    
    if(!tokens){
        fprintf(stderr, "lsh: allocation error\n");
        exit(EXIT_FAILURE);
    }
    
    token = strtok(line, LSH_TOK_DELIM);
    while(token!=NULL){
        tokens[position] = token;
        position++;
        
        if(position >= bufsize){
            bufsize += LSH_TOK_BUFSIZE;
            tokens = realloc(tokens, bufsize*sizeof(char *));
            if(!tokens){
                fprintf(stderr, "lsh: allocation error\n");
                exit(EXIT_FAILURE);
            }
        }
        token = strtok(NULL,LSH_TOK_DELIM);
    }
    tokens[position] = NULL;
    return tokens;
}

int lsh_launch(char **args){
    pid_t pid, wpid;
    int status;
    pid = fork();
    if(pid==0){
        if(execvp(args[0], args)==-1){
            perror("lsh");
        }
        exit(EXIT_FAILURE);
    }else if(pid<0){
        perror("lsh");
    }else{
        do{
            wpid = waitpid(pid,&status, WUNTRACED);
        }while(!WIFEXITED(status) && !WIFSIGNALED(status));
    }
    return 1;
}

char *builtin_str[] = {
    "cd",
    "help",
    "exit"
};

int (*builtin_func[])(char **) = {
    &lsh_cd,
    &lsh_help,
    &lsh_exit,
};

int lsh_num_builtins(){
    return sizeof(builtin_str) / sizeof(char *);
}

int lsh_cd(char **args){
    if(args[1]==NULL){
        fprintf(stderr, "lsh: expected argument to \"cd\"\n");
    }else{
        if(chdir(args[1])!=0){
            perror("lsh");
        }
    }
    return 1;
}

int lsh_help(char **args){
    int i ;
    printf("LSH\n");
      printf("Type program names and arguments, and hit enter.\n");
      printf("The following are built in:\n");

      for (i = 0; i < lsh_num_builtins(); i++) {
        printf("  %s\n", builtin_str[i]);
      }

      printf("Use the man command for information on other programs.\n");
      return 1;
}

int lsh_exit(char **args)
{
  return 0;
}

int lsh_execute(char **args){
    int i ;
    if(args[0]==NULL){
        return 1;
    }
    for(i=0;i<lsh_num_builtins();i++){
        if(strcmp(args[0], builtin_str[i])==0){
            return (*builtin_func[i])(args);
        }
    }
    return lsh_launch(args);
}

void lsh_loop(void){
    char *line;
    char **args;
    int status;
    
    do{
        printf("> ");
        line = lsh_read_line();
        args = lsh_split_line(line);
        status = lsh_execute(args);

        free(line);
        free(args);
    }while(status);
}



int main(int argc, const char * argv[]) {
    lsh_loop();
    return EXIT_SUCCESS;
}

POSIX规范

如果你想实现一个较为完整并且符合标准的shell程序,可以参考The Single UNIX Specification ,API接口可参考 POSIX规范