📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞
【精通 Readline 库】:优化 Shell 外壳程序的艺术
- 需要优化的点
- readline库
- 主要功能
- 常用函数
- Tab键自动补全功能的实现
- 键绑定与回调函数实现自定义快捷键
- Linux下安装readline库
- C语言编译链接使用readline库
- myshell程序增加readline库的相关功能
需要优化的点
上述我们的Shell
外壳程序虽然也能执行一些内部指令和外部指令,但是还有一个点需要我们去改进,那就是是我们发现,我们很容易在终端输错指令,一输错就需要重新输,因为我们的Shell
外壳程序还不支持行编辑。除此之外能否让我们的Shell
外壳程序提供历史记录呢?
-
可以看到我们的
shell
外壳程序目前是不支持记忆搜索和删除键的,它们都被显示成了指令,但是Bash
是支持的:
我们只需要几种方法,把这特定的字符转化为相应的操作即可,而这些操作在我们的readline
库中都提供了,我们只需要学习它即可,下面我们来介绍一下readline
库,以及我们会用到的几种函数。
readline库
readline
是一个常用的库,主要用于命令行输入功能的增强,它能够提供自动补全,行编辑,和提供历史记录等功能。使用readline
库,可以让用户在交互式命令程序中更加方便。
主要功能
- 行编辑:支持在命令行中使用删除键、箭头键等对输入进行编辑。
- 自动补全:可以为用户输入提供补全建议,比如文件名或命令参数。
- 历史记录:记录用户输入的命令,并允许用户通过上下键查看。
- 自定义功能:支持开发者实现自定义补全逻辑和按键绑定。
常用函数
下面是readline
库的一些常用函数:
-
char * readline (const char *prompt)
- 功能:显示提示符并读取输入的字符串,返回一个动态分配的字符串。
- 参数:
prompt
:提示符,可以传空字符串。
- 返回值:用户输入的字符串,需要使用
free
手动释放。 - 头文件:
<readline/readline.h>
。
我们查手册发现,返回值是可能为空的,在
free
的时候判断一下即可,防止无意义的调用:示例代码:
#include<stdio.h> #include<stdlib.h> #include <readline/readline.h> int main() { char* re_str = readline("Enter a command: "); if(re_str) { printf("you enter command:%s\n",re_str); free(re_str); } return 0; }
运行结果:
-
void add_history(const char *line)
-
功能:将用户输入的命令加入历史记录。
-
参数:
line
:用户输入的命令。
示例代码:
add_history(line);
-
-
void rl_initialize(void)
- 功能:显式初始化函数,用于配置
readline
库的某些内部状态,在调用readline
的某些功能函数前,建议调用这个函数以确保readlin
环境正确初始化。 - 什么时候需要调用这个函数:当你调用了某些低级
readline
配置函数,如(手动设置历史记录或者修改补全规则),而没有显示调用readline
函数,这个函数是必须的。大多数简单场景(仅仅调用readline()
和add_history()
)它是可选的,readline()
函数会自动隐式初始化。
- 功能:显式初始化函数,用于配置
-
int using_history(void)
- 功能:
using_history
用于初始化历史记录的支持环境(相应的数据结构),在使用历史记录相关的函数前(如add_history
),一定要调用这个函数。
- 返回值:返回0表示成功,没有实际失败的情况,但是可以作为扩展兼容性。
什么时候调用:
- 在程序开始时调用一次,以确保我们可以使用与历史记录相关的功能。
- 如果你不调用这个函数,直接调用与历史记录相关的函数,可能导致未定义行为。
- 头文件:
<readline/history.h>
。
示例代码:
#include<stdio.h> #include<stdlib.h> #include <readline/readline.h> #include <readline/history.h> int main() { char* input; using_history(); // 初始化历史记录 rl_initialize();//显式初始化readline环境 while((input = readline("Enter command:")) != NULL) { if(*input) { add_history(input);//如果input是有效的字符串 } printf("input:%s\n",input); } return 0; }
运行结果:
- 从运行结果中可以看到,引入
readline
,我们的命令行输入以及支持行编辑(删除键),以及查看历史命令。
- 功能:
Tab键自动补全功能的实现
先介绍一个函数:
char **rl_completion_matches(const char *text, rl_compentry_func_t *entry_func)
-
参数:
-
text
:当前用户输入的待补全的文本。 -
entry_func
:回调函数,用于生成与text
匹配的补全项。它会多次被调用,返回值是匹配项的字符串(动态分配的),直到返回NULL
表示没有更多匹配。rl_compentry_func_t
是生成器的函数类型,typedef char *rl_compentry_func_t(const char *text, int state);
,加上*
就是函数指针类型。
-
- 返回值:返回与
text
匹配的字符串数组,最后一个是NULL
。
示例代码:
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include <readline/readline.h>
#include <readline/history.h>
char *commands[] = {"pwd","ps","top","cd","cat","ping","man","chmod",NULL};
char* command_generator(const char* text,int state)
{
char* name;
static int index,len;
if(!state)
{
index = 0;
len = strlen(text);
}
while(name = commands[index])
{
if(strncmp(name,text,len) == 0)
{
index++;
return strdup(name);
}
index++;
}
return ((char*)NULL);
}
char** my_completion(const char* text,int start,int end)
{
char** matches;
matches = ((char**)NULL);
if(start == 0)
matches = rl_completion_matches(text,command_generator);
return (matches);
}
int main()
{
rl_initialize();//显式初始化readline环境
//将全局变量(回调函数变量)设置成我们的回调函数地址
rl_attempted_completion_function = my_completion;
char* input;
while((input = readline("Enter command:")) != NULL)
{
printf("input:%s\n",input);
if(input)
free(input);
}
return 0;
}
运行结果:
这个自动补全功能是自定义功能,需要自己实现两个函数:
-
char** my_completion(const char* text,int start,int end)
- 参数:
-
text
:用户输入的待补全的指令。 -
start
:当前待补全单词的起始索引,从0开始计算(交互信息不算在索引中)。 -
end
:当前待补全单词的结束索引,它指向待补全单词的最后一个字符的后一个位置。
-
- 返回值:返回值是一个char*的数组,存全部的补全建议,数组的最后一个元素一定是
NULL
,标志补全建议的结束。
利用start和end可以确定待补全的位置,利用它们可以实现不同的补全逻辑,完成更丰富的功能。
- 参数:
-
rl_attempted_completion_function
:这个是一个函数指针的全局变量,在readline
库中定义,它的类型是char**(*)(const char*,int,int)
,当用户按下Tab
键触发了自动补全功能,readline
会去调用这个函数生成补全内容。 -
char* command_generator(const char* text,int state)
:- 参数:
text
:待补全的单词。state
:state
由readline
调用rl_completion_matches
传递过来的,初次调用会初始化为0(用于初始化生成器的状态),后面的调用都不为0,直到没有更多匹配项,下次开始补全时,又会被初始化为0。
- 返回值:返回一个动态分配的
char*
指针,它里面是匹配的字符串,或者传NULL
表示没有更多的匹配项,这个空间不需要我们维护,readline
库帮我们释放。
- 参数:
这三个函数的调用逻辑:
键绑定与回调函数实现自定义快捷键
介绍一些函数:
-
int rl_bind_key(int key, rl_command_func_t *function)
:-
参数:
key
:标识键盘的编码值,这个值一般是表示字符的ASCII值或者一些特殊的组合键的值。像ctrl-字符
,就是经过计算了的,一般是字符的ASCII&0x1F
。普通键直接就是ASCII码值。rl_command_func_t
函数类型,这个函数是需要我们自定义的,表示这个按键和这个函数绑定,一旦检测到这个按键就会回调这个函数。这是这个变量的类型typedef int rl_command_func_t (int count, int key);
- 返回值:返回0,表示绑定成功,返回非0值,表示绑定失败。
-
-
int rl_command_func_t (int count, int key)
函数:-
参数:
-
count
:表示命令执行的重复次数,一般5ctrlx
,这个5就是count
,默认count为1。 -
key
:该按键的编码值。
-
- 返回值:0执行成功,非0未知状态。
-
示例代码,打印所有按键的编码值:
#include<stdio.h>
#include<sys/wait.h>
#include<unistd.h>
#include<stdlib.h>
#include<string.h>
#include <readline/readline.h>
#include <readline/history.h>
int PrintKey(int count,int key)
{
printf("\nkey pressed:%d,(character:'%c')\n",key,key);
}
int main()
{
for(int i = 0;i < 256;++i)
{
rl_bind_key(i,PrintKey);
}
char* input;
while(input = readline("Enter command:"))
{
if(input)
free(input);
}
return 0;
}
运行结果:
根据你使用的Linux发行版,使用不同的命令安装readline
库:
-
在Debian/Ubuntu系统下:
sudo apt update sudo apt install libreadline-dev
-
在Centos/RHEL系统下:
sudo yum install readline-devel
-
在Fedora系统下:
sudo dnf install readline-devel
-
在Arch Linuc系统下:
sudo pacman -S readline
这些命令会安装libreadline
和开发相关的头文件(libreadline-dev
或 readline-devel
),让你能够在 C 程序中链接并使用 Readline
库。
还可以使用其它安装方式不再介绍。
C语言编译链接使用readline库
makefile
文件:
myshell:myshell.c
gcc -o $@ $^ -lreadline -lhistory -g
.PHONY:clean
clean:
rm -f myshell
-
-lreadline
:连接readline
库。 -
-lhistory
:连接-lhistory
库。
myshell程序增加readline库的相关功能
上面我们介绍了readline
库的大部分常用函数和功能,下面我们将给我们的shell外壳程序增加如下功能:
- 支持行编辑。
- 支持上下键查看历史记录。
- 支持Tab键补全命令(部分)
完整代码:
#define SIZE 1024
#define HOSTNAMESIZE 20
#define MAX_ARGS 64
#define SEP " "
char* Args[MAX_ARGS] = {NULL};
char pwd[SIZE];
char env[SIZE];
int last_code = 0;
const char* Username()
{
char* username = getenv("USER");
return username ? username:"None";
}
const char* Hostname()
{
char* hostname = getenv("HOSTNAME");
return hostname ? hostname:"None";
}
const char* Currentpath()
{
char* currentpath = getenv("PWD");
return currentpath ? currentpath:"None";
}
char* Home()
{
return getenv("HOME");
}
int GetCommandPrompt(char** commandline)
{
printf("%s@%s:%s#",Username(),Hostname(),Currentpath());
*commandline = readline("Enter command:");
//添加到历史记录
add_history(*commandline);
return strlen(*commandline);
}
void Split(char in[])
{
int i = 0;
Args[i++] = strtok(in,SEP);
while(Args[i++] = strtok(NULL,SEP));
if(Args[0] && strcmp(Args[0],"ls") == 0)
{
Args[i-1] = "--color";
Args[i] = NULL;
}
}
void execute()
{
pid_t id = fork();
if(id == 0)
{
//子进程开始了
execvp(Args[0],Args);
exit(1);//如果执行到这里说明程序替换失败了
}
//只有父进程能执行到这里
int status = 0;
pid_t rid = waitpid(id,&status,0);//阻塞等待
last_code = WEXITSTATUS(status);
}
int ProcessInCommands()
{
//如果是内置命令,就返回1,不是就返回0
int ret = 0;
if(strcmp(Args[0],"cd") == 0)//先处理cd命令
{
ret = 1;
char* target = Args[1];
if(!target || strcmp(target,"~") == 0)
target = Home();
chdir(target);//改变myshell的工作目录
char tmp[1024];
getcwd(tmp,1024);
snprintf(pwd,SIZE,"PWD=%s",tmp);
putenv(pwd);
}
else if(strcmp(Args[0],"echo") == 0)
{
ret = 1;
if(Args[1] == NULL)
{
printf("\n");
}
else
{
if(Args[1][0] != '$')
{
printf("%s\n",Args[1]);
}
else
{
if(Args[1][1] == '?')
{
printf("%d\n",last_code);
last_code = 0;
}
else
{
char* e = getenv(Args[1]+1);
if(e)
printf("%s\n",e);
}
}
}
}
else if(strcmp(Args[0],"export") == 0)
{
ret = 1;
if(Args[1] != NULL)
{
strcpy(env,Args[1]);
putenv(env);
}
}
else if(strcmp(Args[0],"unset") == 0)
{
ret = 1;
if(Args[1])
{
unsetenv(Args[1]);
}
}
else if(strcmp(Args[0],"exit") == 0)
{
ret = 1;
exit(0);
}
return ret;
}
char *commands[] = {"pwd","ps","top","cd","cat","ping","man","chmod",NULL};
char* command_generator(const char* text,int state)
{
char* name;
static int index,len;
if(!state)
{
index = 0;
len = strlen(text);
}
while(name = commands[index])
{
if(strncmp(name,text,len) == 0)
{
index++;
return strdup(name);
}
index++;
}
return ((char*)NULL);
}
char** my_completion(const char* text,int start,int end)
{
char** matches;
matches = ((char**)NULL);
if(start == 0)
matches = rl_completion_matches(text,command_generator);
return (matches);
}
int main()
{
// 初始化历史记录
rl_initialize();
using_history();
rl_attempted_completion_function = my_completion;
while(1)
{
char* commandline = NULL;
//打印提示符,并获取命令
int n = GetCommandPrompt(&commandline);
if(n == 0)
continue;
Split(commandline);//分割命令参数
if(Args[0])
n = ProcessInCommands();
if(n)
continue;
execute();
if(commandline)//释放空间
free(commandline);
}
return 0;
}
运行结果:
- 本人知识、能力有限,若有错漏,烦请指正,非常非常感谢!!!
- 转发或者引用需标明来源。