--- /dev/null
+/*
+ * 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);
+}