feat(console): add command user context support

Current implementation implicitly forces the developer to use global variables
to enter its context during the command invocation, this change enables each
module to register a context for command to find without the need to manage
global variables.

No API breakage.

Fields added:
   esp_console_cmd_t::func_w_context    - (*)(int argc, char **argv, void *context)

Functions added:
   esp_err_t esp_console_cmd_set_context(const char *cmd, void *context)

Usage:

   esp_console_cmd_register(&cmd));
   esp_console_cmd_set_context(cmd.command, (void *)"context"));

Signed-off-by: Alon Bar-Lev <alon.barlev@gmail.com>
This commit is contained in:
Alon Bar-Lev 2023-10-20 20:48:52 +03:00 committed by Jakob Hasse
parent 142218c2eb
commit bccb2873bd
5 changed files with 196 additions and 6 deletions

View File

@ -32,9 +32,11 @@ typedef struct cmd_item_ {
* May be NULL.
*/
char *hint;
esp_console_cmd_func_t func; //!< pointer to the command handler
void *argtable; //!< optional pointer to arg table
SLIST_ENTRY(cmd_item_) next; //!< next command in the list
esp_console_cmd_func_t func; //!< pointer to the command handler (without user context)
esp_console_cmd_func_with_context_t func_w_context; //!< pointer to the command handler (with user context)
void *argtable; //!< optional pointer to arg table
void *context; //!< optional pointer to user context
SLIST_ENTRY(cmd_item_) next; //!< next command in the list
} cmd_item_t;
/** linked list of command structures */
@ -97,6 +99,10 @@ esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd)
if (strchr(cmd->command, ' ') != NULL) {
return ESP_ERR_INVALID_ARG;
}
if ((cmd->func == NULL && cmd->func_w_context == NULL)
|| (cmd->func != NULL && cmd->func_w_context != NULL)) {
return ESP_ERR_INVALID_ARG;
}
item = (cmd_item_t *)find_command_by_name(cmd->command);
if (!item) {
// not registered before
@ -130,6 +136,7 @@ esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd)
}
item->argtable = cmd->argtable;
item->func = cmd->func;
item->func_w_context = cmd->func_w_context;
cmd_item_t *last = NULL;
cmd_item_t *it;
SLIST_FOREACH(it, &s_cmd_list, next) {
@ -146,6 +153,22 @@ esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd)
return ESP_OK;
}
esp_err_t esp_console_cmd_set_context(const char *cmd, void *context)
{
if (cmd == NULL ) {
return ESP_ERR_INVALID_ARG;
}
cmd_item_t *it;
SLIST_FOREACH(it, &s_cmd_list, next) {
if (strcmp(cmd, it->command) == 0) {
it->context = context;
return ESP_OK;
}
}
return ESP_ERR_NOT_FOUND;
}
void esp_console_get_completion(const char *buf, linenoiseCompletions *lc)
{
size_t len = strlen(buf);
@ -213,7 +236,12 @@ esp_err_t esp_console_run(const char *cmdline, int *cmd_ret)
free(argv);
return ESP_ERR_NOT_FOUND;
}
*cmd_ret = (*cmd->func)(argc, argv);
if (cmd->func) {
*cmd_ret = (*cmd->func)(argc, argv);
}
if (cmd->func_w_context) {
*cmd_ret = (*cmd->func_w_context)(cmd->context, argc, argv);
}
free(argv);
return ESP_OK;
}

View File

@ -156,6 +156,15 @@ esp_err_t esp_console_deinit(void);
*/
typedef int (*esp_console_cmd_func_t)(int argc, char **argv);
/**
* @brief Console command main function, with context
* @param context a user context given at invocation
* @param argc number of arguments
* @param argv array with argc entries, each pointing to a zero-terminated string argument
* @return console command return code, 0 indicates "success"
*/
typedef int (*esp_console_cmd_func_with_context_t)(void *context, int argc, char **argv);
/**
* @brief Console command description
*/
@ -179,6 +188,7 @@ typedef struct {
const char *hint;
/**
* Pointer to a function which implements the command.
* @note: Setting both \c func and \c func_w_context is not allowed.
*/
esp_console_cmd_func_t func;
/**
@ -188,18 +198,44 @@ typedef struct {
* Only used for the duration of esp_console_cmd_register call.
*/
void *argtable;
/**
* Pointer to a context aware function which implements the command.
* @note: Setting both \c func and \c func_w_context is not allowed.
*/
esp_console_cmd_func_with_context_t func_w_context;
} esp_console_cmd_t;
/**
* @brief Register console command
* @param cmd pointer to the command description; can point to a temporary value
*
* @note If the member func_w_context of cmd is set instead of func, then there
* MUST be a subsequent call to \c esp_console_cmd_set_context to initialize the
* function context before it is used!
*
* @return
* - ESP_OK on success
* - ESP_ERR_NO_MEM if out of memory
* - ESP_ERR_INVALID_ARG if command description includes invalid arguments
* - ESP_ERR_INVALID_ARG if both func and func_w_context members of cmd are non-NULL
* - ESP_ERR_INVALID_ARG if both func and func_w_context members of cmd are NULL
*/
esp_err_t esp_console_cmd_register(const esp_console_cmd_t *cmd);
/**
* @brief Register context for a command registered with \c func_w_context before
*
* \c context is only used if \c func_w_context has been set in the structure
* passed to esp_console_cmd_register()
* @param cmd pointer to the command name
* @param context pointer to user-defined per-command context data
* @return
* - ESP_OK on success
* - ESP_ERR_NOT_FOUND if command was not found
* - ESP_ERR_INVALID_ARG if invalid arguments
*/
esp_err_t esp_console_cmd_set_context(const char *cmd, void *context);
/**
* @brief Run command line
* @param cmdline command line (command name followed by a number of arguments)

View File

@ -24,12 +24,60 @@
* https://docs.espressif.com/projects/esp-idf/en/latest/esp32/contribute/esp-idf-tests-with-pytest.html.
*/
typedef struct {
const char *in;
const char *out;
} cmd_context_t;
static int do_hello_cmd_with_context(void *context, int argc, char **argv)
{
cmd_context_t *cmd_context = (cmd_context_t *)context;
cmd_context->out = cmd_context->in;
return 0;
}
static int do_hello_cmd(int argc, char **argv)
{
printf("Hello World\n");
return 0;
}
static int do_not_call(void* context, int argc, char **argv)
{
TEST_ASSERT_MESSAGE(false, "This function is a dummy and must not be called\n");
return 0;
}
TEST_CASE("esp console register with normal and context aware functions set fails", "[console]")
{
esp_console_config_t console_config = ESP_CONSOLE_CONFIG_DEFAULT();
TEST_ESP_OK(esp_console_init(&console_config));
const esp_console_cmd_t cmd = {
.command = "valid_cmd",
.help = "Command which is valid",
.hint = NULL,
.func = do_hello_cmd,
.func_w_context = do_not_call,
};
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_console_cmd_register(&cmd));
TEST_ESP_OK(esp_console_deinit());
}
TEST_CASE("esp console register with normal and context aware function set to NULL fails", "[console]")
{
esp_console_config_t console_config = ESP_CONSOLE_CONFIG_DEFAULT();
TEST_ESP_OK(esp_console_init(&console_config));
const esp_console_cmd_t cmd = {
.command = "valid_cmd",
.help = "Command which is valid",
.hint = NULL,
.func = do_hello_cmd,
.func_w_context = do_not_call,
};
TEST_ASSERT_EQUAL(ESP_ERR_INVALID_ARG, esp_console_cmd_register(&cmd));
TEST_ESP_OK(esp_console_deinit());
}
TEST_CASE("esp console init/deinit test", "[console]")
{
esp_console_config_t console_config = ESP_CONSOLE_CONFIG_DEFAULT();
@ -147,3 +195,71 @@ TEST_CASE("esp console init/deinit test, minimal config", "[console]")
TEST_ESP_OK(esp_console_cmd_register(&cmd));
TEST_ESP_OK(esp_console_deinit());
}
TEST_CASE("esp console test set_context", "[console]")
{
/* Test with minimal init config */
esp_console_config_t console_config = {
.max_cmdline_args = 2,
.max_cmdline_length = 100,
};
TEST_ESP_OK(esp_console_init(&console_config));
TEST_ASSERT_EQUAL(esp_console_cmd_set_context(NULL, NULL), ESP_ERR_INVALID_ARG);
TEST_ASSERT_EQUAL(esp_console_cmd_set_context("invalid", NULL), ESP_ERR_NOT_FOUND);
TEST_ESP_OK(esp_console_deinit());
}
TEST_CASE("esp console test with context", "[console]")
{
/* Test with minimal init config */
esp_console_config_t console_config = {
.max_cmdline_args = 2,
.max_cmdline_length = 100,
};
TEST_ESP_OK(esp_console_init(&console_config));
const esp_console_cmd_t cmds[] = {
{
.command = "hello-c1",
.help = "Print Hello World in context c1",
.hint = NULL,
.func_w_context = do_hello_cmd_with_context,
},
{
.command = "hello-c2",
.help = "Print Hello World in context c2",
.hint = NULL,
.func_w_context = do_hello_cmd_with_context,
},
};
cmd_context_t contexts[] = {
{
.in = "c1",
.out = NULL,
},
{
.in = "c2",
.out = NULL,
},
};
static_assert((sizeof(contexts) / sizeof(contexts[0])) == (sizeof(cmds) / sizeof(cmds[0])));
for (int i=0; i < sizeof(cmds) / sizeof(cmds[0]); i++) {
TEST_ESP_OK(esp_console_cmd_register(&cmds[i]));
TEST_ESP_OK(esp_console_cmd_set_context(cmds[i].command, &contexts[i]));
}
for (int i=0; i < sizeof(cmds) / sizeof(cmds[0]); i++) {
int ret;
TEST_ESP_OK(esp_console_run(cmds[i].command, &ret));
TEST_ASSERT_EQUAL(ret, 0);
TEST_ASSERT_EQUAL(contexts[i].in, contexts[i].out);
}
TEST_ESP_OK(esp_console_deinit());
}

View File

@ -147,7 +147,12 @@ For each command, application provides the following information (in the form of
- Command name (string without spaces)
- Help text explaining what the command does
- Optional hint text listing the arguments of the command. If application uses Argtable3 for argument parsing, hint text can be generated automatically by providing a pointer to argtable argument definitions structure instead.
- The command handler function.
- Command handler function (without context), or
- Command handler function (with context). If this function is given, an additional call to :cpp:func:`esp_console_cmd_set_context` must follow *before* the command may be called to initialize the context.
.. note::
You can either use a command handler function which takes a context or a command handler function which does not take a context, not both. If you use the command handler function which takes a context, you MUST call :cpp:func:`esp_console_cmd_set_context` to initialize its context, otherwise the function may access the uninitialized context.
A few other functions are provided by the command registration module:

View File

@ -147,7 +147,12 @@ Linenoise 库不需要显式地初始化,但是在调用行编辑函数之前
- 命令名字(不含空格的字符串)
- 帮助文档,解释该命令的用途
- 可选的提示文本,列出命令的参数。如果应用程序使用 ``Argtable3`` 库来解析参数,则可以通过提供指向 argtable 参数定义结构体的指针来自动生成提示文本
- 命令处理函数
- 命令处理函数(无上下文),或
- 命令处理函数(有上下文)。如要使用此函数,则必须在调用其他命令 **之前** 调用 :cpp:func:`esp_console_cmd_set_context` 初始化上下文。
.. note::
使用接受上下文的命令处理函数或者不接受上下文的命令处理函数均可,但两者不能同时使用。如果使用接受上下文的命令处理程序函数,则必须调用 :cpp:func:`esp_console_cmd_set_context` 初始化上下文,否则该函数可能会访问未初始化的上下文。
命令注册模块还提供了其它函数: