diff options
| author | 魏曹先生 <1992414357@qq.com> | 2026-01-24 07:03:40 +0800 |
|---|---|---|
| committer | 魏曹先生 <1992414357@qq.com> | 2026-01-24 07:03:40 +0800 |
| commit | 3fa56b997b44caba630a5dbc67687923978c5c7d (patch) | |
| tree | b1f62fe31f88fb666e810637df8ca7a60265a3ee /src | |
| parent | 92f2cdd3dfa378cfcfb9085fedd601b27e499ee7 (diff) | |
Add command aliases, error handling improvements, and flag aliases
- Add aliases for status command: sign, sheet, sheet.add, drop, drop.cat
- Improve error handling with detailed localized messages for prepare,
execute, and render phases
- Add flag aliases: -L for --lang and -R for --renderer
- Implement similar command suggestions using Levenshtein distance
- Fix command matching logic to avoid ambiguous prefix issues
Diffstat (limited to 'src')
| -rw-r--r-- | src/bin/jvn.rs | 172 | ||||
| -rw-r--r-- | src/systems/cmd/processer.rs | 17 | ||||
| -rw-r--r-- | src/utils.rs | 1 | ||||
| -rw-r--r-- | src/utils/levenshtein_distance.rs | 34 |
4 files changed, 199 insertions, 25 deletions
diff --git a/src/bin/jvn.rs b/src/bin/jvn.rs index 25bf219..fda0c26 100644 --- a/src/bin/jvn.rs +++ b/src/bin/jvn.rs @@ -1,5 +1,10 @@ +use std::process::exit; + +use just_enough_vcs_cli::systems::cmd::_registry::jv_cmd_nodes; use just_enough_vcs_cli::systems::cmd::cmd_system::JVCommandContext; +use just_enough_vcs_cli::systems::cmd::errors::{CmdExecuteError, CmdPrepareError, CmdRenderError}; use just_enough_vcs_cli::utils::display::md; +use just_enough_vcs_cli::utils::levenshtein_distance::levenshtein_distance; use just_enough_vcs_cli::{ systems::cmd::{errors::CmdProcessError, processer::jv_cmd_process}, utils::env::current_locales, @@ -46,23 +51,35 @@ async fn main() { let mut args: Vec<String> = std::env::args().skip(1).collect(); // Init i18n - let lang = special_argument!(args, "--lang").unwrap_or(current_locales()); + let lang = special_argument!(args, "--lang") + .or_else(|| special_argument!(args, "-L")) + .unwrap_or(current_locales()); set_locale(&lang); - // Init colored - #[cfg(windows)] - colored::control::set_virtual_terminal(true).unwrap(); - - let renderer_override = special_argument!(args, "--renderer").unwrap_or("default".to_string()); + // Renderer + let renderer_override = special_argument!(args, "--renderer") + .or_else(|| special_argument!(args, "-R")) + .unwrap_or("default".to_string()); + // Other flags let no_error_logs = special_flag!(args, "--no-error-logs"); let quiet = special_flag!(args, "--quiet") || special_flag!(args, "-q"); let help = special_flag!(args, "--help") || special_flag!(args, "-h"); let confirmed = special_flag!(args, "--confirm") || special_flag!(args, "-C"); + // Init colored + #[cfg(windows)] + colored::control::set_virtual_terminal(true).unwrap(); + + // Handle help when no arguments provided + if args.len() < 1 && help { + eprintln!("{}", md(t!("help"))); + exit(1); + } + // Process commands let render_result = match jv_cmd_process( - args, + &args, JVCommandContext { help, confirmed }, renderer_override, ) @@ -73,22 +90,13 @@ async fn main() { if !no_error_logs { match e { CmdProcessError::Prepare(cmd_prepare_error) => { - eprintln!( - "{}", - md(t!("process_error.prepare_error", error = cmd_prepare_error)) - ); + handle_prepare_error(cmd_prepare_error); } CmdProcessError::Execute(cmd_execute_error) => { - eprintln!( - "{}", - md(t!("process_error.execute_error", error = cmd_execute_error)) - ); + handle_execute_error(cmd_execute_error); } CmdProcessError::Render(cmd_render_error) => { - eprintln!( - "{}", - md(t!("process_error.render_error", error = cmd_render_error)) - ); + handle_render_error(cmd_render_error); } CmdProcessError::Error(error) => { eprintln!("{}", md(t!("process_error.other", error = error))); @@ -97,7 +105,7 @@ async fn main() { eprintln!("{}", md(t!("process_error.no_node_found", node = node))); } CmdProcessError::NoMatchingCommand => { - eprintln!("{}", md(t!("process_error.no_matching_command"))); + handle_no_matching_command_error(args); } CmdProcessError::AmbiguousCommand(nodes) => { let nodes_list = nodes @@ -129,3 +137,127 @@ async fn main() { print!("{}", render_result); } } + +fn handle_no_matching_command_error(args: Vec<String>) { + let mut similar_nodes: Vec<String> = Vec::new(); + for node in jv_cmd_nodes() { + let node_len = node.split(" ").collect::<Vec<&str>>().iter().len(); + let args_len = args.len(); + if args_len < node_len { + continue; + } + let args_str = args[..node_len].join(" "); + let distance = levenshtein_distance(args_str.as_str(), node.as_str()); + if distance <= 2 { + similar_nodes.push(node); + } + } + if similar_nodes.len() < 1 { + eprintln!("{}", md(t!("process_error.no_matching_command"))); + } else { + eprintln!( + "{}", + md(t!( + "process_error.no_matching_command_but_similar", + similars = similar_nodes[0] + )) + ); + } +} + +fn handle_prepare_error(cmd_prepare_error: CmdPrepareError) { + match cmd_prepare_error { + CmdPrepareError::Io(error) => { + eprintln!("{}", md(t!("prepare_error.io", error = error.to_string()))); + } + CmdPrepareError::Error(msg) => { + eprintln!("{}", md(t!("prepare_error.error", error = msg))); + } + CmdPrepareError::LocalWorkspaceNotFound => { + eprintln!("{}", md(t!("prepare_error.local_workspace_not_found"))); + } + CmdPrepareError::LocalConfigNotFound => { + eprintln!("{}", md(t!("prepare_error.local_config_not_found"))); + } + CmdPrepareError::LatestInfoNotFound => { + eprintln!("{}", md(t!("prepare_error.latest_info_not_found"))); + } + CmdPrepareError::LatestFileDataNotExist(member_id) => { + eprintln!( + "{}", + md(t!( + "prepare_error.latest_file_data_not_exist", + member_id = member_id + )) + ); + } + CmdPrepareError::CachedSheetNotFound(sheet_name) => { + eprintln!( + "{}", + md(t!( + "prepare_error.cached_sheet_not_found", + sheet_name = sheet_name + )) + ); + } + CmdPrepareError::LocalSheetNotFound(member_id, sheet_name) => { + eprintln!( + "{}", + md(t!( + "prepare_error.local_sheet_not_found", + member_id = member_id, + sheet_name = sheet_name + )) + ); + } + CmdPrepareError::LocalStatusAnalyzeFailed => { + eprintln!("{}", md(t!("prepare_error.local_status_analyze_failed"))); + } + CmdPrepareError::NoSheetInUse => { + eprintln!("{}", md(t!("prepare_error.no_sheet_in_use"))); + } + } +} + +fn handle_execute_error(cmd_execute_error: CmdExecuteError) { + match cmd_execute_error { + CmdExecuteError::Io(error) => { + eprintln!("{}", md(t!("execute_error.io", error = error.to_string()))); + } + CmdExecuteError::Prepare(cmd_prepare_error) => handle_prepare_error(cmd_prepare_error), + CmdExecuteError::Error(msg) => { + eprintln!("{}", md(t!("execute_error.error", error = msg))); + } + } +} + +fn handle_render_error(cmd_render_error: CmdRenderError) { + match cmd_render_error { + CmdRenderError::Io(error) => { + eprintln!("{}", md(t!("render_error.io", error = error.to_string()))); + } + CmdRenderError::Prepare(cmd_prepare_error) => handle_prepare_error(cmd_prepare_error), + CmdRenderError::Execute(cmd_execute_error) => handle_execute_error(cmd_execute_error), + CmdRenderError::Error(msg) => { + eprintln!("{}", md(t!("render_error.error", error = msg))); + } + CmdRenderError::SerializeFailed(error) => { + eprintln!( + "{}", + md(t!( + "render_error.serialize_failed", + error = error.to_string() + )) + ); + } + CmdRenderError::RendererNotFound(renderer_name) => { + eprintln!( + "{}", + md(t!( + "render_error.renderer_not_found", + renderer_name = renderer_name + )) + ); + } + } +} diff --git a/src/systems/cmd/processer.rs b/src/systems/cmd/processer.rs index d357e44..7c464a2 100644 --- a/src/systems/cmd/processer.rs +++ b/src/systems/cmd/processer.rs @@ -4,17 +4,24 @@ use crate::systems::cmd::errors::CmdProcessError; use crate::systems::cmd::renderer::JVRenderResult; pub async fn jv_cmd_process( - args: Vec<String>, + args: &Vec<String>, ctx: JVCommandContext, renderer_override: String, ) -> Result<JVRenderResult, CmdProcessError> { let nodes = jv_cmd_nodes(); - let command = args.join(" "); - // Find nodes that match the beginning of the command + // Why add a space? + // Add a space at the end of the command string for precise command prefix matching. + // For example: when the input command is "bananas", if there are two commands "banana" and "bananas", + // without a space it might incorrectly match "banana" (because "bananas".starts_with("banana") is true). + // After adding a space, "bananas " will not match "banana ", thus avoiding ambiguity caused by overlapping prefixes. + let command = format!("{} ", args.join(" ")); + + // Find all nodes that match the command prefix let matching_nodes: Vec<&String> = nodes .iter() - .filter(|node| command.starts_with(node.as_str())) + // Also add a space to the node string to ensure consistent matching logic + .filter(|node| command.starts_with(&format!("{} ", node))) .collect(); match matching_nodes.len() { @@ -25,7 +32,7 @@ pub async fn jv_cmd_process( 1 => { let matched_prefix = matching_nodes[0]; let prefix_len = matched_prefix.split_whitespace().count(); - let trimmed_args: Vec<String> = args.into_iter().skip(prefix_len).collect(); + let trimmed_args: Vec<String> = args.into_iter().cloned().skip(prefix_len).collect(); return jv_cmd_process_node(matched_prefix, trimmed_args, ctx, renderer_override).await; } _ => { diff --git a/src/utils.rs b/src/utils.rs index 8c2e306..7d3cb5e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -3,6 +3,7 @@ pub mod env; pub mod fs; pub mod globber; pub mod input; +pub mod levenshtein_distance; pub mod logger; pub mod push_version; pub mod socket_addr_helper; diff --git a/src/utils/levenshtein_distance.rs b/src/utils/levenshtein_distance.rs new file mode 100644 index 0000000..6bdb7e7 --- /dev/null +++ b/src/utils/levenshtein_distance.rs @@ -0,0 +1,34 @@ +use std::cmp::min; + +pub fn levenshtein_distance(a: &str, b: &str) -> usize { + let a_chars: Vec<char> = a.chars().collect(); + let b_chars: Vec<char> = b.chars().collect(); + let a_len = a_chars.len(); + let b_len = b_chars.len(); + + if a_len == 0 { + return b_len; + } + if b_len == 0 { + return a_len; + } + + let mut dp = vec![vec![0; b_len + 1]; a_len + 1]; + + for (i, row) in dp.iter_mut().enumerate() { + row[0] = i; + } + + for (j, cell) in dp[0].iter_mut().enumerate() { + *cell = j; + } + + for (i, a_char) in a_chars.iter().enumerate() { + for (j, b_char) in b_chars.iter().enumerate() { + let cost = if a_char == b_char { 0 } else { 1 }; + dp[i + 1][j + 1] = min(dp[i][j + 1] + 1, min(dp[i + 1][j] + 1, dp[i][j] + cost)); + } + } + + dp[a_len][b_len] +} |
