From 3fa56b997b44caba630a5dbc67687923978c5c7d Mon Sep 17 00:00:00 2001 From: 魏曹先生 <1992414357@qq.com> Date: Sat, 24 Jan 2026 07:03:40 +0800 Subject: 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 --- .cargo/registry.toml | 20 +++++ resources/locales/jvn/en.yml | 86 ++++++++++++++++--- resources/locales/jvn/zh-CN.yml | 84 ++++++++++++++++--- src/bin/jvn.rs | 172 +++++++++++++++++++++++++++++++++----- src/systems/cmd/processer.rs | 17 ++-- src/utils.rs | 1 + src/utils/levenshtein_distance.rs | 34 ++++++++ 7 files changed, 364 insertions(+), 50 deletions(-) create mode 100644 src/utils/levenshtein_distance.rs diff --git a/.cargo/registry.toml b/.cargo/registry.toml index 3ebfa17..a957b9b 100644 --- a/.cargo/registry.toml +++ b/.cargo/registry.toml @@ -9,6 +9,26 @@ # node = "name" # type = "your_command::JVUnknownCommand" +[cmd.sign] +node = "sign" +type = "cmds::status::JVStatusCommand" + +[cmd.sheet] +node = "sheet" +type = "cmds::status::JVStatusCommand" + +[cmd.sheet_add] +node = "sheet.add" +type = "cmds::status::JVStatusCommand" + +[cmd.drop] +node = "drop" +type = "cmds::status::JVStatusCommand" + +[cmd.drop_cat] +node = "drop.cat" +type = "cmds::status::JVStatusCommand" + ################# ### Renderers ### ################# diff --git a/resources/locales/jvn/en.yml b/resources/locales/jvn/en.yml index 525d8c5..d7dead3 100644 --- a/resources/locales/jvn/en.yml +++ b/resources/locales/jvn/en.yml @@ -1,19 +1,16 @@ -process_error: - prepare_error: | - [[YELLOW]]Preparation Phase Error:[[/]] - %{error} - execute_error: | - [[RED]]Execution Phase Error:[[/]] - %{error} - render_error: | - [[YELLOW]]Rendering Phase Error:[[/]] - %{error} - - Tip: If you need to ignore error output, - please append the `--no-error-logs` parameter to the command. +help: | + NO +process_error: no_matching_command: | No matching command found! + Use `jv -h` to get help + + no_matching_command_but_similar: | + No matching command found, but similar commands were found: + jv %{similars} + + Use `jv -h` to get help ambiguous_command: | Multiple commands found, unable to determine which one you want: @@ -33,3 +30,66 @@ process_error: other: | %{error} + +prepare_error: + io: | + I/O error in preparation phase! + Error: %{error} + + error: | + Unknown error in preparation phase! + Error: %{error} + + local_workspace_not_found: | + Local workspace not found! + Create or enter a workspace directory first. + + local_config_not_found: | + Failed to read workspace config. File may not exist or format mismatch. + Use `jv update` and try again. + + latest_info_not_found: | + Unable to read latest upstream info! + Use `jv update` and try again. + + latest_file_data_not_exist: | + Unable to read latest file info for member `%{member_id}`! + Use `jv update` and try again. + + cached_sheet_not_found: | + Unable to read cached upstream sheet `%{sheet_name}`! + Use `jv update` and try again. + + local_sheet_not_found: | + Unable to read local sheet `%{sheet_name}` for member `%{member_id}`. + + local_status_analyze_failed: | + Failed to analyze local workspace! + + no_sheet_in_use: | + No sheet in use. Use `jv use `. + +execute_error: + io: | + I/O error in execution phase! + Error: %{error} + + error: | + Error in execution phase! + Error: %{error} + +render_error: + io: | + I/O error in rendering phase! + Error: %{error} + + error: | + Error in rendering phase! + Error: %{error} + + serialize_failed: | + Data serialization error! + Error: %{error} + + renderer_not_found: | + Renderer `%{renderer_name}` not found! diff --git a/resources/locales/jvn/zh-CN.yml b/resources/locales/jvn/zh-CN.yml index ffb033c..194bddd 100644 --- a/resources/locales/jvn/zh-CN.yml +++ b/resources/locales/jvn/zh-CN.yml @@ -1,18 +1,15 @@ +help: | + NO + process_error: - prepare_error: | - [[YELLOW]]准备阶段错误:[[/]] - %{error} - execute_error: | - [[RED]]执行阶段错误:[[/]] - %{error} - render_error: | - [[YELLOW]]渲染阶段错误:[[/]] - %{error} + no_matching_command: | + 无法匹配该命令,使用 `jv -h` 查看帮助 - 提示:若您需要忽略错误输出,请在命令后追加 `--no-error-logs` 参数 + no_matching_command_but_similar: | + 无法找到匹配的命令,但找到相似命令: + jv %{similars} - no_matching_command: | - 无法找到匹配的命令! + 使用 `jv -h` 查看帮助 ambiguous_command: | 找到多个命令,无法确定您想要哪一个: @@ -32,3 +29,66 @@ process_error: other: | %{error} + +prepare_error: + io: | + 命令在准备阶段发生了 I/O 错误! + 错误信息:%{error} + + error: | + 命令在准备阶段发生未知错误! + 错误信息:%{error} + + local_workspace_not_found: | + 无法找到本地工作区! + 请先创建或进入本地工作区目录 + + local_config_not_found: | + 读取本地工作区配置文件失败,它可能不存在或格式不匹配 + 请使用 `jv update` 更新工作区信息后再尝试 + + latest_info_not_found: | + 无法找到或读取最新上游信息! + 请使用 `jv update` 更新工作区信息后再尝试 + + latest_file_data_not_exist: | + 无法找到或读取成员 `%{member_id}` 的最新文件信息! + 请使用 `jv update` 更新工作区信息后再尝试 + + cached_sheet_not_found: | + 无法找到或读取上游结构表 `%{sheet_name}` 的缓存信息! + 请使用 `jv update` 更新工作区信息后再尝试 + + local_sheet_not_found: | + 无法找到或读取成员 `%{member_id}` 的本地结构表 `%{sheet_name}` + + local_status_analyze_failed: | + 分析本地工作区失败! + + no_sheet_in_use: | + 当前没有在使用表,请使用 `jv use <结构表名称>` 使用一张结构表 + +execute_error: + io: | + 命令在运行阶段发生了 I/O 错误! + 错误信息:%{error} + + error: | + 命令在运行阶段发生错误! + 错误信息:%{error} + +render_error: + io: | + 命令在渲染阶段发生了 I/O 错误! + 错误信息:%{error} + + error: | + 命令在渲染阶段发生错误! + 错误信息:%{error} + + serialize_failed: | + 数据在序列化时发生了错误! + 错误信息:%{error} + + renderer_not_found: | + 无法找到渲染器 `%{renderer_name}`! 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 = 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) { + let mut similar_nodes: Vec = Vec::new(); + for node in jv_cmd_nodes() { + let node_len = node.split(" ").collect::>().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, + args: &Vec, ctx: JVCommandContext, renderer_override: String, ) -> Result { 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 = args.into_iter().skip(prefix_len).collect(); + let trimmed_args: Vec = 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 = a.chars().collect(); + let b_chars: Vec = 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] +} -- cgit