From 5b2c30f817457dd3588a28151172f3149ef40b4d Mon Sep 17 00:00:00 2001 From: Eric Wendland Date: Sat, 23 May 2026 02:40:03 +0200 Subject: [PATCH] Add shell completion generation --- Cargo.lock | 10 +++++++ Cargo.toml | 1 + README.md | 4 ++- crates/geth-cli/Cargo.toml | 1 + crates/geth-cli/src/lib.rs | 51 ++++++++++++++++++++++++++++++++-- crates/geth/tests/bootstrap.rs | 30 ++++++++++++++++++++ docs/roadmap.md | 4 +++ 7 files changed, 98 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 707fa39..a5d5156 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -591,6 +591,15 @@ dependencies = [ "strsim", ] +[[package]] +name = "clap_complete" +version = "4.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772" +dependencies = [ + "clap", +] + [[package]] name = "clap_derive" version = "4.6.1" @@ -1512,6 +1521,7 @@ dependencies = [ "anyhow", "base64", "clap", + "clap_complete", "geth-cas", "geth-config", "geth-control", diff --git a/Cargo.toml b/Cargo.toml index d0a83d1..a56b211 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,6 +42,7 @@ base64 = "0.22" blake3 = "1" bytes = "1" clap = { version = "4", features = ["derive", "env"] } +clap_complete = "4" directories = "5" # Compatibility pins for iroh 0.95's ed25519-dalek prerelease dependency. ed25519 = "=3.0.0-rc.1" diff --git a/README.md b/README.md index 3e1b247..8fb5825 100644 --- a/README.md +++ b/README.md @@ -86,9 +86,11 @@ metadata from an authorized peer over Iroh. The bootstrap implementation provides: -- `geth guide [init|owner-setup|enrollment|keys|overlay|service|smoke-test]` for +- `geth guide [init|owner-setup|enrollment|keys|overlay|service|completions|smoke-test]` for embedded workflow help, including `--admin-key` / `--signing-key` setup examples +- `geth completions ` for shell completion + scripts generated from the live CLI command tree - `geth init` - `geth init --admin-key --signing-key --node-name ` - `geth daemon run` diff --git a/crates/geth-cli/Cargo.toml b/crates/geth-cli/Cargo.toml index 39f1451..71b5cc9 100644 --- a/crates/geth-cli/Cargo.toml +++ b/crates/geth-cli/Cargo.toml @@ -9,6 +9,7 @@ license.workspace = true anyhow.workspace = true base64.workspace = true clap.workspace = true +clap_complete.workspace = true serde_json.workspace = true tokio.workspace = true geth-config = { path = "../geth-config" } diff --git a/crates/geth-cli/src/lib.rs b/crates/geth-cli/src/lib.rs index 4abb094..219d81d 100644 --- a/crates/geth-cli/src/lib.rs +++ b/crates/geth-cli/src/lib.rs @@ -1,16 +1,18 @@ use anyhow::{Context, Result, bail}; use base64::Engine; -use clap::{Args, Parser, Subcommand, ValueEnum}; +use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum}; +use clap_complete::{Shell, generate}; use geth_config::GethPaths; use geth_control::{ControlRequest, ControlResponse}; use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport}; -use std::io::Read; +use std::io::{Read, stdout}; use std::path::PathBuf; const TOP_LEVEL_AFTER_HELP: &str = r#"Common starts: geth guide init geth guide owner-setup geth guide overlay + geth guide completions geth init geth init --admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk --node-name laptop geth daemon run @@ -73,6 +75,7 @@ Topics: keys Meaning of --admin-key and --signing-key. overlay Optional Iroh overlay network planning. service Install and manage geth as a user service. + completions Shell completion installation examples. smoke-test Minimal commands to verify a node and daemon."#; const GUIDE_INIT: &str = r#"geth init has two modes. @@ -178,6 +181,34 @@ Preview definitions without installing: geth daemon service print "#; +const GUIDE_COMPLETIONS: &str = r#"Shell completions: + +geth can print completions for bash, zsh, fish, PowerShell, and elvish. The +generated scripts are produced from the same Clap command tree as `geth --help`, +so subcommands and flags stay in sync with the executable. + +Bash: + mkdir -p ~/.local/share/bash-completion/completions + geth completions bash > ~/.local/share/bash-completion/completions/geth + +Zsh: + mkdir -p ~/.zfunc + geth completions zsh > ~/.zfunc/_geth + # Ensure ~/.zfunc is in fpath, then run: compinit + +Fish: + mkdir -p ~/.config/fish/completions + geth completions fish > ~/.config/fish/completions/geth.fish + +PowerShell: + geth completions powershell > geth.ps1 + # Source geth.ps1 from your PowerShell profile. + +Elvish: + mkdir -p ~/.elvish/lib + geth completions elvish > ~/.elvish/lib/geth.elv +"#; + const GUIDE_OVERLAY: &str = r#"Optional overlay network: geth has an experimental overlay-network design inspired by iroh-lan. The @@ -256,6 +287,10 @@ pub enum Command { #[arg(value_enum)] topic: Option, }, + Completions { + #[arg(value_enum)] + shell: Shell, + }, #[command(long_about = INIT_LONG_ABOUT, after_long_help = INIT_AFTER_HELP)] Init { #[arg( @@ -366,6 +401,7 @@ pub enum GuideTopic { Keys, Overlay, Service, + Completions, SmokeTest, } @@ -1063,6 +1099,10 @@ pub struct EmptyArgs {} pub async fn run() -> Result<()> { let cli = Cli::parse(); + if let Command::Completions { shell } = &cli.command { + print_completions(*shell); + return Ok(()); + } let paths = GethPaths::resolve().context("resolve geth paths")?; match cli.command { Command::Guide { topic } => { @@ -1169,6 +1209,7 @@ fn print_guide(topic: Option, json: bool) -> Result<()> { Some(GuideTopic::Keys) => ("keys", GUIDE_KEYS), Some(GuideTopic::Overlay) => ("overlay", GUIDE_OVERLAY), Some(GuideTopic::Service) => ("service", GUIDE_SERVICE), + Some(GuideTopic::Completions) => ("completions", GUIDE_COMPLETIONS), Some(GuideTopic::SmokeTest) => ("smoke-test", GUIDE_SMOKE_TEST), }; if json { @@ -1185,8 +1226,14 @@ fn print_guide(topic: Option, json: bool) -> Result<()> { Ok(()) } +fn print_completions(shell: Shell) { + let mut command = Cli::command(); + generate(shell, &mut command, "geth", &mut stdout()); +} + fn request_for_command(command: Command) -> Result { Ok(match command { + Command::Completions { .. } => bail!("completion generation does not use the daemon"), Command::Status => ControlRequest::Status, Command::Sync { command: SyncCommand::Status, diff --git a/crates/geth/tests/bootstrap.rs b/crates/geth/tests/bootstrap.rs index 2799f85..5fece6d 100644 --- a/crates/geth/tests/bootstrap.rs +++ b/crates/geth/tests/bootstrap.rs @@ -160,6 +160,36 @@ fn guide_command_explains_overlay_boundaries() { assert!(stdout.contains("all overlay packets must be carried over Iroh")); } +#[test] +fn completions_are_generated_without_daemon() { + let home = tempfile::tempdir().expect("tempdir"); + let output = run_geth(home.path(), &["completions", "bash"]); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("_geth")); + assert!(stdout.contains("overlay")); + assert!(stdout.contains("completions")); +} + +#[test] +fn guide_command_explains_completion_installation() { + let home = tempfile::tempdir().expect("tempdir"); + let output = run_geth(home.path(), &["guide", "completions"]); + assert!( + output.status.success(), + "stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert!(stdout.contains("geth completions bash")); + assert!(stdout.contains("PowerShell")); + assert!(stdout.contains("fish")); +} + #[test] fn owner_init_requires_admin_key_and_signing_key() { let home = tempfile::tempdir().expect("tempdir"); diff --git a/docs/roadmap.md b/docs/roadmap.md index d960484..c99a5c2 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -65,6 +65,10 @@ Implementation order: - `[x]` README has a two-machine walkthrough for the main smoke tests. - `[x]` CLI recovery errors tell operators the next command to run. - `[x]` JSON sync status is script-friendly for stale/failed peer detection. + - `[x]` Shell completions are generated from the Clap command tree for + bash, zsh, fish, PowerShell, and elvish. + - `[x]` Completion installation examples are available through + `geth guide completions`. - `[x]` Two-node operator-flow test coverage. Acceptance criteria: