You've already forked uutils-args
mirror of
https://github.com/uutils/uutils-args.git
synced 2026-06-10 16:13:08 -07:00
Merge pull request #55 from tertsdiepraam/completion
Completion, docs and man pages
This commit is contained in:
@@ -23,7 +23,7 @@ jobs:
|
||||
- uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: test
|
||||
args: --all-features --workspace
|
||||
args: --features complete --workspace
|
||||
|
||||
rustfmt:
|
||||
name: Rustfmt
|
||||
|
||||
Vendored
+1
@@ -11,6 +11,7 @@
|
||||
"Nonblank",
|
||||
"nonprinting",
|
||||
"pico",
|
||||
"Roff",
|
||||
"struct",
|
||||
"uutils",
|
||||
"xflags"
|
||||
|
||||
+6
-3
@@ -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"]
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
[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]
|
||||
roff = "0.2.1"
|
||||
Symlink
+1
@@ -0,0 +1 @@
|
||||
../LICENSE
|
||||
@@ -0,0 +1,122 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
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 Flag { flag, .. } in &arg.short {
|
||||
line.push_str(&format!(" -s {flag}"));
|
||||
}
|
||||
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 {
|
||||
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!(" -f -a \"{joined}\"")
|
||||
}
|
||||
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, Flag, Value, ValueHint};
|
||||
|
||||
#[test]
|
||||
fn short() {
|
||||
let c = Command {
|
||||
name: "test",
|
||||
args: vec![Arg {
|
||||
short: vec![Flag {
|
||||
flag: "a",
|
||||
value: Value::No,
|
||||
}],
|
||||
help: "some flag",
|
||||
..Arg::default()
|
||||
}],
|
||||
..Command::default()
|
||||
};
|
||||
assert_eq!(render(&c), "complete -c test -s a -d 'some flag'\n",)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn long() {
|
||||
let c = Command {
|
||||
name: "test",
|
||||
args: vec![Arg {
|
||||
long: vec![Flag {
|
||||
flag: "all",
|
||||
value: Value::No,
|
||||
}],
|
||||
help: "some flag",
|
||||
..Arg::default()
|
||||
}],
|
||||
..Command::default()
|
||||
};
|
||||
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",
|
||||
args: vec![Arg {
|
||||
short: vec![Flag {
|
||||
flag: "a",
|
||||
value: Value::No,
|
||||
}],
|
||||
long: vec![],
|
||||
help: "some flag",
|
||||
value: Some(hint),
|
||||
}],
|
||||
..Command::default()
|
||||
};
|
||||
assert_eq!(
|
||||
render(&c),
|
||||
format!("complete -c test -s a -d 'some flag' {expected}\n")
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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<'a> {
|
||||
pub name: &'a str,
|
||||
pub summary: &'a str,
|
||||
pub version: &'a str,
|
||||
pub after_options: &'a str,
|
||||
pub args: Vec<Arg<'a>>,
|
||||
pub license: &'a str,
|
||||
pub authors: &'a str,
|
||||
}
|
||||
|
||||
/// 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<'a> {
|
||||
pub short: Vec<Flag<'a>>,
|
||||
pub long: Vec<Flag<'a>>,
|
||||
pub help: &'a str,
|
||||
pub value: Option<ValueHint>,
|
||||
}
|
||||
|
||||
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<String>),
|
||||
Unknown,
|
||||
AnyPath,
|
||||
FilePath,
|
||||
DirPath,
|
||||
ExecutablePath,
|
||||
Username,
|
||||
Hostname,
|
||||
}
|
||||
|
||||
pub fn render(c: &Command, shell: &str) -> String {
|
||||
match shell {
|
||||
"md" => md::render(c),
|
||||
"fish" => fish::render(c),
|
||||
"zsh" => zsh::render(c),
|
||||
"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\""),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
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.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 Flag { flag, value } in &arg.long {
|
||||
if !flags.is_empty() {
|
||||
flags.push(roman(", "));
|
||||
}
|
||||
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 Flag { flag, value } in &arg.short {
|
||||
if !flags.is_empty() {
|
||||
flags.push(roman(", "));
|
||||
}
|
||||
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.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()
|
||||
}
|
||||
@@ -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, 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("\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!(
|
||||
"\
|
||||
<div class=\"additional\">\
|
||||
{version}\
|
||||
</div>\n\n\
|
||||
"
|
||||
)
|
||||
}
|
||||
|
||||
fn options(c: &Command) -> String {
|
||||
let mut out = String::from("## Options\n\n");
|
||||
out.push_str("<dl>\n");
|
||||
for arg in &c.args {
|
||||
out.push_str("<dt>");
|
||||
|
||||
let mut flags = Vec::new();
|
||||
|
||||
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!("<code>--{flag}{value_str}</code>"));
|
||||
}
|
||||
|
||||
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!("<code>-{flag}{value_str}</code>"));
|
||||
}
|
||||
|
||||
out.push_str(&flags.join(", "));
|
||||
out.push_str("</dt>\n");
|
||||
out.push_str(&format!("<dd>\n\n{}\n\n</dd>\n", arg.help));
|
||||
}
|
||||
out.push_str("</dl>");
|
||||
out
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use crate::{Arg, Command, Flag};
|
||||
|
||||
/// Create completion script for `zsh`
|
||||
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 Flag { flag, .. } in &arg.short {
|
||||
out.push_str(&format!("{indent}'-{flag}[{help}]' \\\n"));
|
||||
}
|
||||
for Flag { flag, .. } in &arg.long {
|
||||
out.push_str(&format!("{indent}'--{flag}[{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"
|
||||
)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ use crate::{
|
||||
|
||||
pub struct Argument {
|
||||
pub ident: Ident,
|
||||
pub field: Option<syn::Type>,
|
||||
pub name: String,
|
||||
pub arg_type: ArgType,
|
||||
pub help: String,
|
||||
@@ -105,6 +106,7 @@ pub fn parse_argument(v: Variant) -> Vec<Argument> {
|
||||
};
|
||||
Argument {
|
||||
ident: ident.clone(),
|
||||
field: field.clone(),
|
||||
name: name.clone(),
|
||||
arg_type,
|
||||
help: arg_help,
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
// 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, Value},
|
||||
};
|
||||
use proc_macro2::TokenStream;
|
||||
use quote::quote;
|
||||
|
||||
pub fn complete(args: &[Argument], file: &Option<String>) -> 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,
|
||||
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, 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, 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 {
|
||||
quote!(Some(<#ty>::value_hint()))
|
||||
} else {
|
||||
quote!(None)
|
||||
};
|
||||
|
||||
arg_specs.push(quote!(
|
||||
::uutils_args_complete::Arg {
|
||||
short: vec![#(#short),*],
|
||||
long: vec![#(#long),*],
|
||||
help: #help,
|
||||
value: #hint,
|
||||
}
|
||||
))
|
||||
}
|
||||
|
||||
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),*],
|
||||
license: env!("CARGO_PKG_LICENSE"),
|
||||
authors: env!("CARGO_PKG_AUTHORS"),
|
||||
})
|
||||
}
|
||||
+1
-1
@@ -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),
|
||||
|
||||
+1
-1
@@ -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);
|
||||
|
||||
+20
-1
@@ -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 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!(
|
||||
@@ -74,7 +76,6 @@ pub fn arguments(input: TokenStream) -> TokenStream {
|
||||
) -> Result<Option<uutils_args::Argument<Self>>, 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<'static> {
|
||||
use ::uutils_args::Value;
|
||||
#complete_command
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -124,6 +131,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();
|
||||
@@ -140,6 +148,7 @@ pub fn value(input: TokenStream) -> TokenStream {
|
||||
keys
|
||||
};
|
||||
|
||||
all_keys.extend(keys.clone());
|
||||
options.push(quote!(&[#(#keys),*]));
|
||||
|
||||
let stmt = if let Some(v) = value {
|
||||
@@ -188,6 +197,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()
|
||||
)
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
|
||||
@@ -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<Arg> 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);
|
||||
}
|
||||
+45
-5
@@ -108,6 +108,9 @@ pub trait Arguments: Sized {
|
||||
while iter.next_arg()?.is_some() {}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[cfg(feature = "complete")]
|
||||
fn complete() -> uutils_args_complete::Command<'static>;
|
||||
}
|
||||
|
||||
/// An iterator over arguments.
|
||||
@@ -187,17 +190,38 @@ pub trait Options<Arg: Arguments>: Sized {
|
||||
exit_if_err(self.try_parse(args), Arg::EXIT_CODE)
|
||||
}
|
||||
|
||||
#[allow(unused_mut)]
|
||||
fn try_parse<I>(mut self, args: I) -> Result<Self, Error>
|
||||
where
|
||||
I: IntoIterator + 'static,
|
||||
I::Item: Into<OsString>,
|
||||
{
|
||||
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_complete::<_, 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 +248,22 @@ pub fn __echo_style_positional(p: &mut lexopt::Parser, short_args: &[char]) -> O
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "parse-is-complete")]
|
||||
fn print_complete<I, O: Options<Arg>, Arg: Arguments>(mut args: I)
|
||||
where
|
||||
I: Iterator + 'static,
|
||||
I::Item: Into<OsString>,
|
||||
{
|
||||
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,
|
||||
|
||||
@@ -6,6 +6,8 @@ use std::{
|
||||
ffi::{OsStr, OsString},
|
||||
path::PathBuf,
|
||||
};
|
||||
#[cfg(feature = "complete")]
|
||||
use uutils_args_complete::ValueHint;
|
||||
|
||||
pub type ValueResult<T> = Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;
|
||||
|
||||
@@ -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<Self>;
|
||||
|
||||
#[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<Self> {
|
||||
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<Self> {
|
||||
Ok(Some(T::from_value(value)?))
|
||||
}
|
||||
|
||||
#[cfg(feature = "complete")]
|
||||
fn value_hint() -> uutils_args_complete::ValueHint {
|
||||
T::value_hint()
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! value_int {
|
||||
|
||||
Reference in New Issue
Block a user