mirror of
https://oauth2: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@github.com/m1ngsama/TNT.git
synced 2026-06-26 09:14:38 +08:00
commands: centralize usage validation in catalog
This commit is contained in:
parent
0aaba8e1f9
commit
f2942e9c9e
12 changed files with 176 additions and 79 deletions
|
|
@ -250,9 +250,9 @@ TNT/
|
||||||
├── src/ # source code
|
├── src/ # source code
|
||||||
│ ├── main.c # entry point
|
│ ├── main.c # entry point
|
||||||
│ ├── 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, usage, and argument shape
|
||||||
│ ├── commands.c # COMMAND-mode command dispatch
|
│ ├── commands.c # COMMAND-mode command dispatch
|
||||||
│ ├── exec_catalog.c # SSH exec command matching and metadata
|
│ ├── exec_catalog.c # SSH exec command matching, usage, and argument shape
|
||||||
│ ├── 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
|
||||||
|
|
|
||||||
|
|
@ -23,6 +23,9 @@
|
||||||
public documentation does not imply a specific production host.
|
public documentation does not imply a specific production host.
|
||||||
- Moved SSH exec usage text and argument-shape checks into `exec_catalog`, so
|
- Moved SSH exec usage text and argument-shape checks into `exec_catalog`, so
|
||||||
`src/exec.c` no longer duplicates `--json` and required-message validation.
|
`src/exec.c` no longer duplicates `--json` and required-message validation.
|
||||||
|
- Moved interactive command usage text and first-pass argument-shape checks
|
||||||
|
into `command_catalog`, so known commands with bad arguments now show usage
|
||||||
|
instead of unknown-command guidance.
|
||||||
- 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.
|
||||||
|
|
|
||||||
|
|
@ -37,9 +37,9 @@ make check
|
||||||
```
|
```
|
||||||
main.c → entry point, signal handling
|
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, usage, and argument shape
|
||||||
commands.c → COMMAND-mode command dispatch
|
commands.c → COMMAND-mode command dispatch
|
||||||
exec_catalog.c → SSH exec command matching and help metadata
|
exec_catalog.c → SSH exec command matching, usage, and argument shape
|
||||||
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
|
||||||
|
|
@ -83,7 +83,8 @@ utf8.c → UTF-8 string handling
|
||||||
|
|
||||||
## Adding Features
|
## Adding Features
|
||||||
|
|
||||||
1. Add interactive command metadata in `src/command_catalog.c`.
|
1. Add interactive command metadata, usage text, and argument shape in
|
||||||
|
`src/command_catalog.c`.
|
||||||
2. Add interactive command behavior in `src/commands.c`.
|
2. Add interactive command behavior in `src/commands.c`.
|
||||||
3. Add SSH exec metadata in `src/exec_catalog.c` and dispatch in `src/exec.c`
|
3. Add SSH exec metadata in `src/exec_catalog.c` and dispatch in `src/exec.c`
|
||||||
only when the feature should be scriptable.
|
only when the feature should be scriptable.
|
||||||
|
|
|
||||||
|
|
@ -46,12 +46,12 @@ INSERT MODE
|
||||||
STRUCTURE
|
STRUCTURE
|
||||||
src/main.c entry, signals
|
src/main.c entry, signals
|
||||||
src/cli_text.c startup CLI text
|
src/cli_text.c startup CLI text
|
||||||
src/command_catalog.c command metadata
|
src/command_catalog.c command metadata, usage, argument shape
|
||||||
src/ssh_server.c SSH listener and server setup
|
src/ssh_server.c SSH listener and server setup
|
||||||
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 matching and metadata
|
src/exec_catalog.c SSH exec command matching, usage, argument shape
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -22,16 +22,18 @@ typedef struct {
|
||||||
tnt_command_id_t id;
|
tnt_command_id_t id;
|
||||||
const char *canonical;
|
const char *canonical;
|
||||||
const char *names[4];
|
const char *names[4];
|
||||||
bool accepts_args;
|
|
||||||
} tnt_command_spec_t;
|
} tnt_command_spec_t;
|
||||||
|
|
||||||
const tnt_command_spec_t *command_catalog_get(tnt_command_id_t id);
|
const tnt_command_spec_t *command_catalog_get(tnt_command_id_t id);
|
||||||
bool command_catalog_match(const char *line, tnt_command_id_t *id,
|
bool command_catalog_match(const char *line, tnt_command_id_t *id,
|
||||||
const char **args);
|
const char **args);
|
||||||
|
bool command_catalog_args_valid(tnt_command_id_t id, const char *args);
|
||||||
const char *command_catalog_suggest(const char *name);
|
const char *command_catalog_suggest(const char *name);
|
||||||
void command_catalog_append_full(char *buffer, size_t buf_size, size_t *pos,
|
void command_catalog_append_full(char *buffer, size_t buf_size, size_t *pos,
|
||||||
ui_lang_t lang);
|
ui_lang_t lang);
|
||||||
void command_catalog_append_manual(char *buffer, size_t buf_size, size_t *pos,
|
void command_catalog_append_manual(char *buffer, size_t buf_size, size_t *pos,
|
||||||
ui_lang_t lang);
|
ui_lang_t lang);
|
||||||
|
void command_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
||||||
|
tnt_command_id_t id, ui_lang_t lang);
|
||||||
|
|
||||||
#endif /* COMMAND_CATALOG_H */
|
#endif /* COMMAND_CATALOG_H */
|
||||||
|
|
|
||||||
|
|
@ -29,19 +29,15 @@ typedef enum {
|
||||||
I18N_SYSTEM_LEAVE_FORMAT,
|
I18N_SYSTEM_LEAVE_FORMAT,
|
||||||
I18N_SYSTEM_NICK_FORMAT,
|
I18N_SYSTEM_NICK_FORMAT,
|
||||||
I18N_USERS_TITLE,
|
I18N_USERS_TITLE,
|
||||||
I18N_MSG_USAGE,
|
|
||||||
I18N_MSG_SENT_FORMAT,
|
I18N_MSG_SENT_FORMAT,
|
||||||
I18N_MSG_USER_NOT_FOUND_FORMAT,
|
I18N_MSG_USER_NOT_FOUND_FORMAT,
|
||||||
I18N_INBOX_TITLE,
|
I18N_INBOX_TITLE,
|
||||||
I18N_INBOX_EMPTY,
|
I18N_INBOX_EMPTY,
|
||||||
I18N_NICK_USAGE,
|
|
||||||
I18N_NICK_INVALID,
|
I18N_NICK_INVALID,
|
||||||
I18N_NICK_TAKEN_FORMAT,
|
I18N_NICK_TAKEN_FORMAT,
|
||||||
I18N_NICK_UNCHANGED,
|
I18N_NICK_UNCHANGED,
|
||||||
I18N_NICK_CHANGED_FORMAT,
|
I18N_NICK_CHANGED_FORMAT,
|
||||||
I18N_LAST_USAGE,
|
|
||||||
I18N_LAST_HEADER_FORMAT,
|
I18N_LAST_HEADER_FORMAT,
|
||||||
I18N_SEARCH_USAGE,
|
|
||||||
I18N_SEARCH_HEADER_FORMAT,
|
I18N_SEARCH_HEADER_FORMAT,
|
||||||
I18N_MUTE_JOINS_FORMAT,
|
I18N_MUTE_JOINS_FORMAT,
|
||||||
I18N_MUTE_JOINS_MUTED,
|
I18N_MUTE_JOINS_MUTED,
|
||||||
|
|
|
||||||
|
|
@ -10,76 +10,106 @@ typedef struct {
|
||||||
const char *summary_zh;
|
const char *summary_zh;
|
||||||
const char *manual_usage_en;
|
const char *manual_usage_en;
|
||||||
const char *manual_usage_zh;
|
const char *manual_usage_zh;
|
||||||
|
const char *error_usage_en;
|
||||||
|
const char *error_usage_zh;
|
||||||
int manual_group;
|
int manual_group;
|
||||||
|
bool no_args;
|
||||||
|
bool requires_args;
|
||||||
} command_catalog_entry_t;
|
} command_catalog_entry_t;
|
||||||
|
|
||||||
static const command_catalog_entry_t entries[] = {
|
static const command_catalog_entry_t entries[] = {
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_USERS, "users", {"users", "list", "who", NULL}, false},
|
{TNT_COMMAND_USERS, "users", {"users", "list", "who", NULL}},
|
||||||
":users, :list, :who", ":users, :list, :who",
|
":users, :list, :who", ":users, :list, :who",
|
||||||
"Show online users", "显示在线用户",
|
"Show online users", "显示在线用户",
|
||||||
":users", ":users", 1
|
":users", ":users",
|
||||||
|
"Usage: users\n", "用法: users\n",
|
||||||
|
1, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_MSG, "msg", {"msg", "w", NULL}, true},
|
{TNT_COMMAND_MSG, "msg", {"msg", "w", NULL}},
|
||||||
":msg <user> <message>, :w <user> <message>",
|
":msg <user> <message>, :w <user> <message>",
|
||||||
":msg <user> <message>, :w <user> <message>",
|
":msg <user> <message>, :w <user> <message>",
|
||||||
"Send private message", "发送私信",
|
"Send private message", "发送私信",
|
||||||
":msg <user> <message>", ":msg <user> <message>", 2
|
":msg <user> <message>", ":msg <user> <message>",
|
||||||
|
"Usage: msg <user> <message>\n"
|
||||||
|
" w <user> <message>\n",
|
||||||
|
"用法: msg <user> <message>\n"
|
||||||
|
" w <user> <message>\n",
|
||||||
|
2, false, true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_INBOX, "inbox", {"inbox", NULL}, false},
|
{TNT_COMMAND_INBOX, "inbox", {"inbox", NULL}},
|
||||||
":inbox", ":inbox",
|
":inbox", ":inbox",
|
||||||
"Show private messages", "查看私信",
|
"Show private messages", "查看私信",
|
||||||
":inbox", ":inbox", 2
|
":inbox", ":inbox",
|
||||||
|
"Usage: inbox\n", "用法: inbox\n",
|
||||||
|
2, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_NICK, "nick", {"nick", "name", NULL}, true},
|
{TNT_COMMAND_NICK, "nick", {"nick", "name", NULL}},
|
||||||
":nick <name>, :name <name>", ":nick <name>, :name <name>",
|
":nick <name>, :name <name>", ":nick <name>, :name <name>",
|
||||||
"Change nickname", "更改昵称",
|
"Change nickname", "更改昵称",
|
||||||
":nick <name>", ":nick <name>", 2
|
":nick <name>", ":nick <name>",
|
||||||
|
"Usage: nick <name>\n", "用法: nick <name>\n",
|
||||||
|
2, false, true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_LAST, "last", {"last", NULL}, true},
|
{TNT_COMMAND_LAST, "last", {"last", NULL}},
|
||||||
":last [N]", ":last [N]",
|
":last [N]", ":last [N]",
|
||||||
"Show last N messages (max 50)", "显示最后 N 条消息(最多50)",
|
"Show last N messages (max 50)", "显示最后 N 条消息(最多50)",
|
||||||
":last [N]", ":last [N]", 1
|
":last [N]", ":last [N]",
|
||||||
|
"Usage: last [N] (N: 1-50, default 10)\n",
|
||||||
|
"用法: last [N] (N: 1-50,默认 10)\n",
|
||||||
|
1, false, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_SEARCH, "search", {"search", NULL}, true},
|
{TNT_COMMAND_SEARCH, "search", {"search", NULL}},
|
||||||
":search <keyword>", ":search <keyword>",
|
":search <keyword>", ":search <keyword>",
|
||||||
"Search message history", "搜索消息历史",
|
"Search message history", "搜索消息历史",
|
||||||
":search <keyword>", ":search <keyword>", 1
|
":search <keyword>", ":search <keyword>",
|
||||||
|
"Usage: search <keyword>\n", "用法: search <keyword>\n",
|
||||||
|
1, false, true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_MUTE_JOINS, "mute-joins", {"mute-joins", "mute", NULL}, false},
|
{TNT_COMMAND_MUTE_JOINS, "mute-joins", {"mute-joins", "mute", NULL}},
|
||||||
":mute-joins, :mute", ":mute-joins, :mute",
|
":mute-joins, :mute", ":mute-joins, :mute",
|
||||||
"Toggle join/leave notices", "切换加入/离开提示",
|
"Toggle join/leave notices", "切换加入/离开提示",
|
||||||
":mute-joins", ":mute-joins", 3
|
":mute-joins", ":mute-joins",
|
||||||
|
"Usage: mute-joins\n", "用法: mute-joins\n",
|
||||||
|
3, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_HELP, "help", {"help", NULL}, false},
|
{TNT_COMMAND_HELP, "help", {"help", NULL}},
|
||||||
":help", ":help",
|
":help", ":help",
|
||||||
"Show concise manual", "显示简明手册",
|
"Show concise manual", "显示简明手册",
|
||||||
NULL, NULL, 0
|
NULL, NULL,
|
||||||
|
"Usage: help\n", "用法: help\n",
|
||||||
|
0, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_LANG, "lang", {"lang", "language", NULL}, true},
|
{TNT_COMMAND_LANG, "lang", {"lang", "language", NULL}},
|
||||||
":lang <en|zh>", ":lang <en|zh>",
|
":lang <en|zh>", ":lang <en|zh>",
|
||||||
"Switch UI language", "切换界面语言",
|
"Switch UI language", "切换界面语言",
|
||||||
NULL, NULL, 0
|
NULL, NULL,
|
||||||
|
"Usage: lang <en|zh>\n", "用法: lang <en|zh>\n",
|
||||||
|
0, false, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_CLEAR, "clear", {"clear", "cls", NULL}, false},
|
{TNT_COMMAND_CLEAR, "clear", {"clear", "cls", NULL}},
|
||||||
":clear, :cls", ":clear, :cls",
|
":clear, :cls", ":clear, :cls",
|
||||||
"Clear command output", "清空命令输出",
|
"Clear command output", "清空命令输出",
|
||||||
":clear", ":clear", 3
|
":clear", ":clear",
|
||||||
|
"Usage: clear\n", "用法: clear\n",
|
||||||
|
3, true, false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
{TNT_COMMAND_QUIT, "q", {"q", "quit", "exit", NULL}, false},
|
{TNT_COMMAND_QUIT, "q", {"q", "quit", "exit", NULL}},
|
||||||
":q, :quit, :exit", ":q, :quit, :exit",
|
":q, :quit, :exit", ":q, :quit, :exit",
|
||||||
"Disconnect", "断开连接",
|
"Disconnect", "断开连接",
|
||||||
":q", ":q", 3
|
":q", ":q",
|
||||||
|
"Usage: q\n", "用法: q\n",
|
||||||
|
3, true, false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -177,10 +207,6 @@ bool command_catalog_match(const char *line, tnt_command_id_t *id,
|
||||||
if (!name_matches(line, spec->names[n], &candidate_args)) {
|
if (!name_matches(line, spec->names[n], &candidate_args)) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (candidate_args && candidate_args[0] != '\0' &&
|
|
||||||
!spec->accepts_args) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (id) {
|
if (id) {
|
||||||
*id = spec->id;
|
*id = spec->id;
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +220,22 @@ bool command_catalog_match(const char *line, tnt_command_id_t *id,
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool command_catalog_args_valid(tnt_command_id_t id, const char *args) {
|
||||||
|
const command_catalog_entry_t *entry = entry_for_id(id);
|
||||||
|
args = skip_spaces(args);
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (entry->no_args) {
|
||||||
|
return !args || args[0] == '\0';
|
||||||
|
}
|
||||||
|
if (entry->requires_args) {
|
||||||
|
return args && args[0] != '\0';
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
const char *command_catalog_suggest(const char *name) {
|
const char *command_catalog_suggest(const char *name) {
|
||||||
const char *best = NULL;
|
const char *best = NULL;
|
||||||
int best_distance = 99;
|
int best_distance = 99;
|
||||||
|
|
@ -257,3 +299,17 @@ void command_catalog_append_manual(char *buffer, size_t buf_size, size_t *pos,
|
||||||
buffer_appendf(buffer, buf_size, pos, "\n");
|
buffer_appendf(buffer, buf_size, pos, "\n");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void command_catalog_append_usage(char *buffer, size_t buf_size, size_t *pos,
|
||||||
|
tnt_command_id_t id, ui_lang_t lang) {
|
||||||
|
const command_catalog_entry_t *entry = entry_for_id(id);
|
||||||
|
const char *usage;
|
||||||
|
|
||||||
|
if (!entry) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
usage = lang == UI_LANG_ZH ? entry->error_usage_zh
|
||||||
|
: entry->error_usage_en;
|
||||||
|
buffer_appendf(buffer, buf_size, pos, "%s", usage);
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -47,6 +47,11 @@ static void append_highlighted(char *output, size_t buf_size, size_t *pos,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void append_command_usage(char *output, size_t buf_size, size_t *pos,
|
||||||
|
tnt_command_id_t id, ui_lang_t lang) {
|
||||||
|
command_catalog_append_usage(output, buf_size, pos, id, lang);
|
||||||
|
}
|
||||||
|
|
||||||
void commands_dispatch(client_t *client) {
|
void commands_dispatch(client_t *client) {
|
||||||
char cmd_buf[256];
|
char cmd_buf[256];
|
||||||
strncpy(cmd_buf, client->command_input, sizeof(cmd_buf) - 1);
|
strncpy(cmd_buf, client->command_input, sizeof(cmd_buf) - 1);
|
||||||
|
|
@ -107,6 +112,12 @@ void commands_dispatch(client_t *client) {
|
||||||
goto cmd_done;
|
goto cmd_done;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!command_catalog_args_valid(command_id, arg)) {
|
||||||
|
append_command_usage(output, sizeof(output), &pos, command_id,
|
||||||
|
client->ui_lang);
|
||||||
|
goto cmd_done;
|
||||||
|
}
|
||||||
|
|
||||||
if (command_id == TNT_COMMAND_USERS) {
|
if (command_id == TNT_COMMAND_USERS) {
|
||||||
pthread_rwlock_rdlock(&g_room->lock);
|
pthread_rwlock_rdlock(&g_room->lock);
|
||||||
int total = g_room->client_count;
|
int total = g_room->client_count;
|
||||||
|
|
@ -171,8 +182,8 @@ void commands_dispatch(client_t *client) {
|
||||||
while (*rest == ' ') rest++;
|
while (*rest == ' ') rest++;
|
||||||
|
|
||||||
if (target_name[0] == '\0' || rest[0] == '\0') {
|
if (target_name[0] == '\0' || rest[0] == '\0') {
|
||||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
append_command_usage(output, sizeof(output), &pos,
|
||||||
i18n_text(client->ui_lang, I18N_MSG_USAGE));
|
TNT_COMMAND_MSG, client->ui_lang);
|
||||||
} else {
|
} else {
|
||||||
bool found = false;
|
bool found = false;
|
||||||
client_t *target = NULL;
|
client_t *target = NULL;
|
||||||
|
|
@ -267,8 +278,8 @@ void commands_dispatch(client_t *client) {
|
||||||
while (*new_name == ' ') new_name++;
|
while (*new_name == ' ') new_name++;
|
||||||
|
|
||||||
if (new_name[0] == '\0') {
|
if (new_name[0] == '\0') {
|
||||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
append_command_usage(output, sizeof(output), &pos,
|
||||||
i18n_text(client->ui_lang, I18N_NICK_USAGE));
|
TNT_COMMAND_NICK, client->ui_lang);
|
||||||
} else if (!is_valid_username(new_name)) {
|
} else if (!is_valid_username(new_name)) {
|
||||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
buffer_appendf(output, sizeof(output), &pos, "%s",
|
||||||
i18n_text(client->ui_lang, I18N_NICK_INVALID));
|
i18n_text(client->ui_lang, I18N_NICK_INVALID));
|
||||||
|
|
@ -331,8 +342,8 @@ void commands_dispatch(client_t *client) {
|
||||||
char *endp;
|
char *endp;
|
||||||
long val = strtol(arg, &endp, 10);
|
long val = strtol(arg, &endp, 10);
|
||||||
if (*endp != '\0' || val < 1 || val > 50) {
|
if (*endp != '\0' || val < 1 || val > 50) {
|
||||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
append_command_usage(output, sizeof(output), &pos,
|
||||||
i18n_text(client->ui_lang, I18N_LAST_USAGE));
|
TNT_COMMAND_LAST, client->ui_lang);
|
||||||
goto cmd_done;
|
goto cmd_done;
|
||||||
}
|
}
|
||||||
n = (int)val;
|
n = (int)val;
|
||||||
|
|
@ -357,8 +368,8 @@ void commands_dispatch(client_t *client) {
|
||||||
const char *query = arg;
|
const char *query = arg;
|
||||||
while (*query == ' ') query++;
|
while (*query == ' ') query++;
|
||||||
if (*query == '\0') {
|
if (*query == '\0') {
|
||||||
buffer_appendf(output, sizeof(output), &pos, "%s",
|
append_command_usage(output, sizeof(output), &pos,
|
||||||
i18n_text(client->ui_lang, I18N_SEARCH_USAGE));
|
TNT_COMMAND_SEARCH, client->ui_lang);
|
||||||
} else {
|
} else {
|
||||||
message_t *found = NULL;
|
message_t *found = NULL;
|
||||||
int found_count = message_search(query, &found, 15);
|
int found_count = message_search(query, &found, 15);
|
||||||
|
|
|
||||||
|
|
@ -106,12 +106,6 @@ static const i18n_text_entry_t text_catalog[I18N_TEXT_COUNT] = {
|
||||||
"Online users",
|
"Online users",
|
||||||
"在线用户"
|
"在线用户"
|
||||||
},
|
},
|
||||||
[I18N_MSG_USAGE] = {
|
|
||||||
"Usage: msg <user> <message>\n"
|
|
||||||
" w <user> <message>\n",
|
|
||||||
"用法: msg <user> <message>\n"
|
|
||||||
" w <user> <message>\n"
|
|
||||||
},
|
|
||||||
[I18N_MSG_SENT_FORMAT] = {
|
[I18N_MSG_SENT_FORMAT] = {
|
||||||
"Private message sent to %s\n",
|
"Private message sent to %s\n",
|
||||||
"私信已发送给 %s\n"
|
"私信已发送给 %s\n"
|
||||||
|
|
@ -128,10 +122,6 @@ static const i18n_text_entry_t text_catalog[I18N_TEXT_COUNT] = {
|
||||||
"(empty)",
|
"(empty)",
|
||||||
"(空)"
|
"(空)"
|
||||||
},
|
},
|
||||||
[I18N_NICK_USAGE] = {
|
|
||||||
"Usage: nick <name>\n",
|
|
||||||
"用法: nick <name>\n"
|
|
||||||
},
|
|
||||||
[I18N_NICK_INVALID] = {
|
[I18N_NICK_INVALID] = {
|
||||||
"Invalid username\n",
|
"Invalid username\n",
|
||||||
"用户名无效\n"
|
"用户名无效\n"
|
||||||
|
|
@ -148,18 +138,10 @@ static const i18n_text_entry_t text_catalog[I18N_TEXT_COUNT] = {
|
||||||
"Nickname changed: %s -> %s\n",
|
"Nickname changed: %s -> %s\n",
|
||||||
"昵称已修改: %s -> %s\n"
|
"昵称已修改: %s -> %s\n"
|
||||||
},
|
},
|
||||||
[I18N_LAST_USAGE] = {
|
|
||||||
"Usage: last [N] (N: 1-50, default 10)\n",
|
|
||||||
"用法: last [N] (N: 1-50,默认 10)\n"
|
|
||||||
},
|
|
||||||
[I18N_LAST_HEADER_FORMAT] = {
|
[I18N_LAST_HEADER_FORMAT] = {
|
||||||
"--- Last %d message(s) ---\n",
|
"--- Last %d message(s) ---\n",
|
||||||
"--- 最近 %d 条消息 ---\n"
|
"--- 最近 %d 条消息 ---\n"
|
||||||
},
|
},
|
||||||
[I18N_SEARCH_USAGE] = {
|
|
||||||
"Usage: search <keyword>\n",
|
|
||||||
"用法: search <keyword>\n"
|
|
||||||
},
|
|
||||||
[I18N_SEARCH_HEADER_FORMAT] = {
|
[I18N_SEARCH_HEADER_FORMAT] = {
|
||||||
"--- Search: \"%s\" (%d match(es)) ---\n",
|
"--- Search: \"%s\" (%d match(es)) ---\n",
|
||||||
"--- 搜索: \"%s\" (%d 条匹配) ---\n"
|
"--- 搜索: \"%s\" (%d 条匹配) ---\n"
|
||||||
|
|
|
||||||
|
|
@ -305,6 +305,20 @@ send -- "last 999\r"
|
||||||
expect "Usage: last \\[N\\]"
|
expect "Usage: last \\[N\\]"
|
||||||
expect "q:close"
|
expect "q:close"
|
||||||
send -- "q"
|
send -- "q"
|
||||||
|
expect "NORMAL"
|
||||||
|
send -- ":"
|
||||||
|
expect ":"
|
||||||
|
send -- "users extra\r"
|
||||||
|
expect "Usage: users"
|
||||||
|
expect "q:close"
|
||||||
|
send -- "q"
|
||||||
|
expect "NORMAL"
|
||||||
|
send -- ":"
|
||||||
|
expect ":"
|
||||||
|
send -- "help now\r"
|
||||||
|
expect "Usage: help"
|
||||||
|
expect "q:close"
|
||||||
|
send -- "q"
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
send -- "\003"
|
send -- "\003"
|
||||||
sleep 0.2
|
sleep 0.2
|
||||||
|
|
|
||||||
|
|
@ -40,13 +40,37 @@ TEST(matches_canonical_names_and_aliases) {
|
||||||
assert(strcmp(args, "zh") == 0);
|
assert(strcmp(args, "zh") == 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(rejects_arguments_for_no_arg_commands) {
|
TEST(matches_known_commands_before_argument_validation) {
|
||||||
tnt_command_id_t id;
|
tnt_command_id_t id;
|
||||||
const char *args;
|
const char *args;
|
||||||
|
|
||||||
assert(!command_catalog_match("users extra", &id, &args));
|
assert(command_catalog_match("users extra", &id, &args));
|
||||||
assert(!command_catalog_match("help now", &id, &args));
|
assert(id == TNT_COMMAND_USERS);
|
||||||
assert(!command_catalog_match("q now", &id, &args));
|
assert(strcmp(args, "extra") == 0);
|
||||||
|
|
||||||
|
assert(command_catalog_match("help now", &id, &args));
|
||||||
|
assert(id == TNT_COMMAND_HELP);
|
||||||
|
assert(strcmp(args, "now") == 0);
|
||||||
|
|
||||||
|
assert(command_catalog_match("q now", &id, &args));
|
||||||
|
assert(id == TNT_COMMAND_QUIT);
|
||||||
|
assert(strcmp(args, "now") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(validates_argument_shapes) {
|
||||||
|
assert(command_catalog_args_valid(TNT_COMMAND_USERS, NULL));
|
||||||
|
assert(!command_catalog_args_valid(TNT_COMMAND_USERS, "extra"));
|
||||||
|
assert(command_catalog_args_valid(TNT_COMMAND_HELP, NULL));
|
||||||
|
assert(!command_catalog_args_valid(TNT_COMMAND_HELP, "now"));
|
||||||
|
|
||||||
|
assert(!command_catalog_args_valid(TNT_COMMAND_MSG, NULL));
|
||||||
|
assert(command_catalog_args_valid(TNT_COMMAND_MSG, "alice hello"));
|
||||||
|
assert(!command_catalog_args_valid(TNT_COMMAND_SEARCH, ""));
|
||||||
|
assert(command_catalog_args_valid(TNT_COMMAND_SEARCH, "needle"));
|
||||||
|
|
||||||
|
assert(command_catalog_args_valid(TNT_COMMAND_LAST, NULL));
|
||||||
|
assert(command_catalog_args_valid(TNT_COMMAND_LAST, "999"));
|
||||||
|
assert(command_catalog_args_valid(TNT_COMMAND_LANG, "fr"));
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(suggests_from_catalog_aliases) {
|
TEST(suggests_from_catalog_aliases) {
|
||||||
|
|
@ -81,13 +105,31 @@ TEST(generates_localized_help_sections) {
|
||||||
assert_ascii_angle_placeholders(zh);
|
assert_ascii_angle_placeholders(zh);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TEST(generates_localized_usage) {
|
||||||
|
char en[256] = {0};
|
||||||
|
char zh[256] = {0};
|
||||||
|
size_t en_pos = 0;
|
||||||
|
size_t zh_pos = 0;
|
||||||
|
|
||||||
|
command_catalog_append_usage(en, sizeof(en), &en_pos,
|
||||||
|
TNT_COMMAND_LAST, UI_LANG_EN);
|
||||||
|
command_catalog_append_usage(zh, sizeof(zh), &zh_pos,
|
||||||
|
TNT_COMMAND_MSG, UI_LANG_ZH);
|
||||||
|
|
||||||
|
assert(strcmp(en, "Usage: last [N] (N: 1-50, default 10)\n") == 0);
|
||||||
|
assert(strcmp(zh, "用法: msg <user> <message>\n"
|
||||||
|
" w <user> <message>\n") == 0);
|
||||||
|
}
|
||||||
|
|
||||||
int main(void) {
|
int main(void) {
|
||||||
printf("Running command catalog unit tests...\n\n");
|
printf("Running command catalog unit tests...\n\n");
|
||||||
|
|
||||||
RUN_TEST(matches_canonical_names_and_aliases);
|
RUN_TEST(matches_canonical_names_and_aliases);
|
||||||
RUN_TEST(rejects_arguments_for_no_arg_commands);
|
RUN_TEST(matches_known_commands_before_argument_validation);
|
||||||
|
RUN_TEST(validates_argument_shapes);
|
||||||
RUN_TEST(suggests_from_catalog_aliases);
|
RUN_TEST(suggests_from_catalog_aliases);
|
||||||
RUN_TEST(generates_localized_help_sections);
|
RUN_TEST(generates_localized_help_sections);
|
||||||
|
RUN_TEST(generates_localized_usage);
|
||||||
|
|
||||||
printf("\n✓ All %d tests passed!\n", tests_passed);
|
printf("\n✓ All %d tests passed!\n", tests_passed);
|
||||||
return 0;
|
return 0;
|
||||||
|
|
|
||||||
|
|
@ -111,12 +111,6 @@ TEST(text_lookup_matches_language) {
|
||||||
"idle timeout") != NULL);
|
"idle timeout") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_IDLE_TIMEOUT_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_IDLE_TIMEOUT_FORMAT),
|
||||||
"空闲超时") != NULL);
|
"空闲超时") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_EN, I18N_MSG_USAGE),
|
|
||||||
"msg <user>") != NULL);
|
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_MSG_USAGE),
|
|
||||||
"msg <user>") != NULL);
|
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_MSG_USAGE),
|
|
||||||
"<用户>") == NULL);
|
|
||||||
assert(strstr(i18n_text(UI_LANG_EN, I18N_MSG_SENT_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_EN, I18N_MSG_SENT_FORMAT),
|
||||||
"Private message sent") != NULL);
|
"Private message sent") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_MSG_SENT_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_MSG_SENT_FORMAT),
|
||||||
|
|
@ -125,14 +119,10 @@ TEST(text_lookup_matches_language) {
|
||||||
"Private messages") != NULL);
|
"Private messages") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_INBOX_TITLE),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_INBOX_TITLE),
|
||||||
"私信") != NULL);
|
"私信") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_NICK_USAGE),
|
|
||||||
"nick <name>") != NULL);
|
|
||||||
assert(strstr(i18n_text(UI_LANG_EN, I18N_SEARCH_HEADER_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_EN, I18N_SEARCH_HEADER_FORMAT),
|
||||||
"Search") != NULL);
|
"Search") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_SEARCH_HEADER_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_SEARCH_HEADER_FORMAT),
|
||||||
"搜索") != NULL);
|
"搜索") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_SEARCH_USAGE),
|
|
||||||
"search <keyword>") != NULL);
|
|
||||||
assert(strstr(i18n_text(UI_LANG_EN, I18N_LANG_CURRENT_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_EN, I18N_LANG_CURRENT_FORMAT),
|
||||||
"lang <en|zh>") != NULL);
|
"lang <en|zh>") != NULL);
|
||||||
assert(strstr(i18n_text(UI_LANG_ZH, I18N_LANG_CURRENT_FORMAT),
|
assert(strstr(i18n_text(UI_LANG_ZH, I18N_LANG_CURRENT_FORMAT),
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue