exec: centralize command matching in catalog

This commit is contained in:
m1ngsama 2026-05-24 13:12:47 +08:00
parent da0170d2c0
commit bfaafb4b35
9 changed files with 162 additions and 60 deletions

View file

@ -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

View file

@ -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.

View file

@ -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

View file

@ -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)

View file

@ -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

View file

@ -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);

View file

@ -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;
}

View file

@ -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) {

View file

@ -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;