From 42f086ebe589f63c3f31317d8e45652e5f8da2da Mon Sep 17 00:00:00 2001 From: Jeff Dickey <216188+jdx@users.noreply.github.com> Date: Sat, 10 Feb 2024 05:23:38 -0600 Subject: [PATCH] wip --- .mise.toml | 4 +-- Cargo.lock | 2 ++ Cargo.toml | 1 + cli/Cargo.toml | 1 + cli/src/cli/complete_word.rs | 3 +- cli/src/cli/generate/completion.rs | 46 +++++++++++++++---------- cli/tests/complete_word.rs | 4 +-- examples/mise.usage.kdl | 1 - src/complete/bash.rs | 54 +++++++++++++++--------------- src/complete/fish.rs | 26 +++++++------- src/complete/zsh.rs | 31 ++++++++--------- src/env.rs | 10 ++++-- src/parse/cmd.rs | 24 +++++++++++++ src/parse/spec.rs | 18 ---------- 14 files changed, 123 insertions(+), 102 deletions(-) diff --git a/.mise.toml b/.mise.toml index cdb2fd9..85ff02c 100644 --- a/.mise.toml +++ b/.mise.toml @@ -8,6 +8,7 @@ outputs = ['target/debug/rtx'] run = 'cargo build --all' [tasks.cli] +alias = ['x'] depends = ['build'] run = 'usage' raw = true @@ -18,8 +19,7 @@ depends = ['build'] run = 'usage cw' raw = true -[tasks.run] -alias = ['x'] +[tasks.run-example] depends = ['build'] run = './examples/example.sh' raw = true diff --git a/Cargo.lock b/Cargo.lock index a4c7822..f752f2a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1322,6 +1322,7 @@ dependencies = [ "contracts", "env_logger", "exec", + "heck", "indexmap", "itertools", "kdl", @@ -1343,6 +1344,7 @@ name = "usage-lib" version = "0.1.0" dependencies = [ "clap", + "heck", "indexmap", "insta", "itertools", diff --git a/Cargo.toml b/Cargo.toml index 705beef..e8f2529 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -36,6 +36,7 @@ shell-escape = "0.1" strum = { version = "0.26", features = ["derive"] } thiserror = "1" xx = "0.2" +heck = "0.4.1" [features] default = ["clap"] diff --git a/cli/Cargo.toml b/cli/Cargo.toml index 3a681ea..dfcb92b 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -26,6 +26,7 @@ clap = { version = "4", features = ["derive", "string"] } contracts = "0.6" env_logger = "0.11" exec = "0.3" +heck = "0.4" indexmap = "2" itertools = "0.12" kdl = "4" diff --git a/cli/src/cli/complete_word.rs b/cli/src/cli/complete_word.rs index 014d54e..8dbeff4 100644 --- a/cli/src/cli/complete_word.rs +++ b/cli/src/cli/complete_word.rs @@ -114,10 +114,11 @@ impl Display for ParseValue { fn parse(spec: &Spec, mut words: VecDeque) -> miette::Result { let mut cmds = vec![]; let mut cmd = &spec.cmd; + words.pop_front(); cmds.push(cmd); while !words.is_empty() { - if let Some(subcommand) = cmd.subcommands.get(&words[0]) { + if let Some(subcommand) = cmd.find_subcommand(&words[0]) { words.pop_front(); cmds.push(subcommand); cmd = subcommand; diff --git a/cli/src/cli/generate/completion.rs b/cli/src/cli/generate/completion.rs index 79ab427..13ac9a9 100644 --- a/cli/src/cli/generate/completion.rs +++ b/cli/src/cli/generate/completion.rs @@ -1,34 +1,44 @@ -use std::path::PathBuf; - use clap::Args; -use usage::Spec; - #[derive(Args)] -#[clap(visible_alias = "c", aliases=["complete", "completions"])] +#[clap(visible_alias = "c", aliases = ["complete", "completions"])] pub struct Completion { #[clap(value_parser = ["bash", "fish", "zsh"])] shell: String, - #[clap(short, long)] - file: Option, + /// The CLI which we're generates completions for + bin: String, - #[clap(short, long, required_unless_present = "file", overrides_with = "file")] - spec: Option, + /// A command which generates a usage spec + /// e.g.: `mycli --usage` or `mycli completion usage` + /// Defaults to "$bin --usage" + #[clap(long)] + usage_cmd: Option, + // #[clap(short, long)] + // file: Option, + // + // #[clap(short, long, required_unless_present = "file", overrides_with = "file")] + // spec: Option, } impl Completion { pub fn run(&self) -> miette::Result<()> { - let spec = if let Some(file) = &self.file { - let (spec, _) = Spec::parse_file(file)?; - spec - } else { - Spec::parse_spec(self.spec.as_ref().unwrap())? - }; + // let spec = if let Some(file) = &self.file { + // let (spec, _) = Spec::parse_file(file)?; + // spec + // } else { + // Spec::parse_spec(self.spec.as_ref().unwrap())? + // }; + let bin = &self.bin; + let usage_cmd = self + .usage_cmd + .clone() + .unwrap_or_else(|| format!("{bin} --usage")); + let script = match self.shell.as_str() { - "bash" => usage::complete::bash::complete_bash(&spec), - "fish" => usage::complete::fish::complete_fish(&spec), - "zsh" => usage::complete::zsh::complete_zsh(&spec), + "bash" => usage::complete::bash::complete_bash(bin, &usage_cmd), + "fish" => usage::complete::fish::complete_fish(bin, &usage_cmd), + "zsh" => usage::complete::zsh::complete_zsh(bin, &usage_cmd), _ => unreachable!(), }; println!("{}", script.trim()); diff --git a/cli/tests/complete_word.rs b/cli/tests/complete_word.rs index cecfcc0..6e77ded 100644 --- a/cli/tests/complete_word.rs +++ b/cli/tests/complete_word.rs @@ -17,7 +17,7 @@ fn complete_word_subcommands() { #[test] fn complete_word_cword() { - assert_cmd(&["--cword=2", "plugins", "install"]).stdout(contains("plugin-2")); + assert_cmd(&["--cword=3", "plugins", "install"]).stdout(contains("plugin-2")); } #[test] @@ -42,7 +42,7 @@ fn complete_word_short_flag() { fn cmd() -> Command { let mut cmd = Command::cargo_bin("usage").unwrap(); - cmd.args(["cw", "-f", "../examples/basic.usage.kdl"]); + cmd.args(["cw", "-f", "../examples/basic.usage.kdl", "mycli"]); cmd } diff --git a/examples/mise.usage.kdl b/examples/mise.usage.kdl index 2ff331b..52ca7fe 100644 --- a/examples/mise.usage.kdl +++ b/examples/mise.usage.kdl @@ -1,6 +1,5 @@ name "mise" bin "mise" -version "2024.2.9-DEBUG macos-arm64 (494aafd 2024-02-10)" about "The front-end to your dev env" long_about r"mise is a tool for managing runtime versions. https://github.com/jdx/mise diff --git a/src/complete/bash.rs b/src/complete/bash.rs index d0de2eb..a4793e0 100644 --- a/src/complete/bash.rs +++ b/src/complete/bash.rs @@ -1,17 +1,23 @@ -use crate::{env, Spec}; +use heck::ToShoutySnakeCase; -pub fn complete_bash(spec: &Spec) -> String { - let usage = &*env::USAGE_CMD; - let bin = &spec.bin; - let raw = shell_escape::unix::escape(spec.to_string().into()); +use crate::env; + +pub fn complete_bash(bin: &str, usage_cmd: &str) -> String { + let usage = env::USAGE_BIN.display(); + let bin_up = bin.to_shouty_snake_case(); + // let bin = &spec.bin; + // let raw = shell_escape::unix::escape(spec.to_string().into()); format!( r#" _{bin}() {{ - local raw - spec={raw} - - COMPREPLY=($({usage} complete-word -s "$spec" --cword="$COMP_CWORD" -- "${{COMP_WORDS[@]}}")) - #COMPREPLY=($(compgen -W "${{COMPREPLY[*]}}" -- "${{COMP_WORDS[$COMP_CWORD]}}")) + if [[ -z ${{_USAGE_SPEC_{bin_up}:-}} ]]; then + _USAGE_SPEC_{bin_up}="$({usage_cmd})" + fi + + COMPREPLY=( $({usage} complete-word -s "${{_USAGE_SPEC_{bin_up}}}" --cword="$COMP_CWORD" -- "${{COMP_WORDS[@]}}") ) + if [[ $? -ne 0 ]]; then + unset COMPREPLY + fi return 0 }} @@ -21,32 +27,26 @@ complete -F _{bin} {bin} ) } -// fn render_args(cmds: &[&SchemaCmd]) -> String { -// format!("XX") -// } - #[cfg(test)] mod tests { - use crate::parse::spec::Spec; - use super::*; #[test] fn test_complete_bash() { - let spec = r#" - "#; - let spec = Spec::parse(&Default::default(), spec).unwrap(); - assert_snapshot!(complete_bash(&spec).trim(), @r###" - _() { - local raw - spec='' - - COMPREPLY=($(usage complete-word -s "$spec" --cword="$COMP_CWORD" -- "${COMP_WORDS[@]}")) - #COMPREPLY=($(compgen -W "${COMPREPLY[*]}" -- "${COMP_WORDS[$COMP_CWORD]}")) + assert_snapshot!(complete_bash("mycli", "mycli complete --usage").trim(), @r###" + _mycli() { + if [[ -z ${_USAGE_SPEC_MYCLI:-} ]]; then + _USAGE_SPEC_MYCLI="$(mycli complete --usage)" + fi + + COMPREPLY=( $(/Users/jdx/src/usage/target/debug/deps/usage-6b6342071eb3064a complete-word -s "${_USAGE_SPEC_MYCLI}" --cword="$COMP_CWORD" -- "${COMP_WORDS[@]}") ) + if [[ $? -ne 0 ]]; then + unset COMPREPLY + fi return 0 } - complete -F _ + complete -F _mycli mycli # vim: noet ci pi sts=0 sw=4 ts=4 ft=sh "###); } diff --git a/src/complete/fish.rs b/src/complete/fish.rs index 42613a1..8519a1a 100644 --- a/src/complete/fish.rs +++ b/src/complete/fish.rs @@ -1,12 +1,12 @@ -use crate::{env, Spec}; +use crate::env; -pub fn complete_fish(spec: &Spec) -> String { - let usage = &*env::USAGE_CMD; - let bin = &spec.bin; - let raw = spec.to_string().replace('\'', r"\'").to_string(); +pub fn complete_fish(bin: &str, usage_cmd: &str) -> String { + let usage = env::USAGE_BIN.display(); + // let bin = &spec.bin; + // let raw = spec.to_string().replace('\'', r"\'").to_string(); format!( r#" -set _usage_spec_{bin} '{raw}' +set _usage_spec_{bin} ({usage_cmd}) complete -xc {bin} -a '({usage} complete-word -s "$_usage_spec_{bin}" --ctoken=(commandline -t) -- (commandline -op))' "# ) @@ -14,18 +14,16 @@ complete -xc {bin} -a '({usage} complete-word -s "$_usage_spec_{bin}" --ctoken=( #[cfg(test)] mod tests { - use crate::parse::spec::Spec; - use super::*; #[test] fn test_complete_fish() { - let spec = r#" - "#; - let spec = Spec::parse(&Default::default(), spec).unwrap(); - assert_snapshot!(complete_fish(&spec).trim(), @r###" - set _usage_spec_ '' - complete -xc -a '(usage complete-word -s "$_usage_spec_" --ctoken=(commandline -t) -- (commandline -op))' + // let spec = r#" + // "#; + // let spec = Spec::parse(&Default::default(), spec).unwrap(); + assert_snapshot!(complete_fish("mycli", "mycli complete --usage").trim(), @r###" + set _usage_spec_mycli (mycli complete --usage) + complete -xc mycli -a '(/Users/jdx/src/usage/target/debug/deps/usage-6b6342071eb3064a complete-word -s "$_usage_spec_mycli" --ctoken=(commandline -t) -- (commandline -op))' "###); } } diff --git a/src/complete/zsh.rs b/src/complete/zsh.rs index e5be743..f5d71e6 100644 --- a/src/complete/zsh.rs +++ b/src/complete/zsh.rs @@ -1,17 +1,14 @@ -use crate::Spec; - -pub fn complete_zsh(spec: &Spec) -> String { +pub fn complete_zsh(bin: &str, usage_cmd: &str) -> String { // let cmds = vec![&spec.cmd]; // let args = render_args(&cmds); - let bin = &spec.bin; - let raw = spec.to_string(); format!( r#" #compdef {bin} _{bin}() {{ typeset -A opt_args local context state line curcontext=$curcontext - local spec='{raw}' + local spec + spec="$({usage_cmd})" _arguments -s -S \ '-h[Show help information]' \ @@ -37,19 +34,19 @@ fi #[cfg(test)] mod tests { use super::*; - use crate::parse::spec::Spec; #[test] fn test_complete_zsh() { - let spec = r#" - "#; - let spec = Spec::parse(&Default::default(), spec).unwrap(); - assert_snapshot!(complete_zsh(&spec).trim(), @r###" - #compdef - _() { + // let spec = r#" + // "#; + // let spec = Spec::parse(&Default::default(), spec).unwrap(); + assert_snapshot!(complete_zsh("mycli", "mycli complete --usage").trim(), @r###" + #compdef mycli + _mycli() { typeset -A opt_args local context state line curcontext=$curcontext - local spec='' + local spec + spec="$(mycli complete --usage)" _arguments -s -S \ '-h[Show help information]' \ @@ -57,10 +54,10 @@ mod tests { '*:: :->command' && return } - if [ "$funcstack[1]" = "_" ]; then - _ "$@" + if [ "$funcstack[1]" = "_mycli" ]; then + _mycli "$@" else - compdef _ + compdef _mycli mycli fi # vim: noet ci pi sts=0 sw=4 ts=4 diff --git a/src/env.rs b/src/env.rs index 22e2b52..affd11e 100644 --- a/src/env.rs +++ b/src/env.rs @@ -1,6 +1,12 @@ pub use std::env::*; +use std::path::PathBuf; use once_cell::sync::Lazy; -pub static USAGE_CMD: Lazy = - Lazy::new(|| var("USAGE_CMD").unwrap_or_else(|_| "usage".to_string())); +#[cfg(target_os = "macos")] +pub static USAGE_BIN: Lazy = Lazy::new(|| { + var_os("USAGE_BIN") + .map(PathBuf::from) + .or_else(|| current_exe().ok()) + .unwrap_or_else(|| "usage".into()) +}); diff --git a/src/parse/cmd.rs b/src/parse/cmd.rs index 90f7fa6..db873db 100644 --- a/src/parse/cmd.rs +++ b/src/parse/cmd.rs @@ -1,3 +1,6 @@ +use std::collections::HashMap; +use std::sync::OnceLock; + use indexmap::IndexMap; use kdl::{KdlDocument, KdlEntry, KdlNode, KdlValue}; use serde::Serialize; @@ -27,6 +30,9 @@ pub struct SpecCommand { pub after_help: Option, pub after_long_help: Option, pub examples: Vec, + + #[serde(skip)] + pub subcommand_lookup: OnceLock>, } #[derive(Debug, Default, Serialize, Clone)] @@ -208,6 +214,24 @@ impl SpecCommand { } } + pub fn find_subcommand(&self, name: &str) -> Option<&SpecCommand> { + let sl = self.subcommand_lookup.get_or_init(|| { + let mut map = HashMap::new(); + for (name, cmd) in &self.subcommands { + map.insert(name.clone(), name.clone()); + for alias in &cmd.aliases { + map.insert(alias.clone(), name.clone()); + } + for alias in &cmd.hidden_aliases { + map.insert(alias.clone(), name.clone()); + } + } + map + }); + let name = sl.get(name)?; + self.subcommands.get(name) + } + pub fn list_visible_long_flags(&self) -> Vec { self.flags .iter() diff --git a/src/parse/spec.rs b/src/parse/spec.rs index f76e87c..12f77ce 100644 --- a/src/parse/spec.rs +++ b/src/parse/spec.rs @@ -218,24 +218,6 @@ impl From<&clap::Command> for Spec { } } -// #[cfg(feature = "clap")] -// impl From<&Spec> for clap::Command { -// fn from(schema: &Spec) -> Self { -// let mut cmd = clap::Command::new(&schema.name); -// for flag in schema.cmd.flags.iter() { -// cmd = cmd.arg(flag); -// } -// for arg in schema.cmd.args.iter() { -// let a = clap::Arg::new(&arg.name).required(arg.required); -// cmd = cmd.arg(a); -// } -// for scmd in schema.cmd.subcommands.values() { -// cmd = cmd.subcommand(scmd); -// } -// cmd -// } -// } - #[cfg(test)] mod tests { use super::*;