diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7168302..261edda 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -37,6 +37,8 @@ and server survival stay responsive. ### Changed +- `tntctl --help` now gets its exec command list from `exec_catalog`, reducing + duplicate command metadata between the local wrapper and SSH exec mode. - Updated `tnt(1)` to document the current TUI search and pager keys, and added script coverage to keep active help surfaces free of removed support commands. diff --git a/include/exec_catalog.h b/include/exec_catalog.h index cc7d573..211d67f 100644 --- a/include/exec_catalog.h +++ b/include/exec_catalog.h @@ -11,7 +11,8 @@ typedef enum { TNT_EXEC_COMMAND_TAIL, TNT_EXEC_COMMAND_DUMP, TNT_EXEC_COMMAND_POST, - TNT_EXEC_COMMAND_EXIT + TNT_EXEC_COMMAND_EXIT, + TNT_EXEC_COMMAND_COUNT } tnt_exec_command_id_t; bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id, @@ -19,6 +20,8 @@ bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id, 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_command_list(char *buffer, size_t buf_size, + size_t *pos); void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos, tnt_exec_command_id_t id, ui_lang_t lang); diff --git a/include/tntctl_text.h b/include/tntctl_text.h index ed8c385..e987a25 100644 --- a/include/tntctl_text.h +++ b/include/tntctl_text.h @@ -4,7 +4,6 @@ #include "common.h" typedef enum { - TNTCTL_TEXT_USAGE, TNTCTL_TEXT_INVALID_PORT, TNTCTL_TEXT_INVALID_LOGIN, TNTCTL_TEXT_INVALID_HOST_KEY_MODE, @@ -23,6 +22,8 @@ typedef enum { TNTCTL_TEXT_COUNT } tntctl_text_id_t; +void tntctl_text_append_usage(char *buffer, size_t buf_size, size_t *pos, + ui_lang_t lang); const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id); #endif /* TNTCTL_TEXT_H */ diff --git a/src/exec.c b/src/exec.c index a1cccf7..845bfe2 100644 --- a/src/exec.c +++ b/src/exec.c @@ -517,6 +517,8 @@ int exec_dispatch(client_t *client) { return exec_command_post(client, args); case TNT_EXEC_COMMAND_EXIT: return TNT_EXIT_OK; + case TNT_EXEC_COMMAND_COUNT: + break; } } diff --git a/src/exec_catalog.c b/src/exec_catalog.c index d06e594..6f01d8d 100644 --- a/src/exec_catalog.c +++ b/src/exec_catalog.c @@ -155,6 +155,26 @@ void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos, } } +void exec_catalog_append_command_list(char *buffer, size_t buf_size, + size_t *pos) { + bool seen[TNT_EXEC_COMMAND_COUNT] = {0}; + size_t count = 0; + + for (size_t i = 0; i < sizeof(entries) / sizeof(entries[0]); i++) { + tnt_exec_command_id_t id = entries[i].id; + + if (id < 0 || id >= TNT_EXEC_COMMAND_COUNT || seen[id]) { + continue; + } + if (count > 0) { + buffer_appendf(buffer, buf_size, pos, ", "); + } + buffer_appendf(buffer, buf_size, pos, "%s", entries[i].name); + seen[id] = true; + count++; + } +} + 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); diff --git a/src/tntctl.c b/src/tntctl.c index 47dbb0c..e9d9672 100644 --- a/src/tntctl.c +++ b/src/tntctl.c @@ -10,7 +10,12 @@ #include static void print_usage(FILE *stream, ui_lang_t lang) { - fputs(tntctl_text(lang, TNTCTL_TEXT_USAGE), stream); + char output[2048]; + size_t pos = 0; + + output[0] = '\0'; + tntctl_text_append_usage(output, sizeof(output), &pos, lang); + fputs(output, stream); } static void print_error(ui_lang_t lang, tntctl_text_id_t id) { diff --git a/src/tntctl_text.c b/src/tntctl_text.c index a0090b7..3863026 100644 --- a/src/tntctl_text.c +++ b/src/tntctl_text.c @@ -1,36 +1,9 @@ #include "tntctl_text.h" +#include "exec_catalog.h" #include "i18n.h" static const i18n_string_t text_catalog[TNTCTL_TEXT_COUNT] = { - [TNTCTL_TEXT_USAGE] = I18N_STRING( - "Usage: tntctl [options] host command [args...]\n" - "\n" - "Options:\n" - " -p, --port PORT SSH port (default: 2222)\n" - " -l, --login USER SSH login name for exec identity\n" - " --host-key-checking MODE\n" - " OpenSSH host-key mode: yes, accept-new, no\n" - " --known-hosts FILE OpenSSH known_hosts file\n" - " -V, --version Print version and exit\n" - " -h, --help Print this help and exit\n" - "\n" - "Commands mirror the TNT SSH exec interface: health, stats, users,\n" - "tail, dump, post, help, and exit.\n", - "用法: tntctl [options] host command [args...]\n" - "\n" - "选项:\n" - " -p, --port PORT SSH 端口 (默认: 2222)\n" - " -l, --login USER SSH 登录名,用作 exec 身份\n" - " --host-key-checking MODE\n" - " OpenSSH 主机密钥模式: yes, accept-new, no\n" - " --known-hosts FILE OpenSSH known_hosts 文件\n" - " -V, --version 输出版本并退出\n" - " -h, --help 输出此帮助并退出\n" - "\n" - "命令对应 TNT SSH exec 接口: health, stats, users,\n" - "tail, dump, post, help 和 exit.\n" - ), [TNTCTL_TEXT_INVALID_PORT] = I18N_STRING( "invalid port", "端口无效" ), @@ -79,7 +52,45 @@ static const i18n_string_t text_catalog[TNTCTL_TEXT_COUNT] = { ) }; typedef char text_catalog_must_cover_enum[ - sizeof(text_catalog) / sizeof(text_catalog[0]) == TNTCTL_TEXT_COUNT ? 1 : -1]; + sizeof(text_catalog) / sizeof(text_catalog[0]) == TNTCTL_TEXT_COUNT ? 1 : -1 +]; + +void tntctl_text_append_usage(char *buffer, size_t buf_size, size_t *pos, + ui_lang_t lang) { + static const i18n_string_t before_commands = I18N_STRING( + "Usage: tntctl [options] host command [args...]\n" + "\n" + "Options:\n" + " -p, --port PORT SSH port (default: 2222)\n" + " -l, --login USER SSH login name for exec identity\n" + " --host-key-checking MODE\n" + " OpenSSH host-key mode: yes, accept-new, no\n" + " --known-hosts FILE OpenSSH known_hosts file\n" + " -V, --version Print version and exit\n" + " -h, --help Print this help and exit\n" + "\n" + "Commands:\n" + " ", + "用法: tntctl [options] host command [args...]\n" + "\n" + "选项:\n" + " -p, --port PORT SSH 端口 (默认: 2222)\n" + " -l, --login USER SSH 登录名,用作 exec 身份\n" + " --host-key-checking MODE\n" + " OpenSSH 主机密钥模式: yes, accept-new, no\n" + " --known-hosts FILE OpenSSH known_hosts 文件\n" + " -V, --version 输出版本并退出\n" + " -h, --help 输出此帮助并退出\n" + "\n" + "命令:\n" + " " + ); + + buffer_appendf(buffer, buf_size, pos, "%s", + i18n_string(before_commands, lang)); + exec_catalog_append_command_list(buffer, buf_size, pos); + buffer_appendf(buffer, buf_size, pos, "\n"); +} const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id) { if (id < 0 || id >= TNTCTL_TEXT_COUNT) { diff --git a/tests/unit/Makefile b/tests/unit/Makefile index 25bc8b5..bf78445 100644 --- a/tests/unit/Makefile +++ b/tests/unit/Makefile @@ -66,7 +66,7 @@ test_manual_text: test_manual_text.c $(MANUAL_TEXT_SRC) $(COMMAND_CATALOG_SRC) $ test_cli_text: test_cli_text.c $(CLI_TEXT_SRC) $(COMMON_SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) -test_tntctl_text: test_tntctl_text.c $(TNTCTL_TEXT_SRC) +test_tntctl_text: test_tntctl_text.c $(TNTCTL_TEXT_SRC) $(EXEC_CATALOG_SRC) $(COMMON_SRC) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) test_ratelimit: test_ratelimit.c $(RATELIMIT_SRC) $(COMMON_SRC) diff --git a/tests/unit/test_exec_catalog.c b/tests/unit/test_exec_catalog.c index c52bbbc..1587da7 100644 --- a/tests/unit/test_exec_catalog.c +++ b/tests/unit/test_exec_catalog.c @@ -124,6 +124,16 @@ TEST(generates_localized_usage) { assert(strcmp(en, "dump: usage: dump [N] | dump -n N\n") == 0); } +TEST(generates_unique_command_list) { + char output[256] = {0}; + size_t pos = 0; + + exec_catalog_append_command_list(output, sizeof(output), &pos); + + assert(strcmp(output, + "help, health, users, stats, tail, dump, post, exit") == 0); +} + int main(void) { printf("Running exec catalog unit tests...\n\n"); @@ -131,6 +141,7 @@ int main(void) { RUN_TEST(matches_exec_commands_and_args); RUN_TEST(validates_argument_shapes); RUN_TEST(generates_localized_usage); + RUN_TEST(generates_unique_command_list); printf("\n✓ All %d tests passed!\n", tests_passed); return 0; diff --git a/tests/unit/test_tntctl_text.c b/tests/unit/test_tntctl_text.c index b303ad5..4816d7e 100644 --- a/tests/unit/test_tntctl_text.c +++ b/tests/unit/test_tntctl_text.c @@ -16,15 +16,22 @@ static int tests_passed = 0; TEST(usage_matches_language) { - const char *en = tntctl_text(UI_LANG_EN, TNTCTL_TEXT_USAGE); - const char *zh = tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_USAGE); + char en[2048] = {0}; + char zh[2048] = {0}; + size_t en_pos = 0; + size_t zh_pos = 0; + + tntctl_text_append_usage(en, sizeof(en), &en_pos, UI_LANG_EN); + tntctl_text_append_usage(zh, sizeof(zh), &zh_pos, UI_LANG_ZH); assert(strstr(en, "Usage: tntctl [options] host command [args...]") != NULL); assert(strstr(en, "--host-key-checking MODE") != NULL); - assert(strstr(en, "health, stats, users") != NULL); + assert(strstr(en, + "help, health, users, stats, tail, dump, post, exit") != NULL); assert(strstr(zh, "用法: tntctl [options] host command [args...]") != NULL); assert(strstr(zh, "OpenSSH 主机密钥模式") != NULL); - assert(strstr(zh, "health, stats, users") != NULL); + assert(strstr(zh, + "help, health, users, stats, tail, dump, post, exit") != NULL); } TEST(errors_match_language) {