diff --git a/Knot.files b/Knot.files index fc87d0da5fb02f93d1ca122cfd368e0849c6e23b..f6cf5bfb22f1952fcb7871e571bf1be9dc204b6d 100644 --- a/Knot.files +++ b/Knot.files @@ -524,6 +524,8 @@ src/utils/knotd/main.c src/utils/knsec3hash/knsec3hash.c src/utils/knsupdate/knsupdate_exec.c src/utils/knsupdate/knsupdate_exec.h +src/utils/knsupdate/knsupdate_interactive.c +src/utils/knsupdate/knsupdate_interactive.h src/utils/knsupdate/knsupdate_main.c src/utils/knsupdate/knsupdate_params.c src/utils/knsupdate/knsupdate_params.h diff --git a/src/utils/Makefile.inc b/src/utils/Makefile.inc index 806dab5c1199d9065f5938801e505db771adb3ad..c36083e3ef7fdf2322409efc5702aa02ca748675 100644 --- a/src/utils/Makefile.inc +++ b/src/utils/Makefile.inc @@ -64,10 +64,12 @@ knsec3hash_SOURCES = \ utils/knsec3hash/knsec3hash.c knsupdate_SOURCES = \ - utils/knsupdate/knsupdate_exec.c \ - utils/knsupdate/knsupdate_exec.h \ - utils/knsupdate/knsupdate_main.c \ - utils/knsupdate/knsupdate_params.c \ + utils/knsupdate/knsupdate_exec.c \ + utils/knsupdate/knsupdate_exec.h \ + utils/knsupdate/knsupdate_interactive.c \ + utils/knsupdate/knsupdate_interactive.h \ + utils/knsupdate/knsupdate_main.c \ + utils/knsupdate/knsupdate_params.c \ utils/knsupdate/knsupdate_params.h kdig_CPPFLAGS = $(libknotus_la_CPPFLAGS) diff --git a/src/utils/knsupdate/knsupdate_exec.c b/src/utils/knsupdate/knsupdate_exec.c index 6a8108ddffe50acb09c3c53c7c8ab7bea7479bf1..9ffd882254a8710b9b76655c3bc7100badd6ffa7 100644 --- a/src/utils/knsupdate/knsupdate_exec.c +++ b/src/utils/knsupdate/knsupdate_exec.c @@ -23,6 +23,7 @@ #include "libdnssec/random.h" #include "utils/knsupdate/knsupdate_exec.h" +#include "utils/knsupdate/knsupdate_interactive.h" #include "utils/common/exec.h" #include "utils/common/msg.h" #include "utils/common/netio.h" @@ -66,7 +67,7 @@ int cmd_zone(const char* lp, knsupdate_params_t *params); * This way we could identify command byte-per-byte and * cancel early if the next is lexicographically greater. */ -const char* cmd_array[] = { +const char* knsupdate_cmd_array[] = { "\x3" "add", "\x6" "answer", "\x5" "class", /* {classname} */ @@ -474,26 +475,24 @@ static int pkt_sendrecv(knsupdate_params_t *params) return rb; } -static int process_line(char *lp, void *arg) +int knsupdate_process_line(const char *line, knsupdate_params_t *params) { - knsupdate_params_t *params = (knsupdate_params_t *)arg; - /* Check for empty line or comment. */ - if (lp[0] == '\0' || lp[0] == ';') { + if (line[0] == '\0' || line[0] == ';') { return KNOT_EOK; } - int ret = tok_find(lp, cmd_array); + int ret = tok_find(line, knsupdate_cmd_array); if (ret < 0) { return ret; /* Syntax error - do nothing. */ } - const char *cmd = cmd_array[ret]; - const char *val = tok_skipspace(lp + TOK_L(cmd)); + const char *cmd = knsupdate_cmd_array[ret]; + const char *val = tok_skipspace(line + TOK_L(cmd)); ret = cmd_handle[ret](val, params); if (ret != KNOT_EOK) { DBG("operation '%s' failed (%s) on line '%s'\n", - TOK_S(cmd), knot_strerror(ret), lp); + TOK_S(cmd), knot_strerror(ret), line); } return ret; @@ -510,44 +509,23 @@ static int process_lines(knsupdate_params_t *params, FILE *input) { char *buf = NULL; size_t buflen = 0; - bool interactive = is_terminal(input); - int ret = KNOT_EOK; - - /* Print first program prompt if interactive. */ - if (interactive) { - fprintf(stderr, "> "); + if(is_terminal(input)) { + return interactive_loop(params); } + int ret = KNOT_EOK; /* Process lines. */ while (!params->stop && knot_getline(&buf, &buflen, input) != -1) { /* Remove leading and trailing white space. */ char *line = strstrip(buf); - int call_ret = process_line(line, params); + ret = knsupdate_process_line(line, params); memset(line, 0, strlen(line)); free(line); - if (call_ret != KNOT_EOK) { - /* Return the first error. */ - if (ret == KNOT_EOK) { - ret = call_ret; - } - - /* Exit if error and not interactive. */ - if (!interactive) { - break; - } - } - - /* Print program prompt if interactive. */ - if (interactive && !params->stop) { - fprintf(stderr, "> "); + if (ret != KNOT_EOK) { + break; } } - if (interactive && feof(input)) { - /* Terminate line after empty prompt. */ - fprintf(stderr, "\n"); - } - if (buf != NULL) { memset(buf, 0, buflen); free(buf); @@ -564,9 +542,9 @@ int knsupdate_exec(knsupdate_params_t *params) int ret = KNOT_EOK; - /* If no file specified, use stdin. */ + /* If no file specified, enter the interactive mode. */ if (EMPTY_LIST(params->qfiles)) { - ret = process_lines(params, stdin); + ret = interactive_loop(params); } /* Read from each specified file. */ @@ -603,7 +581,7 @@ int cmd_update(const char* lp, knsupdate_params_t *params) DBG("%s: lp='%s'\n", __func__, lp); /* update is optional token, next add|del|delete */ - int bp = tok_find(lp, cmd_array); + int bp = tok_find(lp, knsupdate_cmd_array); if (bp < 0) return bp; /* Syntax error. */ /* allow only specific tokens */ @@ -614,7 +592,7 @@ int cmd_update(const char* lp, knsupdate_params_t *params) return KNOT_EPARSEFAIL; } - return h[bp](tok_skipspace(lp + TOK_L(cmd_array[bp])), params); + return h[bp](tok_skipspace(lp + TOK_L(knsupdate_cmd_array[bp])), params); } int cmd_add(const char* lp, knsupdate_params_t *params) diff --git a/src/utils/knsupdate/knsupdate_exec.h b/src/utils/knsupdate/knsupdate_exec.h index 1cf870833888c8e382cc1e82fcadb4261a9a1376..36ca43b2df5a9113e8e0d70694d3cc3b8eb677f3 100644 --- a/src/utils/knsupdate/knsupdate_exec.h +++ b/src/utils/knsupdate/knsupdate_exec.h @@ -1,4 +1,4 @@ -/* Copyright (C) 2018 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by @@ -18,4 +18,8 @@ #include "utils/knsupdate/knsupdate_params.h" +extern const char* knsupdate_cmd_array[]; + int knsupdate_exec(knsupdate_params_t *params); + +int knsupdate_process_line(const char *line, knsupdate_params_t *params); diff --git a/src/utils/knsupdate/knsupdate_interactive.c b/src/utils/knsupdate/knsupdate_interactive.c new file mode 100644 index 0000000000000000000000000000000000000000..898496415113d9f26629b346af0c4b6292643fd9 --- /dev/null +++ b/src/utils/knsupdate/knsupdate_interactive.c @@ -0,0 +1,176 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#include <histedit.h> + +#include "contrib/string.h" +#include "utils/common/lookup.h" +#include "utils/common/msg.h" +#include "utils/knsupdate/knsupdate_exec.h" +#include "utils/knsupdate/knsupdate_interactive.h" + +#define PROGRAM_NAME "knsupdate" +#define HISTORY_FILE ".knsupdate_history" + +static char *prompt(EditLine *el) +{ + return PROGRAM_NAME"> "; +} + +static void print_commands(void) +{ + for (const char **cmd = knsupdate_cmd_array; *cmd != NULL; cmd++) { + printf(" %-18s\n", (*cmd) + 1); + } +} + +static void cmds_lookup(EditLine *el, const char *str, size_t str_len) +{ + lookup_t lookup; + int ret = lookup_init(&lookup); + if (ret != KNOT_EOK) { + return; + } + + // Fill the lookup with command names. + for (const char **desc = knsupdate_cmd_array; *desc != NULL; desc++) { + ret = lookup_insert(&lookup, (*desc) + 1, NULL); + if (ret != KNOT_EOK) { + goto cmds_lookup_finish; + } + } + + lookup_complete(&lookup, str, str_len, el, true); + +cmds_lookup_finish: + lookup_deinit(&lookup); +} + +static unsigned char complete(EditLine *el, int ch) +{ + int argc, token, pos; + const char **argv; + + const LineInfo *li = el_line(el); + Tokenizer *tok = tok_init(NULL); + + // Parse the line. + int ret = tok_line(tok, li, &argc, &argv, &token, &pos); + if (ret != 0) { + goto complete_exit; + } + + // Show possible commands. + if (argc == 0) { + print_commands(); + goto complete_exit; + } + + // Complete the command name. + if (token == 0) { + cmds_lookup(el, argv[0], pos); + goto complete_exit; + } + + // Find the command descriptor. + const char **desc = knsupdate_cmd_array; + while (*desc != NULL && strcmp((*desc) + 1, argv[0]) != 0) { + (*desc)++; + } + if (*desc == NULL) { + goto complete_exit; + } + +complete_exit: + tok_reset(tok); + tok_end(tok); + + return CC_REDISPLAY; +} + +int interactive_loop(knsupdate_params_t *params) +{ + char *hist_file = NULL; + const char *home = getenv("HOME"); + if (home != NULL) { + hist_file = sprintf_alloc("%s/%s", home, HISTORY_FILE); + } + if (hist_file == NULL) { + INFO("failed to get home directory"); + } + + EditLine *el = el_init(PROGRAM_NAME, stdin, stdout, stderr); + if (el == NULL) { + ERR("interactive mode not available"); + free(hist_file); + return KNOT_ERROR; + } + + History *hist = history_init(); + if (hist == NULL) { + ERR("interactive mode not available"); + el_end(el); + free(hist_file); + return KNOT_ERROR; + } + + HistEvent hev = { 0 }; + history(hist, &hev, H_SETSIZE, 1000); + history(hist, &hev, H_SETUNIQUE, 1); + el_set(el, EL_HIST, history, hist); + history(hist, &hev, H_LOAD, hist_file); + + el_set(el, EL_TERMINAL, NULL); + el_set(el, EL_EDITOR, "emacs"); + el_set(el, EL_PROMPT, prompt); + el_set(el, EL_SIGNAL, 1); + el_source(el, NULL); + + // Warning: these two el_sets()'s always leak -- in libedit2 library! + // For more details see this commit's message. + el_set(el, EL_ADDFN, PROGRAM_NAME"-complete", + "Perform "PROGRAM_NAME" completion.", complete); + el_set(el, EL_BIND, "^I", PROGRAM_NAME"-complete", NULL); + + int count; + const char *line; + while ((line = el_gets(el, &count)) != NULL && count > 0) { + char command[count + 1]; + memcpy(command, line, count); + command[count] = '\0'; + // Removes trailing newline + size_t cmd_len = strcspn(command, "\n"); + command[cmd_len] = '\0'; + + if (cmd_len > 0) { + history(hist, &hev, H_ENTER, command); + history(hist, &hev, H_SAVE, hist_file); + } + + // Process the command. + (void)knsupdate_process_line(command, params); + if (params->stop) { + break; + } + } + + history_end(hist); + free(hist_file); + + el_end(el); + + return KNOT_EOK; +} diff --git a/src/utils/knsupdate/knsupdate_interactive.h b/src/utils/knsupdate/knsupdate_interactive.h new file mode 100644 index 0000000000000000000000000000000000000000..e2a24282ed0268c34ffc4d45dfbef8c4fa9ecf35 --- /dev/null +++ b/src/utils/knsupdate/knsupdate_interactive.h @@ -0,0 +1,26 @@ +/* Copyright (C) 2022 CZ.NIC, z.s.p.o. <knot-dns@labs.nic.cz> + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License + along with this program. If not, see <https://www.gnu.org/licenses/>. + */ + +#pragma once + +#include "utils/knsupdate/knsupdate_params.h" + +/*! + * Executes an interactive processing loop. + * + * \param[in] params Utility parameters. + */ +int interactive_loop(knsupdate_params_t *params);