[PATCH v2 07/21] kconfig: add function support and implement 'shell' function

From: Masahiro Yamada
Date: Tue Mar 27 2018 - 01:35:40 EST


This commit adds a new concept 'function' to do more text processing
in Kconfig.

A function call looks like this:

$(function arg1, arg2, arg3, ...)

(Actually, this syntax was inspired by make.)

Real examples will look like this:

$(shell echo hello world)
$(cc-option -fstackprotector)

This commit adds the basic infrastructure to add, delete, evaluate
functions, and also the first built-in function $(shell ...). This
accepts a single command to execute. It returns the standard output
from it.

[Example code]

config HELLO
string
default "$(shell echo hello world)"

config Y
def_bool $(shell echo y)

[Result]

$ make -s alldefconfig && tail -n 2 .config
CONFIG_HELLO="hello world"
CONFIG_Y=y

Caveat:
Like environments, functions are expanded in the lexer. You cannot
pass symbols to function arguments. This is a limitation to simplify
the implementation. I want to avoid the dynamic function evaluation,
which would introduce much more complexity.

Signed-off-by: Masahiro Yamada <yamada.masahiro@xxxxxxxxxxxxx>
---

Reminder for myself:
Update Documentation/kbuild/kconfig-language.txt


Changes in v2:
- Use 'shell' for getting stdout from the comment.
It was 'shell-stdout' in the previous version.
- Symplify the implementation since the expansion has been moved to
lexer.

scripts/kconfig/function.c | 170 ++++++++++++++++++++++++++++++++++++++++++++
scripts/kconfig/lkc_proto.h | 5 ++
scripts/kconfig/util.c | 46 +++++++++---
scripts/kconfig/zconf.y | 9 +++
4 files changed, 222 insertions(+), 8 deletions(-)
create mode 100644 scripts/kconfig/function.c

diff --git a/scripts/kconfig/function.c b/scripts/kconfig/function.c
new file mode 100644
index 0000000..913685f
--- /dev/null
+++ b/scripts/kconfig/function.c
@@ -0,0 +1,170 @@
+// SPDX-License-Identifier: GPL-2.0
+//
+// Copyright (C) 2018 Masahiro Yamada <yamada.masahiro@xxxxxxxxxxxxx>
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <string.h>
+
+#include "list.h"
+
+#define FUNCTION_MAX_ARGS 10
+
+static LIST_HEAD(function_list);
+
+struct function {
+ char *name;
+ char *(*func)(struct function *f, int argc, char *argv[]);
+ struct list_head node;
+};
+
+static struct function *func_lookup(const char *name)
+{
+ struct function *f;
+
+ list_for_each_entry(f, &function_list, node) {
+ if (!strcmp(name, f->name))
+ return f;
+ }
+
+ return NULL;
+}
+
+static void func_add(const char *name,
+ char *(*func)(struct function *f, int argc, char *argv[]))
+{
+ struct function *f;
+
+ f = func_lookup(name);
+ if (f) {
+ fprintf(stderr, "%s: function already exists. ignored.\n", name);
+ return;
+ }
+
+ f = xmalloc(sizeof(*f));
+ f->name = xstrdup(name);
+ f->func = func;
+
+ list_add_tail(&f->node, &function_list);
+}
+
+static void func_del(struct function *f)
+{
+ list_del(&f->node);
+ free(f->name);
+ free(f);
+}
+
+static char *func_call(int argc, char *argv[])
+{
+ struct function *f;
+
+ f = func_lookup(argv[0]);
+ if (!f) {
+ fprintf(stderr, "%s: function not found\n", argv[0]);
+ return NULL;
+ }
+
+ return f->func(f, argc, argv);
+}
+
+static char *func_eval(const char *func)
+{
+ char *expanded, *saveptr, *str, *token, *res;
+ const char *delim;
+ int argc = 0;
+ char *argv[FUNCTION_MAX_ARGS];
+
+ expanded = expand_string_value(func);
+
+ str = expanded;
+ delim = " ";
+
+ while ((token = strtok_r(str, delim, &saveptr))) {
+ argv[argc++] = token;
+ str = NULL;
+ delim = ",";
+ }
+
+ res = func_call(argc, argv);
+
+ free(expanded);
+
+ return res ?: xstrdup("");
+}
+
+char *func_eval_n(const char *func, size_t n)
+{
+ char *tmp, *res;
+
+ tmp = xmalloc(n + 1);
+ memcpy(tmp, func, n);
+ *(tmp + n) = '\0';
+
+ res = func_eval(tmp);
+
+ free(tmp);
+
+ return res;
+}
+
+/* built-in functions */
+static char *do_shell(struct function *f, int argc, char *argv[])
+{
+ static const char *pre = "(";
+ static const char *post = ") 2>/dev/null";
+ FILE *p;
+ char buf[256];
+ char *cmd;
+ int ret;
+
+ if (argc != 2)
+ return NULL;
+
+ /*
+ * Surround the command with ( ) in case it is piped commands.
+ * Also, redirect stderr to /dev/null.
+ */
+ cmd = xmalloc(strlen(pre) + strlen(argv[1]) + strlen(post) + 1);
+ strcpy(cmd, pre);
+ strcat(cmd, argv[1]);
+ strcat(cmd, post);
+
+ p = popen(cmd, "r");
+ if (!p) {
+ perror(cmd);
+ goto free;
+ }
+ if (fgets(buf, sizeof(buf), p)) {
+ size_t len = strlen(buf);
+
+ if (buf[len - 1] == '\n')
+ buf[len - 1] = '\0';
+ } else {
+ buf[0] = '\0';
+ }
+
+ ret = pclose(p);
+ if (ret == -1)
+ perror(cmd);
+
+free:
+ free(cmd);
+
+ return xstrdup(buf);
+}
+
+void func_init(void)
+{
+ /* register built-in functions */
+ func_add("shell", do_shell);
+}
+
+void func_exit(void)
+{
+ struct function *f, *tmp;
+
+ /* unregister all functions */
+ list_for_each_entry_safe(f, tmp, &function_list, node)
+ func_del(f);
+}
diff --git a/scripts/kconfig/lkc_proto.h b/scripts/kconfig/lkc_proto.h
index 9884adc..09a4f53 100644
--- a/scripts/kconfig/lkc_proto.h
+++ b/scripts/kconfig/lkc_proto.h
@@ -48,5 +48,10 @@ const char * sym_get_string_value(struct symbol *sym);

const char * prop_get_type_name(enum prop_type type);

+/* function.c */
+char *func_eval_n(const char *func, size_t n);
+void func_init(void);
+void func_exit(void);
+
/* expr.c */
void expr_print(struct expr *e, void (*fn)(void *, struct symbol *, const char *), void *data, int prevtoken);
diff --git a/scripts/kconfig/util.c b/scripts/kconfig/util.c
index 3d27c49..218b051 100644
--- a/scripts/kconfig/util.c
+++ b/scripts/kconfig/util.c
@@ -13,9 +13,10 @@
#include "lkc.h"

/*
- * Expand environments embedded in the string given in argument. Environments
- * to be expanded shall be prefixed by a '$'. Unknown environment expands to
- * the empty string.
+ * Expand environments and functions embedded in the string given in argument.
+ * Environments to be expanded shall be prefixed by a '$'. Functions to be
+ * evaluated shall be surrounded by $(). Unknown environment/function expands
+ * to the empty string.
*/
char *expand_string_value(const char *in)
{
@@ -33,11 +34,40 @@ char *expand_string_value(const char *in)
while ((p = strchr(in, '$'))) {
char *new;

- q = p + 1;
- while (isalnum(*q) || *q == '_')
- q++;
-
- new = env_expand_n(p + 1, q - p - 1);
+ /*
+ * If the next character is '(', it is a function.
+ * Otherwise, environment.
+ */
+ if (*(p + 1) == '(') {
+ int nest = 0;
+
+ q = p + 2;
+ while (1) {
+ if (*q == '\0') {
+ fprintf(stderr,
+ "unterminated function: %s\n",
+ p);
+ new = xstrdup("");
+ break;
+ } else if (*q == '(') {
+ nest++;
+ } else if (*q == ')') {
+ if (nest-- == 0) {
+ new = func_eval_n(p + 2,
+ q - p - 2);
+ q++;
+ break;
+ }
+ }
+ q++;
+ }
+ } else {
+ q = p + 1;
+ while (isalnum(*q) || *q == '_')
+ q++;
+
+ new = env_expand_n(p + 1, q - p - 1);
+ }

reslen = strlen(res) + (p - in) + strlen(new) + 1;
res = xrealloc(res, reslen);
diff --git a/scripts/kconfig/zconf.y b/scripts/kconfig/zconf.y
index d8120c7..feaea18 100644
--- a/scripts/kconfig/zconf.y
+++ b/scripts/kconfig/zconf.y
@@ -520,11 +520,19 @@ void conf_parse(const char *name)

zconf_initscan(name);

+ func_init();
_menu_init();

if (getenv("ZCONF_DEBUG"))
yydebug = 1;
yyparse();
+
+ /*
+ * Currently, functions are evaluated only when Kconfig files are
+ * parsed. We can free functions here.
+ */
+ func_exit();
+
if (yynerrs)
exit(1);
if (!modules_sym)
@@ -765,4 +773,5 @@ void zconfdump(FILE *out)
#include "confdata.c"
#include "expr.c"
#include "symbol.c"
+#include "function.c"
#include "menu.c"
--
2.7.4