add ecoli_editline and enhance yaml parser
authorOlivier Matz <zer0@droids-corp.org>
Wed, 31 Oct 2018 18:43:30 +0000 (19:43 +0100)
committerOlivier Matz <zer0@droids-corp.org>
Wed, 31 Oct 2018 18:43:30 +0000 (19:43 +0100)
19 files changed:
Makefile
examples/yaml/parse-yaml.c
examples/yaml/test.yaml
libecoli/ecoli_complete.c
libecoli/ecoli_complete.h
libecoli/ecoli_log.h
libecoli/ecoli_parse.c
libecoli/ecoli_parse.h
libecoli/ecoli_string.c
libecoli/ecoli_string.h
libecoli/ecoli_utils.h [new file with mode: 0644]
libecoli_editline/ecoli_editline.c [new file with mode: 0644]
libecoli_editline/ecoli_editline.h [new file with mode: 0644]
libecoli_editline/editline.c [new file with mode: 0644]
libecoli_yaml/ecoli_yaml.c
parse-yaml.sh [new file with mode: 0644]
test-completion.sh [new file with mode: 0644]
test/test.c
todo.txt

index 7bf0ddb..8ccd7e2 100644 (file)
--- a/Makefile
+++ b/Makefile
@@ -9,7 +9,7 @@ O ?= build/
 
 # XXX -O0
 CFLAGS  = -g -O0 -Wall -Werror -W -Wextra -fPIC -Wmissing-prototypes
-CFLAGS += -Ilibecoli -Ilibecoli_yaml
+CFLAGS += -Ilibecoli -Ilibecoli_yaml -Ilibecoli_editline
 
 # XXX coverage
 CFLAGS += --coverage
@@ -61,6 +61,9 @@ shlib-y-$(O)libecoli.so := $(addprefix libecoli/,$(srcs))
 cflags-$(O)libecoli_yaml.so = -Ilibecoli_yaml
 shlib-y-$(O)libecoli_yaml.so := libecoli_yaml/ecoli_yaml.c
 
+cflags-$(O)libecoli_editline.so = -Ilibecoli_editline
+shlib-y-$(O)libecoli_editline.so := libecoli_editline/ecoli_editline.c
+
 # tests
 ldflags-$(O)test = -rdynamic
 exe-y-$(O)test = $(addprefix libecoli/,$(srcs)) test/test.c
@@ -70,9 +73,10 @@ ldflags-$(O)readline = -lreadline -ltermcap
 exe-y-$(O)readline = $(addprefix libecoli/,$(srcs)) \
        examples/readline/main.c
 
-ldflags-$(O)parse-yaml = -lyaml
+ldflags-$(O)parse-yaml = -lyaml -ledit
 exe-y-$(O)parse-yaml = $(addprefix libecoli/,$(srcs)) \
-       libecoli_yaml/ecoli_yaml.c examples/yaml/parse-yaml.c
+       libecoli_yaml/ecoli_yaml.c libecoli_editline/ecoli_editline.c\
+       examples/yaml/parse-yaml.c
 
 include $(ECOLI)/mk/ecoli-post.mk
 
index d5d8460..9ec8d5f 100644 (file)
 
 #include <stdlib.h>
 #include <stdio.h>
+#include <string.h>
+#include <getopt.h>
 
+#include <ecoli_strvec.h>
 #include <ecoli_node.h>
+#include <ecoli_parse.h>
+#include <ecoli_complete.h>
 #include <ecoli_yaml.h>
+#include <ecoli_editline.h>
+#include <ecoli_node_sh_lex.h>
+
+static char *input_file;
+static char *output_file;
+static bool complete;
+
+static const char short_options[] =
+       "h"  /* help */
+       "i:" /* input-file */
+       "o:" /* output-file */
+       "c"  /* complete */
+       ;
+
+#define OPT_HELP "help"
+#define OPT_INPUT_FILE "input-file"
+#define OPT_OUTPUT_FILE "output-file"
+#define OPT_COMPLETE "complete"
+
+static const struct option long_options[] = {
+       {OPT_HELP, 0, NULL, 'h'},
+       {OPT_INPUT_FILE, 1, NULL, 'i'},
+       {OPT_OUTPUT_FILE, 1, NULL, 'o'},
+       {OPT_COMPLETE, 0, NULL, 'c'},
+       {NULL, 0, NULL, 0}
+};
+
+static void usage(const char *prgname)
+{
+       fprintf(stderr, "%s -o <file.sh> -i <file.yaml>\n"
+               "  -h\n"
+               "  --"OPT_HELP"\n"
+               "      Show this help.\n"
+               "  -i <input-file>\n"
+               "  --"OPT_INPUT_FILE"=<file>\n"
+               "      Set the yaml input file describing the grammar.\n"
+               "  -o <output-file>\n"
+               "  --"OPT_OUTPUT_FILE"=<file>\n"
+               "      Set the output file.\n"
+               "  -c\n"
+               "  --"OPT_COMPLETE"\n"
+               "      Output the completion list."
+               , prgname);
+}
+
+static int parse_args(int argc, char **argv)
+{
+       int ret, opt;
+
+       while ((opt = getopt_long(argc, argv, short_options,
+                               long_options, NULL)) != EOF) {
+
+               switch (opt) {
+               case 'h': /* help */
+                       usage(argv[0]);
+                       exit(0);
+
+               case 'i': /* input-file */
+                       input_file = strdup(optarg);
+                       break;
+
+               case 'o': /* output-file */
+                       output_file = strdup(optarg);
+                       break;
+
+               case 'c': /* complete */
+                       complete = 1;
+                       break;
+
+               default:
+                       usage(argv[0]);
+                       return -1;
+               }
+
+       }
+
+       if (input_file == NULL) {
+               fprintf(stderr, "No input file\n");
+               usage(argv[0]);
+               return -1;
+       }
+       if (output_file == NULL) {
+               fprintf(stderr, "No output file\n");
+               usage(argv[0]);
+               return -1;
+       }
+
+       ret = optind - 1;
+       optind = 1;
+
+       return ret;
+}
+
+static int
+__dump_as_shell(FILE *f, const struct ec_parse *parse, size_t *seq)
+{
+       const struct ec_node *node = ec_parse_get_node(parse);
+       struct ec_parse *child;
+       size_t cur_seq, i, len;
+       const char *s;
+
+       (*seq)++;
+       cur_seq = *seq;
+
+       // XXX protect strings
+
+
+       fprintf(f, "ec_node%zu_id='%s'\n", cur_seq, ec_node_id(node));
+       fprintf(f, "ec_node%zu_type='%s'\n", cur_seq,
+               ec_node_type_name(ec_node_type(node)));
+
+       len = ec_strvec_len(ec_parse_strvec(parse));
+       fprintf(f, "ec_node%zu_strvec_len=%zu\n", cur_seq, len);
+       for (i = 0; i < len; i++) {
+               s = ec_strvec_val(ec_parse_strvec(parse), i);
+               fprintf(f, "ec_node%zu_str%zu='%s'\n", cur_seq, i, s);
+       }
+
+       if (ec_parse_get_first_child(parse) != NULL) {
+               fprintf(f, "ec_node%zu_first_child='ec_node%zu'\n",
+                       cur_seq, cur_seq + 1);
+       }
+
+       EC_PARSE_FOREACH_CHILD(child, parse) {
+               fprintf(f, "ec_node%zu_parent='ec_node%zu'\n",
+                       *seq + 1, cur_seq);
+               __dump_as_shell(f, child, seq);
+       }
+
+       if (ec_parse_get_next(parse) != NULL) {
+               fprintf(f, "ec_node%zu_next='ec_node%zu'\n",
+                       cur_seq, *seq + 1);
+       }
+
+       return 0;
+}
+
+static int
+dump_as_shell(const struct ec_parse *parse)
+{
+       FILE *f;
+       size_t seq = 0;
+       int ret;
+
+       f = fopen(output_file, "w");
+       if (f == NULL)
+               return -1;
+
+       ret = __dump_as_shell(f, parse, &seq);
+
+       fclose(f);
+
+       return ret;
+}
+
+static int
+interact(struct ec_node *node)
+{
+       struct ec_editline *editline = NULL;
+       struct ec_parse *parse = NULL;
+       struct ec_node *shlex = NULL;
+       char *line = NULL;
+
+       shlex = ec_node_sh_lex(EC_NO_ID, ec_node_clone(node)); //XXX
+       if (shlex == NULL) {
+               fprintf(stderr, "Failed to add lexer node\n");
+               goto fail;
+       }
+
+       editline = ec_editline("ecoli", stdin, stdout, stderr, 0);
+       if (editline == NULL) {
+               fprintf(stderr, "Failed to initialize editline\n");
+               goto fail;
+       }
+
+       parse = ec_editline_parse(editline, shlex);
+       if (parse == NULL)
+               goto fail;
+
+       if (!ec_parse_matches(parse))
+               goto fail;
+
+       //ec_parse_dump(stdout, parse);
+
+       if (dump_as_shell(parse) < 0) {
+               fprintf(stderr, "Failed to dump the parsed result\n");
+               goto fail;
+       }
+
+       ec_parse_free(parse);
+       ec_editline_free(editline);
+       ec_node_free(shlex);
+       return 0;
+
+fail:
+       ec_parse_free(parse);
+       ec_editline_free(editline);
+       free(line);
+       ec_node_free(shlex);
+       return -1;
+}
+
+static int
+complete_words(const struct ec_node *node, int argc, char *argv[])
+{
+       struct ec_comp *comp = NULL;
+       struct ec_strvec *strvec = NULL;
+       struct ec_comp_iter *iter = NULL;
+       struct ec_comp_item *item = NULL;
+       size_t count;
+
+       if (argc <= 1)
+               goto fail;
+       strvec = ec_strvec_from_array((const char * const *)&argv[1],
+                               argc - 1);
+       if (strvec == NULL)
+               goto fail;
+
+       comp = ec_node_complete_strvec(node, strvec);
+       if (comp == NULL)
+               goto fail;
+
+       count = ec_comp_count(comp, EC_COMP_UNKNOWN | EC_COMP_FULL |
+                       EC_COMP_PARTIAL);
+
+       iter = ec_comp_iter(comp,
+               EC_COMP_UNKNOWN | EC_COMP_FULL | EC_COMP_PARTIAL);
+       if (iter == NULL)
+               goto fail;
+
+       while ((item = ec_comp_iter_next(iter)) != NULL) {
+
+               /* only one match, display it fully */
+               if (count == 1) {
+                       printf("%s\n", ec_comp_item_get_str(item));
+                       break;
+               }
+
+               /* else show the 'display' part only */
+               printf("%s\n", ec_comp_item_get_display(item));
+       }
+
+       ec_comp_iter_free(iter);
+       ec_comp_free(comp);
+       ec_strvec_free(strvec);
+       return 0;
+
+fail:
+       ec_comp_free(comp);
+       ec_strvec_free(strvec);
+       return -1;
+}
 
 int
 main(int argc, char *argv[])
 {
        struct ec_node *node = NULL;
+       int ret;
 
-       if (argc != 2) {
-               fprintf(stderr, "Invalid args\n");
+       ret = parse_args(argc, argv);
+       if (ret < 0)
                goto fail;
+
+       argc -= ret;
+       argv += ret;
+
+       if (ec_init() < 0) {
+               fprintf(stderr, "cannot init ecoli: %s\n", strerror(errno));
+               return 1;
        }
-       node = ec_yaml_import(argv[1]);
+
+       node = ec_yaml_import(input_file);
        if (node == NULL) {
                fprintf(stderr, "Failed to parse file\n");
                goto fail;
        }
-       ec_node_dump(stdout, node);
+       //ec_node_dump(stdout, node);
+
+       if (complete) {
+               if (complete_words(node, argc, argv) < 0)
+                       goto fail;
+       } else {
+               if (interact(node) < 0)
+                       goto fail;
+       }
+
        ec_node_free(node);
 
        return 0;
index 072fbce..5642766 100644 (file)
@@ -1,16 +1,32 @@
-type: seq
-attrs:
-  toto: 1
-  titi: 2
-help: Say hello to someone
+type: or
 children:
-- type: str
-  string: hello
-- type: or
-  id: name
-  help: Name of the person to greet
+- type: seq
+  id: hello
+  help: Say hello to someone
   children:
   - type: str
-    string: john
+    string: hello
+  - type: or
+    id: name
+    help: Name of the person to greet
+    children:
+    - type: str
+      string: john
+    - type: str
+      string: mike
+- type: seq
+  id: goodbye
+  help: Say good bye to someone
+  children:
+  - type: str
+    string: good
   - type: str
-    string: mike
+    string: bye
+  - type: or
+    id: name
+    help: Name of the person to greet
+    children:
+    - type: str
+      string: mary
+    - type: str
+      string: jessica
index 7ad846c..a9becdf 100644 (file)
@@ -574,7 +574,7 @@ unsigned int ec_comp_count(
 }
 
 struct ec_comp_iter *
-ec_comp_iter(struct ec_comp *comp,
+ec_comp_iter(const struct ec_comp *comp,
        enum ec_comp_type type)
 {
        struct ec_comp_iter *iter;
@@ -594,7 +594,7 @@ ec_comp_iter(struct ec_comp *comp,
 struct ec_comp_item *ec_comp_iter_next(
        struct ec_comp_iter *iter)
 {
-       struct ec_comp *comp;
+       const struct ec_comp *comp;
        struct ec_comp_group *cur_node;
        struct ec_comp_item *cur_match;
 
index 1ed67f0..dee4123 100644 (file)
@@ -201,7 +201,7 @@ unsigned int ec_comp_count(
  */
 struct ec_comp_iter {
        enum ec_comp_type type;
-       struct ec_comp *comp;
+       const struct ec_comp *comp;
        struct ec_comp_group *cur_node;
        struct ec_comp_item *cur_match;
 };
@@ -212,7 +212,7 @@ struct ec_comp_iter {
  *
  */
 struct ec_comp_iter *
-ec_comp_iter(struct ec_comp *comp,
+ec_comp_iter(const struct ec_comp *comp,
        enum ec_comp_type type);
 
 /**
index be7e380..2414dc0 100644 (file)
@@ -34,7 +34,7 @@ enum ec_log_level {
  * Register a log type.
  *
  * This macro defines a function that will be called at startup (using
- * the "constructor" attribute). This function register the named type
+ * the "constructor" attribute). This function registers the named type
  * passed as argument, and sets a static global variable
  * "ec_log_local_type". This variable is used as the default log type
  * for this file when using EC_LOG() or EC_VLOG().
index 6396fc1..917af3a 100644 (file)
@@ -328,6 +328,9 @@ bool ec_parse_has_child(const struct ec_parse *parse)
 
 const struct ec_node *ec_parse_get_node(const struct ec_parse *parse)
 {
+       if (parse == NULL)
+               return NULL;
+
        return parse->node;
 }
 
index 79e644f..9d6d5d5 100644 (file)
@@ -17,6 +17,7 @@
 #include <sys/types.h>
 #include <limits.h>
 #include <stdio.h>
+#include <stdbool.h>
 
 struct ec_node;
 struct ec_parse;
index 7723818..fd427b4 100644 (file)
@@ -6,6 +6,7 @@
 #include <stddef.h>
 #include <string.h>
 #include <stdio.h>
+#include <ctype.h>
 
 #include <ecoli_assert.h>
 #include <ecoli_malloc.h>
@@ -76,3 +77,13 @@ int ec_asprintf(char **buf, const char *fmt, ...)
 
        return ret;
 }
+
+bool ec_str_is_space(const char *s)
+{
+       while (*s) {
+               if (!isspace(*s))
+                       return false;
+               s++;
+       }
+       return true;
+}
index add73a0..a523b88 100644 (file)
@@ -6,6 +6,7 @@
 #define ECOLI_STRING_
 
 #include <stddef.h>
+#include <stdbool.h>
 
 /* count the number of identical chars at the beginning of 2 strings */
 size_t ec_strcmp_count(const char *s1, const char *s2);
@@ -19,4 +20,7 @@ int ec_asprintf(char **buf, const char *fmt, ...);
 /* like vasprintf, but use libecoli allocator */
 int ec_vasprintf(char **buf, const char *fmt, va_list ap);
 
+/* return true if string is only composed of spaces (' ', '\n', ...) */
+bool ec_str_is_space(const char *s);
+
 #endif
diff --git a/libecoli/ecoli_utils.h b/libecoli/ecoli_utils.h
new file mode 100644 (file)
index 0000000..5a14192
--- /dev/null
@@ -0,0 +1,16 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright 2018, Olivier MATZ <zer0@droids-corp.org>
+ */
+
+#ifndef ECOLI_UTILS_
+#define ECOLI_UTILS_
+
+/**
+ * Cast a variable into a type, ensuring its initial type first
+ */
+#define EC_CAST(x, old_type, new_type) ({      \
+       old_type __x = (x);                     \
+       (new_type)__x;                          \
+       })
+
+#endif
diff --git a/libecoli_editline/ecoli_editline.c b/libecoli_editline/ecoli_editline.c
new file mode 100644 (file)
index 0000000..4cc7ec4
--- /dev/null
@@ -0,0 +1,684 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright 2018, Olivier MATZ <zer0@droids-corp.org>
+ */
+
+#include <stdio.h>
+#include <stdlib.h>
+#include <errno.h>
+
+#include <histedit.h>
+
+#include <ecoli_utils.h>
+#include <ecoli_malloc.h>
+#include <ecoli_string.h>
+#include <ecoli_editline.h>
+#include <ecoli_keyval.h>
+#include <ecoli_node.h>
+#include <ecoli_parse.h>
+#include <ecoli_complete.h>
+
+struct ec_editline {
+       EditLine *el;
+       History *history;
+       HistEvent histev;
+       const struct ec_node *node;
+       char *prompt;
+};
+
+/* used by qsort below */
+static int
+strcasecmp_cb(const void *p1, const void *p2)
+{
+       return strcasecmp(*(char * const *)p1, *(char * const *)p2);
+}
+
+/* Show the matches as a multi-columns list */
+int
+ec_editline_print_cols(struct ec_editline *editline,
+               char const * const *matches, size_t n)
+{
+       size_t max_strlen = 0, len, i, j, ncols;
+       int width, height;
+       const char *space;
+       char **matches_copy = NULL;
+       FILE *f;
+
+       if (el_get(editline->el, EL_GETFP, 1, &f))
+               return -1;
+
+       fprintf(f, "\n");
+       if (n == 0)
+               return 0;
+
+       if (el_get(editline->el, EL_GETTC, "li", &height, (void *)0))
+               return -1;
+       if (el_get(editline->el, EL_GETTC, "co", &width, (void *)0))
+               return -1;
+
+       /* duplicate the matches table, and sort it */
+       matches_copy = calloc(n, sizeof(const char *));
+       if (matches_copy == NULL)
+               return -1;
+       memcpy(matches_copy, matches, sizeof(const char *) * n);
+       qsort(matches_copy, n, sizeof(char *), strcasecmp_cb);
+
+       /* get max string length */
+       for (i = 0; i < n; i++) {
+               len = strlen(matches_copy[i]);
+               if (len > max_strlen)
+                       max_strlen = len;
+       }
+
+       /* write the columns */
+       ncols = width / (max_strlen + 4);
+       if (ncols == 0)
+               ncols = 1;
+       for (i = 0; i < n; i+= ncols) {
+               for (j = 0; j < ncols; j++) {
+                       if (i + j >= n)
+                               break;
+                       if (j == 0)
+                               space = "";
+                       else
+                               space = "    ";
+                       fprintf(f, "%s%-*s", space,
+                               (int)max_strlen, matches[i+j]);
+               }
+               fprintf(f, "\n");
+       }
+
+       free(matches_copy);
+       return 0;
+}
+
+/* Show the helps on editline output */
+int
+ec_editline_print_helps(struct ec_editline *editline,
+                       const struct ec_editline_help *helps, size_t len)
+{
+       size_t i;
+       FILE *out;
+
+       if (el_get(editline->el, EL_GETFP, 1, &out))
+               return -1;
+
+       for (i = 0; i < len; i++) {
+               if (fprintf(out, "%-20s %s\n",
+                               helps[i].desc, helps[i].help) < 0)
+                       return -1;
+       }
+
+       return 0;
+}
+
+void
+ec_editline_free_helps(struct ec_editline_help *helps, size_t len)
+{
+       size_t i;
+
+       if (helps == NULL)
+               return;
+       for (i = 0; i < len; i++) {
+               ec_free(helps[i].desc);
+               ec_free(helps[i].help);
+       }
+       ec_free(helps);
+}
+
+int
+ec_editline_set_prompt(struct ec_editline *editline, const char *prompt)
+{
+       char *copy = NULL;
+
+       if (prompt != NULL) {
+               ec_strdup(prompt);
+               if (copy == NULL)
+                       return -1;
+       }
+
+       ec_free(editline->prompt);
+       editline->prompt = copy;
+
+       return 0;
+}
+
+static char *
+prompt_cb(EditLine *el)
+{
+       struct ec_editline *editline;
+       void *clientdata;
+
+       if (el_get(el, EL_CLIENTDATA, &clientdata))
+               return "> ";
+       editline = clientdata;
+
+       if (editline == NULL)
+               return "> ";
+
+       return editline->prompt;
+}
+
+struct ec_editline *
+ec_editline(const char *name, FILE *f_in, FILE *f_out, FILE *f_err,
+       unsigned int flags)
+{
+       struct ec_editline *editline = NULL;
+       EditLine *el;
+
+       if (f_in == NULL || f_out == NULL || f_err == NULL) {
+               errno = EINVAL;
+               goto fail;
+       }
+
+       editline = ec_calloc(1, sizeof(*editline));
+       if (editline == NULL)
+               goto fail;
+
+       el = el_init(name, f_in, f_out, f_err);
+       if (el == NULL)
+               goto fail;
+       editline->el = el;
+
+       /* save editline pointer as user data */
+       if (el_set(el, EL_CLIENTDATA, editline))
+               goto fail;
+
+       /* install default editline signals */
+       if (el_set(el, EL_SIGNAL, 1))
+               goto fail;
+
+       if (el_set(el, EL_PREP_TERM, 0))
+               goto fail;
+
+       /* use emacs bindings */
+       if (el_set(el, EL_EDITOR, "emacs"))
+               goto fail;
+       if (el_set(el, EL_BIND, "^W", "ed-delete-prev-word", NULL))
+               goto fail;
+
+       /* ask terminal to not send signals */
+       if (flags & EC_EDITLINE_DISABLE_SIGNALS) {
+               if (el_set(el, EL_SETTY, "-d", "-isig", NULL))
+                       goto fail;
+       }
+
+       /* set prompt */
+       editline->prompt = ec_strdup("> ");
+       if (editline->prompt == NULL)
+               goto fail;
+       if (el_set(el, EL_PROMPT, prompt_cb))
+               goto fail;
+
+       /* set up history */
+       if ((flags & EC_EDITLINE_DISABLE_HISTORY) == 0) {
+               if (ec_editline_set_history(
+                               editline, EC_EDITLINE_HISTORY_SIZE) < 0)
+                       goto fail;
+       }
+
+       /* register completion callback */
+       if ((flags & EC_EDITLINE_DISABLE_COMPLETION) == 0) {
+               if (el_set(el, EL_ADDFN, "ed-complete", "Complete buffer",
+                               ec_editline_complete))
+                       goto fail;
+               if (el_set(el, EL_BIND, "^I", "ed-complete", NULL))
+                       goto fail;
+               if (el_set(el, EL_BIND, "?", "ed-complete", NULL))
+                       goto fail;
+       }
+
+       return editline;
+
+fail:
+       ec_editline_free(editline);
+       return NULL;
+}
+
+void ec_editline_free(struct ec_editline *editline)
+{
+       if (editline == NULL)
+               return;
+       if (editline->el != NULL)
+               el_end(editline->el);
+       if (editline->history != NULL)
+               history_end(editline->history);
+       ec_free(editline->prompt);
+       ec_free(editline);
+}
+
+EditLine *ec_editline_get_el(struct ec_editline *editline)
+{
+       return editline->el;
+}
+
+const struct ec_node *
+ec_editline_get_node(struct ec_editline *editline)
+{
+       return editline->node;
+}
+
+void
+ec_editline_set_node(struct ec_editline *editline, const struct ec_node *node)
+{
+       editline->node = node;
+}
+
+int ec_editline_set_history(struct ec_editline *editline,
+       size_t hist_size)
+{
+       EditLine *el = editline->el;
+
+       if (editline->history != NULL)
+               history_end(editline->history);
+
+       if (hist_size == 0)
+               return 0;
+
+       editline->history = history_init();
+       if (editline->history == NULL)
+               goto fail;
+       if (history(editline->history, &editline->histev, H_SETSIZE,
+                       hist_size) < 0)
+               goto fail;
+       if (history(editline->history, &editline->histev, H_SETUNIQUE, 1))
+               goto fail;
+       if (el_set(el, EL_HIST, history, editline->history))
+               goto fail;
+
+       return 0;
+
+fail:
+       //XXX errno
+       if (editline->history != NULL) {
+               history_end(editline->history);
+               editline->history = NULL;
+       }
+       return -1;
+}
+
+void ec_editline_free_completions(char **matches, size_t len)
+{
+       size_t i;
+
+       // XXX use ec_malloc/ec_free() instead for consistency
+       if (matches == NULL)
+               return;
+       for (i = 0; i < len; i++)
+               free(matches[i]);
+       free(matches);
+}
+
+ssize_t
+ec_editline_get_completions(const struct ec_comp *cmpl, char ***matches_out)
+{
+       const struct ec_comp_item *item;
+       struct ec_comp_iter *iter = NULL;
+       char **matches = NULL;
+       size_t count = 0;
+
+       iter = ec_comp_iter(cmpl, EC_COMP_FULL | EC_COMP_PARTIAL);
+       if (iter == NULL)
+               goto fail;
+
+       while ((item = ec_comp_iter_next(iter)) != NULL) {
+               char **tmp;
+
+               tmp = realloc(matches, (count + 1) * sizeof(char *));
+               if (tmp == NULL)
+                       goto fail;
+               matches = tmp;
+               matches[count] = strdup(ec_comp_item_get_display(item));
+               if (matches[count] == NULL)
+                       goto fail;
+               count++;
+       }
+
+       *matches_out = matches;
+       return count;
+
+fail:
+       ec_editline_free_completions(matches, count);
+       *matches_out = NULL;
+       ec_comp_iter_free(iter);
+       return -1;
+}
+
+char *
+ec_editline_append_chars(const struct ec_comp *cmpl)
+{
+       const struct ec_comp_item *item;
+       struct ec_comp_iter *iter = NULL;
+       const char *append;
+       char *ret = NULL;
+       size_t n;
+
+       iter = ec_comp_iter(cmpl, EC_COMP_FULL | EC_COMP_PARTIAL);
+       if (iter == NULL)
+               goto fail;
+
+       while ((item = ec_comp_iter_next(iter)) != NULL) {
+               append = ec_comp_item_get_completion(item);
+               if (ret == NULL) {
+                       ret = ec_strdup(append);
+                       if (ret == NULL)
+                               goto fail;
+               } else {
+                       n = ec_strcmp_count(ret, append);
+                       ret[n] = '\0';
+               }
+       }
+       ec_comp_iter_free(iter);
+
+       return ret;
+
+fail:
+       ec_comp_iter_free(iter);
+       ec_free(ret);
+
+       return NULL;
+}
+
+/* this function builds the help string */
+static int get_node_help(const struct ec_comp_item *item,
+                       struct ec_editline_help *help)
+{
+       const struct ec_comp_group *grp;
+       const struct ec_parse *state;
+       const struct ec_node *node;
+       const char *node_help = NULL;
+       const char *node_desc = NULL;
+
+       help->desc = NULL;
+       help->help = NULL;
+
+       grp = ec_comp_item_get_grp(item);
+
+       for (state = grp->state; state != NULL;
+            state = ec_parse_get_parent(state)) {
+               node = ec_parse_get_node(state);
+               if (node_help == NULL)
+                       node_help = ec_keyval_get(ec_node_attrs(node), "help");
+               if (node_desc == NULL)
+                       node_desc = ec_node_desc(node);
+       }
+
+       if (node_help == NULL)
+               node_help = "";
+       if (node_desc == NULL)
+               goto fail;
+
+       help->desc = ec_strdup(node_desc);
+       if (help->desc == NULL)
+               goto fail;
+
+       help->help = ec_strdup(node_help);
+       if (help->help == NULL)
+               goto fail;
+
+       return 0;
+
+fail:
+       ec_free(help->desc);
+       ec_free(help->help);
+       return -1;
+}
+
+ssize_t
+ec_editline_get_helps(const struct ec_editline *editline, const char *line,
+       const char *full_line, struct ec_editline_help **helps_out)
+{
+       struct ec_comp_iter *iter = NULL;
+       const struct ec_comp_group *grp, *prev_grp = NULL;
+       const struct ec_comp_item *item;
+       struct ec_comp *cmpl = NULL;
+       struct ec_parse *parse = NULL;
+       unsigned int count = 0;
+       struct ec_editline_help *helps = NULL;
+
+       *helps_out = NULL;
+
+       /* check if the current line matches */
+       parse = ec_node_parse(editline->node, full_line);
+       if (ec_parse_matches(parse))
+               count = 1;
+       ec_parse_free(parse);
+       parse = NULL;
+
+       /* complete at current cursor position */
+       cmpl = ec_node_complete(editline->node, line);
+       if (cmpl == NULL) //XXX log error
+               goto fail;
+
+       /* let's display one contextual help per node */
+       iter = ec_comp_iter(cmpl,
+               EC_COMP_UNKNOWN | EC_COMP_FULL | EC_COMP_PARTIAL);
+       if (iter == NULL)
+               goto fail;
+
+       helps = ec_calloc(1, sizeof(*helps));
+       if (helps == NULL)
+               goto fail;
+       if (count == 1) {
+               helps[0].desc = ec_strdup("<return>");
+               if (helps[0].desc == NULL)
+                       goto fail;
+               helps[0].help = ec_strdup("Validate command.");
+               if (helps[0].help == NULL)
+                       goto fail;
+       }
+
+       while ((item = ec_comp_iter_next(iter)) != NULL) {
+               struct ec_editline_help *tmp = NULL;
+
+               /* keep one help per group, skip other items  */
+               grp = ec_comp_item_get_grp(item);
+               if (grp == prev_grp)
+                       continue;
+
+               prev_grp = grp;
+
+               tmp = ec_realloc(helps, (count + 1) * sizeof(*helps));
+               if (tmp == NULL)
+                       goto fail;
+               helps = tmp;
+               if (get_node_help(item, &helps[count]) < 0)
+                       goto fail;
+               count++;
+       }
+
+       ec_comp_iter_free(iter);
+       ec_comp_free(cmpl);
+       *helps_out = helps;
+
+       return count;
+
+fail:
+       ec_comp_iter_free(iter);
+       ec_parse_free(parse);
+       ec_comp_free(cmpl);
+       if (helps != NULL) {
+               while (count--) {
+                       ec_free(helps[count].desc);
+                       ec_free(helps[count].help);
+               }
+               ec_free(helps);
+       }
+
+       return -1;
+}
+
+int
+ec_editline_complete(EditLine *el, int c)
+{
+       struct ec_editline *editline;
+       const LineInfo *line_info;
+       int ret = CC_REFRESH;
+       struct ec_comp *cmpl = NULL;
+       char *append = NULL;
+       char *line = NULL;
+       void *clientdata;
+       FILE *out, *err;
+       int len;
+
+       if (el_get(el, EL_GETFP, 1, &out))
+               return -1;
+       if (el_get(el, EL_GETFP, 1, &err))
+               return -1;
+
+       (void)c;
+
+       if (el_get(el, EL_CLIENTDATA, &clientdata)) {
+               fprintf(err, "completion failure: no client data\n");
+               goto fail;
+       }
+       editline = clientdata;
+       (void)editline;
+
+       line_info = el_line(el);
+       if (line_info == NULL) {
+               fprintf(err, "completion failure: no line info\n");
+               goto fail;
+       }
+
+       len = line_info->cursor - line_info->buffer;
+       if (ec_asprintf(&line, "%.*s", len, line_info->buffer) < 0) {
+               fprintf(err, "completion failure: no memory\n");
+               goto fail;
+       }
+
+       if (editline->node == NULL) {
+               fprintf(err, "completion failure: no ec_node\n");
+               goto fail;
+       }
+
+       cmpl = ec_node_complete(editline->node, line);
+       if (cmpl == NULL)
+               goto fail;
+
+       append = ec_editline_append_chars(cmpl);
+
+       if (c == '?') {
+               struct ec_editline_help *helps = NULL;
+               ssize_t count = 0;
+
+               count = ec_editline_get_helps(editline, line, line_info->buffer,
+                               &helps);
+
+               fprintf(out, "\n");
+               if (ec_editline_print_helps(editline, helps, count) < 0) {
+                       fprintf(err, "completion failure: cannot show help\n");
+                       ec_editline_free_helps(helps, count);
+                       goto fail;
+               }
+
+               ec_editline_free_helps(helps, count);
+               ret = CC_REDISPLAY;
+       } else if (append == NULL || strcmp(append, "") == 0) {
+               char **matches = NULL;
+               ssize_t count = 0;
+
+               count = ec_editline_get_completions(cmpl, &matches);
+               if (count < 0) {
+                       fprintf(err, "completion failure: cannot get completions\n");
+                       goto fail;
+               }
+
+               if (ec_editline_print_cols(
+                               editline,
+                               EC_CAST(matches, char **,
+                                       char const * const *),
+                               count) < 0) {
+                       fprintf(err, "completion failure: cannot print\n");
+                       ec_editline_free_completions(matches, count);
+                       goto fail;
+               }
+
+               ec_editline_free_completions(matches, count);
+               ret = CC_REDISPLAY;
+       } else {
+               if (el_insertstr(el, append) < 0) {
+                       fprintf(err, "completion failure: cannot insert\n");
+                       goto fail;
+               }
+               if (ec_comp_count(cmpl, EC_COMP_FULL) +
+                       ec_comp_count(cmpl, EC_COMP_PARTIAL) == 1) {
+                       if (el_insertstr(el, " ") < 0) {
+                               fprintf(err, "completion failure: cannot insert space\n");
+                               goto fail;
+                       }
+               }
+       }
+
+       ec_comp_free(cmpl);
+       ec_free(line);
+       ec_free(append);
+
+       return ret;
+
+fail:
+       ec_comp_free(cmpl);
+       ec_free(line);
+       ec_free(append);
+
+       return CC_ERROR;
+}
+
+char *
+ec_editline_gets(struct ec_editline *editline)
+{
+       EditLine *el = editline->el;
+       char *line_copy = NULL;
+       const char *line;
+       int count;
+
+       line = el_gets(el, &count);
+       if (line == NULL)
+               return NULL;
+
+       line_copy = ec_strdup(line);
+       if (line_copy == NULL)
+               goto fail;
+
+       line_copy[strlen(line_copy) - 1] = '\0'; //XXX needed because of sh_lex bug?
+
+       if (editline->history != NULL && !ec_str_is_space(line_copy)) {
+               history(editline->history, &editline->histev,
+                       H_ENTER, line_copy);
+       }
+
+       return line_copy;
+
+fail:
+       ec_free(line_copy);
+       return NULL;
+}
+
+struct ec_parse *
+ec_editline_parse(struct ec_editline *editline, const struct ec_node *node)
+{
+       char *line = NULL;
+       struct ec_parse *parse = NULL;
+
+       /* XXX add sh_lex automatically? This node is required, parse and
+        * complete are based on it. */
+
+       ec_editline_set_node(editline, node);
+
+       line = ec_editline_gets(editline);
+       if (line == NULL)
+               goto fail;
+
+       parse = ec_node_parse(node, line);
+       if (parse == NULL)
+               goto fail;
+
+       ec_free(line);
+       return parse;
+
+fail:
+       ec_free(line);
+       ec_parse_free(parse);
+
+       return NULL;
+}
+
diff --git a/libecoli_editline/ecoli_editline.h b/libecoli_editline/ecoli_editline.h
new file mode 100644 (file)
index 0000000..58824d2
--- /dev/null
@@ -0,0 +1,163 @@
+/* SPDX-License-Identifier: BSD-3-Clause
+ * Copyright 2018, Olivier MATZ <zer0@droids-corp.org>
+ */
+
+/**
+ * Helpers that can be used to associate an editline instance with
+ * an ecoli node tree.
+ *
+ * XXX support saved history
+ * XXX support multiline edition
+ * XXX set prompt
+ */
+
+#ifndef ECOLI_EDITLINE_
+#define ECOLI_EDITLINE_
+
+#include <histedit.h>
+
+struct ec_editline;
+struct ec_node;
+struct ec_parse;
+struct ec_comp;
+
+struct ec_editline_help {
+       char *desc;
+       char *help;
+};
+
+/**
+ * Default history size.
+ */
+#define EC_EDITLINE_HISTORY_SIZE 128
+
+/**
+ * Ask the terminal to not send signals (STOP, SUSPEND, XXX). The
+ * ctrl-c, ctrl-z will be interpreted as standard characters. An
+ * action can be associated to these characters with:
+ *
+ *     static int cb(EditLine *editline, int c) {
+ *     {
+ *             see editline documentation for details
+ *     }
+ *
+ *     if (el_set(el, EL_ADDFN, "ed-foobar", "Help string about foobar", cb))
+ *             handle_error;
+ *     if (el_set(el, EL_BIND, "^C", "ed-break", NULL))
+ *             handle_error;
+ *
+ * The default behavior (without this flag) is to let the signal pass: ctrl-c
+ * will stop program and ctrl-z will suspend it.
+ */
+#define EC_EDITLINE_DISABLE_SIGNALS 0x01
+
+/**
+ * Disable history. The default behavior creates an history with
+ * EC_EDITLINE_HISTORY_SIZE entries. To change this value, use
+ * ec_editline_set_history().
+ */
+#define EC_EDITLINE_DISABLE_HISTORY 0x02
+
+/**
+ * Disable completion. The default behavior is to complete when
+ * '?' or '<tab>' is hit. You can register your own callback with:
+ *
+ *     if (el_set(el, EL_ADDFN, "ed-complete", "Complete buffer", callback))
+ *             handle_error;
+ *     if (el_set(el, EL_BIND, "^I", "ed-complete", NULL))
+ *             handle_error;
+ *
+ * The default used callback is ec_editline_complete().
+ */
+#define EC_EDITLINE_DISABLE_COMPLETION 0x04
+
+typedef int (*ec_editline_cmpl_t)(struct ec_editline *editline, int c);
+
+/**
+ * Create an editline instance with default behavior.
+ *
+ * XXX Wrapper to editline's el_init() 
+ *
+ * It 
+ */
+struct ec_editline *
+ec_editline(const char *name, FILE *f_in, FILE *f_out, FILE *f_err,
+       unsigned int flags);
+
+/**
+ * Free an editline instance allocated with ec_editline().
+ */
+void ec_editline_free(struct ec_editline *editline);
+
+/**
+ * Return the editline instance attached to the ec_editline object.
+ */
+EditLine *ec_editline_get_el(struct ec_editline *editline);
+
+// XXX public?
+const struct ec_node *ec_editline_get_node(struct ec_editline *editline);
+void ec_editline_set_node(struct ec_editline *editline,
+                       const struct ec_node *node);
+
+//XXX get history, get_...
+
+/**
+ * Change the history size.
+ *
+ * The default behavior is to have an history whose size
+ * is EC_EDITLINE_HISTORY_SIZE. This can be changed with this
+ * function.
+ *
+ * @param editline
+ *   The pointer to the ec_editline structure.
+ * @param hist_size
+ *   The desired size of the history.
+ * @return
+ *   0 on success, or -1 on error (errno is set).
+ */
+int ec_editline_set_history(struct ec_editline *editline,
+       size_t hist_size);
+
+int
+ec_editline_print_cols(struct ec_editline *editline,
+               char const * const *matches, size_t n);
+
+void ec_editline_free_completions(char **matches, size_t len);
+ssize_t
+ec_editline_get_completions(const struct ec_comp *cmpl, char ***matches_out);
+char *
+ec_editline_append_chars(const struct ec_comp *cmpl);
+
+ssize_t
+ec_editline_get_helps(const struct ec_editline *editline, const char *line,
+       const char *full_line, struct ec_editline_help **helps_out);
+int
+ec_editline_print_helps(struct ec_editline *editline,
+                       const struct ec_editline_help *helps, size_t n);
+void
+ec_editline_free_helps(struct ec_editline_help *helps, size_t len);
+
+int
+ec_editline_set_prompt(struct ec_editline *editline, const char *prompt);
+
+
+
+
+/**
+ * Get a line.
+ *
+ * The returned line must be freed by the caller using ec_free().
+ */
+char *ec_editline_gets(struct ec_editline *editline);
+
+/**
+ * Get a line (managing completion) and parse it with passed node
+ * XXX find a better name?
+ */
+struct ec_parse *
+ec_editline_parse(struct ec_editline *editline, const struct ec_node *node);
+
+int
+ec_editline_complete(EditLine *el, int c);
+
+#endif
diff --git a/libecoli_editline/editline.c b/libecoli_editline/editline.c
new file mode 100644 (file)
index 0000000..449aebb
--- /dev/null
@@ -0,0 +1,544 @@
+/*
+ * Copyright 2018 6WIND S.A.
+ */
+
+#define _GNU_SOURCE
+#include <stdio.h>
+#include <stdbool.h>
+#include <stdlib.h>
+#include <errno.h>
+#include <string.h>
+#include <assert.h>
+#include <ctype.h>
+#include <stdarg.h>
+#include <signal.h>
+#include <unistd.h>
+
+#include <histedit.h>
+
+#include "string_utils.h"
+#include "editline.h"
+
+#define NC_CLI_HISTORY_SIZE 128
+
+struct nc_cli_editline {
+       EditLine *editline;
+       History *history;
+       HistEvent histev;
+       bool break_received;
+       nc_cli_editline_complete_t complete;
+       FILE *null_out;
+       bool interactive;
+       bool incomplete_line;
+       char *full_line;
+       char *(*prompt_cb)(EditLine *);
+};
+
+struct nc_cli_editline *nc_cli_el;
+
+static int check_quotes(const char *str)
+{
+       char quote = 0;
+       size_t i = 0;
+
+       while (str[i] != '\0') {
+               if (quote == 0) {
+                       if (str[i] == '"' || str[i] == '\'') {
+                               quote = str[i];
+                       }
+                       i++;
+                       continue;
+               } else {
+                       if (str[i] == quote) {
+                               i++;
+                               quote = 0;
+                       } else if (str[i] == '\\' && str[i+1] == quote) {
+                               i += 2;
+                       } else {
+                               i++;
+                       }
+                       continue;
+               }
+       }
+
+       return quote;
+}
+
+static int
+editline_break(EditLine *editline, int c)
+{
+       struct nc_cli_editline *el;
+       void *ptr;
+
+       (void)c;
+
+       if (el_get(editline, EL_CLIENTDATA, &ptr))
+               return CC_ERROR;
+
+       el = ptr;
+       el->break_received = true;
+       nc_cli_printf(el, "\n");
+
+       return CC_EOF;
+}
+
+static int
+editline_suspend(EditLine *editline, int c)
+{
+       (void)editline;
+       (void)c;
+
+       kill(getpid(), SIGSTOP);
+
+       return CC_NORM;
+}
+
+int
+editline_get_screen_size(struct nc_cli_editline *el, size_t *rows, size_t *cols)
+{
+       int w, h;
+
+       if (rows != NULL) {
+               if (el_get(el->editline, EL_GETTC, "li", &h, (void *)0))
+                       return -1;
+               *rows = h;
+       }
+       if (cols != NULL) {
+               if (el_get(el->editline, EL_GETTC, "co", &w, (void *)0))
+                       return -1;
+               *cols = w;
+       }
+       return 0;
+}
+
+FILE *
+nc_cli_editline_get_file(struct nc_cli_editline *el, int num)
+{
+       FILE *f;
+
+       if (el == NULL)
+               return NULL;
+       if (num > 2)
+               return NULL;
+       if (el_get(el->editline, EL_GETFP, num, &f))
+               return NULL;
+
+       return f;
+}
+
+/* match the prototype expected by qsort() */
+static int
+strcasecmp_cb(const void *p1, const void *p2)
+{
+       return strcasecmp(*(char * const *)p1, *(char * const *)p2);
+}
+
+/* Show the matches as a multi-columns list */
+int
+nc_cli_editline_print_cols(struct nc_cli_editline *el,
+                       char const * const *matches, size_t n)
+{
+       size_t max_strlen = 0, len, i, j, ncols;
+       size_t width, height;
+       const char *space;
+       char **matches_copy = NULL;
+
+       nc_cli_printf(nc_cli_el, "\n");
+       if (n == 0)
+               return 0;
+
+       if (editline_get_screen_size(el, &height, &width) < 0)
+               width = 80;
+
+       /* duplicate the matches table, and sort it */
+       matches_copy = calloc(n, sizeof(const char *));
+       if (matches_copy == NULL)
+               return -1;
+       memcpy(matches_copy, matches, sizeof(const char *) * n);
+       qsort(matches_copy, n, sizeof(char *), strcasecmp_cb);
+
+       /* get max string length */
+       for (i = 0; i < n; i++) {
+               len = strlen(matches_copy[i]);
+               if (len > max_strlen)
+                       max_strlen = len;
+       }
+
+       /* write the columns */
+       ncols = width / (max_strlen + 4);
+       if (ncols == 0)
+               ncols = 1;
+       for (i = 0; i < n; i+= ncols) {
+               for (j = 0; j < ncols; j++) {
+                       if (i + j >= n)
+                               break;
+                       if (j == 0)
+                               space = "";
+                       else
+                               space = "    ";
+                       nc_cli_printf(nc_cli_el, "%s%-*s", space,
+                               (int)max_strlen, matches[i+j]);
+               }
+               nc_cli_printf(nc_cli_el, "\n");
+       }
+
+       free(matches_copy);
+       return 0;
+}
+
+static int
+editline_complete(EditLine *editline, int c)
+{
+       enum nc_cli_editline_complete_status ret;
+       const LineInfo *line_info;
+       struct nc_cli_editline *el;
+       int len;
+       char *line;
+       void *ptr;
+
+       if (el_get(editline, EL_CLIENTDATA, &ptr))
+               return CC_ERROR;
+
+       el = ptr;
+
+       if (el->complete == NULL)
+               return CC_NORM;
+
+       line_info = el_line(editline);
+       if (line_info == NULL)
+               return CC_ERROR;
+
+       len = line_info->cursor - line_info->buffer;
+       if (asprintf(&line, "%s%.*s", el->full_line ? : "", len,
+                       line_info->buffer) < 0)
+               return CC_ERROR;
+
+       if (c == '?' && check_quotes(line) != 0) {
+               free(line);
+               el_insertstr(editline, "?");
+               return CC_REFRESH;
+       }
+
+       ret = el->complete(c, line);
+       free(line);
+
+       if (ret == ERROR)
+               return CC_ERROR;
+       else if (ret == REDISPLAY)
+               return CC_REDISPLAY;
+       else
+               return CC_REFRESH;
+}
+
+static bool is_blank_string(const char *s)
+{
+       while (*s) {
+               if (!isspace(*s))
+                       return false;
+               s++;
+       }
+       return true;
+}
+
+static char *
+multiline_prompt_cb(EditLine *e)
+{
+       (void)e;
+       return strdup("... ");
+}
+
+const char *
+nc_cli_editline_edit(struct nc_cli_editline *el)
+{
+       const char *line;
+       int count;
+
+       assert(el->editline != NULL);
+
+       if (el->incomplete_line == false) {
+               free(el->full_line);
+               el->full_line = NULL;
+       }
+
+       el->break_received = false;
+
+       if (el->incomplete_line)
+               el_set(el->editline, EL_PROMPT, multiline_prompt_cb);
+       else
+               el_set(el->editline, EL_PROMPT, el->prompt_cb);
+
+       line = el_gets(el->editline, &count);
+
+       if (line == NULL && el->break_received) {
+               free(el->full_line);
+               el->full_line = NULL;
+               el->incomplete_line = false;
+               return ""; /* abort current line */
+       }
+
+       if (line == NULL || astrcat(&el->full_line, line) < 0) {
+               free(el->full_line);
+               el->full_line = NULL;
+               el->incomplete_line = false;
+               return NULL; /* error / eof */
+       }
+
+       if (check_quotes(el->full_line) != 0) {
+               el->incomplete_line = true;
+               return "";
+       }
+
+       el->incomplete_line = false;
+       if (el->history != NULL && !is_blank_string(el->full_line))
+               history(el->history, &el->histev,
+                       H_ENTER, el->full_line);
+
+       return el->full_line;
+}
+
+int
+nc_cli_editline_register_complete(struct nc_cli_editline *el,
+                               nc_cli_editline_complete_t complete)
+{
+       const char *name;
+
+       if (el_set(el->editline, EL_ADDFN, "ed-complete",
+                       "Complete buffer",
+                       editline_complete))
+               return -1;
+
+       if (complete != NULL)
+               name = "ed-complete";
+       else
+               name = "ed-unassigned";
+       if (el_set(el->editline, EL_BIND, "^I", name, NULL))
+               return -1;
+
+       if (complete != NULL)
+               name = "ed-complete";
+       else
+               name = "ed-insert";
+       if (el_set(el->editline, EL_BIND, "?", name, NULL))
+               return -1;
+
+       el->complete = complete;
+
+       return 0;
+}
+
+int
+nc_cli_editline_insert_str(struct nc_cli_editline *el, const char *str)
+{
+       return el_insertstr(el->editline, str);
+}
+
+int nc_cli_printf(struct nc_cli_editline *el, const char *format, ...)
+{
+       FILE *out = nc_cli_editline_get_file(el, 1);
+       va_list ap;
+       int ret;
+
+       if (out == NULL)
+               out = stdout;
+
+       va_start(ap, format);
+       ret = vfprintf(out, format, ap);
+       va_end(ap);
+
+       return ret;
+}
+
+int nc_cli_eprintf(struct nc_cli_editline *el, const char *format, ...)
+{
+       FILE *out = nc_cli_editline_get_file(el, 2);
+       va_list ap;
+       int ret;
+
+       if (out == NULL)
+               out = stderr;
+
+       va_start(ap, format);
+       ret = vfprintf(out, format, ap);
+       va_end(ap);
+
+       return ret;
+}
+
+bool
+nc_cli_editline_is_interactive(struct nc_cli_editline *el)
+{
+       return el->interactive;
+}
+
+bool
+nc_cli_editline_is_running(struct nc_cli_editline *el)
+{
+       return el == nc_cli_el;
+}
+
+void
+nc_cli_editline_start(struct nc_cli_editline *el)
+{
+       nc_cli_el = el;
+}
+
+void
+nc_cli_editline_stop(struct nc_cli_editline *el)
+{
+       if (el == nc_cli_el)
+               nc_cli_el = NULL;
+}
+
+int
+nc_cli_editline_getc(struct nc_cli_editline *el)
+{
+       char c;
+
+       if (el->interactive == false)
+               return -1;
+
+       if (el_getc(el->editline, &c) != 1)
+               return -1;
+
+       return c;
+}
+
+int
+nc_cli_editline_resize(struct nc_cli_editline *el)
+{
+       el_resize(el->editline);
+       return 0;
+}
+
+int nc_cli_editline_set_prompt_cb(struct nc_cli_editline *el,
+                               char *(*prompt_cb)(EditLine *el))
+{
+       el->prompt_cb = prompt_cb;
+       return 0;
+}
+
+int
+nc_cli_editline_mask_interrupts(bool do_mask)
+{
+       const char *setty = do_mask ? "-isig" : "+isig";
+
+       if (nc_cli_el == NULL)
+               return -1;
+
+       if (nc_cli_el->interactive == false)
+               return 0;
+
+       if (el_set(nc_cli_el->editline, EL_SETTY, "-d", setty, NULL))
+               return -1;
+
+       return 0;
+}
+
+struct nc_cli_editline *
+nc_cli_editline_init(FILE *f_in, FILE *f_out, FILE *f_err, bool interactive,
+               char *(*prompt_cb)(EditLine *el))
+{
+       struct nc_cli_editline *el = NULL;
+
+       el = calloc(1, sizeof(*el));
+       if (el == NULL)
+               goto fail;
+       if (f_in == NULL) {
+               errno = EINVAL;
+               goto fail;
+       }
+       if (f_out == NULL || f_err == NULL) {
+               el->null_out = fopen("/dev/null", "w");
+               if (el->null_out == NULL)
+                       goto fail;
+       }
+       if (f_out == NULL)
+               f_out = el->null_out;
+       if (f_err == NULL)
+               f_err = el->null_out;
+
+       el->interactive = interactive;
+       el->prompt_cb = prompt_cb;
+
+       el->editline = el_init("nc-cli", f_in, f_out, f_err);
+       if (el->editline == NULL)
+               goto fail;
+
+       if (el_set(el->editline, EL_SIGNAL, 1))
+               goto fail;
+
+       if (el_set(el->editline, EL_CLIENTDATA, el))
+               goto fail;
+
+       if (el_set(el->editline, EL_PROMPT, prompt_cb))
+               goto fail;
+
+       if (interactive == false)
+               goto end;
+
+       if (el_set(el->editline, EL_PREP_TERM, 0))
+               goto fail;
+
+       if (el_set(el->editline, EL_EDITOR, "emacs"))
+               goto fail;
+
+       if (el_set(el->editline, EL_SETTY, "-d", "-isig", NULL))
+               goto fail;
+       if (el_set(el->editline, EL_ADDFN, "ed-break",
+                       "Break and flush the buffer",
+                       editline_break))
+               goto fail;
+       if (el_set(el->editline, EL_BIND, "^C", "ed-break", NULL))
+               goto fail;
+       if (el_set(el->editline, EL_ADDFN, "ed-suspend",
+                       "Suspend the terminal",
+                       editline_suspend))
+               goto fail;
+       if (el_set(el->editline, EL_BIND, "^Z", "ed-suspend", NULL))
+               goto fail;
+       if (el_set(el->editline, EL_BIND, "^W", "ed-delete-prev-word", NULL))
+               goto fail;
+
+       el->history = history_init();
+       if (!el->history)
+               goto fail;
+       if (history(el->history, &el->histev, H_SETSIZE,
+                       NC_CLI_HISTORY_SIZE) < 0)
+               goto fail;
+       if (history(el->history, &el->histev,
+                       H_SETUNIQUE, 1))
+               goto fail;
+       if (el_set(el->editline, EL_HIST, history,
+                       el->history))
+               goto fail;
+
+end:
+       return el;
+
+fail:
+       if (el != NULL) {
+               if (el->null_out != NULL)
+                       fclose(el->null_out);
+               if (el->history != NULL)
+                       history_end(el->history);
+               if (el->editline != NULL)
+                       el_end(el->editline);
+               free(el);
+       }
+       return NULL;
+}
+
+void
+nc_cli_editline_free(struct nc_cli_editline *el)
+{
+       if (el == NULL)
+               return;
+       nc_cli_editline_stop(el);
+       if (el->null_out != NULL)
+               fclose(el->null_out);
+       if (el->history != NULL)
+               history_end(el->history);
+       el_end(el->editline);
+       free(el->full_line);
+       free(el);
+}
index 850b414..76592e2 100644 (file)
@@ -11,6 +11,9 @@
 #include <assert.h>
 
 #include <yaml.h>
+
+#include <ecoli_malloc.h>
+#include <ecoli_keyval.h>
 #include <ecoli_node.h>
 #include <ecoli_config.h>
 #include <ecoli_yaml.h>
@@ -348,7 +351,8 @@ parse_ec_node(struct enode_table *table,
 {
        const struct ec_config_schema *schema;
        const struct ec_node_type *type = NULL;
-       const char *id = NULL, *help = NULL;
+       const char *id = NULL;
+       char *help = NULL;
        struct ec_config *config = NULL;
        const yaml_node_t *attrs = NULL;
        const yaml_node_t *key, *value;
@@ -412,7 +416,11 @@ parse_ec_node(struct enode_table *table,
                                fprintf(stderr, "Help must be a scalar\n");
                                goto fail;
                        }
-                       help = value_str;
+                       help = ec_strdup(value_str);
+                       if (help == NULL) {
+                               fprintf(stderr, "Failed to allocate help\n");
+                               goto fail;
+                       }
                }
        }
 
@@ -446,6 +454,16 @@ parse_ec_node(struct enode_table *table,
                goto fail;
        }
 
+       if (help != NULL) {
+               if (ec_keyval_set(ec_node_attrs(enode), "help", help,
+                                       ec_free_func) < 0) {
+                       fprintf(stderr, "Failed to set help\n");
+                       help = NULL;
+                       goto fail;
+               }
+               help = NULL;
+       }
+
        /* add attributes (all as string) */
        //XXX
 
@@ -454,6 +472,8 @@ parse_ec_node(struct enode_table *table,
 fail:
        ec_node_free(enode);
        ec_config_free(config);
+       ec_free(help);
+
        return NULL;
 }
 
diff --git a/parse-yaml.sh b/parse-yaml.sh
new file mode 100644 (file)
index 0000000..1b7d029
--- /dev/null
@@ -0,0 +1,140 @@
+#!/bin/sh
+
+set -e
+
+# use a safer version of echo (no option)
+echo()
+{
+       printf "%s\n" "$*"
+}
+
+debug()
+{
+       echo "$@" >&2
+}
+
+# $1: node sequence number (ex: ec_node4)
+ec_parse_get_first_child()
+{
+       local first_child=${1}_first_child
+       echo $(eval 'echo ${'$first_child'}')
+}
+
+# $1: node sequence number (ex: ec_node4)
+ec_parse_get_next()
+{
+       local next=${1}_next
+       echo $(eval 'echo ${'$next'}')
+}
+
+# $1: node sequence number (ex: ec_node4)
+ec_parse_iter_next()
+{
+       local seq=${1#ec_node}
+       seq=$((seq+1))
+       local next=ec_node${seq}
+       if [ "$(ec_parse_get_id $next)" != "" ]; then
+               echo $next
+       fi
+}
+
+# $1: node sequence number (ex: ec_node4)
+ec_parse_get_id()
+{
+       local id=${1}_id
+       echo $(eval 'echo ${'$id'}')
+}
+
+# $1: node sequence number (ex: ec_node4)
+ec_parse_get_strvec_len()
+{
+       local strvec_len=${1}_strvec_len
+       echo $(eval 'echo ${'$strvec_len'}')
+}
+
+# $1: node sequence number (ex: ec_node4)
+# $2: index in strvec
+ec_parse_get_str()
+{
+       if [ $# -ne 2 ]; then
+               return
+       fi
+       local str=${1}_str${2}
+       echo $(eval 'echo ${'$str'}')
+}
+
+# $1: node sequence number (ex: ec_node4)
+# $2: node id (string)
+ec_parse_find_first()
+{
+       if [ $# -ne 2 ]; then
+               return
+       fi
+       local node_seq=$1
+       while [ "$node_seq" != "" ]; do
+               local id=$(ec_parse_get_id $node_seq)
+               if [ "$id" = "$2" ]; then
+                       echo $node_seq
+                       return 0
+               fi
+               node_seq=$(ec_parse_iter_next $node_seq)
+       done
+}
+
+path=$(dirname $0)
+
+yaml=$(mktemp)
+cat << EOF > $yaml
+type: or
+children:
+- type: seq
+  id: hello
+  help: Say hello to someone
+  children:
+  - type: str
+    string: hello
+  - type: or
+    id: name
+    help: Name of the person to greet
+    children:
+    - type: str
+      string: john
+    - type: str
+      string: mike
+- type: seq
+  id: goodbye
+  help: Say good bye to someone
+  children:
+  - type: str
+    string: good
+  - type: str
+    string: bye
+  - type: or
+    id: name
+    help: Name of the person to greet
+    children:
+    - type: str
+      string: mary
+    - type: str
+      string: jessica
+EOF
+
+output=$(mktemp)
+match=1
+$path/build/parse-yaml -i $yaml -o $output || match=0
+if [ "$match" = "1" ]; then
+       cat $output
+       . $output
+       name=$(ec_parse_get_str $(ec_parse_find_first ec_node1 name) 0)
+       hello=$(ec_parse_get_str $(ec_parse_find_first ec_node1 hello) 0)
+
+       if [ "$hello" != "" ]; then
+               echo "$name says hello to you!"
+       else
+               echo "$name says good bye to you!"
+       fi
+else
+       echo "no match"
+fi
+rm $output
+rm $yaml
diff --git a/test-completion.sh b/test-completion.sh
new file mode 100644 (file)
index 0000000..e1d1b31
--- /dev/null
@@ -0,0 +1,18 @@
+
+# complete -F _parser_options dummy_command
+
+_parser_options()
+{
+       local curr_arg
+       local i
+
+       curr_arg=${COMP_WORDS[COMP_CWORD]}
+
+       # remove the command name, only keep args
+       words=( "${COMP_WORDS[@]}" )
+       unset words[0]
+
+       IFS=$'\n' read -d '' -r -a COMPREPLY <<- EOF
+       $(./build/parse-yaml --complete -i examples/yaml/test.yaml -o pipo -- "${words[@]}")
+       EOF
+}
index 1b2b7ef..c874af9 100644 (file)
@@ -39,7 +39,7 @@ static const char ec_short_options[] =
 #define EC_OPT_SEED "seed"
 
 static const struct option ec_long_options[] = {
-       {EC_OPT_HELP, 1, NULL, 'h'},
+       {EC_OPT_HELP, 0, NULL, 'h'},
        {EC_OPT_LOG_LEVEL, 1, NULL, 'l'},
        {EC_OPT_RANDOM_ALLOC_FAIL, 1, NULL, 'r'},
        {EC_OPT_SEED, 1, NULL, 's'},
@@ -48,7 +48,7 @@ static const struct option ec_long_options[] = {
 
 static void usage(const char *prgname)
 {
-       printf("%s [options] [test1 test2 test3...]\n"
+       fprintf(stderr, "%s [options] [test1 test2 test3...]\n"
                "  -h\n"
                "  --"EC_OPT_HELP"\n"
                "      Show this help.\n"
index 4e5d94f..2b84064 100644 (file)
--- a/todo.txt
+++ b/todo.txt
@@ -3,7 +3,7 @@ tk_cmd
 
 X evaluate expression tree in ec_tk_expr
 X cmd token
-- example
+X example
 X tk_re
 
 cleanup / rework
@@ -24,7 +24,7 @@ X iterate children nodes without chaining them
 - better logs
 - check return values (-1 or NULL) + use errno
 - check missing static / const
-- license: SPDX
+X license: SPDX
 - check all completion nodes
 X split ecoli_tk.h
 - size_t or unsigned int?
@@ -37,11 +37,11 @@ X rename:
 X save node path in completion to fix help string
 - code coverage
 - try to hide structures
-- anything better than weakref?
+X anything better than weakref?
 - add ec_node_defaults.[ch] providing usual implementations of node methods
 X use vec for strvec
-- ELOOP in case of loop
-- remove weakref?
+/ ELOOP in case of loop
+X remove weakref?
 - sh_lex to provide offsets in attributes
 - accessors for all structs
 
@@ -61,15 +61,15 @@ yaml
 ====
 
 X register nodes by name
-- interface to add attributes: all nodes must be configurable through a
-  generic api
-  - attr string
-  - attr string list
-  - attr node
-  - attr node list
-  - attr int
-
-- yaml interface to create nodes
+X interface to add attributes: all nodes must be configurable through a
+  generic api:
+  X attr string
+  X attr string list
+  X attr node
+  X attr node list
+  X attr int
+
+X yaml interface to create nodes
 - example
 
 examples
@@ -83,6 +83,7 @@ examples
 - configuration file
 - mini shell: cd, ls, cat, stat
 - mini network console based on ip
+- dialog-like for use in shell
 
 doc
 ===
@@ -105,6 +106,7 @@ build framework
 - add make help
 - add make config
 - -fvisibility=
+- use meson
 
 tests
 =====
@@ -115,10 +117,10 @@ new nodes
 =========
 
 - regexp
-- node which always matches
-- file + partial completion
+X node which always matches
+X file + partial completion
 - ether, ip, network
-- fusion node: need to match several children, same for completion
+- fusion node: need to match several children, same for completion?
 - float
 - not
 
@@ -132,7 +134,7 @@ encoding
 netconf example
 ===============
 
-- demonstration example that parses yang file and generate cli
+/ demonstration example that parses yang file and generate cli