blob: b95a835e526f721f50a54bc74c115249796f78cd [file] [log] [blame]
//! Generates [Nushell](https://github.com/nushell/nushell) completions for [`clap`](https://github.com/clap-rs/clap) based CLIs
//!
//! ## Example
//!
//! ```
//! use clap::Command;
//! use clap_complete::generate;
//! use clap_complete_nushell::Nushell;
//! use std::io;
//!
//! let mut cmd = Command::new("myapp")
//! .subcommand(Command::new("test").subcommand(Command::new("config")))
//! .subcommand(Command::new("hello"));
//!
//! generate(Nushell, &mut cmd, "myapp", &mut io::stdout());
//! ```
#![doc(html_logo_url = "https://raw.githubusercontent.com/clap-rs/clap/master/assets/clap.png")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#![forbid(unsafe_code)]
#![warn(missing_docs)]
#![warn(clippy::print_stderr)]
#![warn(clippy::print_stdout)]
use clap::builder::StyledStr;
use clap::ValueHint;
use clap::{builder::PossibleValue, Arg, ArgAction, Command};
use clap_complete::Generator;
/// Generate Nushell complete file
pub struct Nushell;
impl Generator for Nushell {
fn file_name(&self, name: &str) -> String {
format!("{name}.nu")
}
fn generate(&self, cmd: &Command, buf: &mut dyn std::io::Write) {
let mut completions = String::new();
completions.push_str("module completions {\n\n");
generate_completion(&mut completions, cmd, false);
for sub in cmd.get_subcommands() {
generate_completion(&mut completions, sub, true);
}
completions.push_str("}\n\n");
completions.push_str("export use completions *\n");
buf.write_all(completions.as_bytes())
.expect("Failed to write to generated file");
}
}
fn append_value_completion_and_help(
arg: &Arg,
name: &str,
possible_values: &[PossibleValue],
s: &mut String,
) {
let takes_values = arg
.get_num_args()
.map(|r| r.takes_values())
.unwrap_or(false);
if takes_values {
let nu_type = match arg.get_value_hint() {
ValueHint::Unknown => "string",
ValueHint::Other => "string",
ValueHint::AnyPath => "path",
ValueHint::FilePath => "path",
ValueHint::DirPath => "path",
ValueHint::ExecutablePath => "path",
ValueHint::CommandName => "string",
ValueHint::CommandString => "string",
ValueHint::CommandWithArguments => "string",
ValueHint::Username => "string",
ValueHint::Hostname => "string",
ValueHint::Url => "string",
ValueHint::EmailAddress => "string",
_ => "string",
};
s.push_str(format!(": {nu_type}").as_str());
if !possible_values.is_empty() {
s.push_str(format!(r#"@"nu-complete {} {}""#, name, arg.get_id()).as_str());
}
}
if let Some(help) = arg.get_help() {
let indent: usize = 30;
let width = match s.lines().last() {
Some(line) => indent.saturating_sub(line.len()),
None => 0,
};
s.push_str(format!("{:>width$}# {}", ' ', single_line_styled_str(help)).as_str());
}
s.push('\n');
}
fn append_value_completion_defs(arg: &Arg, name: &str, s: &mut String) {
let possible_values = arg.get_possible_values();
if possible_values.is_empty() {
return;
}
s.push_str(format!(r#" def "nu-complete {} {}" [] {{"#, name, arg.get_id()).as_str());
s.push_str("\n [");
for value in possible_values {
let vname = value.get_name();
if vname.contains(|c: char| c.is_whitespace()) {
s.push_str(format!(r#" "\"{vname}\"""#).as_str());
} else {
s.push_str(format!(r#" "{vname}""#).as_str());
}
}
s.push_str(" ]\n }\n\n");
}
fn append_argument(arg: &Arg, name: &str, s: &mut String) {
let possible_values = arg.get_possible_values();
if arg.is_positional() {
// rest arguments
if matches!(arg.get_action(), ArgAction::Append) {
s.push_str(format!(" ...{}", arg.get_id()).as_str());
} else {
s.push_str(format!(" {}", arg.get_id()).as_str());
if !arg.is_required_set() {
s.push('?');
}
}
append_value_completion_and_help(arg, name, &possible_values, s);
return;
}
let shorts = arg.get_short_and_visible_aliases();
let longs = arg.get_long_and_visible_aliases();
match shorts {
Some(shorts) => match longs {
Some(longs) => {
// short options and long options
s.push_str(
format!(
" --{}(-{})",
longs.first().expect("At least one long option expected"),
shorts.first().expect("At lease one short option expected")
)
.as_str(),
);
append_value_completion_and_help(arg, name, &possible_values, s);
// long alias
for long in longs.iter().skip(1) {
s.push_str(format!(" --{long}").as_str());
append_value_completion_and_help(arg, name, &possible_values, s);
}
// short alias
for short in shorts.iter().skip(1) {
s.push_str(format!(" -{short}").as_str());
append_value_completion_and_help(arg, name, &possible_values, s);
}
}
None => {
// short options only
for short in shorts {
s.push_str(format!(" -{short}").as_str());
append_value_completion_and_help(arg, name, &possible_values, s);
}
}
},
None => match longs {
Some(longs) => {
// long options only
for long in longs {
s.push_str(format!(" --{long}").as_str());
append_value_completion_and_help(arg, name, &possible_values, s);
}
}
None => unreachable!("No short or long options found"),
},
}
}
fn generate_completion(completions: &mut String, cmd: &Command, is_subcommand: bool) {
let name = cmd.get_bin_name().expect("Failed to get bin name");
for arg in cmd.get_arguments() {
append_value_completion_defs(arg, name, completions);
}
if let Some(about) = cmd.get_about() {
let about = single_line_styled_str(about);
completions.push_str(format!(" # {about}\n").as_str());
}
if is_subcommand {
completions.push_str(format!(" export extern \"{name}\" [\n").as_str());
} else {
completions.push_str(format!(" export extern {name} [\n").as_str());
}
for arg in cmd.get_arguments() {
append_argument(arg, name, completions);
}
completions.push_str(" ]\n\n");
if is_subcommand {
for sub in cmd.get_subcommands() {
generate_completion(completions, sub, true);
}
}
}
fn single_line_styled_str(text: &StyledStr) -> String {
text.to_string().replace('\n', " ")
}