【精通 Readline 库】:优化 Shell 外壳程序的艺术

news2024/11/19 6:30:17

在这里插入图片描述

📃博客主页: 小镇敲码人
💚代码仓库,欢迎访问
🚀 欢迎关注:👍点赞 👂🏽留言 😍收藏
🌏 任尔江湖满血骨,我自踏雪寻梅香。 万千浮云遮碧月,独傲天下百坚强。 男儿应有龙腾志,盖世一意转洪荒。 莫使此生无痕度,终归人间一捧黄。🍎🍎🍎
❤️ 什么?你问我答案,少年你看,下一个十年又来了 💞 💞 💞


【精通 Readline 库】:优化 Shell 外壳程序的艺术

  • 需要优化的点
  • readline库
    • 主要功能
    • 常用函数
      • Tab键自动补全功能的实现
      • 键绑定与回调函数实现自定义快捷键
    • Linux下安装readline库
    • C语言编译链接使用readline库
  • myshell程序增加readline库的相关功能

需要优化的点

上述我们的Shell外壳程序虽然也能执行一些内部指令和外部指令,但是还有一个点需要我们去改进,那就是是我们发现,我们很容易在终端输错指令,一输错就需要重新输,因为我们的Shell外壳程序还不支持行编辑。除此之外能否让我们的Shell外壳程序提供历史记录呢?

image-20241118100433414

  • 可以看到我们的shell外壳程序目前是不支持记忆搜索和删除键的,它们都被显示成了指令,但是Bash是支持的:

    img

我们只需要几种方法,把这特定的字符转化为相应的操作即可,而这些操作在我们的readline库中都提供了,我们只需要学习它即可,下面我们来介绍一下readline库,以及我们会用到的几种函数。

readline库

readline是一个常用的库,主要用于命令行输入功能的增强,它能够提供自动补全,行编辑,和提供历史记录等功能。使用readline库,可以让用户在交互式命令程序中更加方便。

主要功能

  1. 行编辑:支持在命令行中使用删除键、箭头键等对输入进行编辑。
  2. 自动补全:可以为用户输入提供补全建议,比如文件名或命令参数。
  3. 历史记录:记录用户输入的命令,并允许用户通过上下键查看。
  4. 自定义功能:支持开发者实现自定义补全逻辑和按键绑定。

常用函数

下面是readline库的一些常用函数:

  1. char * readline (const char *prompt)

    • 功能:显示提示符并读取输入的字符串,返回一个动态分配的字符串。
    • 参数
      • prompt:提示符,可以传空字符串。
    • 返回值:用户输入的字符串,需要使用free手动释放。
    • 头文件<readline/readline.h>

    我们查手册发现,返回值是可能为空的,在free的时候判断一下即可,防止无意义的调用:

    image-20241118103758850

    示例代码

    #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;
    }
    

    运行结果

    image-20241118104542845

  2. void add_history(const char *line)

    • 功能:将用户输入的命令加入历史记录。

    • 参数

      • line:用户输入的命令。

    示例代码

    add_history(line);
    
  3. void rl_initialize(void)

    • 功能:显式初始化函数,用于配置readline库的某些内部状态,在调用readline的某些功能函数前,建议调用这个函数以确保readlin环境正确初始化。
    • 什么时候需要调用这个函数:当你调用了某些低级readline配置函数,如(手动设置历史记录或者修改补全规则),而没有显示调用readline函数,这个函数是必须的。大多数简单场景(仅仅调用readline()add_history())它是可选的,readline()函数会自动隐式初始化。
  4. 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键自动补全功能的实现

先介绍一个函数:

  1. 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;
}

运行结果

屏幕录制-2024-11-18-211159

这个自动补全功能是自定义功能,需要自己实现两个函数:

  1. char** my_completion(const char* text,int start,int end)

    • 参数
      • text:用户输入的待补全的指令。

      • start:当前待补全单词的起始索引,从0开始计算(交互信息不算在索引中)。

      • end:当前待补全单词的结束索引,它指向待补全单词的最后一个字符的后一个位置。


    • 返回值:返回值是一个char*的数组,存全部的补全建议,数组的最后一个元素一定是NULL,标志补全建议的结束。

    利用start和end可以确定待补全的位置,利用它们可以实现不同的补全逻辑,完成更丰富的功能。

  2. rl_attempted_completion_function:这个是一个函数指针的全局变量,在readline库中定义,它的类型是char**(*)(const char*,int,int),当用户按下Tab键触发了自动补全功能,readline会去调用这个函数生成补全内容。

  3. char* command_generator(const char* text,int state):

    • 参数
      • text:待补全的单词。
      • state:statereadline调用rl_completion_matches传递过来的,初次调用会初始化为0(用于初始化生成器的状态),后面的调用都不为0,直到没有更多匹配项,下次开始补全时,又会被初始化为0。
    • 返回值:返回一个动态分配的char*指针,它里面是匹配的字符串,或者传NULL表示没有更多的匹配项,这个空间不需要我们维护,readline库帮我们释放。

这三个函数的调用逻辑:

6b2339d382161c49a6c5be18e66fac5

键绑定与回调函数实现自定义快捷键

介绍一些函数:

  1. 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值,表示绑定失败。
  2. 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;
}

运行结果:

image-20241118230123612

  • 某些多字节按键无法使用单个值表示,需要使用rl_generic_bind绑定码值序列,我们不再介绍。

    Linux下安装readline库

根据你使用的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-devreadline-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;
}

运行结果

屏幕录制-2024-11-18-211159_1

  • 本人知识、能力有限,若有错漏,烦请指正,非常非常感谢!!!
  • 转发或者引用需标明来源。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2243216.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

ESP-IDF VScode 项目构建/增加组件 新手友好!!!

项目构建 1.新建文件夹&#xff0c;同时在该文件夹内新建.c和.h文件 如图所示&#xff0c;在components中新建ADC_User.c、ADC_User.h、CMakeLists.txt文件。当然这里你也可以不在components文件夹内新建文件&#xff0c;下面会说没有在components文件夹内新建文件构建项目的方…

玩转N1盒子:速刷OpenWRT软路由系统并实现公网访问管理

文章目录 前言1. 制作刷机固件U盘1.1 制作刷机U盘需要准备以下软件&#xff1a;1.2 制作步骤 2. N1盒子降级与U盘启动2.1 N1盒子降级2.2 N1盒子U盘启动设置2.3 使用U盘刷入OpenWRT2.4 OpenWRT后台IP地址修改2.5 设置旁路由&无线上网 3. 安装cpolar内网穿透3.1 下载公钥3.2 …

机器学习4

九、线性回归 1、概念 假设存在多个点&#xff0c;需要使用一条线来保障尽量拟合这些点&#xff0c;寻找这条线就叫回归。 机器学习中一种有监督学习的算法,回归问题主要关注的是因变量(需要预测的值)和一个或多个数值型的自变量(预测变量)之间的关系。 2、损失函数 存…

【Java EE初阶---多线程(初阶)】初识计算机

乐观学习&#xff0c;乐观生活&#xff0c;才能不断前进啊&#xff01;&#xff01;&#xff01; 我的主页&#xff1a;optimistic_chen 我的专栏&#xff1a;c语言 &#xff0c;Java &#xff0c;Java数据结构 欢迎大家访问~ 创作不易&#xff0c;大佬们点赞鼓励下吧~ 文章目录…

网络安全之国际主流网络安全架构模型

目前&#xff0c;国际主流的网络安全架构模型主要有&#xff1a; ● 信息技术咨询公司Gartner的ASA&#xff08;Adaptive Security Architecture自适应安全架构&#xff09; ● 美国政府资助的非营利研究机构MITRE的ATT&CK&#xff08;Adversarial Tactics Techniques &…

生成式人工智能(AIGC)在软件开发设计模式课程教学中的应用

一、引言 软件设计模式作为软件工程领域的核心组成部分&#xff0c;对于提升软件系统的质量和可维护性至关重要。然而&#xff0c;传统的软件设计模式课程教学方法面临着诸多挑战&#xff0c;例如教师准备教学案例的过程繁琐&#xff0c;学生理解和应用具体案例难度较大&#…

丹摩征文活动|摩智算平台深度解析:Faster R-CNN模型的训练与测试实战

目录 文章前言Faster R-CNN的简介Faster RCNN的训练与测试提前准备1.1 mobaxterm&#xff08;远程连接服务器&#xff09;1.2 本文的源码下载 目标检测模型 Faster-Rcnn2.1云服务器平台 数据上传内置JupyterLab的使用本地连接使用DAMODEL实例获取实例的SSH访问信息通过SSH连接通…

【实用教程】如何利用 JxBrowser 在 Kotlin 中实现屏幕共享

JxBrowser是一个跨平台的 JVM 库&#xff0c;它允许您将基于 Chromium 的 Browser 控件集成到 Compose、Swing、JavaFX、SWT 应用程序中&#xff0c;并使用 Chromium 的数百种功能。为了在 Kotlin 中实现屏幕共享&#xff0c;我们利用了 Chromium 的 WebRTC 支持以及 JxBrowser…

无人机动力系统节能技术的未来发展趋势——CKESC电调小课堂12.1

无人机动力系统节能技术的未来发展趋势包括以下几个方面&#xff1a; 1. 能源类型多元化与高效化 新型电池技术的发展&#xff1a;锂离子电池的性能将不断提升&#xff0c;能量密度增加、充放电速度加快、循环寿命延长。同时&#xff0c;固态电池技术有望取得突破并应用于无人…

【汇编语言】数据处理的两个基本问题(二) —— 解密汇编语言:数据长度与寻址方式的综合应用

文章目录 前言1. 指令要处理的数据有多长&#xff1f;1.1 通过寄存器指明数据的尺寸1.1.1 字操作1.1.2 字节操作 1.2 用操作符X ptr指明内存单元的长度1.2.1 访问字单元1.2.2 访问字节单元1.2.3 为什么要用操作符X ptr指明 1.3 其他方法 2. 寻址方式的综合应用2.1 问题背景&…

【算法】【优选算法】前缀和(下)

目录 一、560.和为K的⼦数组1.1 前缀和1.2 暴力枚举 二、974.和可被K整除的⼦数组2.1 前缀和2.2 暴力枚举 三、525.连续数组3.1 前缀和3.2 暴力枚举 四、1314.矩阵区域和4.1 前缀和4.2 暴力枚举 一、560.和为K的⼦数组 题目链接&#xff1a;560.和为K的⼦数组 题目描述&#x…

【进阶系列】正则表达式 #匹配

正则表达式 正则表达式是一个特殊的字符序列&#xff0c;它能帮助你方便的检查一个字符串是否与某种模式匹配。re模块使 Python 语言拥有全部的正则表达式功能。 一个正则表达式的匹配工具&#xff1a;regex101: build, test, and debug regex s "C:\\a\\b\\c" pri…

【技术解析】Dolphinscheduler实现MapReduce任务的高效管理

MapReduce是一种编程模型&#xff0c;用于处理和生成大数据集&#xff0c;主要用于大规模数据集&#xff08;TB级数据规模&#xff09;的并行运算。本文详细介绍了Dolphinscheduler在MapReduce任务中的应用&#xff0c;包括GenericOptionsParser与args的区别、hadoop jar命令参…

Debezium-MySqlConnectorTask

文章目录 概要整体架构流程技术名词解释技术细节小结 概要 MySqlConnectorTask&#xff0c;用于读取MySQL的二进制日志并生成对应的数据变更事件 整体架构流程 技术名词解释 数据库模式&#xff08;Database Schema&#xff09; 数据库模式是指数据库中数据的组织结构和定义&…

剧本杀门店预约小程序,解锁沉浸式推理体验

一、开发背景 剧本杀作为一种热门娱乐游戏&#xff0c;深受大众的欢迎&#xff0c;但随着市场的快速发展&#xff0c;竞争也在不断加大&#xff0c;对于剧本杀线下商家来说面临着发展创新。 剧本杀线下门店数量目前正在逐渐增加&#xff0c;竞争激烈&#xff0c;而门店的获客…

今天你学C++了吗——C++启航之入门知识

♥♥♥~~~~~~欢迎光临知星小度博客空间~~~~~~♥♥♥ ♥♥♥零星地变得优秀~也能拼凑出星河~♥♥♥ ♥♥♥我们一起努力成为更好的自己~♥♥♥ ♥♥♥如果这一篇博客对你有帮助~别忘了点赞分享哦~♥♥♥ ♥♥♥如果有什么问题可以评论区留言或者私信我哦~♥♥♥ ✨✨✨✨✨✨ 个…

【element-tiptap】Tiptap编辑器核心概念----结构篇

core-concepts 前言&#xff1a;这篇文章来介绍一下 Tiptap 编辑器的一些核心概念 &#xff08;一&#xff09;结构 1、 Schemas 定义文档组成方式。一个文档就是标题、段落以及其他的节点组成的一棵树。 每一个 ProseMirror 的文档都有一个与之相关联的 schema&#xff0c;…

LC69---219存在重复元素(滑动窗口)---Java版

1.题目描述 2.思路 3.代码实现 public class Solution { public boolean containsNearbyDuplicate(int[] nums, int k) {Map<Integer,Integer> m1new HashMap<>();// 1:0, 2:1,3:2,1:3 key存数组的值&#xff0c;value存索引&#xff0c;为getnum[i]做准备&am…

【C++】了解map和set及平衡二叉树和红黑树的原理

目录 ​编辑 一、关联式容器 二、 键值对 三、pair介绍 四、树形结构的关联式容器 4.1 set 4.2 map 4.3 multiset 4.4 multimaps 五、底层结构&#xff08;重点&#xff09; 5.1 AVL 树 5.1.1 AVL树的概念 5.1.2 AVL树节点的定义 5.1.3 AVL树的旋转 5.1.4 AVL树的…

LeetCode 力扣 热题 100道(五)最长回文子串(C++)

最长回文子串 给你一个字符串 s&#xff0c;找到 s 中最长的 回文子串。 回文性 如果字符串向前和向后读都相同&#xff0c;则它满足 回文性 子字符串子字符串 是字符串中连续的 非空 字符序列。 动态规划法 class Solution { public:string longestPalindrome(string s) {i…