aboutsummaryrefslogtreecommitdiff
path: root/mingling_macros
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-17 19:21:52 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-17 19:21:52 +0800
commit89bf57f33a9f0f5a96fc50e272c83d926fa35c4a (patch)
treecc5a72e850bdd9215b60dc1655f162e22ee97d12 /mingling_macros
parent40bb7ffd6954184fac718c8f99c9cdc3e054e4eb (diff)
refactor!: replace build.rs with compile-time macro build steps
BREAKING CHANGE: Replace the `build`/`builds` feature system with compile-time macro-driven generation. `gen_program!()` now automatically invokes `build_comp!()` and `build_pathf!()` when the `comp`/`pathf` features are enabled, eliminating the need for `build.rs` and `[build-dependencies]` blocks. This removes the `build` feature, the `mingling::build` module, and all related build-time API functions. Completion scripts are now written to `{target_directory}/mingling/` instead of `target/release/`. The `mingling_cli` uses `build_comp!("mling")` for its custom binary name.
Diffstat (limited to 'mingling_macros')
-rw-r--r--mingling_macros/Cargo.toml10
-rw-r--r--mingling_macros/src/build.rs56
-rw-r--r--mingling_macros/src/build/comp.rs158
-rw-r--r--mingling_macros/src/build/pathf.rs19
-rw-r--r--mingling_macros/src/func/gen_program.rs65
-rw-r--r--mingling_macros/src/lib.rs47
-rw-r--r--mingling_macros/tmpls/comps/bash.sh67
-rw-r--r--mingling_macros/tmpls/comps/fish.fish120
-rw-r--r--mingling_macros/tmpls/comps/pwsh.ps1136
-rw-r--r--mingling_macros/tmpls/comps/zsh.zsh71
10 files changed, 728 insertions, 21 deletions
diff --git a/mingling_macros/Cargo.toml b/mingling_macros/Cargo.toml
index 137213a..6ae7aeb 100644
--- a/mingling_macros/Cargo.toml
+++ b/mingling_macros/Cargo.toml
@@ -19,11 +19,11 @@ default = []
async = []
clap = []
-comp = []
+comp = ["dep:just_template", "dep:mingling_pathf"]
dispatch_tree = []
structural_renderer = []
repl = []
-pathf = []
+pathf = ["dep:mingling_pathf"]
extras = []
@@ -33,3 +33,9 @@ quote.workspace = true
proc-macro2.workspace = true
just_fmt.workspace = true
+
+# comp — compile-time completion script generation (build_comp!())
+just_template = { workspace = true, optional = true }
+
+# pathf — compile-time type path analysis (build_pathf!())
+mingling_pathf = { workspace = true, optional = true }
diff --git a/mingling_macros/src/build.rs b/mingling_macros/src/build.rs
new file mode 100644
index 0000000..8f2949d
--- /dev/null
+++ b/mingling_macros/src/build.rs
@@ -0,0 +1,56 @@
+//! Compile-time build logic for `build_comp!()` and `build_pathf!()`.
+//!
+//! The build steps run as a side effect of macro expansion (during `gen_program!`),
+//! writing artifacts under `{target_directory}/mingling/`.
+
+#[doc(hidden)]
+#[cfg(feature = "comp")]
+pub(crate) mod comp;
+
+#[doc(hidden)]
+#[cfg(feature = "pathf")]
+pub(crate) mod pathf;
+
+/// Shared implementation behind `build_comp!()`.
+///
+/// Accepts an optional string literal (the binary name); defaults to
+/// `CARGO_PKG_NAME`. Returns an empty token stream on success, or a
+/// `compile_error!` token stream on failure.
+#[cfg(feature = "comp")]
+pub(crate) fn comp_build_impl(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ let bin_name: String = if input.is_empty() {
+ std::env::var("CARGO_PKG_NAME").unwrap_or_default()
+ } else {
+ match syn::parse::<syn::LitStr>(input) {
+ Ok(lit) => lit.value(),
+ Err(e) => return e.to_compile_error().into(),
+ }
+ };
+
+ match comp::build_comp_scripts(&bin_name) {
+ Ok(()) => proc_macro::TokenStream::new(),
+ Err(e) => {
+ let msg = format!("build_comp: failed to generate completion scripts: {e}");
+ syn::Error::new(proc_macro2::Span::call_site(), msg)
+ .to_compile_error()
+ .into()
+ }
+ }
+}
+
+/// Shared implementation behind `build_pathf!()`.
+///
+/// Runs the pathf type-mapping analysis. Returns an empty token stream on
+/// success, or a `compile_error!` token stream on failure.
+#[cfg(feature = "pathf")]
+pub(crate) fn pathf_build_impl(_input: proc_macro::TokenStream) -> proc_macro::TokenStream {
+ match pathf::analyze_and_build_type_mapping() {
+ Ok(()) => proc_macro::TokenStream::new(),
+ Err(e) => {
+ let msg = format!("build_pathf: type mapping analysis failed: {e}");
+ syn::Error::new(proc_macro2::Span::call_site(), msg)
+ .to_compile_error()
+ .into()
+ }
+ }
+}
diff --git a/mingling_macros/src/build/comp.rs b/mingling_macros/src/build/comp.rs
new file mode 100644
index 0000000..81a26e8
--- /dev/null
+++ b/mingling_macros/src/build/comp.rs
@@ -0,0 +1,158 @@
+use std::path::PathBuf;
+
+use just_template::tmpl;
+
+/// Represents the shell environment for which the output format is intended.
+///
+/// This is an internal copy of `mingling_core::ShellFlag`, kept private to the
+/// build module because the macros crate must not depend on `mingling_core`.
+/// Which variants are constructed depends on the target OS (`#[cfg]`), so
+/// platform-gated variants may be unused on any given host.
+#[allow(dead_code)]
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
+pub(crate) enum ShellFlag {
+ /// Represents the Bash shell.
+ #[default]
+ Bash,
+ /// Represents the Zsh shell.
+ Zsh,
+ /// Represents the Fish shell.
+ Fish,
+ /// Represents `PowerShell`.
+ Powershell,
+ /// A custom or unsupported shell type, identified by the provided string.
+ Other(String),
+}
+
+const TMPL_COMP_BASH: &str = include_str!("../../tmpls/comps/bash.sh");
+const TMPL_COMP_ZSH: &str = include_str!("../../tmpls/comps/zsh.zsh");
+const TMPL_COMP_FISH: &str = include_str!("../../tmpls/comps/fish.fish");
+const TMPL_COMP_PWSH: &str = include_str!("../../tmpls/comps/pwsh.ps1");
+
+/// Generate shell completion scripts for a given binary name.
+///
+/// On Windows, generates `PowerShell` completion.
+/// On Linux, generates Zsh, Bash, and Fish completions.
+/// Scripts are written to the `OUT_DIR` (or `target/` if `OUT_DIR` is not set).
+///
+/// # Errors
+///
+/// Returns an [`std::io::Error`] if a script cannot be written.
+pub(crate) fn build_comp_scripts(name: &str) -> Result<(), std::io::Error> {
+ #[cfg(target_os = "windows")]
+ {
+ build_comp_script(&ShellFlag::Powershell, name)?;
+ Ok(())
+ }
+
+ #[cfg(target_os = "linux")]
+ {
+ build_comp_script(&ShellFlag::Zsh, name)?;
+ build_comp_script(&ShellFlag::Bash, name)?;
+ build_comp_script(&ShellFlag::Fish, name)?;
+ Ok(())
+ }
+
+ #[cfg(target_os = "macos")]
+ {
+ build_comp_script(&ShellFlag::Zsh, name)?;
+ build_comp_script(&ShellFlag::Bash, name)?;
+ build_comp_script(&ShellFlag::Fish, name)?;
+ Ok(())
+ }
+}
+
+/// Generate a shell completion script for a specific shell.
+///
+/// This function takes a shell flag and a binary name, selects the appropriate
+/// template, substitutes the binary name into the template, and writes the
+/// resulting completion script to the Mingling build directory
+/// (`{target_directory}/mingling/`, resolved via `cargo metadata`).
+///
+/// # Errors
+///
+/// Returns an [`std::io::Error`] if the script cannot be written.
+pub(crate) fn build_comp_script(
+ shell_flag: &ShellFlag,
+ bin_name: &str,
+) -> Result<(), std::io::Error> {
+ let output_dir = comp_output_dir()?;
+ build_comp_script_to(shell_flag, bin_name, &output_dir.to_string_lossy())
+}
+
+/// The directory where completion scripts are written: `{target_directory}/mingling/`.
+fn comp_output_dir() -> Result<PathBuf, std::io::Error> {
+ mingling_pathf::build_output_dir().map_err(|e| std::io::Error::other(e.to_string()))
+}
+
+/// Generate a shell completion script to a specified directory.
+///
+/// This function takes a shell flag, a binary name, and a target directory path,
+/// selects the appropriate template, substitutes the binary name into the template,
+/// and writes the resulting completion script to the specified directory.
+///
+/// # Errors
+///
+/// Returns an [`std::io::Error`] if the script cannot be written.
+pub(crate) fn build_comp_script_to(
+ shell_flag: &ShellFlag,
+ bin_name: &str,
+ target_dir: &str,
+) -> Result<(), std::io::Error> {
+ let (tmpl_str, ext) = get_tmpl(shell_flag);
+ let mut tmpl = just_template::Template::from(tmpl_str);
+ tmpl!(bin_name = bin_name);
+ let target_path = std::path::PathBuf::from(target_dir);
+ std::fs::create_dir_all(&target_path)?;
+ let output_path = target_path.join(format!("{bin_name}_comp{ext}"));
+ std::fs::write(&output_path, tmpl.to_string())
+}
+
+const fn get_tmpl(shell_flag: &ShellFlag) -> (&'static str, &'static str) {
+ match shell_flag {
+ ShellFlag::Bash | ShellFlag::Other(_) => (TMPL_COMP_BASH, ".sh"),
+ ShellFlag::Zsh => (TMPL_COMP_ZSH, ".zsh"),
+ ShellFlag::Fish => (TMPL_COMP_FISH, ".fish"),
+ ShellFlag::Powershell => (TMPL_COMP_PWSH, ".ps1"),
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn get_tmpl_bash() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Bash);
+ assert_eq!(ext, ".sh");
+ assert!(!tmpl.is_empty(), "bash template should not be empty");
+ }
+
+ #[test]
+ fn get_tmpl_zsh() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Zsh);
+ assert_eq!(ext, ".zsh");
+ assert!(!tmpl.is_empty(), "zsh template should not be empty");
+ }
+
+ #[test]
+ fn get_tmpl_fish() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Fish);
+ assert_eq!(ext, ".fish");
+ assert!(!tmpl.is_empty(), "fish template should not be empty");
+ }
+
+ #[test]
+ fn get_tmpl_powershell() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Powershell);
+ assert_eq!(ext, ".ps1");
+ assert!(!tmpl.is_empty(), "powershell template should not be empty");
+ }
+
+ #[test]
+ fn get_tmpl_other() {
+ let (tmpl, ext) = get_tmpl(&ShellFlag::Other("custom".to_string()));
+ assert_eq!(ext, ".sh");
+ assert!(!tmpl.is_empty(), "fallback template should not be empty");
+ }
+}
diff --git a/mingling_macros/src/build/pathf.rs b/mingling_macros/src/build/pathf.rs
new file mode 100644
index 0000000..788f527
--- /dev/null
+++ b/mingling_macros/src/build/pathf.rs
@@ -0,0 +1,19 @@
+use std::path::PathBuf;
+
+use mingling_pathf::error::MinglingPathfinderError;
+
+/// The directory where pathf's build artifacts are stored for the current
+/// crate: `{target_directory}/mingling/{CARGO_PKG_NAME}`.
+pub fn output_dir() -> Result<PathBuf, MinglingPathfinderError> {
+ Ok(mingling_pathf::build_output_dir()?.join(crate_name()))
+}
+
+/// Runs the pathf type-mapping analysis for the current crate at compile time
+/// (replacing the previous `build.rs` call).
+pub fn analyze_and_build_type_mapping() -> Result<(), MinglingPathfinderError> {
+ mingling_pathf::analyze_and_build_type_mapping()
+}
+
+fn crate_name() -> String {
+ std::env::var("CARGO_PKG_NAME").unwrap_or_default()
+}
diff --git a/mingling_macros/src/func/gen_program.rs b/mingling_macros/src/func/gen_program.rs
index c0a7ea8..35e8352 100644
--- a/mingling_macros/src/func/gen_program.rs
+++ b/mingling_macros/src/func/gen_program.rs
@@ -7,6 +7,11 @@ use quote::quote;
/// Generates the `Next` type alias, `Routable` impl for `ChainProcess`,
/// and delegates to `program_comp_gen!()`, `program_fallback_gen!()`,
/// and `program_final_gen!()`.
+///
+/// When the `comp` / `pathf` features are enabled, the expansion begins by
+/// invoking `build_comp!()` / `build_pathf!()`, which run the build steps
+/// (previously done in `build.rs`) as a compile-time side effect and expand
+/// to nothing.
pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
#[cfg(feature = "comp")]
let comp_gen = quote! {
@@ -16,18 +21,46 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
#[cfg(not(feature = "comp"))]
let comp_gen = quote! {};
- // When pathf is enabled, load the type_using.rs generated by the build script
+ // `build_pathf!()` / `build_comp!()` are invoked at the very beginning of the
+ // expansion: they run the build logic at compile time and expand to nothing.
+ #[cfg(feature = "comp")]
+ let comp_build = quote! {
+ ::mingling::macros::build_comp!();
+ };
+
+ #[cfg(not(feature = "comp"))]
+ let comp_build = quote! {};
+
+ #[cfg(feature = "pathf")]
+ let pathf_build = quote! {
+ ::mingling::macros::build_pathf!();
+ };
+
+ #[cfg(not(feature = "pathf"))]
+ let pathf_build = quote! {};
+
+ // When pathf is enabled, load the type_using.rs generated by the build logic
// and emit its use statements so types from submodules are in scope.
#[cfg(feature = "pathf")]
let pathf_uses: Vec<proc_macro2::TokenStream> = {
+ // The `build_pathf!()` macro emitted above will (re-)run the analysis
+ // during expansion, but the `use` statements are needed right now, so
+ // make sure the mapping exists before reading it.
+ if let Err(e) = crate::build::pathf::analyze_and_build_type_mapping() {
+ let msg = format!("pathf: type mapping analysis failed: {e}");
+ return syn::Error::new(proc_macro2::Span::call_site(), msg)
+ .to_compile_error()
+ .into();
+ }
let uses = load_pathf_uses();
if uses.is_empty() {
- // The file might not exist yet — emit a clear hint
+ // The analyzer found nothing — emit a clear hint
let hint: proc_macro2::TokenStream = syn::parse_quote! {
compile_error!(
- "pathf: `{}` not found or empty.\n\
- Make sure `build.rs` calls `mingling::build::analyze_and_build_type_mapping().unwrap();`\n\
- with features [\"build\", \"pathf\"] enabled."
+ "pathf: no types were found by the analyzer.\n\
+ Make sure the `pathf` feature is enabled (which also enables\n\
+ the `build_pathf!()` macro) and that `gen_program!()` is called\n\
+ in a crate with a `src/` directory."
);
};
vec![hint]
@@ -47,6 +80,8 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
};
TokenStream::from(quote! {
+ #comp_build
+ #pathf_build
pub use __this_program_impl::*;
#[doc(hidden)]
@@ -88,24 +123,16 @@ pub(crate) fn gen_program_impl(_input: TokenStream) -> TokenStream {
})
}
-/// Loads `type_using.rs` generated by the pathf build script and returns each
+/// Loads `type_using.rs` generated by the pathf build logic and returns each
/// `use ...;` line as a token stream, ready to be emitted in the generated output.
#[cfg(feature = "pathf")]
fn load_pathf_uses() -> Vec<proc_macro2::TokenStream> {
- let out_dir = match std::env::var("OUT_DIR") {
- Ok(d) => d,
- Err(_) => return Vec::new(),
- };
- let crate_name = match std::env::var("CARGO_PKG_NAME") {
- Ok(n) => n,
- Err(_) => return Vec::new(),
+ let Ok(output_dir) = crate::build::pathf::output_dir() else {
+ return Vec::new();
};
- let path = std::path::Path::new(&out_dir)
- .join(&crate_name)
- .join("type_using.rs");
- let content = match std::fs::read_to_string(&path) {
- Ok(c) => c,
- Err(_) => return Vec::new(),
+ let path = output_dir.join("type_using.rs");
+ let Ok(content) = std::fs::read_to_string(&path) else {
+ return Vec::new();
};
content
.lines()
diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs
index d00773c..1aaedb1 100644
--- a/mingling_macros/src/lib.rs
+++ b/mingling_macros/src/lib.rs
@@ -20,6 +20,9 @@ mod derive;
mod func;
mod systems;
+#[cfg(any(feature = "comp", feature = "pathf"))]
+mod build;
+
mod extensions;
mod utils;
@@ -1653,6 +1656,50 @@ pub fn gen_program(input: TokenStream) -> TokenStream {
func::gen_program::gen_program_impl(input)
}
+/// Executes the completion-script build at compile time and expands to nothing.
+///
+/// **This macro is only available with the `comp` feature.**
+///
+/// The completion scripts are written to `{target_directory}/mingling/` (the
+/// target directory is resolved via `cargo metadata`).
+///
+/// `gen_program!()` calls this macro automatically when the `comp` feature is
+/// enabled. It can also be invoked manually to customize the binary name:
+///
+/// - `build_comp!()` — uses the current package name (`CARGO_PKG_NAME`).
+/// - `build_comp!("mybin")` — uses the given binary name.
+///
+/// ```rust,ignore
+/// mingling::macros::build_comp!();
+/// // or:
+/// mingling::macros::build_comp!("mybin");
+/// ```
+#[cfg(feature = "comp")]
+#[proc_macro]
+pub fn build_comp(input: TokenStream) -> TokenStream {
+ build::comp_build_impl(input)
+}
+
+/// Executes the pathf type-mapping build at compile time and expands to nothing.
+///
+/// **This macro is only available with the `pathf` feature.**
+///
+/// The mapping files are written to `{target_directory}/mingling/{CARGO_PKG_NAME}/`
+/// (the target directory is resolved via `cargo metadata`), and are consumed by
+/// `gen_program!()` so that types defined in submodules are resolved automatically.
+///
+/// `gen_program!()` calls this macro automatically when the `pathf` feature is
+/// enabled.
+///
+/// ```rust,ignore
+/// mingling::macros::build_pathf!();
+/// ```
+#[cfg(feature = "pathf")]
+#[proc_macro]
+pub fn build_pathf(input: TokenStream) -> TokenStream {
+ build::pathf_build_impl(input)
+}
+
/// Internal macro used by `gen_program!` to generate the completion infrastructure for
/// shell completion support.
///
diff --git a/mingling_macros/tmpls/comps/bash.sh b/mingling_macros/tmpls/comps/bash.sh
new file mode 100644
index 0000000..edec28d
--- /dev/null
+++ b/mingling_macros/tmpls/comps/bash.sh
@@ -0,0 +1,67 @@
+#!/usr/bin/env bash
+_<<<bin_name>>>_bash_completion() {
+ local line="${COMP_LINE:0:COMP_POINT}"
+ local cur="${line##* }"
+ local prev=""
+ local word_index=1
+
+ local before="${line:0:$(( ${#line} - ${#cur} ))}"
+ local -a before_words
+ if [[ -n "$before" ]]; then
+ read -ra before_words <<< "$before"
+ word_index=$(( ${#before_words[@]} + 1 ))
+ if [[ $word_index -gt 1 ]]; then
+ prev="${before_words[${#before_words[@]}-1]}"
+ fi
+ fi
+
+ local args=()
+ args+=(-f "${COMP_LINE//-/^}")
+ args+=(-C "$COMP_POINT")
+ args+=(-w "${cur//-/^}")
+ args+=(-p "${prev//-/^}")
+ args+=(-c "${COMP_WORDS[0]//-/^}")
+ args+=(-i "$word_index")
+ args+=(-F "bash")
+
+ for word in "${COMP_WORDS[@]}"; do
+ args+=(-a "${word//-/^}")
+ done
+
+ local suggestions
+ if suggestions=$(<<<bin_name>>> __comp "${args[@]}" 2>/dev/null); then
+ if [ $? -eq 0 ]; then
+ if [ "$suggestions" = "_file_" ]; then
+ compopt -o default
+ COMPREPLY=()
+ return
+ fi
+
+ if [ -n "$suggestions" ]; then
+ local -a all_suggestions filtered
+ mapfile -t all_suggestions < <(printf '%s\n' "$suggestions")
+
+ for suggestion in "${all_suggestions[@]}"; do
+ [ -z "$cur" ] || [[ "$suggestion" == "$cur"* ]] && filtered+=("$suggestion")
+ done
+
+ if [ ${#filtered[@]} -gt 0 ]; then
+ COMPREPLY=("${filtered[@]}")
+ if [[ "$cur" == *:* && "$COMP_WORDBREAKS" == *:* ]]; then
+ local colon_prefix="${cur%"${cur##*:}"}"
+ local -a ltrimmed=()
+ for suggestion in "${COMPREPLY[@]}"; do
+ ltrimmed+=("${suggestion#"$colon_prefix"}")
+ done
+ COMPREPLY=("${ltrimmed[@]}")
+ fi
+ fi
+ return
+ fi
+ fi
+ fi
+
+ COMPREPLY=()
+}
+
+complete -F _<<<bin_name>>>_bash_completion <<<bin_name>>>
diff --git a/mingling_macros/tmpls/comps/fish.fish b/mingling_macros/tmpls/comps/fish.fish
new file mode 100644
index 0000000..64b4ed3
--- /dev/null
+++ b/mingling_macros/tmpls/comps/fish.fish
@@ -0,0 +1,120 @@
+#!/usr/bin/env fish
+function __<<<bin_name>>>_fish_complete
+ set -l cmdline (commandline -opc)
+ set -l buffer (commandline -b)
+ set -l cursor (commandline -C)
+ set -l current_token (commandline -ct)
+
+ set -l current_word ""
+ set -l previous_word ""
+ set -l word_index 0
+ set -l char_count 0
+
+ set -l found false
+ if test -n "$current_token"
+ for i in (seq (count $cmdline))
+ if test "$cmdline[$i]" = "$current_token"
+ set word_index $i
+ set current_word $current_token
+ if test $i -gt 1
+ set previous_word $cmdline[(math $i - 1)]
+ end
+ set found true
+ break
+ end
+ end
+ end
+
+ if not $found
+ for i in (seq (count $cmdline))
+ set word $cmdline[$i]
+ if test $i -gt 1
+ set char_count (math $char_count + 1)
+ end
+ set char_count (math $char_count + (string length -- "$word"))
+
+ if test $cursor -le $char_count
+ set word_index $i
+ set current_word $word
+ if test $i -gt 1
+ set previous_word $cmdline[(math $i - 1)]
+ end
+ break
+ end
+ end
+ end
+
+ if test $word_index -eq 0 -a (count $cmdline) -gt 0
+ set word_index (count $cmdline)
+ if test -n "$current_token" -a "$current_token" != "$cmdline[-1]"
+ set current_word $current_token
+ else
+ set current_word ""
+ end
+ set previous_word $cmdline[-1]
+ end
+
+ if test $word_index -gt (count $cmdline)
+ set word_index (count $cmdline)
+ end
+
+ set -l buffer_replaced (string replace -a "-" "^" -- "$buffer")
+ set -l current_word_replaced (string replace -a "-" "^" -- "$current_word")
+ set -l previous_word_replaced (string replace -a "-" "^" -- "$previous_word")
+
+ set -l args
+ set -a args -f "$buffer_replaced" -C "$cursor" -w "$current_word_replaced" -p "$previous_word_replaced"
+
+ if test (count $cmdline) -gt 0
+ set -a args -c "$cmdline[1]"
+ else
+ set -a args -c ""
+ end
+
+ set -a args -i "$word_index"
+
+ if test (count $cmdline) -gt 0
+ set -l all_words_replaced
+ for word in $cmdline
+ set -a all_words_replaced (string replace -a "-" "^" -- "$word")
+ end
+
+ if test -n "$current_token" -a "$current_word" = "$current_token"
+ set -l found_in_cmdline false
+ for word in $cmdline
+ if test "$word" = "$current_token"
+ set found_in_cmdline true
+ break
+ end
+ end
+ if not $found_in_cmdline -a $word_index -eq (math (count $cmdline) + 1)
+ set -a all_words_replaced (string replace -a "-" "^" -- "$current_token")
+ end
+ end
+
+ set -a args -a $all_words_replaced
+ else
+ set -a args -a ""
+ end
+
+ set -a args -F "fish"
+
+ set -l output
+ if not <<<bin_name>>> __comp $args 2>/dev/null | read -z output
+ return
+ end
+
+ set -l trimmed_output (string trim -- "$output")
+ if test "$trimmed_output" = "_file_"
+ __fish_complete_path "$current_word"
+ return 0
+ else if test -n "$trimmed_output"
+ string split -n \n -- "$output" | while read -l line
+ test -n "$line" && echo "$line"
+ end
+ return 0
+ end
+ return 1
+end
+
+complete -c <<<bin_name>>> -a '(__<<<bin_name>>>_fish_complete)' -f
diff --git a/mingling_macros/tmpls/comps/pwsh.ps1 b/mingling_macros/tmpls/comps/pwsh.ps1
new file mode 100644
index 0000000..d72a027
--- /dev/null
+++ b/mingling_macros/tmpls/comps/pwsh.ps1
@@ -0,0 +1,136 @@
+# PowerShell completion script for <<<bin_name>>>
+Register-ArgumentCompleter -Native -CommandName '<<<bin_name>>>' -ScriptBlock {
+ param($wordToComplete, $commandAst, $cursorPosition)
+
+ $line = $commandAst.ToString()
+
+ $elements = @()
+ if ($commandAst.CommandElements.Count -gt 0) {
+ $elements = $commandAst.CommandElements | ForEach-Object { $_.Value }
+ }
+
+ $commandName = if ($elements.Count -gt 0) { $elements[0] } else { "" }
+
+ $currentWord = $wordToComplete
+ $previousWord = ""
+ $wordIndex = 0
+
+ $found = $false
+ for ($i = 0; $i -lt $elements.Count; $i++) {
+ if ($elements[$i] -eq $currentWord) {
+ $wordIndex = $i + 1
+ if ($i -gt 0) {
+ $previousWord = $elements[$i - 1]
+ }
+ $found = $true
+ break
+ }
+ }
+
+ if (-not $found) {
+ $wordIndex = $elements.Count + 1
+ if ($elements.Count -gt 0) {
+ $previousWord = $elements[-1]
+ }
+ }
+
+ $args = @(
+ "-C", $cursorPosition.ToString()
+ "-i", $wordIndex.ToString()
+ "-F", "Powershell"
+ )
+
+ if ($line) {
+ $args += "-f"
+ $args += ($line -replace '-', '^')
+ }
+ if ($currentWord) {
+ $args += "-w"
+ $args += ($currentWord -replace '-', '^')
+ }
+ if ($previousWord) {
+ $args += "-p"
+ $args += ($previousWord -replace '-', '^')
+ }
+ if ($commandName) {
+ $args += "-c"
+ $args += ($commandName -replace '-', '^')
+ }
+
+ foreach ($element in $elements) {
+ if ($element) {
+ $args += "-a"
+ $args += ($element -replace '-', '^')
+ }
+ }
+
+ $originalEncoding = [Console]::OutputEncoding
+ $originalPSEncoding = $OutputEncoding
+ [Console]::OutputEncoding = [System.Text.Encoding]::UTF8
+ $OutputEncoding = [System.Text.Encoding]::UTF8
+
+ $output = & <<<bin_name>>> __comp $args 2>&1
+
+ [Console]::OutputEncoding = $originalEncoding
+ $OutputEncoding = $originalPSEncoding
+
+ $output = $output -replace "`r`n", "`n" -replace "`r", "`n"
+
+ if (-not $output) {
+ return @()
+ }
+
+ $lines = $output -split "`n"
+
+ if ($lines.Count -eq 0) {
+ return @()
+ }
+
+ $firstLine = $lines[0].Trim()
+
+ if ($firstLine -eq "_file_") {
+ if ($lines.Count -gt 1) {
+ $fileSuggestions = $lines[1..($lines.Count-1)]
+ } else {
+ $fileSuggestions = @()
+ }
+
+ $completionResults = @()
+ $fileSuggestions | ForEach-Object {
+ $path = $_
+ $isDirectory = $path.EndsWith([System.IO.Path]::DirectorySeparatorChar) -or $path.EndsWith('/')
+ $completionType = if ($isDirectory) { 'ProviderContainer' } else { 'ProviderItem' }
+ $completionResults += [System.Management.Automation.CompletionResult]::new($path, $path, $completionType, $path)
+ }
+
+ return $completionResults
+ } else {
+ $completionResults = @()
+
+ foreach ($line in $lines) {
+ $trimmedLine = $line.Trim()
+
+ if ($trimmedLine -match '^([^$]+)\$\((.+)\)$') {
+ $text = $matches[1]
+ $description = $matches[2]
+ $completionResults += [System.Management.Automation.CompletionResult]::new(
+ $text,
+ $text,
+ 'ParameterValue',
+ $description
+ )
+ } else {
+ $text = $trimmedLine
+ $resultType = if ($text.StartsWith('-')) { 'ParameterName' } else { 'ParameterValue' }
+ $completionResults += [System.Management.Automation.CompletionResult]::new(
+ $text,
+ $text,
+ $resultType,
+ $text
+ )
+ }
+ }
+
+ return $completionResults
+ }
+}
diff --git a/mingling_macros/tmpls/comps/zsh.zsh b/mingling_macros/tmpls/comps/zsh.zsh
new file mode 100644
index 0000000..7cf5f7b
--- /dev/null
+++ b/mingling_macros/tmpls/comps/zsh.zsh
@@ -0,0 +1,71 @@
+#!/usr/bin/env zsh
+_<<<bin_name>>>_completion() {
+ local -a args
+ local suggestions
+
+ local buffer="$BUFFER"
+ local cursor="$CURSOR"
+ local current_word="${words[$CURRENT]}"
+ local previous_word=""
+ local command_name="${words[1]}"
+ local word_index="$CURRENT"
+
+ if [[ $CURRENT -gt 1 ]]; then
+ previous_word="${words[$((CURRENT-1))]}"
+ fi
+
+ args=(
+ -f "${buffer//-/^}"
+ -C "$cursor"
+ -w "${current_word//-/^}"
+ -p "${previous_word//-/^}"
+ -c "$command_name"
+ -i "$word_index"
+ -a "${(@)words//-/^}"
+ -F "zsh"
+ )
+
+ suggestions=$(<<<bin_name>>> __comp "${args[@]}" 2>/dev/null)
+
+ if [[ $? -eq 0 ]] && [[ -n "$suggestions" ]]; then
+ local -a completions
+ completions=(${(f)suggestions})
+
+ if [[ "${completions[1]}" == "_file_" ]]; then
+ shift completions
+ _files
+ else
+ local -a parsed_completions
+ for item in "${completions[@]}"; do
+ if [[ "$item" =~ '^([^$]+)\$\((.+)\)$' ]]; then
+ parsed_completions+=("${match[1]//:/\\:}:${match[2]}")
+ else
+ parsed_completions+=("${item//:/\\:}")
+ fi
+ done
+
+ if (( $+functions[_describe] )); then
+ _describe '<<<bin_name>>> commands' parsed_completions
+ else
+ local -a simple_completions
+ for item in "${completions[@]}"; do
+ if [[ "$item" =~ '^([^$]+)\$\((.+)\)$' ]]; then
+ simple_completions+=("${match[1]}")
+ else
+ simple_completions+=("$item")
+ fi
+ done
+ compadd -a simple_completions
+ fi
+ fi
+ fi
+}
+
+if (( $+functions[compdef] )); then
+ compdef _<<<bin_name>>>_completion <<<bin_name>>>
+ if [[ $? -ne 0 ]]; then
+ compctl -K _<<<bin_name>>>_completion <<<bin_name>>>
+ fi
+else
+ compctl -K _<<<bin_name>>>_completion <<<bin_name>>>
+fi