Add shell completion generation

This commit is contained in:
Eric Wendland 2026-05-23 02:40:03 +02:00
commit 5b2c30f817
7 changed files with 98 additions and 3 deletions

10
Cargo.lock generated
View file

@ -591,6 +591,15 @@ dependencies = [
"strsim", "strsim",
] ]
[[package]]
name = "clap_complete"
version = "4.6.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
dependencies = [
"clap",
]
[[package]] [[package]]
name = "clap_derive" name = "clap_derive"
version = "4.6.1" version = "4.6.1"
@ -1512,6 +1521,7 @@ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"clap", "clap",
"clap_complete",
"geth-cas", "geth-cas",
"geth-config", "geth-config",
"geth-control", "geth-control",

View file

@ -42,6 +42,7 @@ base64 = "0.22"
blake3 = "1" blake3 = "1"
bytes = "1" bytes = "1"
clap = { version = "4", features = ["derive", "env"] } clap = { version = "4", features = ["derive", "env"] }
clap_complete = "4"
directories = "5" directories = "5"
# Compatibility pins for iroh 0.95's ed25519-dalek prerelease dependency. # Compatibility pins for iroh 0.95's ed25519-dalek prerelease dependency.
ed25519 = "=3.0.0-rc.1" ed25519 = "=3.0.0-rc.1"

View file

@ -86,9 +86,11 @@ metadata from an authorized peer over Iroh.
The bootstrap implementation provides: 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 embedded workflow help, including `--admin-key` / `--signing-key` setup
examples examples
- `geth completions <bash|zsh|fish|powershell|elvish>` for shell completion
scripts generated from the live CLI command tree
- `geth init` - `geth init`
- `geth init --admin-key <public-key> --signing-key <private-key> --node-name <name>` - `geth init --admin-key <public-key> --signing-key <private-key> --node-name <name>`
- `geth daemon run` - `geth daemon run`

View file

@ -9,6 +9,7 @@ license.workspace = true
anyhow.workspace = true anyhow.workspace = true
base64.workspace = true base64.workspace = true
clap.workspace = true clap.workspace = true
clap_complete.workspace = true
serde_json.workspace = true serde_json.workspace = true
tokio.workspace = true tokio.workspace = true
geth-config = { path = "../geth-config" } geth-config = { path = "../geth-config" }

View file

@ -1,16 +1,18 @@
use anyhow::{Context, Result, bail}; use anyhow::{Context, Result, bail};
use base64::Engine; 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_config::GethPaths;
use geth_control::{ControlRequest, ControlResponse}; use geth_control::{ControlRequest, ControlResponse};
use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport}; use geth_node::service::{ServiceInstallOptions, ServiceManager, ServiceReport};
use std::io::Read; use std::io::{Read, stdout};
use std::path::PathBuf; use std::path::PathBuf;
const TOP_LEVEL_AFTER_HELP: &str = r#"Common starts: const TOP_LEVEL_AFTER_HELP: &str = r#"Common starts:
geth guide init geth guide init
geth guide owner-setup geth guide owner-setup
geth guide overlay geth guide overlay
geth guide completions
geth init geth init
geth init --admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk --node-name laptop geth init --admin-key ~/.ssh/id_ed25519_sk.pub --signing-key ~/.ssh/id_ed25519_sk --node-name laptop
geth daemon run geth daemon run
@ -73,6 +75,7 @@ Topics:
keys Meaning of --admin-key and --signing-key. keys Meaning of --admin-key and --signing-key.
overlay Optional Iroh overlay network planning. overlay Optional Iroh overlay network planning.
service Install and manage geth as a user service. service Install and manage geth as a user service.
completions Shell completion installation examples.
smoke-test Minimal commands to verify a node and daemon."#; smoke-test Minimal commands to verify a node and daemon."#;
const GUIDE_INIT: &str = r#"geth init has two modes. const GUIDE_INIT: &str = r#"geth init has two modes.
@ -178,6 +181,34 @@ Preview definitions without installing:
geth daemon service print 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: const GUIDE_OVERLAY: &str = r#"Optional overlay network:
geth has an experimental overlay-network design inspired by iroh-lan. The geth has an experimental overlay-network design inspired by iroh-lan. The
@ -256,6 +287,10 @@ pub enum Command {
#[arg(value_enum)] #[arg(value_enum)]
topic: Option<GuideTopic>, topic: Option<GuideTopic>,
}, },
Completions {
#[arg(value_enum)]
shell: Shell,
},
#[command(long_about = INIT_LONG_ABOUT, after_long_help = INIT_AFTER_HELP)] #[command(long_about = INIT_LONG_ABOUT, after_long_help = INIT_AFTER_HELP)]
Init { Init {
#[arg( #[arg(
@ -366,6 +401,7 @@ pub enum GuideTopic {
Keys, Keys,
Overlay, Overlay,
Service, Service,
Completions,
SmokeTest, SmokeTest,
} }
@ -1063,6 +1099,10 @@ pub struct EmptyArgs {}
pub async fn run() -> Result<()> { pub async fn run() -> Result<()> {
let cli = Cli::parse(); let cli = Cli::parse();
if let Command::Completions { shell } = &cli.command {
print_completions(*shell);
return Ok(());
}
let paths = GethPaths::resolve().context("resolve geth paths")?; let paths = GethPaths::resolve().context("resolve geth paths")?;
match cli.command { match cli.command {
Command::Guide { topic } => { Command::Guide { topic } => {
@ -1169,6 +1209,7 @@ fn print_guide(topic: Option<GuideTopic>, json: bool) -> Result<()> {
Some(GuideTopic::Keys) => ("keys", GUIDE_KEYS), Some(GuideTopic::Keys) => ("keys", GUIDE_KEYS),
Some(GuideTopic::Overlay) => ("overlay", GUIDE_OVERLAY), Some(GuideTopic::Overlay) => ("overlay", GUIDE_OVERLAY),
Some(GuideTopic::Service) => ("service", GUIDE_SERVICE), Some(GuideTopic::Service) => ("service", GUIDE_SERVICE),
Some(GuideTopic::Completions) => ("completions", GUIDE_COMPLETIONS),
Some(GuideTopic::SmokeTest) => ("smoke-test", GUIDE_SMOKE_TEST), Some(GuideTopic::SmokeTest) => ("smoke-test", GUIDE_SMOKE_TEST),
}; };
if json { if json {
@ -1185,8 +1226,14 @@ fn print_guide(topic: Option<GuideTopic>, json: bool) -> Result<()> {
Ok(()) 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<ControlRequest> { fn request_for_command(command: Command) -> Result<ControlRequest> {
Ok(match command { Ok(match command {
Command::Completions { .. } => bail!("completion generation does not use the daemon"),
Command::Status => ControlRequest::Status, Command::Status => ControlRequest::Status,
Command::Sync { Command::Sync {
command: SyncCommand::Status, command: SyncCommand::Status,

View file

@ -160,6 +160,36 @@ fn guide_command_explains_overlay_boundaries() {
assert!(stdout.contains("all overlay packets must be carried over Iroh")); 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] #[test]
fn owner_init_requires_admin_key_and_signing_key() { fn owner_init_requires_admin_key_and_signing_key() {
let home = tempfile::tempdir().expect("tempdir"); let home = tempfile::tempdir().expect("tempdir");

View file

@ -65,6 +65,10 @@ Implementation order:
- `[x]` README has a two-machine walkthrough for the main smoke tests. - `[x]` README has a two-machine walkthrough for the main smoke tests.
- `[x]` CLI recovery errors tell operators the next command to run. - `[x]` CLI recovery errors tell operators the next command to run.
- `[x]` JSON sync status is script-friendly for stale/failed peer detection. - `[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. - `[x]` Two-node operator-flow test coverage.
Acceptance criteria: Acceptance criteria: