📜  如何在 c 中为初学者制作 shell - Shell-Bash (1)

📅  最后修改于: 2023-12-03 15:38:13.306000             🧑  作者: Mango

如何在 C 中为初学者制作 Shell

在计算机科学中,shell 是一种命令行解释器,它是用户与系统内核之间的接口。在本文章中,我们将通过 C 语言为初学者介绍如何编写自己的简单 shell。

步骤1:了解 Shell 的基本概念

在开始编写 shell 之前,我们需要了解一些基本概念:

  • 命令行解释器:Shell 是一种命令行解释器,它接受用户输入的命令并将其传递给系统内核。
  • 环境变量:Shell 使用环境变量来存储有关系统和用户的信息,例如 PATH 变量用来指定可执行文件的路径。
  • 管道符:管道符 | 用于将一个命令的输出连接到另一个命令的输入中,以便在命令之间传递数据。
  • 重定向符:重定向符 >< 用于将命令的输出发送到文件或从文件中读取输入。
步骤2:编写基本的 Shell 代码

现在我们已经了解了 Shell 的基本概念,我们可以开始编写基本的 Shell 代码了。

首先,我们需要包含一些头文件(headers):

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

接下来,我们可以定义几个常量和变量:

#define MAX_CMD_LENGTH 100 // 命令的最大长度
#define MAX_ARGS 20 // 命令的最大参数数目

char command[MAX_CMD_LENGTH]; // 用户输入的命令
char *args[MAX_ARGS]; // 用户输入的参数
int num_args; // 用户输入的参数数目

现在,我们可以编写一个循环,每次等待用户输入命令:

while (1) {
    printf("$ "); // 打印提示符
    fgets(command, MAX_CMD_LENGTH, stdin); // 从标准输入读取命令
    command[strcspn(command, "\n")] = 0; // 移除命令字符串中的换行符
    parse_command(); // 解析命令
    execute_command(); // 执行命令
}

在这个循环中,我们首先打印一个提示符 $,然后使用 fgets 从标准输入(stdin)中读取命令。之后,我们调用 parse_command 函数解析命令,并调用 execute_command 函数执行命令。

下面,我们来看看 parse_command 函数的代码:

void parse_command() {
    num_args = 0; // 重置参数计数器

    char *token = strtok(command, " "); // 使用空格分隔符分割命令字符串为多个字符串

    while (token != NULL && num_args < MAX_ARGS - 1) {
        args[num_args] = token;
        num_args++;
        token = strtok(NULL, " ");
    }

    args[num_args] = NULL; // 以 NULL 结尾的参数数组是 execvp() 函数的要求

    // 打印用户输入的参数
    for (int i = 0; i < num_args; i++) {
        printf("arg[%d]: '%s'\n", i, args[i]);
    }
}

这个函数的作用是将用户输入的命令字符串解析为多个参数字符串(参考 man strtok)。我们使用空格分隔符将命令字符串分割为多个字符串,并将每个字符串保存在 args 数组中。我们还使用一个计数器 num_args 来计算参数的数量。最后,我们将 args 数组以 NULL 结尾,这是 execvp 函数的要求。

最后,我们来看看 execute_command 函数的代码:

void execute_command() {
    pid_t pid = fork(); // 创建一个子进程

    if (pid < 0) { // 如果出现错误
        printf("Error: Failed to fork.\n");
        exit(1);
    } else if (pid == 0) { // 如果是子进程
        if (execvp(args[0], args) < 0) { // 执行命令
            printf("Error: Failed to execute command.\n");
            exit(1); // 退出子进程
        }
    } else { // 如果是父进程
        wait(NULL); // 等待子进程执行完毕
    }
}

execute_command 函数的作用是创建一个子进程,并在子进程中执行用户输入的命令。我们使用 fork 函数创建一个子进程,然后在子进程中使用 execvp 函数执行命令。父进程等待子进程完成执行并继续循环。

完整的代码如下:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <sys/wait.h>

#define MAX_CMD_LENGTH 100
#define MAX_ARGS 20

char command[MAX_CMD_LENGTH];
char *args[MAX_ARGS];
int num_args;

void parse_command() {
    num_args = 0;

    char *token = strtok(command, " ");

    while (token != NULL && num_args < MAX_ARGS - 1) {
        args[num_args] = token;
        num_args++;
        token = strtok(NULL, " ");
    }

    args[num_args] = NULL;

    for (int i = 0; i < num_args; i++) {
        printf("arg[%d]: '%s'\n", i, args[i]);
    }
}

void execute_command() {
    pid_t pid = fork();

    if (pid < 0) {
        printf("Error: Failed to fork.\n");
        exit(1);
    } else if (pid == 0) {
        if (execvp(args[0], args) < 0) {
            printf("Error: Failed to execute command.\n");
            exit(1);
        }
    } else {
        wait(NULL);
    }
}

int main() {
    while (1) {
        printf("$ ");
        fgets(command, MAX_CMD_LENGTH, stdin);
        command[strcspn(command, "\n")] = 0;
        parse_command();
        execute_command();
    }

    return 0;
}
步骤3:扩展 Shell 的功能

现在我们的 shell 可以解析并执行用户输入的命令,但是它还没有实现很多其他常见的 shell 功能,例如重定向、管道等。

要实现这些功能,我们需要在 parse_command 函数中添加一些逻辑来检测命令是否包含特殊符号,并相应地处理输入和输出。我们还可以创建额外的子进程来实现管道等操作。

例如,如果用户输入 ls > output.txt,我们需要将 ls 的输出重定向到一个名为 output.txt 的文件中。我们可以在 parse_command 函数中添加以下逻辑:

// 检查是否存在重定向符号 '>'
for (int i = 0; i < num_args; i++) {
    if (strcmp(args[i], ">") == 0) {
        if (i == num_args - 2) { // 检查 '>' 是否在倒数第二个参数位置
            char *output_file = args[num_args - 1];
            freopen(output_file, "w", stdout); // 将标准输出重定向到文件
            args[num_args - 2] = NULL; // 移除 '>' 右边的命令参数
            num_args -= 2; // 更新参数数目
            break;
        } else {
            printf("Error: Invalid command syntax.\n");
            return;
        }
    }
}

在这个示例中,我们检查参数数组中是否存在重定向符号 >,如果是,则打开一个文件作为标准输出,并删除参数数组中的文件名和符号。我们还在循环的末尾使用 break 退出循环。

我们还可以使用类似的逻辑来实现管道、重定向输入等功能。

总结

在本文中,我们介绍了如何使用 C 语言编写一个简单的 shell 程序。我们了解了 shell 的基本概念、解析和执行命令的实现方法,并扩展了 shell 的功能,使其可以支持重定向、管道等操作。

对初学者来说,编写一个自己的 shell 程序是一次很好的练习,它可以帮助我们深入了解操作系统、编译器和 Linux 应用程序的工作方式。希望本篇文章能够对你有所帮助。