Merge pull request #55 from tertsdiepraam/completion

Completion, docs and man pages
This commit is contained in:
Sylvestre Ledru
2023-12-11 15:39:38 +01:00
committed by GitHub
19 changed files with 634 additions and 18 deletions
+1 -1
View File
@@ -23,7 +23,7 @@ jobs:
- uses: actions-rs/cargo@v1
with:
command: test
args: --all-features --workspace
args: --features complete --workspace
rustfmt:
name: Rustfmt
+1
View File
@@ -11,6 +11,7 @@
"Nonblank",
"nonprinting",
"pico",
"Roff",
"struct",
"uutils",
"xflags"
+6 -3
View File
@@ -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"]
+11
View File
@@ -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"
+1
View File
@@ -0,0 +1 @@
../LICENSE
+122
View File
@@ -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")
)
}
}
}
+79
View File
@@ -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\""),
}
}
+68
View File
@@ -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()
}
+67
View File
@@ -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
}
+55
View File
@@ -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"
)
}
+2
View File
@@ -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,
+97
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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()
)
}
}
);
+2 -6
View File
@@ -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),
+38
View File
@@ -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
View File
@@ -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,
+17
View File
@@ -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 {