likes
comments
collection
share

用rust写一个命令行工具(2)

作者站长头像
站长
· 阅读数 9

背景

但是这个命令行工具,在使用的时候有个致命的问题,就是没有命令提示。

比如我要执行my_dev_tool,按tab键我看不到任何命令的提示,这样太不人性化了。

这个文章教会你让自己命令行生成自动补全脚本,让系统shell识别你的命令。

命令补全原理

用clap的生成的命令工具可以通过clap_complete包生成一个补全脚本,把这个脚本加到bash环境就可以自动提示了。

为了生成这些脚本,并让系统能自动提示命令参数,你需要按以下步骤:

1. 修改依赖关系

新增两个依赖clap_completedirs

  1. clap_complete是用来生成补全脚本的。
  2. dirs是用来定位用户目录的。
[dependencies]
clap_complete = "4.4.0"
dirs = "4.0"

2. 在你的 Rust 程序中生成补全脚本

首先,你需要修改你的 Rust 程序,使其能够生成相应的补全脚本。这可以在程序的一个特定命令或选项下实现。

1、重构buildCommand方法

把命令构造的内容单独重构为一个方法,后面生成补全命令还需要这个。并添加一个添加bash脚本的命令add-completion

use clap::{Command, generate};
use clap_complete::{shells::{Bash, Zsh}, generate_to};
use std::env;
use std::io;
fn build_cli() -> Command {
    Command::new("my_dev_tool")
        .version("1.0")
        .author("tommy <mcg91881127@163.com>")
        .about("Developer's tool for urlencode and time format!")
        .subcommand_required(true)
        .arg_required_else_help(true)
        // 省略。。。。
        .subcommand(Command::new("add-completion")
            .about("Generates completion scripts for your shell"))
}

2. 生成补全脚本

这一步主要是生成补全命令的脚本,bash或者zsh脚本是根据这个脚本进行命令提示的。

fn add_completion(matches: &ArgMatches){
    let mut app = build_cli();
    let shell = env::var("SHELL").unwrap_or_default();
    let home_dir = dirs::home_dir().expect("Could not find the home directory");
    let config_file;
    if shell.contains("zsh") {
        config_file = home_dir.join(".zshrc");
        let _ = generate_to(Zsh, &mut app, "my_dev_tool", "./").expect("generate_to failed");
        println!("Generated Zsh completion script.");
    } else {
        config_file = home_dir.join(".bashrc");
        // 默认生成 Bash 补全脚本
        let _ = generate_to(Bash, &mut app, "my_dev_tool", "./").expect("generate_to failed");
        println!("Generated Bash completion script.");
    }
    let completion_script_path = PathBuf::from("./_my_dev_tool");

    let _ = add_completion_to_shell(&config_file, &completion_script_path);
}

在上面的代码中,我们添加了一个名为 add-completions 的子命令,当运行此命令时,程序将生成 Bash 的自动补全脚本。这里需要注意的是:

  1. generate_to(Bash, &mut app, "my_dev_tool", "./")这行代码里的my_dev_tool,是你要补全命令的名字,比如你的工具叫啥这里就填啥。我这个工具命令就是my_dev_tool,所以这里传my_dev_tool
  2. 这里生成的自动补全脚本的名字是_my_dev_tool,这个是默认的命名规则,生成的补全脚本叫“_”+“命令名”。
  3. 为了程序能够区分并生成 .bashrc.zshrc 的补全命令,你需要确定用户正在使用的是哪种 shell。这可以通过检查环境变量 SHELL 来实现,然后根据这个环境变量的值决定修改 .bashrc.zshrc

3. 将生成的补全脚本添加到你的 shell 配置中

生成的补全脚本需要被 source(或等效地添加)到你的 shell 配置文件中(例如 .bashrc, .zshrc 等),这样你的 shell 就能够利用这些脚本进行命令补全。有两种方式可以实现:

第一种,手动添加

将以下行添加到 .bashrc.bash_profile,zsh为.zshrc

source ~/_my_dev_tool

然后,重新加载配置文件,zsh为source ~/.zshrc

source ~/.bashrc

或者重新启动你的终端。

第二种,自动添加到bash或者zsh

然后,使用以下 Rust 代码来自动化添加 shell 配置的过程:

use std::env;
use std::fs::{self, OpenOptions};
use std::io::Write;
use std::path::PathBuf;

fn add_completion_to_shell(config_file: &PathBuf, completion_script_path: &PathBuf) -> std::io::Result<()> {
    let completion_script_str = format!("source {}", completion_script_path.display());
    
    let mut config = OpenOptions::new().append(true).open(config_file)?;

    if fs::read_to_string(config_file)?.contains(&completion_script_str) {
        println!("Completion script already added to {}.", config_file.display());
    } else {
        config.write_all(completion_script_str.as_bytes())?;
        println!("Added completion script to {}.", config_file.display());
    }

    Ok(())
}

为了程序能够区分并自动向 .bashrc.zshrc 添加 shell 配置,你需要确定用户正在使用的是哪种 shell。这可以通过检查环境变量 SHELL 来实现,然后根据这个环境变量的值决定修改 .bashrc.zshrc。以下是一个示例实现:

4. source一下命令行或者重启命令行

### zsh执行
source ~/.zshrc
### bash执行
source ~/bashrc

展示成果

在命令行输入 my_dev_tool后,按tab键会提示所有命令。

~ % my_dev_tool time
add-completion  -- Generates completion scripts for your shell
help            -- Print this message or the help of the given subcommand(s)
timestamp       -- Convert a UNIX timestamp to local datetime
urldecode       -- URL-decode a string
urlencode       -- URL-encode a string