aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-08-15 03:28:02 +0800
committer魏曹先生 <1992414357@qq.com>2026-08-15 03:28:02 +0800
commitec6c87d12062576271231b88364a4aa00765ae65 (patch)
treec1fcc3583aaffa06c0857403b557231719dde1af
parent372eb14ab9a9b7fc0e5334ca15a9d8b656140e54 (diff)
feat(core): allow pre_dispatch hooks to mutate arguments
Change `HookPreDispatchInfo.arguments` from `&[String]` to `&mut Vec<String>` so hooks can rewrite command-line arguments before dispatch.
-rw-r--r--CHANGELOG.md35
-rw-r--r--mingling_core/src/program/exec.rs9
-rw-r--r--mingling_core/src/program/hook.rs31
-rw-r--r--mingling_core/src/program/hook/hook_info.rs5
-rw-r--r--mingling_core/src/program/once_exec.rs14
-rw-r--r--mingling_core/src/program/repl_exec.rs10
6 files changed, 81 insertions, 23 deletions
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 713e7a5..c836b04 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -462,6 +462,41 @@ None
_This is a pure deletion change with no behavioral replacement. If downstream code used the `Title`, `Lower`, or `Upper` variants, it needs to switch to other naming styles (such as `Pascal`, `Kebab`, `Snake`, or `Dot`)._
+6. **[`core:hook`]** **[BREAKING]** Changed `HookPreDispatchInfo.arguments` from `&'a [String]` (immutable slice) to `&'a mut Vec<String>` (mutable reference), and updated the `pre_dispatch` hook signature accordingly. The `pre_dispatch` hook can now **rewrite the program's command-line arguments before they are matched against registered dispatchers**, enabling argument normalization, injection, or filtering at the hook level.
+
+ **Type changes:**
+
+ - `HookPreDispatchInfo.arguments: &'a [String]` → `arguments: &'a mut Vec<String>`
+ - `ProgramHook<C>::pre_dispatch` field type: `Box<dyn for<'a> Fn(&HookPreDispatchInfo<'a>) -> ProgramControls<C>>` → `Box<dyn for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> ProgramControls<C>>`
+ - `ProgramHookBuilder::on_pre_dispatch<F, R>` bound: `F: for<'a> Fn(&HookPreDispatchInfo<'a>) -> R` → `F: for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> R`
+ - `Program::run_hook_pre_dispatch` parameter: `&HookPreDispatchInfo` → `&mut HookPreDispatchInfo`
+
+ **Execution pipeline changes:**
+
+ - `exec()` now clones `program.args` into a local mutable `Vec<String>` and passes `&mut` to `exec_with_args`, so hooks rewriting arguments do not mutate the program's stored args (the rewritten copy is what gets dispatched).
+ - `exec_with_args` now takes `args: &mut Vec<String>` instead of `args: &[String]`, and passes `&mut *args` into `run_hook_pre_dispatch`.
+ - `ProgramOnceExec::once_exec` takes the program args via `std::mem::take` into a local mutable `Vec<String>` and passes `&mut` through to `exec_with_args`, ensuring the same mutable-args semantics in the `once_exec` path.
+ - `ReplExec::exec` and `exec_once` in `repl_exec.rs` now pass `&mut Vec<String>` through the same execution path.
+
+ **Migration guide:**
+
+ - Any `pre_dispatch` hook closures must now accept `&mut HookPreDispatchInfo` instead of `&HookPreDispatchInfo`:
+
+ ```rust
+ // Before
+ .on_pre_dispatch(|info: &HookPreDispatchInfo| { ... })
+
+ // After
+ .on_pre_dispatch(|info: &mut HookPreDispatchInfo| {
+ // info.arguments is now &mut Vec<String> — can be mutated
+ })
+ ```
+
+ - References to `info.arguments` inside hooks that previously treated it as `&[String]` will need to adapt to `&mut Vec<String>` (e.g., `info.arguments.as_slice()` instead of `info.arguments` in comparison positions, or explicit dereference / indexing changes).
+ - While most `&[String]`-style usages are transparently compatible via `Deref`, code that relied on the immutability guarantee of the slice (e.g., passing `info.arguments` to functions expecting `&[String]` via coercion) may need `info.arguments.as_slice()` or `&*info.arguments`.
+
+ _Behavioral change: hooks can now rewrite the argument list before dispatch — e.g., inserting default flags, removing deprecated options, or expanding shorthand syntax. Hook authors should be careful to preserve the program's expected argument layout when mutating the list._
+
---
### Release 0.3.0 (2026-07-27)
diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs
index 923b47b..b0a5b16 100644
--- a/mingling_core/src/program/exec.rs
+++ b/mingling_core/src/program/exec.rs
@@ -15,13 +15,14 @@ pub fn exec<C>(program: &'static Program<C>) -> Result<RenderResult, ProgramInte
where
C: ProgramCollect<Enum = C> + Send + Sync,
{
- might_be_async::invoke!(exec_with_args(program, &program.args))
+ let mut args = program.args.clone();
+ might_be_async::invoke!(exec_with_args(program, &mut args))
}
#[might_be_async::func]
pub fn exec_with_args<C>(
program: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync,
@@ -51,7 +52,9 @@ where
// Run hooks
control!(
- program.run_hook_pre_dispatch(&crate::hook::HookPreDispatchInfo { arguments: args }),
+ program.run_hook_pre_dispatch(&mut crate::hook::HookPreDispatchInfo {
+ arguments: &mut *args,
+ }),
current
);
diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs
index 8fd2ba1..92106f9 100644
--- a/mingling_core/src/program/hook.rs
+++ b/mingling_core/src/program/hook.rs
@@ -64,8 +64,9 @@ where
pub begin: Option<Box<dyn Fn(&HookBeginInfo) + Send + Sync>>,
/// Executes before the program dispatches
- pub pre_dispatch:
- Option<Box<dyn for<'a> Fn(&HookPreDispatchInfo<'a>) -> ProgramControls<C> + Send + Sync>>,
+ pub pre_dispatch: Option<
+ Box<dyn for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> ProgramControls<C> + Send + Sync>,
+ >,
/// Executes after the program dispatches
pub post_dispatch: Option<
@@ -162,7 +163,10 @@ where
}
}
- pub(crate) fn run_hook_pre_dispatch(&self, info: &HookPreDispatchInfo) -> ProgramControls<C> {
+ pub(crate) fn run_hook_pre_dispatch(
+ &self,
+ info: &mut HookPreDispatchInfo,
+ ) -> ProgramControls<C> {
if !self.user_context.run_hook {
return ProgramControls::Empty;
}
@@ -475,7 +479,7 @@ where
#[must_use]
pub fn on_pre_dispatch<F, R>(mut self, handler: F) -> Self
where
- F: for<'a> Fn(&HookPreDispatchInfo<'a>) -> R + 'static + Send + Sync,
+ F: for<'a> Fn(&mut HookPreDispatchInfo<'a>) -> R + 'static + Send + Sync,
R: Into<ProgramControls<C>>,
{
self.pre_dispatch = Some(Box::new(move |info| handler(info).into()));
@@ -801,16 +805,21 @@ mod tests {
#[test]
fn test_hook_on_pre_dispatch() {
static CALLED: AtomicBool = AtomicBool::new(false);
- let hook =
- ProgramHook::<MockHookEnum>::empty().on_pre_dispatch(|info: &HookPreDispatchInfo| {
- assert_eq!(info.arguments, &["a", "b"]);
+ let mut args = vec!["a".to_string(), "b".to_string()];
+ let hook = ProgramHook::<MockHookEnum>::empty().on_pre_dispatch(
+ |info: &mut HookPreDispatchInfo| {
+ assert_eq!(info.arguments.as_slice(), &["a", "b"]);
+ // The hook may rewrite the arguments before dispatch
+ info.arguments.push("c".to_string());
CALLED.store(true, Ordering::SeqCst);
- });
+ },
+ );
assert!(hook.pre_dispatch.is_some());
- (hook.pre_dispatch.as_ref().unwrap())(&HookPreDispatchInfo {
- arguments: &["a".to_string(), "b".to_string()],
+ (hook.pre_dispatch.as_ref().unwrap())(&mut HookPreDispatchInfo {
+ arguments: &mut args,
});
assert!(CALLED.load(Ordering::SeqCst));
+ assert_eq!(args.as_slice(), &["a", "b", "c"]);
}
#[test]
@@ -910,7 +919,7 @@ mod tests {
fn test_hook_builder_chaining() {
let hook = ProgramHook::<MockHookEnum>::empty()
.on_begin::<_, ()>(|_: &HookBeginInfo| ())
- .on_pre_dispatch(|_: &HookPreDispatchInfo| ())
+ .on_pre_dispatch(|_: &mut HookPreDispatchInfo| ())
.on_post_dispatch(|_: &HookPostDispatchInfo<MockHookEnum>| ())
.on_pre_chain(|_: &HookPreChainInfo<MockHookEnum>| ())
.on_post_chain(|_: &HookPostChainInfo<MockHookEnum>| ())
diff --git a/mingling_core/src/program/hook/hook_info.rs b/mingling_core/src/program/hook/hook_info.rs
index 768f652..25cf272 100644
--- a/mingling_core/src/program/hook/hook_info.rs
+++ b/mingling_core/src/program/hook/hook_info.rs
@@ -7,7 +7,10 @@ pub struct HookBeginInfo {}
/// Represents the data passed to `pre_dispatch` hook.
pub struct HookPreDispatchInfo<'a> {
/// Arguments entered by the user before dispatching
- pub arguments: &'a [String],
+ ///
+ /// The reference is mutable so the hook can rewrite the arguments before
+ /// they are matched against the registered dispatchers.
+ pub arguments: &'a mut Vec<String>,
}
/// Represents the data passed to `post_dispatch` hook.
diff --git a/mingling_core/src/program/once_exec.rs b/mingling_core/src/program/once_exec.rs
index e9927b5..203bbf5 100644
--- a/mingling_core/src/program/once_exec.rs
+++ b/mingling_core/src/program/once_exec.rs
@@ -25,15 +25,19 @@ where
self.run_hook_on_begin(&crate::hook::HookBeginInfo {});
self.args = self.args.iter().skip(1).cloned().collect();
+ let mut args = std::mem::take(&mut self.args);
#[cfg(not(feature = "async"))]
{
#[cfg(panic = "abort")]
- return self.exec_wrapper(|p| crate::exec::exec(p).map_err(|e| e.into()));
+ return self
+ .exec_wrapper(|p| crate::exec::exec_with_args(p, &mut args).map_err(|e| e.into()));
#[cfg(not(panic = "abort"))]
match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
- self.exec_wrapper(|p| crate::exec::exec(p).map_err(std::convert::Into::into))
+ self.exec_wrapper(|p| {
+ crate::exec::exec_with_args(p, &mut args).map_err(std::convert::Into::into)
+ })
})) {
Ok(result) => result,
Err(panic_info) => {
@@ -60,7 +64,11 @@ where
#[cfg(feature = "async")]
{
return self
- .exec_wrapper(|p| async { crate::exec::exec(p).await.map_err(Into::into) })
+ .exec_wrapper(|p| async move {
+ crate::exec::exec_with_args(p, &mut args)
+ .await
+ .map_err(Into::into)
+ })
.await;
}
}
diff --git a/mingling_core/src/program/repl_exec.rs b/mingling_core/src/program/repl_exec.rs
index 9d9be30..b91f7bc 100644
--- a/mingling_core/src/program/repl_exec.rs
+++ b/mingling_core/src/program/repl_exec.rs
@@ -58,10 +58,10 @@ where
line: &mut readline,
});
- let args = split_input_string(&readline);
+ let mut args = split_input_string(&readline);
p.run_hook_repl_pre_exec(&crate::hook::HookREPLPreExecInfo { args: &args });
- match might_be_async::invoke!(exec_once(p, &args)) {
+ match might_be_async::invoke!(exec_once(p, &mut args)) {
Ok(r) => {
p.run_hook_repl_on_receive_result(&crate::hook::HookREPLOnReceiveResultInfo {
result: &r,
@@ -91,13 +91,13 @@ where
#[cfg(not(feature = "async"))]
fn exec_once<C>(
p: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync + 'static,
{
#[cfg(panic = "abort")]
- let exec_result = super::exec::exec_with_args(p, &args);
+ let exec_result = super::exec::exec_with_args(p, args);
#[cfg(not(panic = "abort"))]
let exec_result = {
@@ -130,7 +130,7 @@ where
#[cfg(feature = "async")]
async fn exec_once<C>(
p: &'static Program<C>,
- args: &[String],
+ args: &mut Vec<String>,
) -> Result<RenderResult, ProgramInternalExecuteError>
where
C: ProgramCollect<Enum = C> + Send + Sync + 'static,