From 181334a4cd586d3c958898dd19291b856c461dee Mon Sep 17 00:00:00 2001 From: Olivier Matz Date: Wed, 31 Oct 2018 19:43:30 +0100 Subject: [PATCH] add ecoli_editline and enhance yaml parser --- Makefile | 10 +- examples/yaml/parse-yaml.c | 283 +++++++++++- examples/yaml/test.yaml | 40 +- libecoli/ecoli_complete.c | 4 +- libecoli/ecoli_complete.h | 4 +- libecoli/ecoli_log.h | 2 +- libecoli/ecoli_parse.c | 3 + libecoli/ecoli_parse.h | 1 + libecoli/ecoli_string.c | 11 + libecoli/ecoli_string.h | 4 + libecoli/ecoli_utils.h | 16 + libecoli_editline/ecoli_editline.c | 684 +++++++++++++++++++++++++++++ libecoli_editline/ecoli_editline.h | 163 +++++++ libecoli_editline/editline.c | 544 +++++++++++++++++++++++ libecoli_yaml/ecoli_yaml.c | 24 +- parse-yaml.sh | 140 ++++++ test-completion.sh | 18 + test/test.c | 4 +- todo.txt | 38 +- 19 files changed, 1947 insertions(+), 46 deletions(-) create mode 100644 libecoli/ecoli_utils.h create mode 100644 libecoli_editline/ecoli_editline.c create mode 100644 libecoli_editline/ecoli_editline.h create mode 100644 libecoli_editline/editline.c create mode 100644 parse-yaml.sh create mode 100644 test-completion.sh diff --git a/Makefile b/Makefile index 7bf0ddb..8ccd7e2 100644 --- 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 diff --git a/examples/yaml/parse-yaml.c b/examples/yaml/parse-yaml.c index d5d8460..9ec8d5f 100644 --- a/examples/yaml/parse-yaml.c +++ b/examples/yaml/parse-yaml.c @@ -4,25 +4,300 @@ #include #include +#include +#include +#include #include +#include +#include #include +#include +#include + +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 -i \n" + " -h\n" + " --"OPT_HELP"\n" + " Show this help.\n" + " -i \n" + " --"OPT_INPUT_FILE"=\n" + " Set the yaml input file describing the grammar.\n" + " -o \n" + " --"OPT_OUTPUT_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; diff --git a/examples/yaml/test.yaml b/examples/yaml/test.yaml index 072fbce..5642766 100644 --- a/examples/yaml/test.yaml +++ b/examples/yaml/test.yaml @@ -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 diff --git a/libecoli/ecoli_complete.c b/libecoli/ecoli_complete.c index 7ad846c..a9becdf 100644 --- a/libecoli/ecoli_complete.c +++ b/libecoli/ecoli_complete.c @@ -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; diff --git a/libecoli/ecoli_complete.h b/libecoli/ecoli_complete.h index 1ed67f0..dee4123 100644 --- a/libecoli/ecoli_complete.h +++ b/libecoli/ecoli_complete.h @@ -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); /** diff --git a/libecoli/ecoli_log.h b/libecoli/ecoli_log.h index be7e380..2414dc0 100644 --- a/libecoli/ecoli_log.h +++ b/libecoli/ecoli_log.h @@ -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(). diff --git a/libecoli/ecoli_parse.c b/libecoli/ecoli_parse.c index 6396fc1..917af3a 100644 --- a/libecoli/ecoli_parse.c +++ b/libecoli/ecoli_parse.c @@ -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; } diff --git a/libecoli/ecoli_parse.h b/libecoli/ecoli_parse.h index 79e644f..9d6d5d5 100644 --- a/libecoli/ecoli_parse.h +++ b/libecoli/ecoli_parse.h @@ -17,6 +17,7 @@ #include #include #include +#include struct ec_node; struct ec_parse; diff --git a/libecoli/ecoli_string.c b/libecoli/ecoli_string.c index 7723818..fd427b4 100644 --- a/libecoli/ecoli_string.c +++ b/libecoli/ecoli_string.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include @@ -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; +} diff --git a/libecoli/ecoli_string.h b/libecoli/ecoli_string.h index add73a0..a523b88 100644 --- a/libecoli/ecoli_string.h +++ b/libecoli/ecoli_string.h @@ -6,6 +6,7 @@ #define ECOLI_STRING_ #include +#include /* 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 index 0000000..5a14192 --- /dev/null +++ b/libecoli/ecoli_utils.h @@ -0,0 +1,16 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright 2018, Olivier MATZ + */ + +#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 index 0000000..4cc7ec4 --- /dev/null +++ b/libecoli_editline/ecoli_editline.c @@ -0,0 +1,684 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright 2018, Olivier MATZ + */ + +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +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(""); + 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 index 0000000..58824d2 --- /dev/null +++ b/libecoli_editline/ecoli_editline.h @@ -0,0 +1,163 @@ +/* SPDX-License-Identifier: BSD-3-Clause + * Copyright 2018, Olivier MATZ + */ + +/** + * 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 + +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 '' 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 index 0000000..449aebb --- /dev/null +++ b/libecoli_editline/editline.c @@ -0,0 +1,544 @@ +/* + * Copyright 2018 6WIND S.A. + */ + +#define _GNU_SOURCE +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#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); +} diff --git a/libecoli_yaml/ecoli_yaml.c b/libecoli_yaml/ecoli_yaml.c index 850b414..76592e2 100644 --- a/libecoli_yaml/ecoli_yaml.c +++ b/libecoli_yaml/ecoli_yaml.c @@ -11,6 +11,9 @@ #include #include + +#include +#include #include #include #include @@ -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 index 0000000..1b7d029 --- /dev/null +++ b/parse-yaml.sh @@ -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 index 0000000..e1d1b31 --- /dev/null +++ b/test-completion.sh @@ -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 +} diff --git a/test/test.c b/test/test.c index 1b2b7ef..c874af9 100644 --- a/test/test.c +++ b/test/test.c @@ -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" diff --git a/todo.txt b/todo.txt index 4e5d94f..2b84064 100644 --- 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 -- 2.20.1