本文最后更新于 18 天前
前言
这篇文章会从头开始使用C
语言编写一个可以交互的SHELL,并为其添加以下功能:
运行可执行文件
输入中断退出
cd/export/env/exit 内建命令
实现了|
管道通信
实现了部分内部命令自动高亮ls/grep/cat
记录历史命令
检查运行环境
未实现功能:
& 后台任务 / fg / bg 恢复执行
&& 依赖任务
|| 多任务
/ >> 重定向
任务要求
(1) 实现一个模拟的shell
(2) 实现一个管道通信程序
其实书上的写的任务里这些是独立的几个任务,但是我没注意看书上的要求,于是把(1)(2)都写在了SHELL中,也就是说我实现了一个可以使用
1 cat /path/to/file | grep symbel | grep symbel2
BASH
类似机制的指令的SHELL。这里记录、分析一下用到的一些系统调用/库函数。
前提
使用了readline
库,并且使用了Linux
管道调用,所以不能在Windows
上复现。
安装readline
库:
1 sudo apt update && sudo apt install libreadline-dev
BASH
创建简单的SHELL部分
对于shell,最重要最简单的功能就是能够读懂用户的指令了,比如最简单的:
1 2 3 4 5 6 7 8 9 ls cat file vim filepwd cd pathexit export something=anotherthingenv ^D
BASH
又或者复杂一些的命令:
1 2 ls | grep pro ./exe && ./exe2 || ./exe3
BASH
我们的主函数的逻辑就像下面的结构:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 int main () { char *input; char *prompt; char **args; setup_signal_handlers(); rl_attempted_completion_function = shell_completion; welcome(); while (1 ) { prompt = get_prompt(); input = readline(prompt); free (prompt); if (!input) { exit_shell(); } if (strlen (input) > 0 ) { add_history(input); execute_command(input); } free (input); } return 0 ; }
C
信号处理
从上往下看,首先第一个函数setup_signal_handlers
,你需要在这里处理shell的信号机制,比如用户的中断信号Ctrl+C
。普通的程序会使用正常的中断退出,但是作为一个shell,不应该在用户发出Ctrl+C
中断信号的时候退出,而是应该处理Ctrl+D
的输入终止信号才退出,所以我们可以得到一个简单的setup_ignal_handlers
函数的定义:
1 2 3 4 #include <signal.h> void setup_signal_handlers () { signal(SIGINT, SIG_IGN); }
C
这里的signal
函数很重要,它的函数声明和使用示例为:
1 2 3 4 5 6 7 **void (*signal(int sig, void (*func)(int )))(int )** signal(SIGINT, handleInterupt); signal(SIGINT, SIG_IGN); signal(SIGILL, SIG_DFL);
C
具体的细节可以参考菜鸟教程:C 库函数 – signal() | 菜鸟教程
我们只需要忽略掉SIGINT
信号即可。
正则补全
然后是下面这行神奇的代码:
1 2 #include <readline/readline.h> rl_attempted_completion_function = shell_completion;
C
右边是一个形如:
1 typedef char **rl_completion_func_t (const char * test, int start, int end)
C
的函数指针。该函数需要处理用户输入的不完整字符串的补全结果。readline
库提供了一个正则匹配的函数:
1 2 3 4 extern char **rl_completion_matches PARAMS ((const char *, rl_compentry_func_t *)) ;rl_compentry_func_t *generator = command_generator;char * result = rl_completion_matches(text, generator);
C
上面的示例中,我们需要定义一个正则规则函数,形如:
1 char *command_generator (const char *text, int state) ;
C
的匹配函数,在这个函数中返回我们补全的结果。如果想要了解更多Readline
中的补全机制,可以参考这篇博客。也可以直接读我的源码。
ReadLine自动补全分析 - LiuYanYGZ - 博客园
其实re_attempted_aompeltion_function
类似于一个回调函数,会在用户按下Tab
的时候尝试补全。
命令处理主体
再往后看,就是我们的函数主体了,在while循环中,我们需要处理用户的Enter
输入,并执行对应的操作。并且需要为用户提供当前运行环境的一些信息,包括用户名、机器信息、路径信息等。这部分操作我们在get_prompt
中实现,你可以随心所欲地实现你想要的提示词:
prompt
1 2 3 4 5 6 7 8 9 10 11 char *get_prompt () { char cwd[1024 ]; char *prompt = malloc (1024 ); struct passwd *pw = getpwuid(getuid()); if (getcwd(cwd, sizeof (cwd)) == NULL ) { strcpy (cwd, "?" ); } snprintf (prompt, 1024 , "%s%s@%s%s:%s%s%s$ " , GREEN, pw->pw_name, "myshell" , RESET, BLUE, cwd, RESET); return prompt; }
C
这里调用了getpwuid
、getuid
、getcwd
三个函数,你需要引入这些头文件:
1 2 #include <pwd.h> #include <unistd.h>
C
add_history
add_history
是readline
库提供的一个保存命令历史的函数,允许用户使用上下来查阅历史记录。因为上下箭头其实输入的也是一个字符串,可以判断并执行对应的操作。
execute_command
执行命令!到这里,用户已经输入了一个完整/不完整的命令,并按下了回车键,他希望你能帮他调用这些可执行文件,并得到对应的输出/或重定向到其他地方。首先允许我为你介绍一个分词函数:
1 char * strtok (char *str, const char * delimiters) ;
C
需要注意的是,这个函数会将第一参数分割,所以在处理它的时候需要进行复制处理。
下面是一个简单的循环取词的示例:
1 2 3 4 5 6 7 8 char * input = "I need a friend." ;char output[10 ][10 ];char * word = strtok(input, " \t\n" );int i = 0 ;while (word!=NULL ){ output[i] = word; word = strtok(NULL , " \t\n" ); }
C
更多有关strtok的信息,请参考:(十六)strtok、strtok_s、strtok_r 字符串分割函数 - xtusir - 博客园