From 5f84e8b958aa0e4a3ab2612fd1b2ab3a0f9ddd21 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 7 Dec 2023 15:31:32 +0100 Subject: [PATCH 01/12] initial implementation of completion for fish --- .github/workflows/ci.yml | 2 +- Cargo.toml | 9 ++++-- complete/Cargo.toml | 10 ++++++ complete/LICENSE | 1 + complete/src/fish.rs | 67 ++++++++++++++++++++++++++++++++++++++++ complete/src/lib.rs | 40 ++++++++++++++++++++++++ derive/src/complete.rs | 51 ++++++++++++++++++++++++++++++ derive/src/lib.rs | 9 +++++- examples/hello_world.rs | 8 ++--- src/lib.rs | 49 ++++++++++++++++++++++++++--- src/value.rs | 17 ++++++++++ 11 files changed, 247 insertions(+), 16 deletions(-) create mode 100644 complete/Cargo.toml create mode 120000 complete/LICENSE create mode 100644 complete/src/fish.rs create mode 100644 complete/src/lib.rs create mode 100644 derive/src/complete.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3039939..c03e0c1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - uses: actions-rs/cargo@v1 with: command: test - args: --all-features --workspace + args: --features complete --workspace rustfmt: name: Rustfmt diff --git a/Cargo.toml b/Cargo.toml index 415b33a..138b034 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,13 @@ readme = "README.md" [dependencies] uutils-args-derive = { version = "0.1.0", path = "derive" } +uutils-args-complete = { version = "0.1.0", path = "complete", optional = true } strsim = "0.10.0" lexopt = "0.3.0" +[features] +parse-is-complete = ["complete"] +complete = ["uutils-args-complete"] + [workspace] -members = [ - "derive", -] +members = ["derive", "complete"] diff --git a/complete/Cargo.toml b/complete/Cargo.toml new file mode 100644 index 0000000..54abe1a --- /dev/null +++ b/complete/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "uutils-args-complete" +version = "0.1.0" +edition = "2021" +authors = ["Terts Diepraam"] +license = "MIT" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] diff --git a/complete/LICENSE b/complete/LICENSE new file mode 120000 index 0000000..ea5b606 --- /dev/null +++ b/complete/LICENSE @@ -0,0 +1 @@ +../LICENSE \ No newline at end of file diff --git a/complete/src/fish.rs b/complete/src/fish.rs new file mode 100644 index 0000000..8c42c7d --- /dev/null +++ b/complete/src/fish.rs @@ -0,0 +1,67 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::{Command, ValueHint}; + +pub fn render(c: &Command) -> String { + let mut out = String::new(); + let name = &c.name; + for arg in &c.args { + let mut line = format!("complete -c {name}"); + for short in &arg.short { + line.push_str(&format!(" -s {short}")); + } + for long in &arg.long { + line.push_str(&format!(" -l {long}")); + } + line.push_str(&format!(" -d '{}'", arg.help)); + if let Some(value) = &arg.value { + line.push_str(&render_value_hint(value)); + } + out.push_str(&line); + out.push('\n'); + } + out +} + +fn render_value_hint(value: &ValueHint) -> String { + match value { + ValueHint::Strings(s) => { + let joined = s.join(", "); + format!(" -a {{ {joined} }}") + } + _ => todo!(), + } +} + +#[cfg(test)] +mod test { + use super::render; + use crate::{Arg, Command}; + + #[test] + fn short() { + let c = Command { + name: "test".into(), + args: vec![Arg { + short: vec!["a".into()], + help: "some flag".into(), + ..Arg::default() + }], + }; + assert_eq!(render(&c), "complete -c test -s a -d 'some flag'\n",) + } + + #[test] + fn long() { + let c = Command { + name: "test".into(), + args: vec![Arg { + long: vec!["all".into()], + help: "some flag".into(), + ..Arg::default() + }], + }; + assert_eq!(render(&c), "complete -c test -l all -d 'some flag'\n",) + } +} diff --git a/complete/src/lib.rs b/complete/src/lib.rs new file mode 100644 index 0000000..32a88fb --- /dev/null +++ b/complete/src/lib.rs @@ -0,0 +1,40 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +mod fish; + +pub struct Command { + pub name: String, + pub args: Vec, +} + +#[derive(Default)] +pub struct Arg { + pub short: Vec, + pub long: Vec, + pub help: String, + pub value: Option, +} + +pub enum ValueHint { + Strings(Vec), + Unknown, + // Other, + AnyPath, + // FilePath, + // DirPath, + // ExecutablePath, + // CommandName, + // CommandString, + // CommandWithArguments, + // Username, + // Hostname, +} + +pub fn render(c: &Command, shell: &str) -> String { + match shell { + "fish" => fish::render(c), + "sh" | "zsh" | "bash" | "csh" => panic!("shell '{shell}' completion is not supported yet!"), + _ => panic!("unknown shell '{shell}'!"), + } +} diff --git a/derive/src/complete.rs b/derive/src/complete.rs new file mode 100644 index 0000000..92bcf98 --- /dev/null +++ b/derive/src/complete.rs @@ -0,0 +1,51 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::{ + argument::{ArgType, Argument}, + flags::{Flag, Flags}, +}; +use proc_macro2::TokenStream; +use quote::quote; + +pub fn complete(args: &[Argument]) -> TokenStream { + let mut arg_specs = Vec::new(); + + for Argument { help, arg_type, .. } in args { + let ArgType::Option { + flags, + hidden: false, + .. + } = arg_type + else { + continue; + }; + + let Flags { short, long, .. } = flags; + if short.is_empty() && long.is_empty() { + continue; + } + let short: Vec<_> = short + .iter() + .map(|Flag { flag, .. }| quote!(String::from(#flag))) + .collect(); + let long: Vec<_> = long + .iter() + .map(|Flag { flag, .. }| quote!(String::from(#flag))) + .collect(); + + arg_specs.push(quote!( + Arg { + short: vec![#(#short),*], + long: vec![#(#long),*], + help: String::from(#help), + value: None, + } + )) + } + + quote!(Command { + name: String::from(option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME"))), + args: vec![#(#arg_specs),*] + }) +} diff --git a/derive/src/lib.rs b/derive/src/lib.rs index fc463bf..79406b9 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -3,6 +3,7 @@ mod argument; mod attributes; +mod complete; mod flags; mod help; mod help_parser; @@ -44,6 +45,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { &arguments_attr.version_flags, &arguments_attr.file, ); + let completion = complete::complete(&arguments); let help = help_handling(&arguments_attr.help_flags); let version = version_handling(&arguments_attr.version_flags); let version_string = quote!(format!( @@ -74,7 +76,6 @@ pub fn arguments(input: TokenStream) -> TokenStream { ) -> Result>, uutils_args::Error> { use uutils_args::{Value, lexopt, Error, Argument}; - // #number_argment #free let arg = match { #next_arg } { @@ -104,6 +105,12 @@ pub fn arguments(input: TokenStream) -> TokenStream { fn version() -> String { #version_string } + + #[cfg(feature = "complete")] + fn complete() -> ::uutils_args_complete::Command { + use ::uutils_args_complete::{Command, Arg, ValueHint}; + #completion + } } ); diff --git a/examples/hello_world.rs b/examples/hello_world.rs index 66137b4..1adf4b1 100644 --- a/examples/hello_world.rs +++ b/examples/hello_world.rs @@ -3,15 +3,11 @@ use uutils_args::{Arguments, Options}; #[derive(Arguments)] #[arguments(file = "examples/hello_world_help.md")] enum Arg { - /// The *name* to **greet** - /// - /// Just to show off, I can do multiple paragraphs and wrap text! - /// - /// # Also headings! + /// The name to greet #[arg("-n NAME", "--name=NAME", "name=NAME")] Name(String), - /// The **number of times** to `greet` + /// The number of times to greet #[arg("-c N", "--count=N")] Count(u8), diff --git a/src/lib.rs b/src/lib.rs index 604e21f..4b429bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,9 @@ pub trait Arguments: Sized { while iter.next_arg()?.is_some() {} Ok(()) } + + #[cfg(feature = "complete")] + fn complete() -> uutils_args_complete::Command; } /// An iterator over arguments. @@ -192,12 +195,32 @@ pub trait Options: Sized { I: IntoIterator + 'static, I::Item: Into, { - let mut iter = Arg::parse(args); - while let Some(arg) = iter.next_arg()? { - self.apply(arg); + // Hacky but it works: if the parse-is-complete flag is active the + // parse function becomes the complete function so that no additional + // functionality is necessary for users to generate completions. It is + // important that we exit the program here, because the program does + // not expect us to print the completion here and therefore will behave + // incorrectly. + #[cfg(feature = "parse-is-complete")] + { + print_completion::<_, Self, Arg>(args.into_iter()); + std::process::exit(0); } - Arg::check_missing(iter.positional_idx)?; - Ok(self) + + #[cfg(not(feature = "parse-is-complete"))] + { + let mut iter = Arg::parse(args); + while let Some(arg) = iter.next_arg()? { + self.apply(arg); + } + Arg::check_missing(iter.positional_idx)?; + Ok(self) + } + } + + #[cfg(feature = "complete")] + fn complete(shell: &str) -> String { + uutils_args_complete::render(&Arg::complete(), shell) } } @@ -224,6 +247,22 @@ pub fn __echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> O } } +#[cfg(feature = "parse-is-complete")] +fn print_completion, Arg: Arguments>(mut args: I) +where + I: Iterator + 'static, + I::Item: Into, +{ + let _exec_name = args.next(); + let shell = args + .next() + .expect("Need a shell argument for completion.") + .into(); + let shell = shell.to_string_lossy(); + assert!(args.next().is_none(), "completion only takes one argument"); + println!("{}", O::complete(&shell)); +} + fn is_echo_style_positional(s: &OsStr, short_args: &[char]) -> bool { let s = match s.to_str() { Some(x) => x, diff --git a/src/value.rs b/src/value.rs index e95b9a4..ae9e3f7 100644 --- a/src/value.rs +++ b/src/value.rs @@ -6,6 +6,8 @@ use std::{ ffi::{OsStr, OsString}, path::PathBuf, }; +#[cfg(feature = "complete")] +use uutils_args_complete::ValueHint; pub type ValueResult = Result>; @@ -51,6 +53,11 @@ impl std::fmt::Display for ValueError { /// If an error is returned, it will be wrapped in [`Error::ParsingFailed`] pub trait Value: Sized { fn from_value(value: &OsStr) -> ValueResult; + + #[cfg(feature = "complete")] + fn value_hint() -> ValueHint { + ValueHint::Unknown + } } impl Value for OsString { @@ -63,6 +70,11 @@ impl Value for PathBuf { fn from_value(value: &OsStr) -> ValueResult { Ok(PathBuf::from(value)) } + + #[cfg(feature = "complete")] + fn value_hint() -> ValueHint { + ValueHint::AnyPath + } } impl Value for String { @@ -81,6 +93,11 @@ where fn from_value(value: &OsStr) -> ValueResult { Ok(Some(T::from_value(value)?)) } + + #[cfg(feature = "complete")] + fn value_hint() -> uutils_args_complete::ValueHint { + T::value_hint() + } } macro_rules! value_int { From efe8e323bee326b8bd7e297d955e90538ef7c461 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Thu, 7 Dec 2023 16:30:16 +0100 Subject: [PATCH 02/12] value hints in completions --- complete/src/fish.rs | 1 + derive/src/argument.rs | 2 ++ derive/src/complete.rs | 18 ++++++++++++++++-- derive/src/flags.rs | 2 +- derive/src/lib.rs | 13 +++++++++++++ examples/value.rs | 38 ++++++++++++++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 3 deletions(-) create mode 100644 examples/value.rs diff --git a/complete/src/fish.rs b/complete/src/fish.rs index 8c42c7d..5bbb451 100644 --- a/complete/src/fish.rs +++ b/complete/src/fish.rs @@ -30,6 +30,7 @@ fn render_value_hint(value: &ValueHint) -> String { let joined = s.join(", "); format!(" -a {{ {joined} }}") } + ValueHint::Unknown => String::new(), _ => todo!(), } } diff --git a/derive/src/argument.rs b/derive/src/argument.rs index 7e3928a..9f0a1c6 100644 --- a/derive/src/argument.rs +++ b/derive/src/argument.rs @@ -14,6 +14,7 @@ use crate::{ pub struct Argument { pub ident: Ident, + pub field: Option, pub name: String, pub arg_type: ArgType, pub help: String, @@ -105,6 +106,7 @@ pub fn parse_argument(v: Variant) -> Vec { }; Argument { ident: ident.clone(), + field: field.clone(), name: name.clone(), arg_type, help: arg_help, diff --git a/derive/src/complete.rs b/derive/src/complete.rs index 92bcf98..b2d4bae 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -11,7 +11,13 @@ use quote::quote; pub fn complete(args: &[Argument]) -> TokenStream { let mut arg_specs = Vec::new(); - for Argument { help, arg_type, .. } in args { + for Argument { + help, + field, + arg_type, + .. + } in args + { let ArgType::Option { flags, hidden: false, @@ -25,21 +31,29 @@ pub fn complete(args: &[Argument]) -> TokenStream { if short.is_empty() && long.is_empty() { continue; } + let short: Vec<_> = short .iter() .map(|Flag { flag, .. }| quote!(String::from(#flag))) .collect(); + let long: Vec<_> = long .iter() .map(|Flag { flag, .. }| quote!(String::from(#flag))) .collect(); + let hint = if let Some(ty) = field { + quote!(Some(<#ty>::value_hint())) + } else { + quote!(None) + }; + arg_specs.push(quote!( Arg { short: vec![#(#short),*], long: vec![#(#long),*], help: String::from(#help), - value: None, + value: #hint, } )) } diff --git a/derive/src/flags.rs b/derive/src/flags.rs index 82cf489..3d69dca 100644 --- a/derive/src/flags.rs +++ b/derive/src/flags.rs @@ -11,7 +11,7 @@ pub struct Flags { pub dd_style: Vec<(String, String)>, } -#[derive(Clone)] +#[derive(Clone, PartialEq, Eq)] pub enum Value { No, Optional(String), diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 79406b9..23b90da 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -109,6 +109,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { #[cfg(feature = "complete")] fn complete() -> ::uutils_args_complete::Command { use ::uutils_args_complete::{Command, Arg, ValueHint}; + use ::uutils_args::Value; #completion } } @@ -131,6 +132,7 @@ pub fn value(input: TokenStream) -> TokenStream { let mut options = Vec::new(); let mut match_arms = vec![]; + let mut all_keys = Vec::new(); for variant in data.variants { let variant_name = variant.ident.to_string(); let attrs = variant.attrs.clone(); @@ -147,6 +149,7 @@ pub fn value(input: TokenStream) -> TokenStream { keys }; + all_keys.extend(keys.clone()); options.push(quote!(&[#(#keys),*])); let stmt = if let Some(v) = value { @@ -195,6 +198,16 @@ pub fn value(input: TokenStream) -> TokenStream { _ => unreachable!("Should be caught by (None, []) case above.") }) } + + #[cfg(feature = "complete")] + fn value_hint() -> ::uutils_args_complete::ValueHint { + ::uutils_args_complete::ValueHint::Strings( + [#(#all_keys),*] + .into_iter() + .map(ToString::to_string) + .collect() + ) + } } ); diff --git a/examples/value.rs b/examples/value.rs new file mode 100644 index 0000000..a0139f3 --- /dev/null +++ b/examples/value.rs @@ -0,0 +1,38 @@ +use uutils_args::{Arguments, Options, Value}; + +#[derive(Arguments)] +#[arguments(file = "examples/hello_world_help.md")] +enum Arg { + /// Color! + #[arg("-c NAME", "--color=NAME")] + Color(Color), +} + +#[derive(Value, Debug, Default)] +enum Color { + #[value("never")] + Never, + #[default] + #[value("auto")] + Auto, + #[value("always")] + Always, +} + +#[derive(Default)] +struct Settings { + color: Color, +} + +impl Options for Settings { + fn apply(&mut self, arg: Arg) { + match arg { + Arg::Color(c) => self.color = c, + } + } +} + +fn main() { + let color = Settings::default().parse(std::env::args_os()).color; + println!("{:?}", color); +} From 2f4656622114f16e33c2a45559522daf34dd2af7 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 10:51:50 +0100 Subject: [PATCH 03/12] allow unused mut in try_parse --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index 4b429bf..5aa06bf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -190,6 +190,7 @@ pub trait Options: Sized { exit_if_err(self.try_parse(args), Arg::EXIT_CODE) } + #[allow(unused_mut)] fn try_parse(mut self, args: I) -> Result where I: IntoIterator + 'static, From 2f6c32ddfc520c3f734cc001052a638302f4bdc1 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 10:59:00 +0100 Subject: [PATCH 04/12] rename completion to complete for consistency --- derive/src/lib.rs | 4 ++-- src/lib.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 23b90da..5df241f 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -45,7 +45,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { &arguments_attr.version_flags, &arguments_attr.file, ); - let completion = complete::complete(&arguments); + let complete_command = complete::complete(&arguments); let help = help_handling(&arguments_attr.help_flags); let version = version_handling(&arguments_attr.version_flags); let version_string = quote!(format!( @@ -110,7 +110,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { fn complete() -> ::uutils_args_complete::Command { use ::uutils_args_complete::{Command, Arg, ValueHint}; use ::uutils_args::Value; - #completion + #complete_command } } ); diff --git a/src/lib.rs b/src/lib.rs index 5aa06bf..22d124b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -204,7 +204,7 @@ pub trait Options: Sized { // incorrectly. #[cfg(feature = "parse-is-complete")] { - print_completion::<_, Self, Arg>(args.into_iter()); + print_complete::<_, Self, Arg>(args.into_iter()); std::process::exit(0); } @@ -249,7 +249,7 @@ pub fn __echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> O } #[cfg(feature = "parse-is-complete")] -fn print_completion, Arg: Arguments>(mut args: I) +fn print_complete, Arg: Arguments>(mut args: I) where I: Iterator + 'static, I::Item: Into, From ba7a40a5f08718f5fdb7abe1cc65def845fee49e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 11:26:38 +0100 Subject: [PATCH 05/12] more value hints in fish --- complete/src/fish.rs | 48 +++++++++++++++++++++++++++++++++++++++----- complete/src/lib.rs | 15 ++++++-------- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/complete/src/fish.rs b/complete/src/fish.rs index 5bbb451..3571b01 100644 --- a/complete/src/fish.rs +++ b/complete/src/fish.rs @@ -27,18 +27,21 @@ pub fn render(c: &Command) -> String { fn render_value_hint(value: &ValueHint) -> String { match value { ValueHint::Strings(s) => { - let joined = s.join(", "); - format!(" -a {{ {joined} }}") + let joined = s.join(" "); + format!(" -f -a \"{joined}\"") } - ValueHint::Unknown => String::new(), - _ => todo!(), + ValueHint::AnyPath | ValueHint::FilePath | ValueHint::ExecutablePath => String::from(" -F"), + ValueHint::DirPath => " -f -a \"(__fish_complete_directories)\"".into(), + ValueHint::Unknown => " -f".into(), + ValueHint::Username => " -f -a \"(__fish_complete_users)\"".into(), + ValueHint::Hostname => " -f -a \"(__fish_print_hostnames)\"".into(), } } #[cfg(test)] mod test { use super::render; - use crate::{Arg, Command}; + use crate::{Arg, Command, ValueHint}; #[test] fn short() { @@ -65,4 +68,39 @@ mod test { }; assert_eq!(render(&c), "complete -c test -l all -d 'some flag'\n",) } + + #[test] + fn value_hints() { + let args = [ + ( + ValueHint::Strings(vec!["all".into(), "none".into()]), + "-f -a \"all none\"", + ), + (ValueHint::Unknown, "-f"), + (ValueHint::AnyPath, "-F"), + (ValueHint::FilePath, "-F"), + ( + ValueHint::DirPath, + "-f -a \"(__fish_complete_directories)\"", + ), + (ValueHint::ExecutablePath, "-F"), + (ValueHint::Username, "-f -a \"(__fish_complete_users)\""), + (ValueHint::Hostname, "-f -a \"(__fish_print_hostnames)\""), + ]; + for (hint, expected) in args { + let c = Command { + name: "test".into(), + args: vec![Arg { + short: vec!["a".into()], + long: vec![], + help: "some flag".into(), + value: Some(hint), + }], + }; + assert_eq!( + render(&c), + format!("complete -c test -s a -d 'some flag' {expected}\n") + ) + } + } } diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 32a88fb..2be302e 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -16,19 +16,16 @@ pub struct Arg { pub value: Option, } +// Modelled after claps ValueHint pub enum ValueHint { Strings(Vec), Unknown, - // Other, AnyPath, - // FilePath, - // DirPath, - // ExecutablePath, - // CommandName, - // CommandString, - // CommandWithArguments, - // Username, - // Hostname, + FilePath, + DirPath, + ExecutablePath, + Username, + Hostname, } pub fn render(c: &Command, shell: &str) -> String { From 675bbb95c85e4d0c05731f726de6ed0738ebe0bc Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 11:45:08 +0100 Subject: [PATCH 06/12] add elvish and powershell to unimplemented shells --- complete/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 2be302e..949b224 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -31,7 +31,7 @@ pub enum ValueHint { pub fn render(c: &Command, shell: &str) -> String { match shell { "fish" => fish::render(c), - "sh" | "zsh" | "bash" | "csh" => panic!("shell '{shell}' completion is not supported yet!"), + "sh" | "zsh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not supported yet!"), _ => panic!("unknown shell '{shell}'!"), } } From c30d5e893bdb30b2cfee1b1868f853f4b7d64e91 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 13:20:03 +0100 Subject: [PATCH 07/12] super basic zsh completion --- complete/src/lib.rs | 4 +++- complete/src/zsh.rs | 58 +++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 1 deletion(-) create mode 100644 complete/src/zsh.rs diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 949b224..a6e5365 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -2,6 +2,7 @@ // file that was distributed with this source code. mod fish; +mod zsh; pub struct Command { pub name: String, @@ -31,7 +32,8 @@ pub enum ValueHint { pub fn render(c: &Command, shell: &str) -> String { match shell { "fish" => fish::render(c), - "sh" | "zsh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not supported yet!"), + "zsh" => zsh::render(c), + "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not supported yet!"), _ => panic!("unknown shell '{shell}'!"), } } diff --git a/complete/src/zsh.rs b/complete/src/zsh.rs new file mode 100644 index 0000000..143596f --- /dev/null +++ b/complete/src/zsh.rs @@ -0,0 +1,58 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::{Command, Arg}; + +pub fn render(c: &Command) -> String { + template(&c.name, &render_args(&c.args)) +} + +fn render_args(args: &[Arg]) -> String { + let mut out = String::new(); + let indent = " ".repeat(8); + for arg in args { + let help = &arg.help; + for short in &arg.short { + out.push_str( + &format!("{indent}'-{short}[{help}]' \\\n") + ); + } + for long in &arg.long { + out.push_str( + &format!("{indent}'--{long}[{help}]' \\\n") + ); + } + } + out +} + +fn template(name: &str, args: &str) -> String { + format!( + "\ +#compdef {name} + +autoload -U is-at-least + +_{name}() {{ + typeset -A opt_args + typeset -a _arguments_options + local ret=1 + + if is-at-least 5.2; then + _arguments_options=(-s -S -C) + else + _arguments_options=(-s -C) + fi + + local context curcontext=\"$curcontext\" state line + _arguments \"${{_arguments_options[@]}}\" \\\n{args} +&& ret=0 +}} + +if [ \"$funcstack[1]\" = \"_{name}\" ]; then + {name} \"$@\" +else + compdef _{name} {name} +fi" + ) +} \ No newline at end of file From 5600b4a4a699f9cdb78decd1c21a2ce22c904205 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 14:09:57 +0100 Subject: [PATCH 08/12] add basic mdbook rendering --- complete/src/fish.rs | 4 +++ complete/src/lib.rs | 11 ++++++-- complete/src/md.rs | 57 ++++++++++++++++++++++++++++++++++++++++++ complete/src/zsh.rs | 13 ++++------ derive/src/complete.rs | 11 +++++++- derive/src/help.rs | 2 +- derive/src/lib.rs | 2 +- 7 files changed, 87 insertions(+), 13 deletions(-) create mode 100644 complete/src/md.rs diff --git a/complete/src/fish.rs b/complete/src/fish.rs index 3571b01..f86fc93 100644 --- a/complete/src/fish.rs +++ b/complete/src/fish.rs @@ -3,6 +3,7 @@ use crate::{Command, ValueHint}; +/// Create completion script for `fish` pub fn render(c: &Command) -> String { let mut out = String::new(); let name = &c.name; @@ -52,6 +53,7 @@ mod test { help: "some flag".into(), ..Arg::default() }], + ..Command::default() }; assert_eq!(render(&c), "complete -c test -s a -d 'some flag'\n",) } @@ -65,6 +67,7 @@ mod test { help: "some flag".into(), ..Arg::default() }], + ..Command::default() }; assert_eq!(render(&c), "complete -c test -l all -d 'some flag'\n",) } @@ -96,6 +99,7 @@ mod test { help: "some flag".into(), value: Some(hint), }], + ..Command::default() }; assert_eq!( render(&c), diff --git a/complete/src/lib.rs b/complete/src/lib.rs index a6e5365..4504797 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -2,10 +2,15 @@ // file that was distributed with this source code. mod fish; +mod md; mod zsh; +#[derive(Default)] pub struct Command { pub name: String, + pub summary: String, + pub version: String, + pub after_options: String, pub args: Vec, } @@ -31,9 +36,11 @@ pub enum ValueHint { pub fn render(c: &Command, shell: &str) -> String { match shell { + "md" => md::render(c), "fish" => fish::render(c), "zsh" => zsh::render(c), - "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not supported yet!"), - _ => panic!("unknown shell '{shell}'!"), + "man" => panic!("manpages are not implemented yet"), + "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), + _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), } } diff --git a/complete/src/md.rs b/complete/src/md.rs new file mode 100644 index 0000000..08f502f --- /dev/null +++ b/complete/src/md.rs @@ -0,0 +1,57 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + +use crate::Command; + +/// Render command to a markdown file for mdbook +pub fn render(c: &Command) -> String { + let mut out = String::new(); + out.push_str(&title(c)); + out.push_str(&additional(c)); + out.push_str(&c.summary); + out.push_str("\n\n"); + out.push_str(&options(c)); + out.push_str("\n\n"); + out.push_str(&c.after_options); + out.push('\n'); + out +} + +fn title(c: &Command) -> String { + format!("# {}\n\n", c.name) +} + +fn additional(c: &Command) -> String { + let version = &c.version; + format!( + "\ +
\ + {version}\ +
\n\n\ + " + ) +} + +fn options(c: &Command) -> String { + let mut out = String::from("## Options\n\n"); + out.push_str("
\n"); + for arg in &c.args { + out.push_str("
"); + + let mut flags = Vec::new(); + + for long in &arg.long { + flags.push(format!("--{long}")); + } + + for short in &arg.short { + flags.push(format!("-{short}")) + } + + out.push_str(&flags.join(", ")); + out.push_str("
\n"); + out.push_str(&format!("
\n\n{}\n\n
\n", arg.help)); + } + out.push_str("
"); + out +} diff --git a/complete/src/zsh.rs b/complete/src/zsh.rs index 143596f..814c904 100644 --- a/complete/src/zsh.rs +++ b/complete/src/zsh.rs @@ -1,8 +1,9 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Command, Arg}; +use crate::{Arg, Command}; +/// Create completion script for `zsh` pub fn render(c: &Command) -> String { template(&c.name, &render_args(&c.args)) } @@ -13,14 +14,10 @@ fn render_args(args: &[Arg]) -> String { for arg in args { let help = &arg.help; for short in &arg.short { - out.push_str( - &format!("{indent}'-{short}[{help}]' \\\n") - ); + out.push_str(&format!("{indent}'-{short}[{help}]' \\\n")); } for long in &arg.long { - out.push_str( - &format!("{indent}'--{long}[{help}]' \\\n") - ); + out.push_str(&format!("{indent}'--{long}[{help}]' \\\n")); } } out @@ -55,4 +52,4 @@ else compdef _{name} {name} fi" ) -} \ No newline at end of file +} diff --git a/derive/src/complete.rs b/derive/src/complete.rs index b2d4bae..2d7b365 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -8,9 +8,15 @@ use crate::{ use proc_macro2::TokenStream; use quote::quote; -pub fn complete(args: &[Argument]) -> TokenStream { +pub fn complete(args: &[Argument], file: &Option) -> TokenStream { let mut arg_specs = Vec::new(); + let (summary, _usage, after_options) = if let Some(file) = file { + crate::help::read_help_file(file) + } else { + ("".into(), "{} [OPTIONS] [ARGUMENTS]".into(), "".into()) + }; + for Argument { help, field, @@ -60,6 +66,9 @@ pub fn complete(args: &[Argument]) -> TokenStream { quote!(Command { name: String::from(option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME"))), + summary: String::from(#summary), + after_options: String::from(#after_options), + version: String::from(env!("CARGO_PKG_VERSION")), args: vec![#(#arg_specs),*] }) } diff --git a/derive/src/help.rs b/derive/src/help.rs index 877a3d8..9ebeaf2 100644 --- a/derive/src/help.rs +++ b/derive/src/help.rs @@ -99,7 +99,7 @@ pub fn help_string( ) } -fn read_help_file(file: &str) -> (String, String, String) { +pub fn read_help_file(file: &str) -> (String, String, String) { let path = Path::new(file); let manifest_dir = std::env::var("CARGO_MANIFEST_DIR").unwrap(); let mut location = PathBuf::from(manifest_dir); diff --git a/derive/src/lib.rs b/derive/src/lib.rs index 5df241f..e855127 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -45,7 +45,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { &arguments_attr.version_flags, &arguments_attr.file, ); - let complete_command = complete::complete(&arguments); + let complete_command = complete::complete(&arguments, &arguments_attr.file); let help = help_handling(&arguments_attr.help_flags); let version = version_handling(&arguments_attr.version_flags); let version_string = quote!(format!( From 72894a920e2c2d03ea8534ebfce921aaf2ecabf6 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Fri, 8 Dec 2023 16:14:15 +0100 Subject: [PATCH 09/12] initial man page generation --- .vscode/settings.json | 1 + complete/Cargo.toml | 1 + complete/src/lib.rs | 3 ++- complete/src/man.rs | 33 +++++++++++++++++++++++++++++++++ 4 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 complete/src/man.rs diff --git a/.vscode/settings.json b/.vscode/settings.json index ae1e84e..7fca93f 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -11,6 +11,7 @@ "Nonblank", "nonprinting", "pico", + "Roff", "struct", "uutils", "xflags" diff --git a/complete/Cargo.toml b/complete/Cargo.toml index 54abe1a..16c73d0 100644 --- a/complete/Cargo.toml +++ b/complete/Cargo.toml @@ -8,3 +8,4 @@ license = "MIT" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +roff = "0.2.1" \ No newline at end of file diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 4504797..b703d64 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -2,6 +2,7 @@ // file that was distributed with this source code. mod fish; +mod man; mod md; mod zsh; @@ -39,7 +40,7 @@ pub fn render(c: &Command, shell: &str) -> String { "md" => md::render(c), "fish" => fish::render(c), "zsh" => zsh::render(c), - "man" => panic!("manpages are not implemented yet"), + "man" => man::render(c), "sh" | "bash" | "csh" | "elvish" | "powershell" => panic!("shell '{shell}' completion is not implemented yet!"), _ => panic!("unknown option '{shell}'! Expected one of: \"md\", \"fish\", \"zsh\", \"man\", \"sh\", \"bash\", \"csh\", \"elvish\", \"powershell\""), } diff --git a/complete/src/man.rs b/complete/src/man.rs new file mode 100644 index 0000000..44500a6 --- /dev/null +++ b/complete/src/man.rs @@ -0,0 +1,33 @@ +use crate::Command; +use roff::{bold, roman, Roff}; + +pub fn render(c: &Command) -> String { + let mut page = Roff::new(); + page.control("TH", [&c.name.to_uppercase(), "1"]); + page.control("SH", ["NAME"]); + page.text([roman(&c.name)]); + page.control("SH", ["DESCRIPTION"]); + page.text([roman(&c.summary)]); + page.control("SH", ["OPTIONS"]); + + for arg in &c.args { + page.control("TP", []); + + let mut flags = Vec::new(); + for l in &arg.long { + if !flags.is_empty() { + flags.push(roman(", ")); + } + flags.push(bold(format!("--{l}"))); + } + for s in &arg.short { + if !flags.is_empty() { + flags.push(roman(", ")); + } + flags.push(bold(format!("-{s}"))); + } + page.text(flags); + page.text([roman(&arg.help)]); + } + page.render() +} From c4cb26cab8e5d54952a6861fbc47820067d5f3e1 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 9 Dec 2023 14:37:02 +0100 Subject: [PATCH 10/12] add license header to man.rs --- complete/src/man.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/complete/src/man.rs b/complete/src/man.rs index 44500a6..34d92aa 100644 --- a/complete/src/man.rs +++ b/complete/src/man.rs @@ -1,3 +1,6 @@ +// For the full copyright and license information, please view the LICENSE +// file that was distributed with this source code. + use crate::Command; use roff::{bold, roman, Roff}; From cab0fb2cc9220b0f2e6b620adeb18130d01a1cc4 Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 9 Dec 2023 16:51:45 +0100 Subject: [PATCH 11/12] add value names for flags in md and man --- complete/src/fish.rs | 42 ++++++++++++++++++++++------------- complete/src/lib.rs | 50 +++++++++++++++++++++++++++++++++--------- complete/src/man.rs | 43 ++++++++++++++++++++++++++++-------- complete/src/md.rs | 24 ++++++++++++++------ complete/src/zsh.rs | 12 +++++----- derive/src/complete.rs | 41 +++++++++++++++++++++++++--------- derive/src/lib.rs | 3 +-- src/lib.rs | 2 +- 8 files changed, 157 insertions(+), 60 deletions(-) diff --git a/complete/src/fish.rs b/complete/src/fish.rs index f86fc93..d6f0f66 100644 --- a/complete/src/fish.rs +++ b/complete/src/fish.rs @@ -1,19 +1,22 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Command, ValueHint}; +use crate::{Command, Flag, ValueHint}; /// Create completion script for `fish` +/// +/// Short and long options are combined into single `complete` calls, even if +/// they differ in whether they take arguments or not. pub fn render(c: &Command) -> String { let mut out = String::new(); let name = &c.name; for arg in &c.args { let mut line = format!("complete -c {name}"); - for short in &arg.short { - line.push_str(&format!(" -s {short}")); + for Flag { flag, .. } in &arg.short { + line.push_str(&format!(" -s {flag}")); } - for long in &arg.long { - line.push_str(&format!(" -l {long}")); + for Flag { flag, .. } in &arg.long { + line.push_str(&format!(" -l {flag}")); } line.push_str(&format!(" -d '{}'", arg.help)); if let Some(value) = &arg.value { @@ -42,15 +45,18 @@ fn render_value_hint(value: &ValueHint) -> String { #[cfg(test)] mod test { use super::render; - use crate::{Arg, Command, ValueHint}; + use crate::{Arg, Command, Flag, Value, ValueHint}; #[test] fn short() { let c = Command { - name: "test".into(), + name: "test", args: vec![Arg { - short: vec!["a".into()], - help: "some flag".into(), + short: vec![Flag { + flag: "a", + value: Value::No, + }], + help: "some flag", ..Arg::default() }], ..Command::default() @@ -61,10 +67,13 @@ mod test { #[test] fn long() { let c = Command { - name: "test".into(), + name: "test", args: vec![Arg { - long: vec!["all".into()], - help: "some flag".into(), + long: vec![Flag { + flag: "all", + value: Value::No, + }], + help: "some flag", ..Arg::default() }], ..Command::default() @@ -92,11 +101,14 @@ mod test { ]; for (hint, expected) in args { let c = Command { - name: "test".into(), + name: "test", args: vec![Arg { - short: vec!["a".into()], + short: vec![Flag { + flag: "a", + value: Value::No, + }], long: vec![], - help: "some flag".into(), + help: "some flag", value: Some(hint), }], ..Command::default() diff --git a/complete/src/lib.rs b/complete/src/lib.rs index b703d64..14a3a47 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -1,28 +1,58 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. +//! Generation of completion and documentation +//! +//! All formats use the [`Command`] struct as input, which specifies all +//! information needed. This struct is similar to some structs in the derive +//! crate for uutils-args, but there are some key differences: +//! +//! - This is meant to be more general. +//! - Some information is added (such as fields for the summary) +//! - We have [`ValueHint`] in this crate. +//! - Some information is removed because it is irrelevant for completion and documentation +//! - This struct is meant to exist at runtime of the program +//! mod fish; mod man; mod md; mod zsh; +/// A description of a CLI command +/// +/// The completions and documentation will be generated based on this struct. #[derive(Default)] -pub struct Command { - pub name: String, - pub summary: String, - pub version: String, - pub after_options: String, - pub args: Vec, +pub struct Command<'a> { + pub name: &'a str, + pub summary: &'a str, + pub version: &'a str, + pub after_options: &'a str, + pub args: Vec>, } +/// Description of an argument +/// +/// An argument may consist of several flags. In completions and documentation +/// formats that support it, these flags will be grouped. #[derive(Default)] -pub struct Arg { - pub short: Vec, - pub long: Vec, - pub help: String, +pub struct Arg<'a> { + pub short: Vec>, + pub long: Vec>, + pub help: &'a str, pub value: Option, } +pub struct Flag<'a> { + pub flag: &'a str, + pub value: Value<'a>, +} + +pub enum Value<'a> { + Required(&'a str), + Optional(&'a str), + No, +} + // Modelled after claps ValueHint pub enum ValueHint { Strings(Vec), diff --git a/complete/src/man.rs b/complete/src/man.rs index 34d92aa..494f828 100644 --- a/complete/src/man.rs +++ b/complete/src/man.rs @@ -1,36 +1,61 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::Command; -use roff::{bold, roman, Roff}; +use crate::{Command, Flag, Value}; +use roff::{bold, italic, roman, Roff}; pub fn render(c: &Command) -> String { let mut page = Roff::new(); page.control("TH", [&c.name.to_uppercase(), "1"]); page.control("SH", ["NAME"]); - page.text([roman(&c.name)]); + page.text([roman(c.name)]); page.control("SH", ["DESCRIPTION"]); - page.text([roman(&c.summary)]); + page.text([roman(c.summary)]); page.control("SH", ["OPTIONS"]); for arg in &c.args { page.control("TP", []); let mut flags = Vec::new(); - for l in &arg.long { + for Flag { flag, value } in &arg.long { if !flags.is_empty() { flags.push(roman(", ")); } - flags.push(bold(format!("--{l}"))); + flags.push(bold(format!("--{flag}"))); + match value { + Value::Required(name) => { + flags.push(roman("=")); + flags.push(italic(*name)); + } + Value::Optional(name) => { + flags.push(roman("[")); + flags.push(roman("=")); + flags.push(italic(*name)); + flags.push(roman("]")); + } + Value::No => {} + } } - for s in &arg.short { + for Flag { flag, value } in &arg.short { if !flags.is_empty() { flags.push(roman(", ")); } - flags.push(bold(format!("-{s}"))); + flags.push(bold(format!("-{flag}"))); + match value { + Value::Required(name) => { + flags.push(roman(" ")); + flags.push(italic(*name)); + } + Value::Optional(name) => { + flags.push(roman("[")); + flags.push(italic(*name)); + flags.push(roman("]")); + } + Value::No => {} + } } page.text(flags); - page.text([roman(&arg.help)]); + page.text([roman(arg.help)]); } page.render() } diff --git a/complete/src/md.rs b/complete/src/md.rs index 08f502f..1c2fa1b 100644 --- a/complete/src/md.rs +++ b/complete/src/md.rs @@ -1,18 +1,18 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::Command; +use crate::{Command, Flag, Value}; /// Render command to a markdown file for mdbook pub fn render(c: &Command) -> String { let mut out = String::new(); out.push_str(&title(c)); out.push_str(&additional(c)); - out.push_str(&c.summary); + out.push_str(c.summary); out.push_str("\n\n"); out.push_str(&options(c)); out.push_str("\n\n"); - out.push_str(&c.after_options); + out.push_str(c.after_options); out.push('\n'); out } @@ -40,12 +40,22 @@ fn options(c: &Command) -> String { let mut flags = Vec::new(); - for long in &arg.long { - flags.push(format!("--{long}")); + for Flag { flag, value } in &arg.long { + let value_str = match value { + Value::Required(name) => format!("={name}"), + Value::Optional(name) => format!("[={name}]"), + Value::No => String::new(), + }; + flags.push(format!("--{flag}{value_str}")); } - for short in &arg.short { - flags.push(format!("-{short}")) + for Flag { flag, value } in &arg.short { + let value_str = match value { + Value::Required(name) => format!(" {name}"), + Value::Optional(name) => format!("[{name}]"), + Value::No => String::new(), + }; + flags.push(format!("-{flag}{value_str}")); } out.push_str(&flags.join(", ")); diff --git a/complete/src/zsh.rs b/complete/src/zsh.rs index 814c904..5087697 100644 --- a/complete/src/zsh.rs +++ b/complete/src/zsh.rs @@ -1,11 +1,11 @@ // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -use crate::{Arg, Command}; +use crate::{Arg, Command, Flag}; /// Create completion script for `zsh` pub fn render(c: &Command) -> String { - template(&c.name, &render_args(&c.args)) + template(c.name, &render_args(&c.args)) } fn render_args(args: &[Arg]) -> String { @@ -13,11 +13,11 @@ fn render_args(args: &[Arg]) -> String { let indent = " ".repeat(8); for arg in args { let help = &arg.help; - for short in &arg.short { - out.push_str(&format!("{indent}'-{short}[{help}]' \\\n")); + for Flag { flag, .. } in &arg.short { + out.push_str(&format!("{indent}'-{flag}[{help}]' \\\n")); } - for long in &arg.long { - out.push_str(&format!("{indent}'--{long}[{help}]' \\\n")); + for Flag { flag, .. } in &arg.long { + out.push_str(&format!("{indent}'--{flag}[{help}]' \\\n")); } } out diff --git a/derive/src/complete.rs b/derive/src/complete.rs index 2d7b365..9e9ea96 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -3,7 +3,7 @@ use crate::{ argument::{ArgType, Argument}, - flags::{Flag, Flags}, + flags::{Flag, Flags, Value}, }; use proc_macro2::TokenStream; use quote::quote; @@ -40,12 +40,33 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { let short: Vec<_> = short .iter() - .map(|Flag { flag, .. }| quote!(String::from(#flag))) + .map(|Flag { flag, value }| { + let flag = flag.to_string(); + let value = match value { + Value::No => quote!(::uutils_args_complete::Value::No), + Value::Optional(name) => quote!(::uutils_args_complete::Value::Optional(#name)), + Value::Required(name) => quote!(::uutils_args_complete::Value::Optional(#name)), + }; + quote!(::uutils_args_complete::Flag { + flag: #flag, + value: #value + }) + }) .collect(); let long: Vec<_> = long .iter() - .map(|Flag { flag, .. }| quote!(String::from(#flag))) + .map(|Flag { flag, value }| { + let value = match value { + Value::No => quote!(::uutils_args_complete::Value::No), + Value::Optional(name) => quote!(::uutils_args_complete::Value::Optional(#name)), + Value::Required(name) => quote!(::uutils_args_complete::Value::Optional(#name)), + }; + quote!(::uutils_args_complete::Flag { + flag: #flag, + value: #value + }) + }) .collect(); let hint = if let Some(ty) = field { @@ -55,20 +76,20 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { }; arg_specs.push(quote!( - Arg { + ::uutils_args_complete::Arg { short: vec![#(#short),*], long: vec![#(#long),*], - help: String::from(#help), + help: #help, value: #hint, } )) } - quote!(Command { - name: String::from(option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME"))), - summary: String::from(#summary), - after_options: String::from(#after_options), - version: String::from(env!("CARGO_PKG_VERSION")), + quote!(::uutils_args_complete::Command { + name: option_env!("CARGO_BIN_NAME").unwrap_or(env!("CARGO_PKG_NAME")), + summary: #summary, + after_options: #after_options, + version: env!("CARGO_PKG_VERSION"), args: vec![#(#arg_specs),*] }) } diff --git a/derive/src/lib.rs b/derive/src/lib.rs index e855127..cd8ccbf 100644 --- a/derive/src/lib.rs +++ b/derive/src/lib.rs @@ -107,8 +107,7 @@ pub fn arguments(input: TokenStream) -> TokenStream { } #[cfg(feature = "complete")] - fn complete() -> ::uutils_args_complete::Command { - use ::uutils_args_complete::{Command, Arg, ValueHint}; + fn complete() -> ::uutils_args_complete::Command<'static> { use ::uutils_args::Value; #complete_command } diff --git a/src/lib.rs b/src/lib.rs index 22d124b..5ec42d2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -110,7 +110,7 @@ pub trait Arguments: Sized { } #[cfg(feature = "complete")] - fn complete() -> uutils_args_complete::Command; + fn complete() -> uutils_args_complete::Command<'static>; } /// An iterator over arguments. From 0bf84d564251b3f3f6060f1af9f7caf0a419112e Mon Sep 17 00:00:00 2001 From: Terts Diepraam Date: Sat, 9 Dec 2023 14:28:11 +0100 Subject: [PATCH 12/12] add authors and copyright info to man page --- complete/src/lib.rs | 2 ++ complete/src/man.rs | 7 +++++++ derive/src/complete.rs | 4 +++- 3 files changed, 12 insertions(+), 1 deletion(-) diff --git a/complete/src/lib.rs b/complete/src/lib.rs index 14a3a47..15dc7ba 100644 --- a/complete/src/lib.rs +++ b/complete/src/lib.rs @@ -28,6 +28,8 @@ pub struct Command<'a> { pub version: &'a str, pub after_options: &'a str, pub args: Vec>, + pub license: &'a str, + pub authors: &'a str, } /// Description of an argument diff --git a/complete/src/man.rs b/complete/src/man.rs index 494f828..046cf08 100644 --- a/complete/src/man.rs +++ b/complete/src/man.rs @@ -57,5 +57,12 @@ pub fn render(c: &Command) -> String { page.text(flags); page.text([roman(arg.help)]); } + + page.control("SH", ["AUTHORS"]); + page.text([roman(c.authors)]); + + page.control("SH", ["COPYRIGHT"]); + page.text([roman(format!("Copyright © {}.", &c.authors))]); + page.text([roman(format!("License: {}", &c.license))]); page.render() } diff --git a/derive/src/complete.rs b/derive/src/complete.rs index 9e9ea96..64f7d05 100644 --- a/derive/src/complete.rs +++ b/derive/src/complete.rs @@ -90,6 +90,8 @@ pub fn complete(args: &[Argument], file: &Option) -> TokenStream { summary: #summary, after_options: #after_options, version: env!("CARGO_PKG_VERSION"), - args: vec![#(#arg_specs),*] + args: vec![#(#arg_specs),*], + license: env!("CARGO_PKG_LICENSE"), + authors: env!("CARGO_PKG_AUTHORS"), }) }