Split tntctl local text catalog

This commit is contained in:
m1ngsama 2026-05-28 09:40:55 +08:00
parent 4175bd520f
commit fab8b315a5
11 changed files with 187 additions and 110 deletions

1
.gitignore vendored
View file

@ -24,4 +24,5 @@ tests/unit/test_help_text
tests/unit/test_manual_text
tests/unit/test_support_text
tests/unit/test_cli_text
tests/unit/test_tntctl_text
tests/unit/test_ratelimit

View file

@ -20,12 +20,12 @@ SRC_DIR = src
INC_DIR = include
OBJ_DIR = obj
SOURCES = $(filter-out $(SRC_DIR)/tntctl.c,$(wildcard $(SRC_DIR)/*.c))
SOURCES = $(filter-out $(SRC_DIR)/tntctl.c $(SRC_DIR)/tntctl_text.c,$(wildcard $(SRC_DIR)/*.c))
OBJECTS = $(SOURCES:$(SRC_DIR)/%.c=$(OBJ_DIR)/%.o)
DEPS = $(OBJECTS:.o=.d) $(CTL_OBJECTS:.o=.d)
TARGET = tnt
CTL_TARGET = tntctl
CTL_OBJECTS = $(OBJ_DIR)/tntctl.o $(OBJ_DIR)/exec_catalog.o $(OBJ_DIR)/common.o $(OBJ_DIR)/i18n.o
CTL_OBJECTS = $(OBJ_DIR)/tntctl.o $(OBJ_DIR)/tntctl_text.o $(OBJ_DIR)/exec_catalog.o $(OBJ_DIR)/common.o $(OBJ_DIR)/i18n.o
TARGETS = $(TARGET) $(CTL_TARGET)
PREFIX ?= /usr/local

View file

@ -324,6 +324,7 @@ TNT/
│ ├── exec_catalog.c # SSH exec command matching, usage, and argument shape
│ ├── exec.c # SSH exec command dispatch
│ ├── tntctl.c # local wrapper around the SSH exec interface
│ ├── tntctl_text.c # tntctl help and option text
│ ├── ssh_server.c # SSH server implementation
│ ├── bootstrap.c # SSH authentication and session bootstrap
│ ├── chat_room.c # chat room logic

View file

@ -3,6 +3,8 @@
## Unreleased
### Added
- Added a dedicated `tntctl_text` module with unit coverage for local
`tntctl` help and validation diagnostics.
- Documented the stable SSH exec interface contract, including exit statuses
and JSON field shapes for package tests, scripts, and future `tntctl` work.
- Documented `messages.log` v1 as the stable TNT 1.x persisted history format,

View file

@ -40,6 +40,7 @@ make check
```
main.c → entry point, signal handling
cli_text.c → startup CLI text
tntctl_text.c → tntctl local help and diagnostics
command_catalog.c → COMMAND-mode command metadata, usage, and argument shape
commands.c → COMMAND-mode command dispatch
exec_catalog.c → SSH exec command matching, usage, and argument shape

View file

@ -66,6 +66,7 @@ MAINTENANCE
STRUCTURE
src/main.c entry, signals
src/cli_text.c startup CLI text
src/tntctl_text.c tntctl local help and diagnostics
src/command_catalog.c command metadata, usage, argument shape
src/ssh_server.c SSH listener and server setup
src/bootstrap.c SSH auth/session bootstrap

28
include/tntctl_text.h Normal file
View file

@ -0,0 +1,28 @@
#ifndef TNTCTL_TEXT_H
#define TNTCTL_TEXT_H
#include "common.h"
typedef enum {
TNTCTL_TEXT_USAGE,
TNTCTL_TEXT_INVALID_PORT,
TNTCTL_TEXT_INVALID_LOGIN,
TNTCTL_TEXT_INVALID_HOST_KEY_MODE,
TNTCTL_TEXT_INVALID_KNOWN_HOSTS,
TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT,
TNTCTL_TEXT_MISSING_HOST,
TNTCTL_TEXT_INVALID_HOST,
TNTCTL_TEXT_LOGIN_HOST_CONFLICT,
TNTCTL_TEXT_UNKNOWN_COMMAND,
TNTCTL_TEXT_INVALID_REMOTE_COMMAND,
TNTCTL_TEXT_DESTINATION_TOO_LONG,
TNTCTL_TEXT_INVALID_DESTINATION,
TNTCTL_TEXT_OUT_OF_MEMORY,
TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG,
TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG,
TNTCTL_TEXT_COUNT
} tntctl_text_id_t;
const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id);
#endif /* TNTCTL_TEXT_H */

View file

@ -1,6 +1,7 @@
#include "common.h"
#include "exec_catalog.h"
#include "i18n.h"
#include "tntctl_text.h"
#include <ctype.h>
#include <errno.h>
@ -8,113 +9,6 @@
#include <sys/wait.h>
#include <unistd.h>
typedef enum {
TNTCTL_TEXT_USAGE,
TNTCTL_TEXT_INVALID_PORT,
TNTCTL_TEXT_INVALID_LOGIN,
TNTCTL_TEXT_INVALID_HOST_KEY_MODE,
TNTCTL_TEXT_INVALID_KNOWN_HOSTS,
TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT,
TNTCTL_TEXT_MISSING_HOST,
TNTCTL_TEXT_INVALID_HOST,
TNTCTL_TEXT_LOGIN_HOST_CONFLICT,
TNTCTL_TEXT_UNKNOWN_COMMAND,
TNTCTL_TEXT_INVALID_REMOTE_COMMAND,
TNTCTL_TEXT_DESTINATION_TOO_LONG,
TNTCTL_TEXT_INVALID_DESTINATION,
TNTCTL_TEXT_OUT_OF_MEMORY,
TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG,
TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG,
TNTCTL_TEXT_COUNT
} tntctl_text_id_t;
static const i18n_string_t tntctl_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", "端口无效"
),
[TNTCTL_TEXT_INVALID_LOGIN] = I18N_STRING(
"invalid login", "登录名无效"
),
[TNTCTL_TEXT_INVALID_HOST_KEY_MODE] = I18N_STRING(
"invalid host-key checking mode", "主机密钥检查模式无效"
),
[TNTCTL_TEXT_INVALID_KNOWN_HOSTS] = I18N_STRING(
"invalid known_hosts path", "known_hosts 路径无效"
),
[TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT] = I18N_STRING(
"unknown option: %s", "未知选项: %s"
),
[TNTCTL_TEXT_MISSING_HOST] = I18N_STRING(
"missing host", "缺少 host"
),
[TNTCTL_TEXT_INVALID_HOST] = I18N_STRING(
"invalid host", "host 无效"
),
[TNTCTL_TEXT_LOGIN_HOST_CONFLICT] = I18N_STRING(
"use either --login or user@host, not both",
"只能使用 --login 或 user@host 之一"
),
[TNTCTL_TEXT_UNKNOWN_COMMAND] = I18N_STRING(
"unknown or missing command", "未知命令或缺少命令"
),
[TNTCTL_TEXT_INVALID_REMOTE_COMMAND] = I18N_STRING(
"invalid or too-long command", "命令无效或过长"
),
[TNTCTL_TEXT_DESTINATION_TOO_LONG] = I18N_STRING(
"destination too long", "目标地址过长"
),
[TNTCTL_TEXT_INVALID_DESTINATION] = I18N_STRING(
"invalid destination", "目标地址无效"
),
[TNTCTL_TEXT_OUT_OF_MEMORY] = I18N_STRING(
"out of memory", "内存不足"
),
[TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG] = I18N_STRING(
"host-key option too long", "主机密钥选项过长"
),
[TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG] = I18N_STRING(
"known_hosts option too long", "known_hosts 选项过长"
)
};
typedef char tntctl_text_catalog_must_cover_enum[
sizeof(tntctl_text_catalog) / sizeof(tntctl_text_catalog[0]) ==
TNTCTL_TEXT_COUNT ? 1 : -1];
static const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id) {
if (id < 0 || id >= TNTCTL_TEXT_COUNT) {
return "";
}
return i18n_string(tntctl_text_catalog[id], lang);
}
static void print_usage(FILE *stream, ui_lang_t lang) {
fputs(tntctl_text(lang, TNTCTL_TEXT_USAGE), stream);
}

89
src/tntctl_text.c Normal file
View file

@ -0,0 +1,89 @@
#include "tntctl_text.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", "端口无效"
),
[TNTCTL_TEXT_INVALID_LOGIN] = I18N_STRING(
"invalid login", "登录名无效"
),
[TNTCTL_TEXT_INVALID_HOST_KEY_MODE] = I18N_STRING(
"invalid host-key checking mode", "主机密钥检查模式无效"
),
[TNTCTL_TEXT_INVALID_KNOWN_HOSTS] = I18N_STRING(
"invalid known_hosts path", "known_hosts 路径无效"
),
[TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT] = I18N_STRING(
"unknown option: %s", "未知选项: %s"
),
[TNTCTL_TEXT_MISSING_HOST] = I18N_STRING(
"missing host", "缺少 host"
),
[TNTCTL_TEXT_INVALID_HOST] = I18N_STRING(
"invalid host", "host 无效"
),
[TNTCTL_TEXT_LOGIN_HOST_CONFLICT] = I18N_STRING(
"use either --login or user@host, not both",
"只能使用 --login 或 user@host 之一"
),
[TNTCTL_TEXT_UNKNOWN_COMMAND] = I18N_STRING(
"unknown or missing command", "未知命令或缺少命令"
),
[TNTCTL_TEXT_INVALID_REMOTE_COMMAND] = I18N_STRING(
"invalid or too-long command", "命令无效或过长"
),
[TNTCTL_TEXT_DESTINATION_TOO_LONG] = I18N_STRING(
"destination too long", "目标地址过长"
),
[TNTCTL_TEXT_INVALID_DESTINATION] = I18N_STRING(
"invalid destination", "目标地址无效"
),
[TNTCTL_TEXT_OUT_OF_MEMORY] = I18N_STRING(
"out of memory", "内存不足"
),
[TNTCTL_TEXT_HOST_KEY_OPTION_TOO_LONG] = I18N_STRING(
"host-key option too long", "主机密钥选项过长"
),
[TNTCTL_TEXT_KNOWN_HOSTS_OPTION_TOO_LONG] = I18N_STRING(
"known_hosts option too long", "known_hosts 选项过长"
)
};
typedef char text_catalog_must_cover_enum[
sizeof(text_catalog) / sizeof(text_catalog[0]) == TNTCTL_TEXT_COUNT ? 1 : -1];
const char *tntctl_text(ui_lang_t lang, tntctl_text_id_t id) {
if (id < 0 || id >= TNTCTL_TEXT_COUNT) {
return "";
}
return i18n_string(text_catalog[id], lang);
}

View file

@ -16,6 +16,7 @@ MESSAGE_LOG_SRC = ../../src/message_log.c
COMMON_SRC = ../../src/common.c
COMMAND_CATALOG_SRC = ../../src/command_catalog.c
CLI_TEXT_SRC = ../../src/cli_text.c
TNTCTL_TEXT_SRC = ../../src/tntctl_text.c
CHAT_ROOM_SRC = ../../src/chat_room.c
HISTORY_VIEW_SRC = ../../src/history_view.c
I18N_SRC = ../../src/i18n.c
@ -26,7 +27,7 @@ HELP_TEXT_SRC = ../../src/help_text.c
MANUAL_TEXT_SRC = ../../src/manual_text.c
RATELIMIT_SRC = ../../src/ratelimit.c
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_command_catalog test_exec_catalog test_help_text test_manual_text test_cli_text test_ratelimit
TESTS = test_utf8 test_message test_chat_room test_history_view test_i18n test_system_message test_command_catalog test_exec_catalog test_help_text test_manual_text test_cli_text test_tntctl_text test_ratelimit
.PHONY: all clean run
@ -65,6 +66,9 @@ 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)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
test_ratelimit: test_ratelimit.c $(RATELIMIT_SRC) $(COMMON_SRC)
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
@ -102,6 +106,9 @@ run: all
@echo "=== Running CLI Text Tests ==="
./test_cli_text
@echo ""
@echo "=== Running tntctl Text Tests ==="
./test_tntctl_text
@echo ""
@echo "=== Running Rate Limit Tests ==="
./test_ratelimit

View file

@ -0,0 +1,53 @@
/* Unit tests for tntctl local help and diagnostic text */
#include "../../include/tntctl_text.h"
#include <assert.h>
#include <stdio.h>
#include <string.h>
#define TEST(name) static void test_##name()
#define RUN_TEST(name) do { \
printf("Running %s... ", #name); \
test_##name(); \
printf("\n"); \
tests_passed++; \
} while(0)
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);
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(zh, "用法: tntctl [options] host command [args...]") != NULL);
assert(strstr(zh, "OpenSSH 主机密钥模式") != NULL);
assert(strstr(zh, "health, stats, users") != NULL);
}
TEST(errors_match_language) {
assert(strcmp(tntctl_text(UI_LANG_EN, TNTCTL_TEXT_INVALID_PORT),
"invalid port") == 0);
assert(strcmp(tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_INVALID_PORT),
"端口无效") == 0);
assert(strcmp(tntctl_text(UI_LANG_EN, TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT),
"unknown option: %s") == 0);
assert(strcmp(tntctl_text(UI_LANG_ZH, TNTCTL_TEXT_UNKNOWN_OPTION_FORMAT),
"未知选项: %s") == 0);
assert(strcmp(tntctl_text((ui_lang_t)99, TNTCTL_TEXT_INVALID_PORT),
"invalid port") == 0);
assert(strcmp(tntctl_text(UI_LANG_EN,
(tntctl_text_id_t)TNTCTL_TEXT_COUNT), "") == 0);
}
int main(void) {
printf("Running tntctl text unit tests...\n\n");
RUN_TEST(usage_matches_language);
RUN_TEST(errors_match_language);
printf("\n✓ All %d tests passed!\n", tests_passed);
return 0;
}