C语言实现一个简单shell(包括重定向,多管道),并且持续更新!

文章详细介绍了作者使用C语言实现的一个简易shell,包括cd命令、简单命令、管道、重定向等功能,并列出了待实现的特性如后台运行、命令历史保存等。文章深入探讨了shell中管道和重定向的处理,以及进程间通信和信号处理。同时,提到了使用readline库实现tab补全的可能性,以及调试技巧和进程管理策略。
摘要由CSDN通过智能技术生成

#shell

我的shell是基于C语言和《UNIX-Linux系统编程手册》实现的,如有基础知识不懂,请自行查阅。
my_shell持续更新中,并不是最终版本。

###实现的功能
1.cd(包括"-"和 “~”)

2.简单命令

3.单个管道

4.多级管道

5.输入重定向

6.输出重定向

7.追加重定向

以及管道加单个重定向符号

###待实现功能
1.真正的后台运行

2.cd -还有一定问题,在前一个命令cd了一个错误目录,依旧进行了备份,不能cd -回到上一次路径

3.美化不好看

4.管道加多个重定向符号(包含在管道中间的子命令)

5.对历史命令进行保存

6.信号处理ctrl+c在cd后会出错

7.实现tab补全

##美化
printf(“\033[01;33m”);

\033[0m:重置所有属性为默认值

\033[1m:加粗文本

\033[3m:斜体文本

\033[4m:下划线文本

\033[5m:闪烁文本

\033[7m:反色文本(即背景色变为前景色,前景色变为背景色)

\033[30m - \033[37m:设置文本颜色为黑、红、绿、黄、蓝、紫、青、白

\033[40m - \033[47m:设置背景颜色为黑、红、绿、黄、蓝、紫、青、白

这里的\033也可以用\e或\x1b来代替,因此,以上的控制码也可以写成\e[0m、\x1b[1m等形式。

##优先级
1.在shell程序中,不同的操作符有不同的优先级。下面是常见操作符的优先级列表(从高到低):

圆括号: ( )[待实现]

引号: " ", ’ '[待实现]

重定向: >, >>, <。[后面待实现]<<, <>, >&, <&

管道: |

逻辑操作符: &&, ||p[待实现]

分号: ;[待实现]

后台运行: &[待实现]

优先级高的操作符会先被处理,而优先级相同的操作符则按照从左到右的顺序依次处理。

如果一个命令中同时存在重定向和管道,那么先处理重定向,再处理管道。

例如,假设有以下Shell命令:
command1 < inputfile | command2 | command3 > outputfile

其中,command1从inputfile中读取数据,经过管道传递给command2,再经过管道传递给command3,最终将command3的输出重定向到outputfile中。

处理方式如下:

首先将command1的输入重定向为inputfile,将command3的输出重定向为outputfile。

然后创建两个管道,分别用于将command1的输出传递给command2,将command2的输出传递给command3。

在管道中依次执行每个命令,将前一个命令的输出通过管道传递给下一个命令,直到command3执行完毕。

请注意,在处理多级管道时,需要按照从左到右的顺序依次处理每个命令,确保数据传递的正确性。同时,也需要注意管道的读写顺序,避免出现死锁等问题。

2.输入输出重定向优先级
在一条命令中同时使用输入重定向和输出重定向时,输入重定向会先被执行,然后才是输出重定向。这是因为在执行命令之前,操作系统会首先打开输入文件并准备好输入数据,然后才是执行命令和输出数据。

以下是一个示例命令:

command1 < input_file > output_file
在这个命令中,<和>分别表示输入重定向和输出重定向。命令执行的顺序如下:

打开文件 input_file 并将其作为 command1 的标准输入。
打开文件 output_file 并将其作为 command1 的标准输出。
启动 command1 进程,并将其标准输入和输出分别连接到文件描述符。
command1 从标准输入读取数据,并将处理结果写入标准输出。
当 command1 执行完毕后,操作系统关闭文件描述符并保存输出文件。
从上述执行过程可以看出,输入重定向必须在输出重定向之前执行,以确保输入数据能够正确地传递给命令,而不会被输出到文件中。如果输入重定向和输出重定向的顺序颠倒,可能会导致输入数据丢失或者输出数据错误的结果。

##重定向与直接操作文件的差异
cat 1.txt 和 cat < 1.txt 都是使用 cat 命令将文件 1.txt 的内容输出到标准输出(通常是屏幕)上,但它们的实现方式略有不同。

cat 1.txt 是将文件名 1.txt 作为参数传递给 cat 命令。在执行时,cat 命令将打开文件 1.txt,读取其中的内容并将其输出到标准输出上。

cat < 1.txt 是将文件 1.txt 通过标准输入重定向传递给 cat 命令。在执行时,cat 命令将从标准输入中读取数据,而此时标准输入已经被重定向为文件 1.txt,因此 cat 命令将读取文件 1.txt 中的内容并将其输出到标准输出上。

因此,两者的区别在于数据来源不同:cat 1.txt 直接将文件名作为参数传递给命令,而 cat < 1.txt 将文件的内容通过标准输入重定向传递给命令。

##管道细节
虽然第一个进程已经关闭了管道的读取端和写入端,但是后续的进程仍然可以通过 dup2 函数将上一个进程的标准输出与当前管道的写入端进行连接,从而将数据传输到管道中。这样,后续的进程就可以从已经连接好的管道中读取数据,并将自己的输出写入到该管道中。

例如,在一个有两个管道的命令中,假设第一个子进程已经关闭了第一个管道的读取端和第二个管道的写入端,那么第二个子进程可以使用 dup2 函数将第一个管道的写入端与自己的标准输入进行连接,从而从第一个管道中读取数据;同时,它可以将自己的标准输出与第二个管道的写入端进行连接,从而将自己的输出写入到第二个管道中。

总之,通过管道连接起来的进程可以互相传递数据,即使中间的进程关闭了管道的某个端口,数据也可以继续流动。这是管道的一个重要特性,也是在实现 shell 的多管道时需要注意的地方。

###为什么要关闭管道的读取和写入端(难点)
从管道读取数据的进程会关闭其持有的管道写入描述符。这是因为当其他进程完成输出并关闭管道写入描述符后,读端进程就能看到文件结束。如果读取端进程没有关闭写入端,则在其他写入端进程关闭写入端后,读取端也不会看到文件结束,即使其已经读取完管道中的数据。此外,读取端的read()函数会一直阻塞以等待数据到来,这是因为内核还知道至少存在一个管道的写入描述符还打开着,即读取进程自己打开的写入文件描述符,理论上讲这个进程仍然可以向管道写入数据,即使其已经被读取操作阻塞了。因为在真实的环境下,read()有可能会被一个向管道写入数据的信号处理器中断。

写入进程关闭其读取文件描述符的原因则与读取进程不同。当一个进程试图向一个没有打开着的读取文件描述符发管道写入数据时,内核会向写入进程发送一个SIGPIPE信号。默认情况下,这个信号会杀死一个进程。但是进程可以捕获或忽略这信号,这样会导致管道的write()操作因EPIPE错误而失败。(已损坏的管道)。此外如果写入进程没有关闭读取端,那么即使在其他进程已经关闭了管道的读取端之后,写入端进程仍然能够向管道写入数据,最后数据将充满整个管道,后续的写入请求会被永远阻塞。

因此:比如有三个管道的情况下,执行第一个子命令,其余管道必须关闭。

##调试小技巧
因为涉及子进程的问题,默认调试子进程会直接跳过,就需要下面的操作进入子进程
注:如果有更深入的需求,搜索《100个gdb调试的小技巧》

follow-fork-mode的用法为:

set follow-fork-mode [parent|child]

parent: fork之后继续调试父进程,子进程不受影响。
child: fork之后调试子进程,父进程不受影响。

如果需要调试子进程,在启动gdb后:

(gdb) set follow-fork-mode child

##处理基础命令的函数

eg: execvp(“ls”, “ls”, “-l”, NULL);

execv 函数将参数作为一个数组传递给新程序,而不会对环境进行任何更改。它需要提供完整路径和名称,因为它不会搜索 $PATH 中的可执行文件。例如,调用 execv(“/bin/ls”, args) 会在新进程中执行 /bin/ls 程序,并使用 args 数组作为命令行参数。

execve 函数与 execv 类似,但允许传递一个环境变量数组,可以在新程序中设置新的环境变量。例如,调用 execve(“/bin/ls”, args, env) 会在新进程中执行 /bin/ls 程序,并使用 args 数组作为命令行参数,使用 env 数组设置环境变量。

execvp 函数允许在 $PATH 环境变量中搜索可执行文件。它接受一个文件名和一个参数数组,并在 $PATH 中搜索该文件名对应的可执行文件。例如,调用 execvp(“ls”, args) 会在新进程中执行名为 ls 的可执行文件,并使用 args 数组作为命令行参数。

总的来说,这三个函数的区别主要在于它们处理参数和搜索可执行文件的方式。execv 和 execve 需要提供完整的路径和名称来指定要执行的可执行文件,而 execvp 可以在 $PATH 环境变量中搜索可执行文件。execve 允许在新程序中设置新的环境变量,而 execv 和 execvp 不会更改环境变量。

因此,我优先考虑execvp函数,这样就不用考虑PATH路径的问题

##父子进程与兄弟进程

在Linux系统中,每个进程都有一个父进程和一个进程ID(PID)。当一个进程通过调用fork()系统调用创建一个新进程时,新进程就成为原进程的子进程,并且新进程被分配一个新的PID。这种父子进程关系可以视为特殊的兄弟进程关系,其中子进程是父进程的一个特殊的兄弟进程,因为它们有相同的父进程。

兄弟进程之间可以通过共享相同的资源来进行通信,比如共享内存或管道等,父子进程之间也可以这样做。父子进程之间可以通过共享文件描述符来进行通信,比如一个进程打开一个文件并把它的文件描述符传递给它的子进程。

因此,父子进程之间的关系可以被视为一种特殊的兄弟进程关系,它们共享某些资源并可以通过这些资源进行通信。

##tab补全(暂未实现)
需要用到readline库,这个库提供了readline和rl_completion函数,可以帮助我们实现TAB补全功能。

##多级管道
解析命令行输入,将多个命令按照管道符 “|” 分隔开来。
对于每一个被分隔开的命令,创建一个子进程,并将子进程的标准输出重定向到管道中。
对于除了第一个命令以外的每个命令,将它的标准输入从前一个命令的管道中读取。
在主进程中,等待所有子进程执行完毕。
如果需要输出结果,则从最后一个子进程的标准输出中读取结果。
举个例子,假设我们要执行命令 “ls | grep foo | wc -l”:

解析命令行输入,将命令按照管道符 “|” 分隔为三个命令: “ls”,“grep foo”,和 “wc -l”。
创建第一个子进程,执行命令 “ls”,将标准输出重定向到管道中。
创建第二个子进程,执行命令 “grep foo”,将标准输入从前一个命令的管道中读取,将标准输出重定向到管道中。
创建第三个子进程,执行命令 “wc -l”,将标准输入从前一个命令的管道中读取。
在主进程中,等待所有子进程执行完毕。
从最后一个子进程的标准输出中读取结果,并将结果输出到屏幕上。

####shell思路
先根据输入的命令,将其通过管道符分割为一个个子命令,如果没有管道符就将输入的命令当作一个子命令,子命令需要新开子进程运行,再判断有无管道,有的话进入PipeCommand函数,进行重定向操作并进入do_execvp函数处理子命令,在该函数内,判断有无重定向符号,如果有进行对应操作。

这个shell的细节方面处理很粗糙,还需要大量修改,下面是半成品代码:




#include <dirent.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>
// 444
void Prompt();
void Pwd();

void ChangeDirectory(char[]);
void ShowEnvironment();
void SetEnvironment(char[]);
void UnSetEnvironment(char[]);
void do_execvp(char**, int, int);
void PipeCommand(char **,int,int);
void signalhandle(int);

volatile sig_atomic_t signal_flag = false;

/*现在只能处理没有管道的命令*/


char home[500];

int main(int argc, char* argv[]) {
    char command[500] = "";
    char backup[500];

    getcwd(home, 500);

    system("clear");

    char* command_son[1024];
    /*在 C 语言中,字符串是以 null
终止的字符数组。这意味着在内存中必须为字符串分配足够的空间来存储每个字符,以及一个用于表示字符串结束的
null 字符。

当您定义一个字符数组时,编译器会为其分配一定的内存空间,但是这个空间的大小是固定的,不能动态调整。因此,如果您想存储不定长度的字符串,或者您不知道要存储的字符串的长度,您需要使用动态内存分配函数,例如
malloc() 函数,来分配足够的内存空间。

在 C
语言中,动态内存分配是一种将内存空间分配给变量的方法,它可以在程序运行时动态地分配和释放内存,从而允许您动态存储和处理数据。在使用动态内存分配时,您需要手动分配和释放内存,以确保内存使用的正确性和效率。*/

    // 注册了一个SIGINT信号[ctrl+c]的处理函数,就是说进程接收到ctrl+c调用signalhandle的信号处理函数处理该信号
    signal(SIGINT, signalhandle);

    // The Main Prompt of Shell
    while (1) {
        // used for printing prompt
        Prompt();
        // char temp[500];

        // getcwd(temp, 500);
        // printf("shell.1.8@ %s \n$ ", temp);
        // used for reading a whole line and removing new line character with
        // null
        if (fgets(command, 500, stdin) == 0)
            exit(0);
        if (command[0] == '\n')
            continue;
        command[strlen(command) - 1] = '\0';

        char command_backup[500];
        strcpy(command_backup, command);

        /*记录pipe数量*/
        int pipe_num = 0;
        for (int i = 0; i < strlen(command); i++) {
            if (command[i] == '|')
                pipe_num++;
        }
        int command_son_num = 0;

        /*如果没有管道符号,就malloc开辟一块内存存放命令*/
        if (pipe_num == 0) {
            command_son[0] =
                (char*)malloc(sizeof(char) * (1 + strlen(command)));
            strcpy(command_son[0], command);
            command_son_num++;

        }
        /*如果有管道符号,将输入的命令按照管道符分为多个子命令,子命令需要根据管道的个数n分别分配n+1块内存*/
        else {
            char* token = strtok(command_backup, "|");

            // while (token != NULL)
            for (int i = 0; token != NULL && i < pipe_num + 1; i++) {
                command_son[i] =
                    (char*)malloc(sizeof(char) * (strlen(token) + 1));
                strcpy(command_son[command_son_num], token);
                command_son_num++;
                token = strtok(NULL, "|");
            }
        }

        // printf("%s\n", command_son[0]);
        // printf("%s\n", command_son[1]);
        // printf("%s\n", command_son[2]);

        /*      command1 < input_file | command2 > output_file
        在这个命令中,<和>分别表示输入重定向和输出重定向。命令执行的顺序如下:

        1.打开文件 input_file 并将其作为 command1 的标准输入。
        2.启动 command1 进程,并将其标准输出连接到管道。
        3.启动 command2 进程,并将其标准输入连接到管道。
        4.打开文件 output_file 并将其作为 command2 的标准输出。
        5.command1 将输出写入管道,command2 从管道读取输入,并将输出写入
        output_file。*/

        /*int dup2(int oldfd, int newfd);
        dup2函数,把指定的newfd也指向oldfd指向的文件*/

        pid_t pid;
        int status;
        pid = fork();
        if (pid < 0) {
            printf("Error : failed to fork\n");
        } /*子进程处理*/
        else if (pid == 0) {
            if (pipe_num != 0)
                PipeCommand(command_son, pipe_num, command_son_num);
            if (command_son[0][0] == 'c' && command_son[0][1] == 'd') {
                ChangeDirectory(command_son[0]);
            }
            if (pipe_num == 0)
                /*如果没有管道,那么command_son里面只有一条命令*/
                do_execvp(command_son, 0, command_son_num);

        } else {
            // printf("awa");
            // pid_t result = wait4(pid, &status, 0, NULL);
            // printf("Child process with PID %d has terminated with status
            // %d\n",
            //        result, status);
            waitpid(pid, &status, 0);
            // fflush(stdin);
            // fflush(stdout);
        }

        // free(command_son[1]);
        // 将输出的颜色改成黄色
        // printf("\033[01;33m");

        /*最终free的地方*/
        for (int i = 0; i < command_son_num; i++) {
            free(command_son[i]);
        }
        fflush(stdin);
        fflush(stdout);
        fflush(stderr);
    }

    exit(0);  // exit normally
}

void Prompt() {
    // for current working directory
    char temp[500];

    getcwd(temp, 500);
    printf("\033[1;32m%s  ", getenv("USER"));
    printf("%s\033[1;31m$ ", temp);
    // }

    return;
}

void Pwd() {
    // for current working directory
    char cwd[500];

    getcwd(cwd, 500);

    printf("%s \n", cwd);

    return;
}

/*cd*/
char backup_history[500] = {"~"};
char awa[500];
int judge = 0;
char newpath[500];
void ChangeDirectory(char path[]) {
    char backup_history_1[500];
    for (int i = 3; path[i] != '\0'; i++) {
        if (path[i] == '-') {
            if (judge % 2 != 0) {
                memset(backup_history, 0, 500);
                getcwd(backup_history, 500);
                chdir(awa);
            }
            if (judge % 2 == 0) {
                memset(awa, 0, 500);
                getcwd(awa, 500);
                chdir(backup_history);
            }
            judge++;
            return;
        }
    }
    if (chdir(newpath) >= 0) {
        memset(backup_history, 0, 500);
        getcwd(backup_history, 500);
        strcpy(backup_history_1, home);
    }

    // 没路径
    if (path[0] == 'c' && path[1] == 'd' && path[2] == '\0') {
        if (chdir(home) < 0) /*change directory error*/
        {
            printf("ERROR CHANGING DIRECTORY TO HOME\n");
            exit(1);
        }
    }
    // 有路径
    else {
        int j = 0;
        for (int i = 3; path[i] != '\0'; i++, j++) {
            if (path[i] == '-') {
                chdir(backup_history);
                return;
            }
            if (path[i] == '~') {
                chdir(backup_history_1);
                return;
            }
            newpath[j] = path[i];
        }

        newpath[j] = '\0';

        if (chdir(newpath) < 0) {
            printf("shell: cd: %s: No such file or directory\n", newpath);
        }
    }
}

void do_execvp(char** command_son, int count, int command_son_num) {
    int symbol = 0;
    // for (int i = 0; i < command_son_num; i++) {
    for (int j = 0; j < strlen(command_son[count]); j++) {
        if (command_son[count][j] == '>' || command_son[count][j] == '<') {
            int fd1, fd2, fd3;
            // int in_fd = STDIN_FILENO, out_fd = STDOUT_FILENO;
            /*如果是输入重定向*/
            if (command_son[count][j] == '<') {
                command_son[count][j] = '\0';
                fd1 = open(command_son[count] + j + 2, O_RDONLY);
                dup2(fd1, STDIN_FILENO);
                symbol = j;
            }
            /*>>*/
            else if (command_son[count][j] == '>' &&
                     command_son[count][j + 1] == '>') {
                command_son[count][j] = '\0';
                command_son[count][j + 1] = '\0';
                fd3 = open(command_son[count] + j + 3,
                           O_WRONLY | O_CREAT | O_APPEND, 0644);
                dup2(fd3, STDOUT_FILENO);
                symbol = j;
            }
            /*如果是输出重定向*/
            else if (command_son[count][j] == '>') {
                // char * file_name =;
                使用open函数打开一个文件,该文件将成为新的标准输出
                command_son[count][j] = '\0';
                fd2 = open(command_son[count] + j + 2,
                           O_WRONLY | O_CREAT | O_TRUNC, 0666);
                // 标准输出(stdout)的文件描述符重定向到fd所指向的文件描述符上
                // execvp(command_son[i], command_son + i);
                dup2(fd2, STDOUT_FILENO);
                symbol = j;
            }
            /*非法字符*/
            else {
                perror("error");
            }
        }
    }
    // }
    /*part数组存储command_son的部分*/
    // for(int a=0;a<command_son_num;a++){
    char* part[1024];
    char* token = strtok(command_son[count], " ");
    int part_num = 0;
    int j = 0;
    while (token != NULL) {
        part[j] = token;
        token = strtok(NULL, " ");
        j++;
    }
    part[j] = NULL;
    token = strtok(command_son[count], " ");
    if(execvp(token, part)==-1){
    printf("Cann't found this command\n");}
    exit(1);
    // }
}

void PipeCommand(char** command_son, int pipe_num, int command_son_num) {
    int pipes[pipe_num][2];
    pid_t pid[command_son_num];
    /*创建管道*/
    for (int i = 0; i < pipe_num; i++) {
        if (pipe(pipes[i]) < 0) {
            printf("Pipe falied\n");
            exit(1);
        }
    }
    /*新建pipe_num+1个进程*/
    for (int i = 0; i < command_son_num; i++) {
        if ((pid[i] = fork()) == 0) {
            if (i == 0) {
                // First process
                dup2(pipes[i][1], STDOUT_FILENO);
                /*如果第一个进程不关闭管道的读取端,它会一直等待从管道中读取数据,因为在后续进程中,前面的进程已经将数据通过管道传递给了后面的进程,这时第一个进程已经没有必要从管道中读取数据了。所以第一个进程关闭管道的读取端是为了让后续进程可以从管道中获取数据。*/
                close(pipes[i][0]);
                close(pipes[i][1]);
                // do_execvp(command_son, i, command_son_num);
                // exit(0);
            } else if (i == command_son_num - 1) {
                // Last process
                dup2(pipes[i - 1][0], STDIN_FILENO);
                close(pipes[i - 1][0]);
                close(pipes[i - 1][1]);
                // do_execvp(command_son, i, command_son_num);
                // exit(0);
            } else {
                // Middle process
                dup2(pipes[i - 1][0], STDIN_FILENO);
                dup2(pipes[i][1], STDOUT_FILENO);
                close(pipes[i - 1][0]);
                close(pipes[i - 1][1]);
                close(pipes[i][0]);
                close(pipes[i][1]);
                // do_execvp(command_son, i, command_son_num);
                // exit(0);
            }
            for (int i = 0; i < pipe_num; i++) {
                close(pipes[i][0]);
                close(pipes[i][1]);
            }
            do_execvp(command_son, i, command_son_num);
            exit(0);
        }
    }
    /*关闭所有管道*/
    for (int i = 0; i < pipe_num; i++) {
        close(pipes[i][0]);
        close(pipes[i][1]);
    }
    /*通过调用waitpid()函数等待子进程的退出,父进程可以及时获得子进程的退出状态,并且在所有子进程都退出之后结束自己的执行,以便能够释放它们占用的系统资源,避免产生僵尸进程。*/
    for (int i = 0; i < command_son_num; i++) {
        waitpid(pid[i], NULL, 0);
    }
}
void signalhandle(int sig) {
    // if(signal_flag)    return;
    // signal_flag =true;
    // printing new line
    printf("\n");

    //打印prompt
    Prompt();

    //清空
    fflush(stdin);
    fflush(stdout);
    fflush(stderr);
}

敬请期待!

  • 0
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值