From 57e20f69efea609d8332cc39d223f4aa46be3c78 Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Sat, 15 Aug 2026 03:42:31 +0800 Subject: feat(core): add public ArgumentSplitter utility Move the REPL's argument splitting logic into a new public `utils` module exposing the `ArgumentSplitter` trait for reuse by downstream code. --- CHANGELOG.md | 25 ++++ mingling_core/src/lib.rs | 3 + mingling_core/src/program/repl_exec.rs | 6 +- mingling_core/src/program/repl_exec/splitter.rs | 134 ------------------ mingling_core/src/utils.rs | 3 + mingling_core/src/utils/splitter.rs | 178 ++++++++++++++++++++++++ 6 files changed, 211 insertions(+), 138 deletions(-) delete mode 100644 mingling_core/src/program/repl_exec/splitter.rs create mode 100644 mingling_core/src/utils.rs create mode 100644 mingling_core/src/utils/splitter.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index c836b04..1712b4b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -352,6 +352,31 @@ None `Default` is still derived (all fields default to empty/`None`). +13. **[`core:utils`]** Added a new `utils` module to `mingling_core` with the `ArgumentSplitter` trait, providing a reusable implementation of shell-style command-line argument splitting. The trait is implemented for `str` and internally for `String`, and provides a `split_args()` method that splits a string input into a `Vec` of arguments, respecting single quotes, double quotes, and backslash escaping. + + **Trait definition:** + + ```rust + pub trait ArgumentSplitter { + /// Splits the string into a `Vec` of arguments, respecting + /// single quotes, double quotes, and backslash escaping. + fn split_args(&self) -> Vec; + } + + impl ArgumentSplitter for str { /* ... */ } + impl ArgumentSplitter for String { /* ... */ } + ``` + + **Splitting rules** (identical semantics to the previously private `splitter` module): + + - **Whitespace separation** — Arguments are separated by spaces (`' '`); consecutive spaces collapse and empty tokens are dropped. + - **Single/double quotes** — Text inside `'...'` or `"..."` is treated as a single argument; the quote characters are stripped. Within quoted segments, backslash escapes the next character (so `"b c"` produces `b c`, and `"b\"c"` produces `b"c`). + - **Backslash escaping** (outside quotes) — A backslash takes the next character literally (e.g., `b\ c` produces `b c`; `b\"c` produces `b"c`); a trailing backslash with no following character is ignored/lost. + + **Integration:** The previously private `split_input` / `split_input_string` functions in `mingling_core::program::repl_exec::splitter` have been removed; `repl_exec` now uses `readline.split_args()` on the input string. The `utils` module is exported as `mingling_core::utils`. + + The `ArgumentSplitter` trait is a public API addition, so downstream code can now reuse the same argument-splitting logic that the REPL uses for parsing input lines. + #### **BREAKING CHANGES** (API CHANGES): 1. **[`macros`]** **[BREAKING]** Renamed the `extra_macros` feature to `extras`. All feature-gated macro re-exports in `mingling/src/lib.rs` (and throughout the codebase) have been updated from `#[cfg(feature = "extra_macros")]` to `#[cfg(feature = "extras")]`. diff --git a/mingling_core/src/lib.rs b/mingling_core/src/lib.rs index 7048307..641118c 100644 --- a/mingling_core/src/lib.rs +++ b/mingling_core/src/lib.rs @@ -110,3 +110,6 @@ pub mod __private { /// Mingling's convention metadatas, which can be bound to types using `#[metadata]`, to provide identification for types pub mod metadata; + +/// Some common utilities in Mingling, providing a collection of functionality needed by many modules. +pub mod utils; diff --git a/mingling_core/src/program/repl_exec.rs b/mingling_core/src/program/repl_exec.rs index b91f7bc..2db5c5a 100644 --- a/mingling_core/src/program/repl_exec.rs +++ b/mingling_core/src/program/repl_exec.rs @@ -7,10 +7,8 @@ use std::io::Write; #[doc(hidden)] pub mod res; -mod splitter; - use crate::error::{ProgramInternalExecuteError, ProgramPanic}; -use crate::program::repl_exec::splitter::split_input_string; +use crate::utils::ArgumentSplitter; use crate::{Program, ProgramCollect, RenderResult}; use crate::{program::repl_exec::res::ResREPL, this}; @@ -58,7 +56,7 @@ where line: &mut readline, }); - let mut args = split_input_string(&readline); + let mut args = readline.split_args(); p.run_hook_repl_pre_exec(&crate::hook::HookREPLPreExecInfo { args: &args }); match might_be_async::invoke!(exec_once(p, &mut args)) { diff --git a/mingling_core/src/program/repl_exec/splitter.rs b/mingling_core/src/program/repl_exec/splitter.rs deleted file mode 100644 index 312ad73..0000000 --- a/mingling_core/src/program/repl_exec/splitter.rs +++ /dev/null @@ -1,134 +0,0 @@ -// Doc Not Optimize -/// Wraps `split_input` to work with owned `String` inputs. -pub fn split_input_string(input: &str) -> Vec { - split_input(input) -} - -/// Splits a string input into arguments, respecting single quotes, double quotes, -/// and backslash escaping. -pub fn split_input(input: &str) -> Vec { - let mut result: Vec = Vec::new(); - let mut current = String::new(); - let mut chars = input.chars(); - - while let Some(ch) = chars.next() { - match ch { - '\\' => { - // Take the next character literally (if any) and add it to current. - if let Some(next) = chars.next() { - current.push(next); - } - // If there's no next character, the backslash is just ignored/lost. - } - '"' | '\'' => { - // Start of a quoted segment. - let quote_char = ch; - let mut escaped = false; - loop { - match chars.next() { - None => break, - Some(c) => { - if escaped { - current.push(c); - escaped = false; - } else if c == '\\' { - escaped = true; - } else if c == quote_char { - break; - } else { - current.push(c); - } - } - } - } - } - ' ' => { - if !current.is_empty() { - result.push(current.clone()); - current.clear(); - } - } - _ => { - current.push(ch); - } - } - } - - if !current.is_empty() { - result.push(current); - } - - result -} - -#[cfg(test)] -mod splitter_tests { - use crate::program::repl_exec::splitter::split_input; - - #[test] - fn test_split_with_double_quotes() { - let input = r#"a "b c" d"#; - let result = split_input(input); - assert_eq!(result, vec!["a", "b c", "d"]); - } - - #[test] - fn test_split_with_single_quotes() { - let input = "a 'b c' d"; - let result = split_input(input); - assert_eq!(result, vec!["a", "b c", "d"]); - } - - #[test] - fn test_empty_input() { - assert!(split_input("").is_empty()); - } - - #[test] - fn test_no_quotes() { - let result = split_input("hello world"); - assert_eq!(result, vec!["hello", "world"]); - } - - #[test] - fn test_double_quotes_at_edges() { - let result = split_input(r#""hello world" foo"#); - assert_eq!(result, vec!["hello world", "foo"]); - } - - #[test] - fn test_single_quotes_at_edges() { - let result = split_input("'hello world' foo"); - assert_eq!(result, vec!["hello world", "foo"]); - } - - #[test] - fn test_multiple_double_quoted_parts() { - let result = split_input(r#"a "b c" d "e f g""#); - assert_eq!(result, vec!["a", "b c", "d", "e f g"]); - } - - #[test] - fn test_multiple_single_quoted_parts() { - let result = split_input("a 'b c' d 'e f g'"); - assert_eq!(result, vec!["a", "b c", "d", "e f g"]); - } - - #[test] - fn test_backslash_escaped_space() { - let result = split_input("a b\\ c d"); - assert_eq!(result, vec!["a", "b c", "d"]); - } - - #[test] - fn test_backslash_escaped_double_quote() { - let result = split_input(r#"a b\"c d"#); - assert_eq!(result, vec!["a", r#"b"c"#, "d"]); - } - - #[test] - fn test_backslash_escaped_single_quote() { - let result = split_input("a b\\'c d"); - assert_eq!(result, vec!["a", "b'c", "d"]); - } -} diff --git a/mingling_core/src/utils.rs b/mingling_core/src/utils.rs new file mode 100644 index 0000000..89fcd8f --- /dev/null +++ b/mingling_core/src/utils.rs @@ -0,0 +1,3 @@ +mod splitter; + +pub use splitter::ArgumentSplitter; diff --git a/mingling_core/src/utils/splitter.rs b/mingling_core/src/utils/splitter.rs new file mode 100644 index 0000000..6b2ea11 --- /dev/null +++ b/mingling_core/src/utils/splitter.rs @@ -0,0 +1,178 @@ +/// A trait for splitting strings into arguments, respecting quotes and escapes. +/// +/// # Examples +/// +/// ``` +/// use mingling_core::utils::ArgumentSplitter; +/// +/// let args = "echo \"hello world\"".split_args(); +/// assert_eq!(args, vec!["echo", "hello world"]); +/// ``` +pub trait ArgumentSplitter { + /// Splits the input string into a vector of argument strings. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::utils::ArgumentSplitter; + /// + /// let args = "a 'b c' d".split_args(); + /// assert_eq!(args, vec!["a", "b c", "d"]); + /// ``` + fn split_args(self) -> Vec; +} + +impl> ArgumentSplitter for S { + /// Splits the string into arguments, respecting single quotes, double + /// quotes, and backslash escaping. + /// + /// # Examples + /// + /// ``` + /// use mingling_core::utils::ArgumentSplitter; + /// + /// let args = r#"cmd --flag "value with spaces""#.split_args(); + /// assert_eq!(args, vec!["cmd", "--flag", "value with spaces"]); + /// ``` + /// + /// Escaped characters are unescaped: + /// + /// ``` + /// use mingling_core::utils::ArgumentSplitter; + /// + /// let args = r#"echo a\ b"#.split_args(); + /// assert_eq!(args, vec!["echo", "a b"]); + /// ``` + fn split_args(self) -> Vec { + split_args(self.as_ref()) + } +} + +/// Splits a string input into arguments, respecting single quotes, double quotes, +/// and backslash escaping. +fn split_args(input: &str) -> Vec { + let mut result: Vec = Vec::new(); + let mut current = String::new(); + let mut chars = input.chars(); + + while let Some(ch) = chars.next() { + match ch { + '\\' => { + // Take the next character literally (if any) and add it to current. + if let Some(next) = chars.next() { + current.push(next); + } + // If there's no next character, the backslash is just ignored/lost. + } + '"' | '\'' => { + // Start of a quoted segment. + let quote_char = ch; + let mut escaped = false; + loop { + match chars.next() { + None => break, + Some(c) => { + if escaped { + current.push(c); + escaped = false; + } else if c == '\\' { + escaped = true; + } else if c == quote_char { + break; + } else { + current.push(c); + } + } + } + } + } + ' ' => { + if !current.is_empty() { + result.push(current.clone()); + current.clear(); + } + } + _ => { + current.push(ch); + } + } + } + + if !current.is_empty() { + result.push(current); + } + + result +} + +#[cfg(test)] +mod splitter_tests { + use crate::utils::splitter::split_args; + + #[test] + fn test_split_with_double_quotes() { + let input = r#"a "b c" d"#; + let result = split_args(input); + assert_eq!(result, vec!["a", "b c", "d"]); + } + + #[test] + fn test_split_with_single_quotes() { + let input = "a 'b c' d"; + let result = split_args(input); + assert_eq!(result, vec!["a", "b c", "d"]); + } + + #[test] + fn test_empty_input() { + assert!(split_args("").is_empty()); + } + + #[test] + fn test_no_quotes() { + let result = split_args("hello world"); + assert_eq!(result, vec!["hello", "world"]); + } + + #[test] + fn test_double_quotes_at_edges() { + let result = split_args(r#""hello world" foo"#); + assert_eq!(result, vec!["hello world", "foo"]); + } + + #[test] + fn test_single_quotes_at_edges() { + let result = split_args("'hello world' foo"); + assert_eq!(result, vec!["hello world", "foo"]); + } + + #[test] + fn test_multiple_double_quoted_parts() { + let result = split_args(r#"a "b c" d "e f g""#); + assert_eq!(result, vec!["a", "b c", "d", "e f g"]); + } + + #[test] + fn test_multiple_single_quoted_parts() { + let result = split_args("a 'b c' d 'e f g'"); + assert_eq!(result, vec!["a", "b c", "d", "e f g"]); + } + + #[test] + fn test_backslash_escaped_space() { + let result = split_args("a b\\ c d"); + assert_eq!(result, vec!["a", "b c", "d"]); + } + + #[test] + fn test_backslash_escaped_double_quote() { + let result = split_args(r#"a b\"c d"#); + assert_eq!(result, vec!["a", r#"b"c"#, "d"]); + } + + #[test] + fn test_backslash_escaped_single_quote() { + let result = split_args("a b\\'c d"); + assert_eq!(result, vec!["a", "b'c", "d"]); + } +} -- cgit