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 │ ├── cli_text.c # startup CLI help and option text
│ ├── command_catalog.c # command metadata │ ├── command_catalog.c # command metadata
│ ├── commands.c # COMMAND-mode command dispatch │ ├── 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 │ ├── exec.c # SSH exec command dispatch
│ ├── ssh_server.c # SSH server implementation │ ├── ssh_server.c # SSH server implementation
│ ├── bootstrap.c # SSH authentication and session bootstrap │ ├── bootstrap.c # SSH authentication and session bootstrap

View file

@ -17,6 +17,8 @@
- Refreshed contributor and development guidance so new commands are added - Refreshed contributor and development guidance so new commands are added
through `command_catalog`, `exec_catalog`, and `i18n_text` instead of stale through `command_catalog`, `exec_catalog`, and `i18n_text` instead of stale
`ssh_server.c` / inline-`strcmp` instructions. `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 - Renamed the internal language state from help-oriented names to
UI-language names (`ui_lang_t`, `client->ui_lang`, and UI-language names (`ui_lang_t`, `client->ui_lang`, and
`i18n_*_ui_lang`) so future i18n work has a correctly named seam. `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 cli_text.c → startup CLI text
command_catalog.c → COMMAND-mode command metadata command_catalog.c → COMMAND-mode command metadata
commands.c → COMMAND-mode command dispatch 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 exec.c → SSH exec command dispatch
ssh_server.c → SSH listener setup ssh_server.c → SSH listener setup
bootstrap.c → SSH authentication/session bootstrap bootstrap.c → SSH authentication/session bootstrap

View file

@ -74,7 +74,7 @@ src/
├── input.c - Interactive session loop and key handling ├── input.c - Interactive session loop and key handling
├── commands.c - COMMAND-mode command dispatch ├── commands.c - COMMAND-mode command dispatch
├── command_catalog.c - COMMAND-mode names, aliases, and help summaries ├── 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 ├── exec.c - SSH exec command dispatch
├── chat_room.c - Chat room logic and message broadcasting ├── chat_room.c - Chat room logic and message broadcasting
├── message.c - Message persistence (RFC3339 format) ├── message.c - Message persistence (RFC3339 format)

View file

@ -51,7 +51,7 @@ STRUCTURE
src/bootstrap.c SSH auth/session bootstrap src/bootstrap.c SSH auth/session bootstrap
src/chat_room.c broadcast and room state src/chat_room.c broadcast and room state
src/commands.c COMMAND-mode command dispatch 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/exec.c SSH exec command dispatch
src/message.c persistence, search src/message.c persistence, search
src/history_view.c message viewport / scroll state src/history_view.c message viewport / scroll state

View file

@ -3,6 +3,18 @@
#include "common.h" #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, void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos,
ui_lang_t lang); 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) { int exec_dispatch(client_t *client) {
char command_copy[MAX_EXEC_COMMAND_LEN]; char command_copy[MAX_EXEC_COMMAND_LEN];
char *cmd; tnt_exec_command_id_t command_id;
char *args; const char *args = NULL;
strncpy(command_copy, client->exec_command, sizeof(command_copy) - 1); strncpy(command_copy, client->exec_command, sizeof(command_copy) - 1);
command_copy[sizeof(command_copy) - 1] = '\0'; command_copy[sizeof(command_copy) - 1] = '\0';
trim_ascii_whitespace(command_copy); trim_ascii_whitespace(command_copy);
cmd = command_copy; if (command_copy[0] == '\0') {
if (*cmd == '\0') {
return exec_command_help(client); return exec_command_help(client);
} }
args = cmd; if (exec_catalog_match(command_copy, &command_id, &args)) {
while (*args && !isspace((unsigned char)*args)) { switch (command_id) {
args++; case TNT_EXEC_COMMAND_HELP:
} return exec_command_help(client);
if (*args) { case TNT_EXEC_COMMAND_HEALTH:
*args++ = '\0'; return exec_command_health(client);
while (*args && isspace((unsigned char)*args)) { case TNT_EXEC_COMMAND_USERS:
args++; 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) { for (char *p = command_copy; *p; p++) {
return exec_command_help(client); if (isspace((unsigned char)*p)) {
} *p = '\0';
if (strcmp(cmd, "health") == 0) { break;
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;
} }
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, client_printf(client,
i18n_text(client->ui_lang, i18n_text(client->ui_lang,
I18N_EXEC_UNKNOWN_COMMAND_FORMAT), I18N_EXEC_UNKNOWN_COMMAND_FORMAT),
cmd); command_copy);
return 64; return 64;
} }

View file

@ -1,23 +1,89 @@
#include "exec_catalog.h" #include "exec_catalog.h"
typedef struct { typedef struct {
tnt_exec_command_id_t id;
const char *name;
const char *alias;
const char *usage; const char *usage;
const char *summary_en; const char *summary_en;
const char *summary_zh; const char *summary_zh;
} exec_catalog_entry_t; } exec_catalog_entry_t;
static const exec_catalog_entry_t entries[] = { static const exec_catalog_entry_t entries[] = {
{"help", "Show this help", "显示此帮助"}, {TNT_EXEC_COMMAND_HELP, "help", "--help",
{"health", "Print service health", "输出服务健康状态"}, "help", "Show this help", "显示此帮助"},
{"users [--json]", "List online users", "列出在线用户"}, {TNT_EXEC_COMMAND_HEALTH, "health", NULL,
{"stats [--json]", "Print room statistics", "输出房间统计"}, "health", "Print service health", "输出服务健康状态"},
{"tail [N]", "Print recent messages", "输出最近消息"}, {TNT_EXEC_COMMAND_USERS, "users", NULL,
{"tail -n N", "Print recent messages", "输出最近消息"}, "users [--json]", "List online users", "列出在线用户"},
{"post MESSAGE", "Post a message non-interactively", "非交互发送消息"}, {TNT_EXEC_COMMAND_STATS, "stats", NULL,
{"post \"/me act\"", "Post an action message", "发送动作消息"}, "stats [--json]", "Print room statistics", "输出房间统计"},
{"exit", "Exit successfully", "成功退出"} {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, void exec_catalog_append_help(char *buffer, size_t buf_size, size_t *pos,
ui_lang_t lang) { ui_lang_t lang) {
if (lang == UI_LANG_ZH) { if (lang == UI_LANG_ZH) {

View file

@ -39,10 +39,43 @@ TEST(generates_localized_exec_help) {
assert_ascii_angle_placeholders(zh); 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) { int main(void) {
printf("Running exec catalog unit tests...\n\n"); printf("Running exec catalog unit tests...\n\n");
RUN_TEST(generates_localized_exec_help); RUN_TEST(generates_localized_exec_help);
RUN_TEST(matches_exec_commands_and_args);
printf("\n✓ All %d tests passed!\n", tests_passed); printf("\n✓ All %d tests passed!\n", tests_passed);
return 0; return 0;