🦀

Clap製CLIツールでtab補完スクリプトを生成して.debでインストールできるようにする

2024/08/22に公開

Clap製CLIツールでtab補完スクリプトを生成して.debでインストールできるようにする記事です。

tab補完スクリプトを生成する

今回は例としてこんな感じのツールのtab補完スクリプトを生成してみようと思います。

src/main.rs
use std::path::PathBuf;

use clap::{Parser, Subcommand, ValueEnum};

#[derive(Parser)]
struct Cli {
    #[clap(subcommand)]
    command: Commands,
}

#[derive(Subcommand)]
enum Commands {
    /// Command1 description
    Command1 {
        #[clap(long, help = "String argument")]
        arg1: String,
        #[clap(value_enum, long, help = "MyEnum variant")]
        arg2: MyEnum,
        #[clap(long, help = "Path to a directory")]
        arg3: PathBuf,
        #[clap(long, help = "Optional argument")]
        arg4: Option<String>,
    },
    /// Command2 description
    Command2 {
        // none
    },
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
enum MyEnum {
    Variant1,
    Variant2,
}

fn main() {
    let args = Cli::parse();
    match args.command {
        Commands::Command1 {
            arg1,
            arg2,
            arg3,
            arg4,
        } => {
            println!(
                "Command1: arg1={}, arg2={:?}, arg3={:?}, arg4={:?}",
                arg1, arg2, arg3, arg4
            );
        }
        Commands::Command2 {} => {
            println!("Command2");
        }
    }
}

1. コマンドの定義部分を別ファイルに切り出す

ここではsrc/commands.rsに切り出してみます。

src/commands.rs
src/commands.rs
use clap::{Parser, Subcommand, ValueEnum, ValueHint};
use std::path::PathBuf;

#[derive(Parser)]
pub struct Cli {
    #[clap(subcommand)]
    pub command: Commands,
}

#[derive(Subcommand)]
pub enum Commands {
    /// Command1 description
    Command1 {
        #[clap(long, help = "String argument")]
        arg1: String,
        #[clap(value_enum, long, help = "MyEnum variant")]
        arg2: MyEnum,
        #[clap(long, help = "Path to a directory")]
        arg3: PathBuf,
        #[clap(long, help = "Optional argument")]
        arg4: Option<String>,
    },
    /// Command2 description
    Command2 {
        // none
    },
}

#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, ValueEnum, Debug)]
pub enum MyEnum {
    Variant1,
    Variant2,
}
src/main.rs
src/main.rs
mod commands;
use clap::Parser;
use commands::{Cli, Commands};

fn main() {
    let args = Cli::parse();
    match args.command {
        Commands::Command1 {
            arg1,
            arg2,
            arg3,
            arg4,
        } => {
            println!(
                "Command1: arg1={}, arg2={:?}, arg3={:?}, arg4={:?}",
                arg1, arg2, arg3, arg4
            );
        }
        Commands::Command2 {} => {
            println!("Command2");
        }
    }
}

main.rsから参照可能にするために適宜pubをつけるのを忘れないでください。

2. 一部のargにvalue_hintをつける

src/commands.rs
+ use clap::ValueHint;

pub enum Commands {
    Command1 {
        #[clap(
            long, 
            help = "Path to a directory",
+             value_hint = ValueHint::DirPath,
        )]
        arg3: PathBuf,
    }
}

例えばこんな感じにすると、arg3のtab補完にはファイルへのパスは出なくなり、ディレクトリへのパスだけが表示されるようになります。

3. build.rsでtab補完スクリプトを生成する

Rustには、プロジェクトのルートにbuild.rsを準備しておくとビルド時に自動で実行してくれる機能があります。これを使ってビルドするたびにtab補完スクリプトを生成するようにします。

tab補完スクリプトはclap_completeクレートを使うと生成できます。

build時のdependenciesは通常時のdependenciesとは完全に別で扱われるため、とりあえずCargo.tomlに以下を追加します

Cargo.toml
[package]
name = "foo"
version = "0.1.0"
edition = "2021"

+ [build-dependencies]
+ clap = {version = "4.5.16", features = ["derive"]}
+ clap_complete = "4.5.22"

[dependencies]
clap = {version = "4.5.16", features = ["derive"]}

(commands.rsでclap以外のdependencyを使っている場合はそれも追加してください)

次にプロジェクトのルートにbuild.rsを設置します。(src配下ではなくプロジェクトのルートです)

build.rs
use std::{env, fs, io};
use clap::{CommandFactory, ValueEnum};
use clap_complete::Shell;

#[path = "src/commands.rs"]
mod commands;
use commands::Cli;

fn main() -> io::Result<()> {
    fs::create_dir_all("dist/completions")?;

    for &shell in Shell::value_variants() {
        clap_complete::generate_to(
            shell,
            &mut Cli::command(),
            env!("CARGO_PKG_NAME"),
            "dist/completions",
        )?;
    }

    Ok(())
}
#[path = "src/commands.rs"]
mod commands;

ここでbuild.rsからcommands.rsを読みに行って使えるようにしています。build.rsがsrc配下ではなくプロジェクトのルートにあるため、#[path = "src/commands.rs"]が必要になっています。

    fs::create_dir_all("dist/completions")?;

ここで(無ければ)completionを出力するディレクトリを作成して、

    for &shell in Shell::value_variants() {
        clap_complete::generate_to(
            shell,
            &mut Cli::command(),
            env!("CARGO_PKG_NAME"),
            "dist/completions",
        )?;
    }

ここでclap_completeを使って各シェルについてtab補完スクリプトを生成しています。

.debパッケージでインストールできるようにする

Rustで作ったバイナリを.debパッケージ化するツールにcargo-debというものがあります
単純に.debパッケージ化するだけならcargo install cargo-debして、Cargo.tomlにlicenseやらauthorsやらを指定してcargo debすればもう動きます

ただ、これだけだとtab補完まではインストールできません
そのための設定をもう少し書き足します

Cargo.toml
[package.metadata.deb]
assets = [
  ["target/release/foo", "usr/bin/foo", "755"],
  ["dist/completions/foo.bash", "usr/share/bash-completion/completions/foo.bash", "644"],
  ["dist/completions/foo.fish", "usr/share/fish/vendor_completions.d/foo.fish", "664"],
  ["dist/completions/_foo", "usr/share/zsh/vendor-completions/_foo", "644"],
]

(fooのところはおのおののCARGO_PKG_NAMEに置き換えてください)
こんな感じで指定してやると自動でインストールしてくれるようになります

["target/release/foo", "usr/bin/foo", "755"],を忘れないように注意してください(デフォルトのassetsを上書きしてしまうためこれも必要になる)

完成

完成です🎉

Discussion