[PATCH 2/3] cli_hush.c: add "call" command

Rasmus Villemoes
2020-09-25T11:19:41Z

Currently, the only way to emulate functions with arguments in the
busybox shell is by doing "foo=arg1; bar=arg2; run func" and having
"func" refer to $foo and $bar. That works, but is a bit clunky, and
also suffers from foo and bar being set globally - if func itself wants
to run other "functions" defined in the environment, those other
functions better not use the same parameter names:

setenv g 'do_g_stuff $foo'
setenv f 'do_f_stuff $foo $bar; foo=123; run g; do_more_f_stuff $foo $bar'

Sure, f could do a "saved_foo=$foo; .... foo=$saved_foo" dance, but
that makes everything even more clunky.

In order to increase readability, add a little helper "call" that is
like "run", but which sets local shell variables $1 through
$9 (and $#). As in a "real" shell, they are local to the current
function, so if f is called with two arguments, and f calls g with one
argument, g sees $2 as unset. Then the above can be written

setenv g 'do_g_stuff $1'
setenv f 'do_f_stuff $1 $2; call g 123; do_more_f_stuff $1 $2'

Everything except

- b_addchr(dest, '?');
+ b_addchr(dest, ch);

is under CONFIG_CMD_CALL, and when CONFIG_CMD_CALL=n, the ch there can
only be '?'. So no functional change when CONFIG_CMD_CALL is not
selected.

"Real shells" have special syntax for defining a function, but calling
a function is the same as calling builtins or external commands. So
the "call" may admittedly be seen as a bit of a kludge. It
should be rather easy to make custom (i.e., defined in the
environment) functions "transparently callable" on top of this
infrastructure, i.e. so one could just say

f a b c

instead of

call f a b c

However, that behaviour should be controlled by a separate config
knob, and can be added later if anyone actually wants it.

Signed-off-by: Rasmus Villemoes <rasmus.villemoes@prevas.dk>
---
cmd/Kconfig | 8 +++++
common/cli_hush.c | 75 ++++++++++++++++++++++++++++++++++++++++++++++-
2 files changed, 82 insertions(+), 1 deletion(-)

diff --git a/cmd/Kconfig b/cmd/Kconfig
index 0c984d735d..306f115c32 100644
--- a/cmd/Kconfig
+++ b/cmd/Kconfig
@@ -443,6 +443,14 @@ config CMD_RUN
help
Run the command in the given environment variable.

+config CMD_CALL
+ bool "call"
+ depends on HUSH_PARSER
+ depends on CMD_RUN
+ help
+ Call function defined in environment variable, setting
+ positional arguments $1..$9.
+
config CMD_IMI
bool "iminfo"
default y
diff --git a/common/cli_hush.c b/common/cli_hush.c
index 072b871f1e..e17fba99ee 100644
--- a/common/cli_hush.c
+++ b/common/cli_hush.c
@@ -135,6 +135,17 @@ DECLARE_GLOBAL_DATA_PTR;
#define syntax() syntax_err()
#define xstrdup strdup
#define error_msg printf
+
+#ifdef CONFIG_CMD_CALL
+#define MAX_CALL_ARGS 9
+struct call_args {
+ struct call_args *prev;
+ int count;
+ char *args[MAX_CALL_ARGS]; /* [0] holds $1 etc. */
+};
+static struct call_args *current_call_args;
+#endif
+
#else
typedef enum {
REDIRECT_INPUT = 1,
@@ -2144,6 +2155,10 @@ char *get_local_var(const char *s)
#ifdef __U_BOOT__
if (*s == '$')
return get_dollar_var(s[1]);
+ /* To make ${1:-default} work: */
+ if (IS_ENABLED(CONFIG_CMD_CALL) &&
+ '1' <= s[0] && s[0] <= '9' && !s[1])
+ return get_dollar_var(s[0]);
#endif

for (cur = top_vars; cur; cur=cur->next)
@@ -2826,6 +2841,23 @@ static char *get_dollar_var(char ch)
case '?':
sprintf(buf, "%u", (unsigned int)last_return_code);
break;
+#ifdef CONFIG_CMD_CALL
+ case '#':
+ if (!current_call_args)
+ return NULL;
+ sprintf(buf, "%u", current_call_args->count);
+ break;
+ case '1' ... '9': {
+ const struct call_args *ca = current_call_args;
+ int i = ch - '1';
+
+ if (!ca)
+ return NULL;
+ if (i >= ca->count)
+ return NULL;
+ return ca->args[i];
+ }
+#endif
default:
return NULL;
}
@@ -2865,10 +2897,14 @@ static int handle_dollar(o_string *dest, struct p_context *ctx, struct in_str *i
} else switch (ch) {
#ifdef __U_BOOT__
case '?':
+#ifdef CONFIG_CMD_CALL
+ case '1' ... '9':
+ case '#':
+#endif
ctx->child->sp++;
b_addchr(dest, SPECIAL_VAR_SYMBOL);
b_addchr(dest, '$');
- b_addchr(dest, '?');
+ b_addchr(dest, ch);
b_addchr(dest, SPECIAL_VAR_SYMBOL);
advance = 1;
break;
@@ -3711,5 +3747,42 @@ U_BOOT_CMD(
" - print value of hushshell variable 'name'"
);

+#ifdef CONFIG_CMD_CALL
+static int do_cmd_call(struct cmd_tbl *cmdtp, int flag, int argc,
+ char *const argv[])
+{
+ struct call_args ca;
+ char *run_args[2];
+ int i, ret;
+
+ if (argc < 2)
+ return CMD_RET_USAGE;
+
+ ca.count = argc - 2;
+ for (i = 2; i < argc; ++i)
+ ca.args[i - 2] = argv[i];
+ ca.prev = current_call_args;
+ current_call_args = &ca;
+
+ run_args[0] = "run";
+ run_args[1] = argv[1];
+ ret = do_run(cmdtp, flag, 2, run_args);
+
+ current_call_args = ca.prev;
+
+ return ret;
+}
+
+U_BOOT_CMD_COMPLETE(
+ call, 1 + 1 + MAX_CALL_ARGS, 0, do_cmd_call,
+ "call command in environment variable, setting positional arguments $1..$9",
+ "var [args...]\n"
+ " - run the command(s) in the environment variable 'var',\n"
+ " with $1..$9 set to the positional arguments",
+ var_complete
+);
+
+#endif
+
#endif
/****************************************************************************/
--
2.23.0