aboutsummaryrefslogtreecommitdiff
path: root/mingling_core/src/program
diff options
context:
space:
mode:
author魏曹先生 <1992414357@qq.com>2026-05-18 17:25:29 +0800
committer魏曹先生 <1992414357@qq.com>2026-05-18 17:26:37 +0800
commitda5a750965dbec5a2c003faa8fb9f1dd110ccce8 (patch)
treeb793769ec40494c1ab056c65748c3393b5a849ed /mingling_core/src/program
parentab7c5785fb290541ad4361c0d46241817c3ff5f9 (diff)
Implement REPL execution with hooks and argument splitting
Diffstat (limited to 'mingling_core/src/program')
-rw-r--r--mingling_core/src/program/exec.rs40
-rw-r--r--mingling_core/src/program/exec/error.rs10
-rw-r--r--mingling_core/src/program/hook.rs81
-rw-r--r--mingling_core/src/program/once_exec.rs95
-rw-r--r--mingling_core/src/program/repl_exec.rs103
-rw-r--r--mingling_core/src/program/repl_exec/splitter.rs133
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(&current.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(&current.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"]);
+ }
+}