diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-07-28 19:11:14 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-07-28 19:11:14 +0800 |
| commit | 2dff21cc78fd2d6a4fa4f227f6ad94e3f7baa943 (patch) | |
| tree | de694a2d34a6dd60b08d6da31c4f27fcd5eec416 | |
| parent | bf35af6bbf30492bcd23eb6103fdac3382dd6d33 (diff) | |
feat(mingling_macros, mingling_pathf): add `#[command]` attribute macro
Add a new `#[command]` attribute macro (feature-gated behind
`extra_macros`) that converts a plain function with a `Vec<String>`
parameter into a fully wired Mingling command. The macro generates a
`dispatcher!` call, a `#[chain]` wrapper, and a hidden module
re-exporting
all generated types.
Also add `CommandPattern` to the pathf pattern analyzer to recognize
`#[command]`-annotated functions and track their generated hidden
modules.
| -rw-r--r-- | CHANGELOG.md | 40 | ||||
| -rw-r--r-- | mingling/src/lib.rs | 6 | ||||
| -rw-r--r-- | mingling_macros/src/attr.rs | 2 | ||||
| -rw-r--r-- | mingling_macros/src/attr/command.rs | 340 | ||||
| -rw-r--r-- | mingling_macros/src/lib.rs | 87 | ||||
| -rw-r--r-- | mingling_pathf/src/pattern_analyzer.rs | 1 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns.rs | 2 | ||||
| -rw-r--r-- | mingling_pathf/src/patterns/command.rs | 81 |
8 files changed, 559 insertions, 0 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md index 15a5f7b..b53d3d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -93,6 +93,46 @@ None These methods provide ergonomic alternatives to `to_result()` + `unwrap()` / `unwrap_or_default()` / `unwrap_or_else()` / `expect()` chaining, reducing boilerplate when working with `PickArgParsed` tuples directly. +3. **[`macros:command`]** Added the `#[command]` attribute macro (feature-gated behind `extra_macros`) that converts a plain function with a `Vec<String>` parameter into a fully wired Mingling command. The macro: + + - Calls `dispatcher!("command_name")` to register the dispatcher entry. + - Generates a `#[chain]` wrapper that bridges the entry type (`Entry{Pascal}`) to the original function. + - Preserves the original function unchanged (including attributes, extensions, visibility, and asyncness). + + **Syntax variants:** + + ```rust,ignore + // Simple form — auto-derives names from function name + #[command] + fn greet(args: Vec<String>) -> Next { /* ... */ } + // → dispatcher!("greet"), CMDGreet, EntryGreet + + // Explicit node path + #[command(node = "hello.world")] + fn greet(args: Vec<String>) -> Next { /* ... */ } + // → dispatcher!("hello.world", CMDGreet => EntryGreet) + + // Explicit name/entry overrides + #[command(name = MyDispatcher, entry = MyEntry)] + fn greet(args: Vec<String>) -> Next { /* ... */ } + // → dispatcher!("greet", MyDispatcher => MyEntry) + ``` + + **Extension attributes** (e.g. `buffer`, `routeify`) passed as bare paths in `#[command(...)]` are applied as `#[ext]` attributes **on the original function**, not on the generated chain wrapper. The chain wrapper always uses bare `#[::mingling::macros::chain]`. + + **Resource injection:** Parameters after the first are treated as resource injections and passed through to the generated `#[chain]` wrapper unchanged. + + **Hidden module:** Each `#[command]` generates a `#[doc(hidden)]` module `__command_{fn_name}_module` that re-exports all generated types (`CMD*`, `Entry*`, chain struct, dispatcher static) for pathf / external access. + + Internally, the implementation: + - Parses `#[command(...)]` arguments via `CommandArgs` supporting `node`, `name`, `entry` keys and extension paths. + - Validates function constraints (no `self`, at least one parameter). + - Handles async functions (rejected without the `async` feature). + - Resolves default names via `just_fmt::dot_case!` / `just_fmt::pascal_case!`. + - Builds a chain wrapper that calls the original function with `entry.into()` for the first argument. + +4. **[`pathf:patterns`]** Added `CommandPattern` to the `pathf` pattern analyzer, matching functions annotated with `#[command]`. The pattern tracks the generated hidden module (`__command_{fn}_module`) and marks it as a local module item via `AnalyzeItem::local_module()`. The build system generates a glob re-export `use path::__command_{fn}_module::*;` to bring all generated types (`Entry*`, `CMD*`, chain struct, dispatcher static) into scope. + #### **BREAKING CHANGES** (API CHANGES): None diff --git a/mingling/src/lib.rs b/mingling/src/lib.rs index 19f2188..d7c8e8f 100644 --- a/mingling/src/lib.rs +++ b/mingling/src/lib.rs @@ -52,6 +52,9 @@ pub mod macros { pub use mingling_macros::buffer; /// `#[chain]` - Used to generate a struct implementing the `Chain` trait via a method pub use mingling_macros::chain; + /// `#[command]` - Declares a command from a plain function with `Vec<String>` entry + #[cfg(feature = "extra_macros")] + pub use mingling_macros::command; /// `#[completion(EntryType)]` - Used to generate completion entry #[cfg(feature = "comp")] pub use mingling_macros::completion; @@ -223,6 +226,9 @@ pub mod prelude { /// Re-export of the `chain` macro for defining a chain of commands. #[cfg(feature = "macros")] pub use crate::macros::chain; + /// Re-export of the `#[command]` macro for declaring commands from plain functions. + #[cfg(all(feature = "extra_macros", feature = "macros"))] + pub use crate::macros::command; /// Re-export of the `dispatcher` macro for routing commands. #[cfg(feature = "macros")] pub use crate::macros::dispatcher; diff --git a/mingling_macros/src/attr.rs b/mingling_macros/src/attr.rs index ed7f58b..8c17878 100644 --- a/mingling_macros/src/attr.rs +++ b/mingling_macros/src/attr.rs @@ -1,4 +1,6 @@ pub(crate) mod chain; +#[cfg(feature = "extra_macros")] +pub(crate) mod command; #[cfg(feature = "comp")] pub(crate) mod completion; #[cfg(feature = "clap")] diff --git a/mingling_macros/src/attr/command.rs b/mingling_macros/src/attr/command.rs new file mode 100644 index 0000000..2e05cc9 --- /dev/null +++ b/mingling_macros/src/attr/command.rs @@ -0,0 +1,340 @@ +use proc_macro::TokenStream; +use proc_macro2::TokenStream as TokenStream2; +use quote::quote; +use syn::parse::{Parse, ParseStream}; +use syn::parse_macro_input; +use syn::spanned::Spanned; +use syn::token::Comma; +use syn::{FnArg, Ident, ItemFn, LitStr, PatType, Token, Type}; + +/// Parsed arguments for `#[command(...)]`. +/// +/// Supports: +/// - `node = "dot.separated.path"` — explicit command path +/// - `name = CMDName` — explicit CMD struct name +/// - `entry = EntryName` — explicit Entry struct name +/// - bare paths like `routeify`, `::mingling::macros::routeify` — extension attrs for the original fn +struct CommandArgs { + node: Option<LitStr>, + name: Option<Ident>, + entry: Option<Ident>, + exts: Vec<syn::Path>, +} + +impl Parse for CommandArgs { + fn parse(input: ParseStream) -> syn::Result<Self> { + let mut node = None; + let mut name = None; + let mut entry = None; + let mut exts = Vec::new(); + + while !input.is_empty() { + if input.peek(Ident) && input.peek2(Token![=]) { + // key = value pair + let key: Ident = input.parse()?; + input.parse::<Token![=]>()?; + + if key == "node" { + if node.is_some() { + return Err(input.error("duplicate `node` argument")); + } + node = Some(input.parse()?); + } else if key == "name" { + if name.is_some() { + return Err(input.error("duplicate `name` argument")); + } + name = Some(input.parse()?); + } else if key == "entry" { + if entry.is_some() { + return Err(input.error("duplicate `entry` argument")); + } + entry = Some(input.parse()?); + } else { + return Err(input.error(format!( + "unknown key `{}`; expected `node`, `name`, or `entry`", + key + ))); + } + } else { + // Extension path (e.g. `routeify` or `::mingling::macros::routeify`) + let ext: syn::Path = input.parse()?; + exts.push(ext); + } + + // Skip optional trailing comma + if input.peek(Comma) { + let _ = input.parse::<Comma>(); + } + } + + Ok(CommandArgs { + node, + name, + entry, + exts, + }) + } +} + +/// Returns the default node path as a dot-separated string, derived from the function name. +/// +/// Example: `greet_someone` -> `"greet.someone"`, `greet` -> `"greet"` +fn default_node_from_fn(fn_name: &Ident) -> String { + just_fmt::dot_case!(fn_name.to_string()) +} + +/// Checks basic function constraints for `#[command]`. +/// Returns `Err(compile_error token stream)` on failure. +fn validate_function(f: &ItemFn) -> Result<(), TokenStream2> { + if f.sig + .inputs + .iter() + .any(|arg| matches!(arg, FnArg::Receiver(_))) + { + return Err(syn::Error::new( + f.sig.span(), + "#[command] function cannot have a `self` parameter", + ) + .to_compile_error()); + } + + if f.sig.inputs.is_empty() { + return Err(syn::Error::new( + f.sig.span(), + "#[command] function must have at least one parameter (e.g., `args: Vec<String>`)", + ) + .to_compile_error()); + } + + Ok(()) +} + +/// Returns `(wrapper_async_token, await_call)` for the chain wrapper. +/// +/// When the original function is async and the `async` feature is enabled, +/// the wrapper needs to be `async` and call `.await` on the original function. +/// Without the feature, async functions are rejected. +fn handle_async(f: &ItemFn) -> Result<(TokenStream2, TokenStream2), TokenStream2> { + let is_async = f.sig.asyncness.is_some(); + + #[cfg(not(feature = "async"))] + if is_async { + return Err(syn::Error::new( + f.sig.span(), + "#[command] function cannot be async when the `async` feature is disabled", + ) + .to_compile_error()); + } + + let wrapper_async = is_async.then_some(quote! { async }).unwrap_or_default(); + let await_call = is_async.then_some(quote! { .await }).unwrap_or_default(); + Ok((wrapper_async, await_call)) +} + +/// All resolved identifiers derived from `#[command]` arguments + function name. +struct ResolvedNames { + /// `node_str` as a string literal token + node_lit: LitStr, + /// Whether the user supplied any explicit override (node/name/entry) + has_overrides: bool, + /// CMD struct name (e.g. `CMDGreet`) + cmd_name: Ident, + /// Entry struct name (e.g. `EntryGreet`) + entry_type: Ident, + /// Chain wrapper function name (e.g. `__command_chain_greet`) + chain_fn_name: Ident, +} + +/// Resolves `node`, `cmd_name`, `entry_type`, and `chain_fn_name` from +/// the attribute args and the original function name. +fn resolve_names(fn_name: &Ident, args: &CommandArgs) -> ResolvedNames { + let fn_name_str = fn_name.to_string(); + + let node_str = match &args.node { + Some(lit) => lit.value(), + None => default_node_from_fn(fn_name), + }; + let node_lit = syn::LitStr::new(&node_str, fn_name.span()); + + let has_overrides = args.node.is_some() || args.name.is_some() || args.entry.is_some(); + + let cmd_name = args.name.clone().unwrap_or_else(|| { + let pascal = just_fmt::pascal_case!(&node_str); + Ident::new(&format!("CMD{pascal}"), fn_name.span()) + }); + + let entry_type = args.entry.clone().unwrap_or_else(|| { + let pascal = just_fmt::pascal_case!(&node_str); + Ident::new(&format!("Entry{pascal}"), fn_name.span()) + }); + + let chain_fn_name = Ident::new(&format!("__command_chain_{}", fn_name_str), fn_name.span()); + + ResolvedNames { + node_lit, + has_overrides, + cmd_name, + entry_type, + chain_fn_name, + } +} + +/// Converts extension paths into `#[ext]` attribute token streams. +fn build_ext_attrs(exts: &[syn::Path]) -> Vec<TokenStream2> { + exts.iter().map(|ext| quote! { #[#ext] }).collect() +} + +/// Builds the wrapper function's parameter list by replacing the first +/// parameter's type with the entry type (e.g. `Vec<String>` → `EntryGreet`). +fn build_wrapper_params( + sig: &syn::Signature, + entry_type: &Ident, +) -> syn::punctuated::Punctuated<FnArg, syn::token::Comma> { + let mut params = sig.inputs.clone(); + if let Some(FnArg::Typed(first)) = params.first_mut() { + first.ty = Box::new(Type::Path(syn::TypePath { + qself: None, + path: syn::Path::from(entry_type.clone()), + })); + } + params +} + +/// Builds call arguments for the original function call inside the wrapper. +/// +/// - The first argument gets `.into()` (converts `EntryGreet` → `Vec<String>`). +/// - Resource injection params (2nd onward) pass through as-is. +fn build_call_args(sig: &syn::Signature) -> Vec<TokenStream2> { + sig.inputs + .iter() + .enumerate() + .map(|(i, arg)| { + let pat = match arg { + FnArg::Typed(PatType { pat, .. }) => quote! { #pat }, + FnArg::Receiver(_) => unreachable!(), + }; + if i == 0 { + quote! { #pat.into() } + } else { + pat + } + }) + .collect() +} + +/// Generates the `dispatcher!(...)` call. +/// +/// - No overrides → abbreviated form: `dispatcher!("node")` +/// - Any override → explicit form: `dispatcher!("node", CMDName => EntryName)` +fn build_dispatcher_invoke(names: &ResolvedNames) -> TokenStream2 { + let node_lit = &names.node_lit; + if names.has_overrides { + let cmd_name = &names.cmd_name; + let entry_type = &names.entry_type; + quote! { ::mingling::macros::dispatcher!(#node_lit, #cmd_name => #entry_type); } + } else { + quote! { ::mingling::macros::dispatcher!(#node_lit); } + } +} + +pub(crate) fn command_attr(attr: TokenStream, item: TokenStream) -> TokenStream { + let input_fn = parse_macro_input!(item as ItemFn); + + // validation + if let Err(err) = validate_function(&input_fn) { + return err.into(); + } + + // parse attribute + let args: CommandArgs = if attr.is_empty() { + CommandArgs { + node: None, + name: None, + entry: None, + exts: Vec::new(), + } + } else { + parse_macro_input!(attr as CommandArgs) + }; + + // async handling + let (wrapper_async, await_call) = match handle_async(&input_fn) { + Ok(pair) => pair, + Err(err) => return err.into(), + }; + + // resolve node / name / entry + let fn_name = &input_fn.sig.ident; + let names = resolve_names(fn_name, &args); + let chain_fn_name = &names.chain_fn_name; + + // build extension attributes (applied to the ORIGINAL function) + let ext_attrs = build_ext_attrs(&args.exts); + + // build wrapper + let wrapper_params = build_wrapper_params(&input_fn.sig, &names.entry_type); + let call_args = build_call_args(&input_fn.sig); + + // build dispatcher invocation + let dispatcher_invoke = build_dispatcher_invoke(&names); + + // preserve original function + let mut fn_attrs = input_fn.attrs.clone(); + fn_attrs.retain(|attr| !attr.path().is_ident("command")); + + let vis = &input_fn.vis; + let asyncness = input_fn.sig.asyncness; + let generics = &input_fn.sig.generics; + let orig_params = &input_fn.sig.inputs; + let orig_return = &input_fn.sig.output; + let fn_body = &input_fn.block; + + // compute names for the re‑export module and internal structs + let fn_name_s = fn_name.to_string(); + let mod_name = Ident::new(&format!("__command_{}_module", &fn_name_s), fn_name.span()); + let wrapper_full = format!("__command_chain_{}", &fn_name_s); + let snaked_wrapper = just_fmt::snake_case!(wrapper_full); + let chain_internal = Ident::new( + &format!("__internal_chain_{}", snaked_wrapper), + fn_name.span(), + ); + + // dispatcher internal static (only exists with dispatch_tree feature) + #[cfg(feature = "dispatch_tree")] + let snaked_node = just_fmt::snake_case!(names.node_lit.value()); + #[cfg(feature = "dispatch_tree")] + let dispatcher_internal = Ident::new( + &format!("__internal_dispatcher_{}", snaked_node), + fn_name.span(), + ); + #[cfg(not(feature = "dispatch_tree"))] + let dispatcher_internal = quote! {}; + + let cmd_name = &names.cmd_name; + let entry_type = &names.entry_type; + + // assemble output + let expanded = quote! { + #dispatcher_invoke + + #[::mingling::macros::chain] + #vis #wrapper_async fn #chain_fn_name #generics(#wrapper_params) -> crate::Next { + #fn_name(#(#call_args),*)#await_call.into() + } + + #(#fn_attrs)* + #(#ext_attrs)* + #vis #asyncness fn #fn_name #generics(#orig_params) #orig_return #fn_body + + // hidden module gathering all generated types for pathf / external access + #[doc(hidden)] + #vis mod #mod_name { + #vis use super::#cmd_name; + #vis use super::#entry_type; + #vis use super::#chain_internal; + #vis use super::#dispatcher_internal; + } + }; + + expanded.into() +} diff --git a/mingling_macros/src/lib.rs b/mingling_macros/src/lib.rs index 27563a4..ae874af 100644 --- a/mingling_macros/src/lib.rs +++ b/mingling_macros/src/lib.rs @@ -1100,6 +1100,93 @@ pub fn program_setup(attr: TokenStream, item: TokenStream) -> TokenStream { program_setup::setup_attr(attr, item) } +/// Declares a command from a plain function. +/// +/// **This macro is only available with the `extra_macros` feature.** +/// +/// The `#[command]` attribute converts a function taking `Vec<String>` into a +/// Mingling command by: +/// 1. Calling `dispatcher!("command_name")` to register the dispatcher entry. +/// 2. Generating a `#[chain]` wrapper that bridges the entry type (`Entry{Pascal}`) +/// to the original function. +/// 3. Preserving the original function unchanged (including attributes, extensions, +/// visibility, and asyncness). +/// +/// # Syntax +/// +/// ## Simple form (auto-derives names from function name) +/// +/// ```rust,ignore +/// #[command] +/// fn greet(args: Vec<String>) -> Next { +/// // ... +/// } +/// ``` +/// +/// This deduces: +/// - Command path: `"greet"` (via `dot_case` of function name) +/// - Dispatcher struct: `CMDGreet` +/// - Entry struct: `EntryGreet` +/// - Dispatches via `dispatcher!("greet")` +/// +/// ## Explicit attributes +/// +/// ```rust,ignore +/// #[command(node = "hello.world")] +/// fn greet(args: Vec<String>) -> Next { +/// // ... +/// } +/// // → dispatcher!("hello.world", CMDGreet => EntryGreet) +/// ``` +/// +/// ```rust,ignore +/// #[command(name = MyDispatcher, entry = MyEntry)] +/// fn greet(args: Vec<String>) -> Next { +/// // ... +/// } +/// // → dispatcher!("greet", MyDispatcher => MyEntry) +/// ``` +/// +/// ## Extension attributes +/// +/// Extra bare paths (e.g. `buffer`, `routeify`, `::mingling::macros::routeify`) +/// are emitted as `#[ext]` attributes **on the original function**, not on the +/// chain wrapper. The chain wrapper always uses bare `#[::mingling::macros::chain]`. +/// +/// ```rust,ignore +/// #[command(buffer)] +/// fn greet(args: Vec<String>) { +/// r_println!("Hello!"); +/// } +/// ``` +/// +/// # Resource injection +/// +/// Parameters after the first are treated as resource injections and passed +/// through to the generated `#[chain]` wrapper unchanged (as reference params): +/// +/// ```rust,ignore +/// #[command] +/// fn greet(args: Vec<String>, ec: &mut ResExitCode) -> Next { +/// ec.exit_code = 0; +/// // ... +/// } +/// ``` +/// +/// The generated chain wrapper calls the original function with `entry.into()` +/// for the first argument and passes all subsequent arguments directly. +/// +/// # Requirements +/// +/// - The function must have at least one parameter (the `Vec<String>` entry argument). +/// - The function must not have a `self` parameter. +/// - Visibility (`pub` etc.) and `async` are preserved on the original function. +#[cfg(feature = "extra_macros")] +#[proc_macro_attribute] +pub fn command(attr: TokenStream, item: TokenStream) -> TokenStream { + attr::command::command_attr(attr, item) +} + /// Declares a `Dispatcher` that uses `clap::Parser` for argument parsing. /// /// **This macro is only available with the `clap` feature.** diff --git a/mingling_pathf/src/pattern_analyzer.rs b/mingling_pathf/src/pattern_analyzer.rs index e88d9f6..79b1c5a 100644 --- a/mingling_pathf/src/pattern_analyzer.rs +++ b/mingling_pathf/src/pattern_analyzer.rs @@ -33,6 +33,7 @@ pub fn init_with_config(config: PathfinderConfig) -> PatternAnalyzer { analyzer.add_pattern(GroupPattern); analyzer.add_pattern(GroupedDerivePattern); analyzer.add_pattern(ChainPattern); + analyzer.add_pattern(CommandPattern); analyzer.add_pattern(RendererPattern); analyzer.add_pattern(HelpPattern); analyzer.add_pattern(CompletionPattern); diff --git a/mingling_pathf/src/patterns.rs b/mingling_pathf/src/patterns.rs index 9845b73..964fe7c 100644 --- a/mingling_pathf/src/patterns.rs +++ b/mingling_pathf/src/patterns.rs @@ -2,6 +2,7 @@ pub use basic_struct::*; pub use chain::*; +pub use command::*; pub use completion::*; pub use dispatcher::*; pub use dispatcher_clap::*; @@ -13,6 +14,7 @@ pub use renderer::*; mod basic_struct; mod chain; +mod command; mod completion; mod dispatcher; mod dispatcher_clap; diff --git a/mingling_pathf/src/patterns/command.rs b/mingling_pathf/src/patterns/command.rs new file mode 100644 index 0000000..fac70af --- /dev/null +++ b/mingling_pathf/src/patterns/command.rs @@ -0,0 +1,81 @@ +//! The `CommandPattern` matches functions annotated with `#[command]` and +//! extracts the generated hidden module name (`__command_<fn>_module`). +//! +//! Supported forms: +//! ```rust,ignore +//! #[command] +//! fn greet(args: Vec<String>) -> Next { ... } +//! +//! #[command(node = "foo.foo-bar")] +//! fn greet(args: Vec<String>) -> Next { ... } +//! +//! #[command(name = CMDGreet, entry = EntryGreet)] +//! fn greet(args: Vec<String>) -> Next { ... } +//! +//! #[command(node = "greet", name = CMDGreet, entry = EntryGreet, buffer)] +//! fn greet(args: Vec<String>) { ... } +//! ``` + +use syn::Item; + +use crate::pattern_analyzer::{AnalyzeItem, AnalyzePattern}; + +/// Match `#[command]` functions. +/// +/// The `#[command]` macro generates a hidden `__command_<fn>_module` that +/// re-exports all internal types (`Entry*`, `CMD*`, chain struct, dispatcher +/// static). This pattern tracks that module; the build system generates a +/// glob re-export `use path::__command_<fn>_module::*;` to bring everything +/// into scope. +pub struct CommandPattern; + +impl AnalyzePattern for CommandPattern { + fn contains(&self, content: &str) -> bool { + content.contains("command") + } + + fn analyze(&self, content: &str) -> Vec<AnalyzeItem> { + let Ok(syntax) = syn::parse_file(content) else { + return Vec::new(); + }; + + let mut items = Vec::new(); + for item in &syntax.items { + collect_from_item(item, "", &mut items); + } + items + } +} + +fn collect_from_item(item: &Item, current_mod: &str, items: &mut Vec<AnalyzeItem>) { + match item { + Item::Fn(f) if has_command_attr(&f.attrs) => { + let fn_name = f.sig.ident.to_string(); + let mod_name = format!("__command_{}_module", &fn_name); + items.push(AnalyzeItem::local_module(current_mod.to_string(), mod_name)); + } + Item::Mod(item_mod) => { + if let Some((_, nested)) = &item_mod.content { + let mod_name = &item_mod.ident.to_string(); + let nested_mod = if current_mod.is_empty() { + mod_name.clone() + } else { + format!("{current_mod}::{mod_name}") + }; + for n in nested { + collect_from_item(n, &nested_mod, items); + } + } + } + _ => {} + } +} + +fn has_command_attr(attrs: &[syn::Attribute]) -> bool { + attrs.iter().any(|a| { + a.path() + .segments + .last() + .is_some_and(|s| s.ident == "command") + }) +} |
