From db6fe2fc50660c5b1081119f1cdc806b82ec53c7 Mon Sep 17 00:00:00 2001 From: Colin Nelson Date: Thu, 12 Mar 2026 21:55:41 +0000 Subject: [PATCH] argh_complete: add support for nushell --- argh/examples/completion_example.rs | 8 ++- argh_complete/src/lib.rs | 1 + argh_complete/src/nushell.rs | 84 +++++++++++++++++++++++++++++ argh_complete/src/tests.rs | 11 ++++ 4 files changed, 103 insertions(+), 1 deletion(-) create mode 100644 argh_complete/src/nushell.rs diff --git a/argh/examples/completion_example.rs b/argh/examples/completion_example.rs index 9bdc476..4d41dad 100644 --- a/argh/examples/completion_example.rs +++ b/argh/examples/completion_example.rs @@ -24,7 +24,7 @@ enum Subcommands { /// Generate shell completions. #[argh(subcommand, name = "completion")] struct CompletionCmd { - /// the shell to generate for (bash, zsh, fish) + /// the shell to generate for (bash, zsh, fish, nushell) #[argh(positional)] shell: String, } @@ -102,6 +102,12 @@ fn main() { "fish" => { println!("{}", argh_complete::fish::Fish::generate(&command_name, &cmd_info)) } + "nushell" => { + println!( + "{}", + argh_complete::nushell::Nushell::generate(&command_name, &cmd_info) + ) + } _ => eprintln!("Unsupported shell: {}", cmd.shell), } } diff --git a/argh_complete/src/lib.rs b/argh_complete/src/lib.rs index 070a1e1..fe7a70d 100644 --- a/argh_complete/src/lib.rs +++ b/argh_complete/src/lib.rs @@ -6,6 +6,7 @@ pub mod bash; pub mod fish; +pub mod nushell; pub mod zsh; use argh_shared::CommandInfoWithArgs; diff --git a/argh_complete/src/nushell.rs b/argh_complete/src/nushell.rs new file mode 100644 index 0000000..cfb5a72 --- /dev/null +++ b/argh_complete/src/nushell.rs @@ -0,0 +1,84 @@ +// Copyright (c) 2026 Google LLC All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +//! Generation of completions for Nushell. + +use crate::Generator; +use argh_shared::{CommandInfoWithArgs, FlagInfoKind, Optionality}; +use std::fmt::Write; + +/// A generator for Nushell shell completions. +pub struct Nushell; + +impl Generator for Nushell { + fn generate(cmd_name: &str, cmd: &CommandInfoWithArgs<'_>) -> String { + let mut out = String::new(); + generate_nushell_cmd(&mut out, cmd_name, cmd); + out + } +} + +fn generate_nushell_cmd(out: &mut String, cmd_name: &str, cmd: &CommandInfoWithArgs<'_>) { + // Generate the extern block for the current command + writeln!(out, "export extern \"{}\" [", cmd_name).unwrap(); + + // Generate flags + for flag in cmd.flags { + let mut flag_def = String::new(); + + if !flag.long.is_empty() { + flag_def.push_str(flag.long); + } + + if let Some(short) = flag.short { + if flag.long.is_empty() { + // If there is only a short flag, we must output it as a short option. + // Nushell requires some long name for just `-s`, but standard is `-s`. + // If it's only short, typically `-$short` + flag_def.push_str(&format!("-{}", short)); + } else { + flag_def.push_str(&format!("(-{})", short)); + } + } + + if let FlagInfoKind::Option { .. } = flag.kind { + flag_def.push_str(": string"); + } + + if !flag.description.is_empty() { + flag_def.push_str(&format!(" # {}", flag.description)); + } + + writeln!(out, " {}", flag_def).unwrap(); + } + + // Generate positional arguments + for pos in cmd.positionals { + let name = if pos.name.is_empty() { "arg" } else { pos.name }; + + let mut pos_def = String::new(); + match pos.optionality { + Optionality::Required => pos_def.push_str(&format!("{}: string", name)), + Optionality::Optional => pos_def.push_str(&format!("{}?: string", name)), + Optionality::Repeating | Optionality::Greedy => { + pos_def.push_str(&format!("...{}: string", name)) + } + } + + if !pos.description.is_empty() { + pos_def.push_str(&format!(" # {}", pos.description)); + } + + writeln!(out, " {}", pos_def).unwrap(); + } + + writeln!(out, "]").unwrap(); + writeln!(out).unwrap(); + + // Recurse for subcommands + for subcmd in &cmd.commands { + let next_cmd_name = format!("{} {}", cmd_name, subcmd.name); + generate_nushell_cmd(out, &next_cmd_name, &subcmd.command); + } +} diff --git a/argh_complete/src/tests.rs b/argh_complete/src/tests.rs index 0818124..4a14b36 100644 --- a/argh_complete/src/tests.rs +++ b/argh_complete/src/tests.rs @@ -59,3 +59,14 @@ fn test_fish_generator() { assert!(fish_out.contains("complete -c mycmd -n 'not __fish_seen_subcommand_from subcmd' -f -l verbose -s v -d 'verbose output'")); assert!(fish_out.contains("complete -c mycmd -n 'not __fish_seen_subcommand_from subcmd' -f -a 'subcmd' -d 'a sub command'")); } + +#[cfg(test)] +#[test] +fn test_nushell_generator() { + let cmd = make_mock_command(); + let nushell_out = crate::nushell::Nushell::generate("mycmd", &cmd); + + assert!(nushell_out.contains("export extern \"mycmd\" [")); + assert!(nushell_out.contains("--verbose(-v) # verbose output")); + assert!(nushell_out.contains("export extern \"mycmd subcmd\" [")); +}