Generate tntctl command list from exec catalog

This commit is contained in:
m1ngsama 2026-05-28 10:36:22 +08:00
parent f2be702a15
commit 8affea2508
10 changed files with 99 additions and 37 deletions

View file

@ -37,6 +37,8 @@
and server survival stay responsive. and server survival stay responsive.
### Changed ### 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 - 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 added script coverage to keep active help surfaces free of removed support
commands. commands.

View file

@ -11,7 +11,8 @@ typedef enum {
TNT_EXEC_COMMAND_TAIL, TNT_EXEC_COMMAND_TAIL,
TNT_EXEC_COMMAND_DUMP, TNT_EXEC_COMMAND_DUMP,
TNT_EXEC_COMMAND_POST, TNT_EXEC_COMMAND_POST,
TNT_EXEC_COMMAND_EXIT TNT_EXEC_COMMAND_EXIT,
TNT_EXEC_COMMAND_COUNT
} tnt_exec_command_id_t; } tnt_exec_command_id_t;
bool exec_catalog_match(const char *line, tnt_exec_command_id_t *id, 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); 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, void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos,
ui_lang_t lang); 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, void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
tnt_exec_command_id_t id, ui_lang_t lang); tnt_exec_command_id_t id, ui_lang_t lang);

View file

@ -4,7 +4,6 @@
#include "common.h" #include "common.h"
typedef enum { typedef enum {
TNTCTL_TEXT_USAGE,
TNTCTL_TEXT_INVALID_PORT, TNTCTL_TEXT_INVALID_PORT,
TNTCTL_TEXT_INVALID_LOGIN, TNTCTL_TEXT_INVALID_LOGIN,
TNTCTL_TEXT_INVALID_HOST_KEY_MODE, TNTCTL_TEXT_INVALID_HOST_KEY_MODE,
@ -23,6 +22,8 @@ typedef enum {
TNTCTL_TEXT_COUNT TNTCTL_TEXT_COUNT
} tntctl_text_id_t; } 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); const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id);
#endif /* TNTCTL_TEXT_H */ #endif /* TNTCTL_TEXT_H */

View file

@ -517,6 +517,8 @@ int exec_dispatch(client_t *client) {
return exec_command_post(client, args); return exec_command_post(client, args);
case TNT_EXEC_COMMAND_EXIT: case TNT_EXEC_COMMAND_EXIT:
return TNT_EXIT_OK; return TNT_EXIT_OK;
case TNT_EXEC_COMMAND_COUNT:
break;
} }
} }

View file

@ -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, void exec_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
tnt_exec_command_id_t id, ui_lang_t lang) { tnt_exec_command_id_t id, ui_lang_t lang) {
const exec_catalog_entry_t *entry = entry_for_id(id); const exec_catalog_entry_t *entry = entry_for_id(id);

View file

@ -10,7 +10,12 @@
#include <unistd.h> #include <unistd.h>
static void print_usage(FILE *stream, ui_lang_t lang) { 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) { static void print_error(ui_lang_t lang, tntctl_text_id_t id) {

View file

@ -1,36 +1,9 @@
#include "tntctl_text.h" #include "tntctl_text.h"
#include "exec_catalog.h"
#include "i18n.h" #include "i18n.h"
static const i18n_string_t text_catalog[TNTCTL_TEXT_COUNT] = { 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( [TNTCTL_TEXT_INVALID_PORT] = I18N_STRING(
"invalid port", "端口无效" "invalid port", "端口无效"
), ),
@ -79,7 +52,45 @@ static const i18n_string_t text_catalog[TNTCTL_TEXT_COUNT] = {
) )
}; };
typedef char text_catalog_must_cover_enum[ 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) { const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id) {
if (id < 0 || id >= TNTCTL_TEXT_COUNT) { if (id < 0 || id >= TNTCTL_TEXT_COUNT) {

View file

@ -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) test_cli_text: test_cli_text.c $(CLI_TEXT_SRC) $(COMMON_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS) $(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) $(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_ratelimit: test_ratelimit.c $(RATELIMIT_SRC) $(COMMON_SRC) test_ratelimit: test_ratelimit.c $(RATELIMIT_SRC) $(COMMON_SRC)

View file

@ -124,6 +124,16 @@ TEST(generates_localized_usage) {
assert(strcmp(en, "dump: usage: dump [N] | dump -n N\n") == 0); 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) { int main(void) {
printf("Running exec catalog unit tests...\n\n"); printf("Running exec catalog unit tests...\n\n");
@ -131,6 +141,7 @@ int main(void) {
RUN_TEST(matches_exec_commands_and_args); RUN_TEST(matches_exec_commands_and_args);
RUN_TEST(validates_argument_shapes); RUN_TEST(validates_argument_shapes);
RUN_TEST(generates_localized_usage); RUN_TEST(generates_localized_usage);
RUN_TEST(generates_unique_command_list);
printf("\n✓ All %d tests passed!\n", tests_passed); printf("\n✓ All %d tests passed!\n", tests_passed);
return 0; return 0;

View file

@ -16,15 +16,22 @@
static int tests_passed = 0; static int tests_passed = 0;
TEST(usage_matches_language) { TEST(usage_matches_language) {
const char *en = tntctl_text(UI_LANG_EN, TNTCTL_TEXT_USAGE); char en[2048] = {0};
const char *zh = tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_USAGE); 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, "Usage: tntctl [options] host command [args...]") != NULL);
assert(strstr(en, "--host-key-checking MODE") != 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, "用法: tntctl [options] host command [args...]") != NULL);
assert(strstr(zh, "OpenSSH 主机密钥模式") != 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) { TEST(errors_match_language) {