From bfaafb4b35ebe06f258d0da42e5709cf66b4c472 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Sun, 24 May 2026 13:12:47 +0800 Subject: [PATCH] exec: centralize command matching in catalog --- README.md | 2 +- docs/CHANGELOG.md | 2 + docs/CONTRIBUTING.md | 2 +- docs/Development-Guide.md | 2 +- docs/QUICKREF.md | 2 +- include/exec_catalog.h | 12 +++++ src/exec.c | 83 +++++++++++++++------------------ src/exec_catalog.c | 84 ++++++++++++++++++++++++++++++---- tests/unit/test_exec_catalog.c | 33 +++++++++++++ 9 files changed, 162 insertions(+), 60 deletions(-) diff --git a/README.md b/README.md index 4332739..aaeaa87 100644 --- a/README.md +++ b/README.md @@ -252,7 +252,7 @@ TNT/ │ ├── cli_text.c # startup CLI help and option text │ ├── command_catalog.c # command metadata │ ├── commands.c # COMMAND-mode command dispatch -│ ├── exec_catalog.c # SSH exec command metadata +│ ├── exec_catalog.c # SSH exec command matching and metadata │ ├── exec.c # SSH exec command dispatch │ ├── ssh_server.c # SSH server implementation │ ├── bootstrap.c # SSH authentication and session bootstrap diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 90df3da..e1a1083 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -17,6 +17,8 @@ - Refreshed contributor and development guidance so new commands are added through `command_catalog`, `exec_catalog`, and `i18n_text` instead of stale `ssh_server.c` / inline-`strcmp` instructions. +- `exec_catalog` now owns SSH exec command matching as well as help metadata, + reducing duplicate command knowledge in `src/exec.c`. - Renamed the internal language state from help-oriented names to UI-language names (`ui_lang_t`, `client->ui_lang`, and `i18n_*_ui_lang`) so future i18n work has a correctly named seam. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index cb31305..c727b4f 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -39,7 +39,7 @@ main.c → entry point, signal handling cli_text.c → startup CLI text command_catalog.c → COMMAND-mode command metadata commands.c → COMMAND-mode command dispatch -exec_catalog.c → SSH exec help metadata +exec_catalog.c → SSH exec command matching and help metadata exec.c → SSH exec command dispatch ssh_server.c → SSH listener setup bootstrap.c → SSH authentication/session bootstrap diff --git a/docs/Development-Guide.md b/docs/Development-Guide.md index 15c1f10..da29fe0 100644 --- a/docs/Development-Guide.md +++ b/docs/Development-Guide.md @@ -74,7 +74,7 @@ src/ ├── input.c - Interactive session loop and key handling ├── commands.c - COMMAND-mode command dispatch ├── command_catalog.c - COMMAND-mode names, aliases, and help summaries -├── exec_catalog.c - SSH exec command help metadata +├── exec_catalog.c - SSH exec command matching and help metadata ├── exec.c - SSH exec command dispatch ├── chat_room.c - Chat room logic and message broadcasting ├── message.c - Message persistence (RFC3339 format) diff --git a/docs/QUICKREF.md b/docs/QUICKREF.md index 87a3a83..c68481f 100644 --- a/docs/QUICKREF.md +++ b/docs/QUICKREF.md @@ -51,7 +51,7 @@ STRUCTURE src/bootstrap.c SSH auth/session bootstrap src/chat_room.c broadcast and room state src/commands.c COMMAND-mode command dispatch - src/exec_catalog.c SSH exec command metadata + src/exec_catalog.c SSH exec command matching and metadata src/exec.c SSH exec command dispatch src/message.c persistence, search src/history_view.c message viewport / scroll state diff --git a/include/exec_catalog.h b/include/exec_catalog.h index 8698317..816dbb1 100644 --- a/include/exec_catalog.h +++ b/include/exec_catalog.h @@ -3,6 +3,18 @@ #include "common.h" +typedef enum { + TNT_EXEC_COMMAND_HELP, + TNT_EXEC_COMMAND_HEALTH, + TNT_EXEC_COMMAND_USERS, + TNT_EXEC_COMMAND_STATS, + TNT_EXEC_COMMAND_TAIL, + TNT_EXEC_COMMAND_POST, + TNT_EXEC_COMMAND_EXIT +} tnt_exec_command_id_t; + +bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id, + const char **args); void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos, ui_lang_t lang); diff --git a/src/exec.c b/src/exec.c index 74e3884..ae3feb0 100644 --- a/src/exec.c +++ b/src/exec.c @@ -397,68 +397,57 @@ static int exec_command_post(client_t *client, const char *args) { int exec_dispatch(client_t *client) { char command_copy[MAX_EXEC_COMMAND_LEN]; - char *cmd; - char *args; + tnt_exec_command_id_t command_id; + const char *args = NULL; strncpy(command_copy, client->exec_command, sizeof(command_copy) - 1); command_copy[sizeof(command_copy) - 1] = '\0'; trim_ascii_whitespace(command_copy); - cmd = command_copy; - if (*cmd == '\0') { + if (command_copy[0] == '\0') { return exec_command_help(client); } - args = cmd; - while (*args && !isspace((unsigned char)*args)) { - args++; - } - if (*args) { - *args++ = '\0'; - while (*args && isspace((unsigned char)*args)) { - args++; + if (exec_catalog_match(command_copy, &command_id, &args)) { + switch (command_id) { + case TNT_EXEC_COMMAND_HELP: + return exec_command_help(client); + case TNT_EXEC_COMMAND_HEALTH: + return exec_command_health(client); + case TNT_EXEC_COMMAND_USERS: + if (args && strcmp(args, "--json") != 0) { + client_printf(client, "%s", + i18n_text(client->ui_lang, + I18N_EXEC_USERS_USAGE)); + return 64; + } + return exec_command_users(client, args != NULL); + case TNT_EXEC_COMMAND_STATS: + if (args && strcmp(args, "--json") != 0) { + client_printf(client, "%s", + i18n_text(client->ui_lang, + I18N_EXEC_STATS_USAGE)); + return 64; + } + return exec_command_stats(client, args != NULL); + case TNT_EXEC_COMMAND_TAIL: + return exec_command_tail(client, args); + case TNT_EXEC_COMMAND_POST: + return exec_command_post(client, args); + case TNT_EXEC_COMMAND_EXIT: + return 0; } - } else { - args = NULL; } - if (strcmp(cmd, "help") == 0 || strcmp(cmd, "--help") == 0) { - return exec_command_help(client); - } - if (strcmp(cmd, "health") == 0) { - return exec_command_health(client); - } - if (strcmp(cmd, "users") == 0) { - if (args && strcmp(args, "--json") != 0) { - client_printf(client, "%s", - i18n_text(client->ui_lang, - I18N_EXEC_USERS_USAGE)); - return 64; + for (char *p = command_copy; *p; p++) { + if (isspace((unsigned char)*p)) { + *p = '\0'; + break; } - return exec_command_users(client, args != NULL); } - if (strcmp(cmd, "stats") == 0) { - if (args && strcmp(args, "--json") != 0) { - client_printf(client, "%s", - i18n_text(client->ui_lang, - I18N_EXEC_STATS_USAGE)); - return 64; - } - return exec_command_stats(client, args != NULL); - } - if (strcmp(cmd, "tail") == 0) { - return exec_command_tail(client, args); - } - if (strcmp(cmd, "post") == 0) { - return exec_command_post(client, args); - } - if (strcmp(cmd, "exit") == 0) { - return 0; - } - client_printf(client, i18n_text(client->ui_lang, I18N_EXEC_UNKNOWN_COMMAND_FORMAT), - cmd); + command_copy); return 64; } diff --git a/src/exec_catalog.c b/src/exec_catalog.c index b311e32..5665e8e 100644 --- a/src/exec_catalog.c +++ b/src/exec_catalog.c @@ -1,23 +1,89 @@ #include "exec_catalog.h" typedef struct { + tnt_exec_command_id_t id; + const char *name; + const char *alias; const char *usage; const char *summary_en; const char *summary_zh; } exec_catalog_entry_t; static const exec_catalog_entry_t entries[] = { - {"help", "Show this help", "显示此帮助"}, - {"health", "Print service health", "输出服务健康状态"}, - {"users [--json]", "List online users", "列出在线用户"}, - {"stats [--json]", "Print room statistics", "输出房间统计"}, - {"tail [N]", "Print recent messages", "输出最近消息"}, - {"tail -n N", "Print recent messages", "输出最近消息"}, - {"post MESSAGE", "Post a message non-interactively", "非交互发送消息"}, - {"post \"/me act\"", "Post an action message", "发送动作消息"}, - {"exit", "Exit successfully", "成功退出"} + {TNT_EXEC_COMMAND_HELP, "help", "--help", + "help", "Show this help", "显示此帮助"}, + {TNT_EXEC_COMMAND_HEALTH, "health", NULL, + "health", "Print service health", "输出服务健康状态"}, + {TNT_EXEC_COMMAND_USERS, "users", NULL, + "users [--json]", "List online users", "列出在线用户"}, + {TNT_EXEC_COMMAND_STATS, "stats", NULL, + "stats [--json]", "Print room statistics", "输出房间统计"}, + {TNT_EXEC_COMMAND_TAIL, "tail", NULL, + "tail [N]", "Print recent messages", "输出最近消息"}, + {TNT_EXEC_COMMAND_TAIL, "tail", NULL, + "tail -n N", "Print recent messages", "输出最近消息"}, + {TNT_EXEC_COMMAND_POST, "post", NULL, + "post MESSAGE", "Post a message non-interactively", "非交互发送消息"}, + {TNT_EXEC_COMMAND_POST, "post", NULL, + "post \"/me act\"", "Post an action message", "发送动作消息"}, + {TNT_EXEC_COMMAND_EXIT, "exit", NULL, + "exit", "Exit successfully", "成功退出"} }; +static const char *skip_spaces(const char *value) { + while (value && *value && (*value == ' ' || *value == '\t')) { + value++; + } + return value; +} + +static bool name_matches(const char *line, const char *name, + const char **args) { + size_t len; + + if (!line || !name) { + return false; + } + + len = strlen(name); + if (strncmp(line, name, len) != 0) { + return false; + } + if (line[len] != '\0' && line[len] != ' ' && line[len] != '\t') { + return false; + } + + if (args) { + const char *candidate_args = skip_spaces(line + len); + *args = candidate_args && candidate_args[0] != '\0' + ? candidate_args + : NULL; + } + return true; +} + +bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id, + const char **args) { + line = skip_spaces(line); + if (!line || line[0] == '\0') { + return false; + } + + for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) { + if (!name_matches(line, entries[i].name, args) && + !name_matches(line, entries[i].alias, args)) { + continue; + } + + if (id) { + *id = entries[i].id; + } + return true; + } + + return false; +} + void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos, ui_lang_t lang) { if (lang == UI_LANG_ZH) { diff --git a/tests/unit/test_exec_catalog.c b/tests/unit/test_exec_catalog.c index 2a40cbb..7d2d35d 100644 --- a/tests/unit/test_exec_catalog.c +++ b/tests/unit/test_exec_catalog.c @@ -39,10 +39,43 @@ TEST(generates_localized_exec_help) { assert_ascii_angle_placeholders(zh); } +TEST(matches_exec_commands_and_args) { + tnt_exec_command_id_t id; + const char *args; + + assert(exec_catalog_match("help", &id, &args)); + assert(id == TNT_EXEC_COMMAND_HELP); + assert(args == NULL); + + assert(exec_catalog_match("--help", &id, &args)); + assert(id == TNT_EXEC_COMMAND_HELP); + assert(args == NULL); + + assert(exec_catalog_match("users --json", &id, &args)); + assert(id == TNT_EXEC_COMMAND_USERS); + assert(strcmp(args, "--json") == 0); + + assert(exec_catalog_match("tail -n 20", &id, &args)); + assert(id == TNT_EXEC_COMMAND_TAIL); + assert(strcmp(args, "-n 20") == 0); + + assert(exec_catalog_match("post hello world", &id, &args)); + assert(id == TNT_EXEC_COMMAND_POST); + assert(strcmp(args, "hello world") == 0); + + assert(exec_catalog_match("exit", &id, &args)); + assert(id == TNT_EXEC_COMMAND_EXIT); + assert(args == NULL); + + assert(!exec_catalog_match("usersx", &id, &args)); + assert(!exec_catalog_match("nope", &id, &args)); +} + int main(void) { printf("Running exec catalog unit tests...\n\n"); RUN_TEST(generates_localized_exec_help); + RUN_TEST(matches_exec_commands_and_args); printf("\n✓ All %d tests passed!\n", tests_passed); return 0;