diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index ed8afb2..6f044de 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -21,6 +21,8 @@ reducing duplicate command knowledge in `src/exec.c`. - Replaced hard-coded `chat.m1ng.space` examples with `chat.example.com` so public documentation does not imply a specific production host. +- Moved SSH exec usage text and argument-shape checks into `exec_catalog`, so + `src/exec.c` no longer duplicates `--json` and required-message validation. - 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/include/exec_catalog.h b/include/exec_catalog.h index 816dbb1..69ae5ac 100644 --- a/include/exec_catalog.h +++ b/include/exec_catalog.h @@ -15,7 +15,10 @@ typedef enum { bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id, const char **args); +bool exec_catalog_args_valid(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); +void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos, + tnt_exec_command_id_t id, ui_lang_t lang); #endif /* EXEC_CATALOG_H */ diff --git a/include/i18n.h b/include/i18n.h index b7ee83d..c54fdcc 100644 --- a/include/i18n.h +++ b/include/i18n.h @@ -53,10 +53,6 @@ typedef enum { I18N_UNKNOWN_COMMAND_FORMAT, I18N_DID_YOU_MEAN_FORMAT, I18N_UNKNOWN_GUIDANCE, - I18N_EXEC_USERS_USAGE, - I18N_EXEC_STATS_USAGE, - I18N_EXEC_TAIL_USAGE, - I18N_EXEC_POST_USAGE, I18N_EXEC_POST_EMPTY, I18N_EXEC_POST_INVALID_UTF8, I18N_EXEC_UNKNOWN_COMMAND_FORMAT, diff --git a/src/exec.c b/src/exec.c index ae3feb0..0824510 100644 --- a/src/exec.c +++ b/src/exec.c @@ -126,6 +126,17 @@ static int exec_command_help(client_t *client) { return client_send(client, help_text, pos) == 0 ? 0 : 1; } +static int exec_command_usage(client_t *client, tnt_exec_command_id_t id) { + char usage[128]; + size_t pos = 0; + + usage[0] = '\0'; + exec_catalog_append_usage(usage, sizeof(usage), &pos, id, + client->ui_lang); + client_printf(client, "%s", usage); + return 64; +} + static int exec_command_health(client_t *client) { static const char ok[] = "ok\n"; return client_send(client, ok, sizeof(ok) - 1) == 0 ? 0 : 1; @@ -289,9 +300,7 @@ static int exec_command_tail(client_t *client, const char *args) { int rc; if (parse_tail_count(args, &requested) < 0) { - client_printf(client, "%s", - i18n_text(client->ui_lang, I18N_EXEC_TAIL_USAGE)); - return 64; + return exec_command_usage(client, TNT_EXEC_COMMAND_TAIL); } pthread_rwlock_rdlock(&g_room->lock); @@ -343,9 +352,7 @@ static int exec_command_post(client_t *client, const char *args) { }; if (!args || args[0] == '\0') { - client_printf(client, "%s", - i18n_text(client->ui_lang, I18N_EXEC_POST_USAGE)); - return 64; + return exec_command_usage(client, TNT_EXEC_COMMAND_POST); } strncpy(content, args, sizeof(content) - 1); @@ -409,26 +416,18 @@ int exec_dispatch(client_t *client) { } if (exec_catalog_match(command_copy, &command_id, &args)) { + if (!exec_catalog_args_valid(command_id, args)) { + return exec_command_usage(client, command_id); + } + 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); diff --git a/src/exec_catalog.c b/src/exec_catalog.c index 5665e8e..2970fe7 100644 --- a/src/exec_catalog.c +++ b/src/exec_catalog.c @@ -5,31 +5,52 @@ typedef struct { const char *name; const char *alias; const char *usage; + const char *usage_syntax; const char *summary_en; const char *summary_zh; + bool no_args; + bool optional_json; + bool requires_args; } exec_catalog_entry_t; static const exec_catalog_entry_t entries[] = { {TNT_EXEC_COMMAND_HELP, "help", "--help", - "help", "Show this help", "显示此帮助"}, + "help", "help", "Show this help", "显示此帮助", true, false, false}, {TNT_EXEC_COMMAND_HEALTH, "health", NULL, - "health", "Print service health", "输出服务健康状态"}, + "health", "health", "Print service health", "输出服务健康状态", + true, false, false}, {TNT_EXEC_COMMAND_USERS, "users", NULL, - "users [--json]", "List online users", "列出在线用户"}, + "users [--json]", "users [--json]", + "List online users", "列出在线用户", false, true, false}, {TNT_EXEC_COMMAND_STATS, "stats", NULL, - "stats [--json]", "Print room statistics", "输出房间统计"}, + "stats [--json]", "stats [--json]", + "Print room statistics", "输出房间统计", false, true, false}, {TNT_EXEC_COMMAND_TAIL, "tail", NULL, - "tail [N]", "Print recent messages", "输出最近消息"}, + "tail [N]", "tail [N] | tail -n N", + "Print recent messages", "输出最近消息", false, false, false}, {TNT_EXEC_COMMAND_TAIL, "tail", NULL, - "tail -n N", "Print recent messages", "输出最近消息"}, + "tail -n N", "tail [N] | tail -n N", + "Print recent messages", "输出最近消息", false, false, false}, {TNT_EXEC_COMMAND_POST, "post", NULL, - "post MESSAGE", "Post a message non-interactively", "非交互发送消息"}, + "post MESSAGE", "post MESSAGE", + "Post a message non-interactively", "非交互发送消息", + false, false, true}, {TNT_EXEC_COMMAND_POST, "post", NULL, - "post \"/me act\"", "Post an action message", "发送动作消息"}, + "post \"/me act\"", "post MESSAGE", + "Post an action message", "发送动作消息", false, false, true}, {TNT_EXEC_COMMAND_EXIT, "exit", NULL, - "exit", "Exit successfully", "成功退出"} + "exit", "exit", "Exit successfully", "成功退出", true, false, false} }; +static const exec_catalog_entry_t *entry_for_id(tnt_exec_command_id_t id) { + for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) { + if (entries[i].id == id) { + return &entries[i]; + } + } + return NULL; +} + static const char *skip_spaces(const char *value) { while (value && *value && (*value == ' ' || *value == '\t')) { value++; @@ -84,6 +105,24 @@ bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id, return false; } +bool exec_catalog_args_valid(tnt_exec_command_id_t id, const char *args) { + const exec_catalog_entry_t *entry = entry_for_id(id); + + if (!entry) { + return false; + } + if (entry->no_args) { + return !args || args[0] == '\0'; + } + if (entry->optional_json) { + return !args || strcmp(args, "--json") == 0; + } + if (entry->requires_args) { + return args && args[0] != '\0'; + } + return true; +} + void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos, ui_lang_t lang) { if (lang == UI_LANG_ZH) { @@ -99,3 +138,19 @@ void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos, entries[i].usage, summary); } } + +void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos, + tnt_exec_command_id_t id, ui_lang_t lang) { + const exec_catalog_entry_t *entry = entry_for_id(id); + + if (!entry) { + return; + } + if (lang == UI_LANG_ZH) { + buffer_appendf(buffer, buf_size, pos, "%s: 用法: %s\n", + entry->name, entry->usage_syntax); + return; + } + buffer_appendf(buffer, buf_size, pos, "%s: usage: %s\n", + entry->name, entry->usage_syntax); +} diff --git a/src/i18n_text.c b/src/i18n_text.c index 7c51be5..31ff12a 100644 --- a/src/i18n_text.c +++ b/src/i18n_text.c @@ -208,22 +208,6 @@ static const i18n_text_entry_t text_catalog[I18N_TEXT_COUNT] = { "Type :help for help\n", "输入 :help 查看帮助\n" }, - [I18N_EXEC_USERS_USAGE] = { - "users: usage: users [--json]\n", - "users: 用法: users [--json]\n" - }, - [I18N_EXEC_STATS_USAGE] = { - "stats: usage: stats [--json]\n", - "stats: 用法: stats [--json]\n" - }, - [I18N_EXEC_TAIL_USAGE] = { - "tail: usage: tail [N] | tail -n N\n", - "tail: 用法: tail [N] | tail -n N\n" - }, - [I18N_EXEC_POST_USAGE] = { - "post: usage: post MESSAGE\n", - "post: 用法: post MESSAGE\n" - }, [I18N_EXEC_POST_EMPTY] = { "post: message cannot be empty\n", "post: 消息不能为空\n" diff --git a/tests/test_exec_mode.sh b/tests/test_exec_mode.sh index 5578b8e..8b0e26a 100755 --- a/tests/test_exec_mode.sh +++ b/tests/test_exec_mode.sh @@ -51,6 +51,17 @@ else FAIL=$((FAIL + 1)) fi +HEALTH_USAGE=$(ssh $SSH_OPTS localhost health extra 2>/dev/null || true) +printf '%s\n' "$HEALTH_USAGE" | grep -q '^health: 用法: health$' +if [ $? -eq 0 ]; then + echo "✓ no-arg exec usage follows TNT_LANG" + PASS=$((PASS + 1)) +else + echo "✗ no-arg exec usage output unexpected" + printf '%s\n' "$HEALTH_USAGE" + FAIL=$((FAIL + 1)) +fi + STATS_OUTPUT=$(ssh $SSH_OPTS localhost stats 2>/dev/null || true) printf '%s\n' "$STATS_OUTPUT" | grep -q '^status ok$' && printf '%s\n' "$STATS_OUTPUT" | grep -q '^online_users 0$' @@ -109,6 +120,17 @@ else FAIL=$((FAIL + 1)) fi +USERS_USAGE=$(ssh $SSH_OPTS localhost users --xml 2>/dev/null || true) +printf '%s\n' "$USERS_USAGE" | grep -q '^users: 用法: users \[--json\]$' +if [ $? -eq 0 ]; then + echo "✓ users usage follows TNT_LANG" + PASS=$((PASS + 1)) +else + echo "✗ users usage output unexpected" + printf '%s\n' "$USERS_USAGE" + FAIL=$((FAIL + 1)) +fi + POST_OUTPUT=$(ssh $SSH_OPTS execposter@localhost post "hello from exec" 2>/dev/null || true) if [ "$POST_OUTPUT" = "posted" ]; then echo "✓ post publishes a message" diff --git a/tests/unit/test_exec_catalog.c b/tests/unit/test_exec_catalog.c index 7d2d35d..ac387d0 100644 --- a/tests/unit/test_exec_catalog.c +++ b/tests/unit/test_exec_catalog.c @@ -71,11 +71,45 @@ TEST(matches_exec_commands_and_args) { assert(!exec_catalog_match("nope", &id, &args)); } +TEST(validates_argument_shapes) { + assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_HELP, NULL)); + assert(!exec_catalog_args_valid(TNT_EXEC_COMMAND_HELP, "now")); + assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_HEALTH, NULL)); + assert(!exec_catalog_args_valid(TNT_EXEC_COMMAND_HEALTH, "now")); + + assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_USERS, NULL)); + assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_USERS, "--json")); + assert(!exec_catalog_args_valid(TNT_EXEC_COMMAND_USERS, "--xml")); + + assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_TAIL, NULL)); + assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_TAIL, "-n 20")); + + assert(!exec_catalog_args_valid(TNT_EXEC_COMMAND_POST, NULL)); + assert(exec_catalog_args_valid(TNT_EXEC_COMMAND_POST, "hello")); +} + +TEST(generates_localized_usage) { + char en[256] = {0}; + char zh[256] = {0}; + size_t en_pos = 0; + size_t zh_pos = 0; + + exec_catalog_append_usage(en, sizeof(en), &en_pos, + TNT_EXEC_COMMAND_TAIL, UI_LANG_EN); + exec_catalog_append_usage(zh, sizeof(zh), &zh_pos, + TNT_EXEC_COMMAND_POST, UI_LANG_ZH); + + assert(strcmp(en, "tail: usage: tail [N] | tail -n N\n") == 0); + assert(strcmp(zh, "post: 用法: post MESSAGE\n") == 0); +} + int main(void) { printf("Running exec catalog unit tests...\n\n"); RUN_TEST(generates_localized_exec_help); RUN_TEST(matches_exec_commands_and_args); + RUN_TEST(validates_argument_shapes); + RUN_TEST(generates_localized_usage); printf("\n✓ All %d tests passed!\n", tests_passed); return 0;