diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-05-18 17:25:29 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-05-18 17:26:37 +0800 |
| commit | da5a750965dbec5a2c003faa8fb9f1dd110ccce8 (patch) | |
| tree | b793769ec40494c1ab056c65748c3393b5a849ed /mingling_core/src/program | |
| parent | ab7c5785fb290541ad4361c0d46241817c3ff5f9 (diff) | |
Implement REPL execution with hooks and argument splitting
Diffstat (limited to 'mingling_core/src/program')
| -rw-r--r-- | mingling_core/src/program/exec.rs | 40 | ||||
| -rw-r--r-- | mingling_core/src/program/exec/error.rs | 10 | ||||
| -rw-r--r-- | mingling_core/src/program/hook.rs | 81 | ||||
| -rw-r--r-- | mingling_core/src/program/once_exec.rs | 95 | ||||
| -rw-r--r-- | mingling_core/src/program/repl_exec.rs | 103 | ||||
| -rw-r--r-- | mingling_core/src/program/repl_exec/splitter.rs | 133 |
6 files changed, 410 insertions, 52 deletions
diff --git a/mingling_core/src/program/exec.rs b/mingling_core/src/program/exec.rs index 1b756a1..72a20b9 100644 --- a/mingling_core/src/program/exec.rs +++ b/mingling_core/src/program/exec.rs @@ -15,14 +15,35 @@ pub async fn exec<C>( where C: ProgramCollect<Enum = C>, { + let args = program.args.clone(); + exec_with_args(program, args).await +} + +#[cfg(not(feature = "async"))] +pub fn exec<C>(program: &'static Program<C>) -> Result<RenderResult, ProgramInternalExecuteError> +where + C: ProgramCollect<Enum = C>, +{ + let args = program.args.clone(); + exec_with_args(program, args) +} + +#[cfg(feature = "async")] +pub async fn exec_with_args<C>( + program: &'static Program<C>, + args: Vec<String>, +) -> Result<RenderResult, ProgramInternalExecuteError> +where + C: ProgramCollect<Enum = C>, +{ // Run hooks - program.run_hook_pre_dispatch(&program.args); + program.run_hook_pre_dispatch(&args); #[cfg(not(feature = "dispatch_tree"))] - let mut current = dispatch_args_dynamic(program, &program.args)?; + let mut current = dispatch_args_dynamic(program, &args)?; #[cfg(feature = "dispatch_tree")] - let mut current = C::dispatch_args_trie(&program.args)?; + let mut current = C::dispatch_args_trie(&args)?; // Run hook program.run_hook_post_dispatch(¤t.member_id); @@ -102,18 +123,21 @@ where } #[cfg(not(feature = "async"))] -pub fn exec<C>(program: &'static Program<C>) -> Result<RenderResult, ProgramInternalExecuteError> +pub fn exec_with_args<C>( + program: &'static Program<C>, + args: Vec<String>, +) -> Result<RenderResult, ProgramInternalExecuteError> where C: ProgramCollect<Enum = C>, { // Run hooks - program.run_hook_pre_dispatch(&program.args); + program.run_hook_pre_dispatch(&args); #[cfg(not(feature = "dispatch_tree"))] - let mut current = dispatch_args_dynamic(program, &program.args)?; + let mut current = dispatch_args_dynamic(program, &args)?; #[cfg(feature = "dispatch_tree")] - let mut current = C::dispatch_args_trie(&program.args)?; + let mut current = C::dispatch_args_trie(&args)?; // Run hook program.run_hook_post_dispatch(¤t.member_id); @@ -212,7 +236,7 @@ where } Err(ProgramInternalExecuteError::DispatcherNotFound) => { // No matching Dispatcher is found - C::build_dispatcher_not_found(program.args.clone()) + C::build_dispatcher_not_found(args.clone()) } Err(e) => return Err(e), }; diff --git a/mingling_core/src/program/exec/error.rs b/mingling_core/src/program/exec/error.rs index 24112e1..0f2d875 100644 --- a/mingling_core/src/program/exec/error.rs +++ b/mingling_core/src/program/exec/error.rs @@ -60,6 +60,9 @@ pub enum ProgramInternalExecuteError { /// An other internal error occurred. Other(String), + /// A single REPL execution failed + REPLPanic(ProgramPanic), + /// An I/O error occurred during execution. IO(std::io::Error), } @@ -75,6 +78,9 @@ impl fmt::Display for ProgramInternalExecuteError { } ProgramInternalExecuteError::Other(s) => write!(f, "Other error: {}", s), ProgramInternalExecuteError::IO(e) => write!(f, "IO error: {}", e), + ProgramInternalExecuteError::REPLPanic(panic) => { + write!(f, "A single REPL execution failed: {}", panic) + } } } } @@ -105,6 +111,10 @@ impl From<ProgramInternalExecuteError> for ProgramExecuteError { } ProgramInternalExecuteError::Other(s) => ProgramExecuteError::Other(s), ProgramInternalExecuteError::IO(e) => ProgramExecuteError::Other(format!("{}", e)), + ProgramInternalExecuteError::REPLPanic(p) => ProgramExecuteError::Other(format!( + "A single REPL execution failed: {}", + p + )), } } } diff --git a/mingling_core/src/program/hook.rs b/mingling_core/src/program/hook.rs index 19f7baf..448949f 100644 --- a/mingling_core/src/program/hook.rs +++ b/mingling_core/src/program/hook.rs @@ -33,6 +33,18 @@ where /// Executes when the program panics pub exec_panic: Option<fn(&ProgramPanic)>, + + /// Executes when the REPL starts (only available with `repl` feature) + #[cfg(feature = "repl")] + pub repl_on_begin: Option<fn()>, + + /// Executes when the REPL receives a render result (only available with `repl` feature) + #[cfg(feature = "repl")] + pub repl_on_receive_result: Option<fn(&RenderResult)>, + + /// Executes when the REPL panics (only available with `repl` feature) + #[cfg(feature = "repl")] + pub repl_on_panic: Option<fn(&ProgramPanic)>, } impl<C> Program<C> @@ -158,6 +170,48 @@ where } exit_code } + + /// Runs the REPL begin hooks (only available with `repl` feature) + #[cfg(feature = "repl")] + pub(crate) fn run_hook_repl_on_begin(&self) { + if !self.user_context.run_hook { + return; + } + + for hook in &self.hooks { + if let Some(repl_on_begin) = hook.repl_on_begin { + repl_on_begin() + } + } + } + + /// Runs the REPL receive result hooks (only available with `repl` feature) + #[cfg(feature = "repl")] + pub(crate) fn run_hook_repl_on_receive_result(&self, result: &RenderResult) { + if !self.user_context.run_hook { + return; + } + + for hook in &self.hooks { + if let Some(repl_on_receive_result) = hook.repl_on_receive_result { + repl_on_receive_result(result) + } + } + } + + /// Runs the REPL panic hooks (only available with `repl` feature) + #[cfg(feature = "repl")] + pub(crate) fn run_hook_repl_on_panic(&self, panic_info: &ProgramPanic) { + if !self.user_context.run_hook { + return; + } + + for hook in &self.hooks { + if let Some(repl_on_panic) = hook.repl_on_panic { + repl_on_panic(panic_info) + } + } + } } impl<C> ProgramHook<C> @@ -176,6 +230,12 @@ where post_render: None, finish: None, exec_panic: None, + #[cfg(feature = "repl")] + repl_on_begin: None, + #[cfg(feature = "repl")] + repl_on_receive_result: None, + #[cfg(feature = "repl")] + repl_on_panic: None, } } @@ -232,4 +292,25 @@ where let _ = self.exec_panic.insert(handler); self } + + /// Sets the handler for the REPL begin event (only available with `repl` feature). + #[cfg(feature = "repl")] + pub fn on_repl_begin(mut self, handler: fn()) -> Self { + let _ = self.repl_on_begin.insert(handler); + self + } + + /// Sets the handler for the REPL receive result event (only available with `repl` feature). + #[cfg(feature = "repl")] + pub fn on_repl_receive_result(mut self, handler: fn(result: &RenderResult)) -> Self { + let _ = self.repl_on_receive_result.insert(handler); + self + } + + /// Sets the handler for the REPL panic event (only available with `repl` feature). + #[cfg(feature = "repl")] + pub fn on_repl_panic(mut self, handler: fn(panic: &ProgramPanic)) -> Self { + let _ = self.repl_on_panic.insert(handler); + self + } } diff --git a/mingling_core/src/program/once_exec.rs b/mingling_core/src/program/once_exec.rs index ac985e2..b68eb01 100644 --- a/mingling_core/src/program/once_exec.rs +++ b/mingling_core/src/program/once_exec.rs @@ -10,7 +10,7 @@ impl<C> Program<C> where C: ProgramCollect<Enum = C>, { - async fn exec_wrapper<F, Fut>(self, f: F) -> Result<Fut::Output, ProgramPanic> + pub(crate) async fn exec_wrapper<F, Fut>(self, f: F) -> Fut::Output where C: 'static + Send + Sync, F: FnOnce(&'static Program<C>) -> Fut + Send + Sync, @@ -30,20 +30,7 @@ where std::panic::set_hook(Box::new(|_| {})); } - #[cfg(panic = "abort")] - return Ok(f(program)); - - #[cfg(not(panic = "abort"))] - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(program))) { - Ok(fut) => Ok(fut.await), - Err(panic_info) => { - let panic_payload = ProgramPanic { - payload: panic_info, - }; - program.run_hook_exec_panic(&panic_payload); - Err(panic_payload) - } - } + f(program).await } /// Run the command line program @@ -55,12 +42,33 @@ where self.run_hook_on_begin(); self.args = self.args.iter().skip(1).cloned().collect(); - match self + + #[cfg(panic = "abort")] + return self .exec_wrapper(|p| async { crate::exec::exec(p).await.map_err(|e| e.into()) }) - .await - { - Ok(r) => r, - Err(e) => Err(ProgramExecuteError::Panic(e)), + .await; + + #[cfg(not(panic = "abort"))] + match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + self.exec_wrapper(|p| async { crate::exec::exec(p).await.map_err(|e| e.into()) }) + })) { + Ok(fut) => fut.await, + Err(panic_info) => { + let panic_payload = ProgramPanic { + payload: panic_info, + }; + + let program = THIS_PROGRAM + .get() + .unwrap() + .as_ref() + .unwrap() + .downcast_ref::<Program<C>>() + .unwrap(); + + program.run_hook_exec_panic(&panic_payload); + Err(ProgramExecuteError::Panic(panic_payload)) + } } } @@ -125,7 +133,7 @@ impl<C> Program<C> where C: ProgramCollect<Enum = C>, { - fn exec_wrapper<F, R>(self, f: F) -> Result<R, ProgramPanic> + pub(crate) fn exec_wrapper<F, R>(self, f: F) -> R where C: 'static + Send + Sync, F: FnOnce(&'static Program<C>) -> R + Send + Sync, @@ -144,22 +152,7 @@ where std::panic::set_hook(Box::new(|_| {})); } - #[cfg(panic = "abort")] - return Ok(f(program)); - - #[cfg(not(panic = "abort"))] - match std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| f(program))) { - Ok(result) => Ok(result), - Err(panic_info) => { - use crate::error::ProgramPanic; - - let panic_payload = ProgramPanic { - payload: panic_info, - }; - program.run_hook_exec_panic(&panic_payload); - Err(panic_payload) - } - } + f(program) } /// Run the command line program @@ -171,9 +164,31 @@ where self.run_hook_on_begin(); self.args = self.args.iter().skip(1).cloned().collect(); - match self.exec_wrapper(|p| crate::exec::exec(p).map_err(|e| e.into())) { - Ok(r) => r, - Err(e) => Err(ProgramExecuteError::Panic(e)), + + #[cfg(panic = "abort")] + return self.exec_wrapper(|p| crate::exec::exec(p).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(|e| e.into())) + })) { + Ok(result) => result, + Err(panic_info) => { + let panic_payload = ProgramPanic { + payload: panic_info, + }; + + let program = THIS_PROGRAM + .get() + .unwrap() + .as_ref() + .unwrap() + .downcast_ref::<Program<C>>() + .unwrap(); + + program.run_hook_exec_panic(&panic_payload); + Err(ProgramExecuteError::Panic(panic_payload)) + } } } diff --git a/mingling_core/src/program/repl_exec.rs b/mingling_core/src/program/repl_exec.rs index feeaad9..c182f78 100644 --- a/mingling_core/src/program/repl_exec.rs +++ b/mingling_core/src/program/repl_exec.rs @@ -1,12 +1,107 @@ -use crate::{Program, ProgramCollect}; +use std::io::Write; + +mod splitter; + +use crate::error::{ProgramInternalExecuteError, ProgramPanic}; +use crate::program::repl_exec::splitter::split_input_string; +use crate::{Program, ProgramCollect, RenderResult}; #[cfg(not(feature = "async"))] impl<C> Program<C> where - C: ProgramCollect<Enum = C>, + C: ProgramCollect<Enum = C> + Send + Sync + 'static, { - pub fn exec_repl(self) {} + pub fn exec_repl(self) { + self.run_hook_repl_on_begin(); + + self.exec_wrapper(|p| -> ! { + loop { + let args = split_input_string(readline_or_empty()); + match exec_once(p, args) { + Ok(r) => { + p.run_hook_repl_on_receive_result(&r); + } + Err(ProgramInternalExecuteError::REPLPanic(panic)) => { + p.run_hook_repl_on_panic(&panic); + } + _ => {} + } + } + }); + } } #[cfg(feature = "async")] -impl<C> Program<C> where C: ProgramCollect<Enum = C> {} +impl<C> Program<C> +where + C: ProgramCollect<Enum = C> + Send + Sync, +{ + pub async fn exec_repl(self) { + self.run_hook_repl_on_begin(); + + self.exec_wrapper(|p| -> ! { + loop { + let args = split_input_string(readline_or_empty()); + match exec_once(p, args) { + Ok(r) => { + p.run_hook_repl_on_receive_result(&r); + } + Err(ProgramInternalExecuteError::REPLPanic(panic)) => { + p.run_hook_repl_on_panic(&panic); + } + _ => {} + } + } + }) + .await; + } +} + +fn readline() -> Result<String, std::io::Error> { + let mut input = String::new(); + std::io::stdout().flush()?; + std::io::stdin().read_line(&mut input)?; + Ok(input.trim().to_string()) +} + +fn readline_or_empty() -> String { + readline().unwrap_or("".to_string()) +} + +fn exec_once<C>( + p: &'static Program<C>, + args: 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); + + #[cfg(not(panic = "abort"))] + let exec_result = { + let exec_unwind_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { + super::exec::exec_with_args(p, args) + })); + + match exec_unwind_result { + Err(panic_info) => { + let panic_payload = ProgramPanic { + payload: panic_info, + }; + let program = crate::program::THIS_PROGRAM + .get() + .unwrap() + .as_ref() + .unwrap() + .downcast_ref::<Program<C>>() + .unwrap(); + program.run_hook_repl_on_panic(&panic_payload); + Err(ProgramInternalExecuteError::REPLPanic(panic_payload)) + } + Ok(r) => r, + } + }; + + exec_result +} diff --git a/mingling_core/src/program/repl_exec/splitter.rs b/mingling_core/src/program/repl_exec/splitter.rs new file mode 100644 index 0000000..267f42c --- /dev/null +++ b/mingling_core/src/program/repl_exec/splitter.rs @@ -0,0 +1,133 @@ +/// Wraps `split_input` to work with owned `String` inputs. +pub(crate) fn split_input_string(input: String) -> Vec<String> { + split_input(&input) +} + +/// Splits a string input into arguments, respecting single quotes, double quotes, +/// and backslash escaping. +pub(crate) fn split_input(input: &str) -> Vec<String> { + let mut result: Vec<String> = Vec::new(); + let mut current = String::new(); + let mut chars = input.chars().peekable(); + + 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"]); + } +} |
