diff --git a/CHANGELOG.md b/CHANGELOG.md index 385a1c10..e690a544 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,15 @@ ## 3.10.0 (Upcoming) ### Major New features +* CLI history: [Preserve CLI command history across sessions. The up/down arrows](https://github.com/clicon/clixon/issues/79) + * The design is similar to bash history: + * The CLI loads/saves its complete history to a file on entry and exit, respectively + * The size (number of lines) of the file is the same as the history in memory + * Only the latest session dumping its history will survive (bash merges multiple session history). + * Tilde-expansion is supported + * Files not found or without appropriate access will not cause an exit but will be logged at debug level + * New config options: CLICON_CLI_HIST_FILE with default value `~/.clixon_cli_history` + * New config options: CLICON_CLI_HIST_SIZE with default value 300. * New backend startup and upgrade support, see [doc/startup.md] for details * Enable with CLICON_XMLDB_MODSTATE config option * Check modules-state tags when loading a datastore at startup diff --git a/apps/cli/cli_handle.c b/apps/cli/cli_handle.c index e2f25121..6e8c61fb 100644 --- a/apps/cli/cli_handle.c +++ b/apps/cli/cli_handle.c @@ -114,13 +114,13 @@ cli_handle_init(void) int cli_handle_exit(clicon_handle h) { - cligen_handle ch = cligen(h); + cligen_handle ch = cligen(h); struct cli_handle *cl = handle(h); if (cl->cl_stx) free(cl->cl_stx); clicon_handle_exit(h); /* frees h and options */ - + cligen_exit(ch); return 0; diff --git a/apps/cli/cli_main.c b/apps/cli/cli_main.c index 8fce8dbc..66c3dd75 100644 --- a/apps/cli/cli_main.c +++ b/apps/cli/cli_main.c @@ -55,6 +55,7 @@ #include #include #include +#include /* cligen */ #include @@ -72,6 +73,84 @@ /* Command line options to be passed to getopt(3) */ #define CLI_OPTS "hD:f:l:F:1a:u:d:m:qp:GLy:c:U:o:" +/*! Check if there is a CLI history file and if so dump the CLI histiry to it + * Just log if file does not exist or is not readable + * @param[in] h CLICON handle + */ +static int +cli_history_load(clicon_handle h) +{ + int retval = -1; + int lines; + char *filename; + FILE *f = NULL; + wordexp_t result = {0,}; /* for tilde expansion */ + + lines = clicon_option_int(h,"CLICON_CLI_HIST_SIZE"); + /* Re-init history with clixon lines (1st time was w cligen defaults) */ + if (cligen_hist_init(cli_cligen(h), lines) < 0) + goto done; + if ((filename = clicon_option_str(h,"CLICON_CLI_HIST_FILE")) == NULL) + goto ok; /* ignore */ + if (wordexp(filename, &result, 0) < 0){ + clicon_err(OE_UNIX, errno, "wordexp"); + goto done; + } + if ((f = fopen(result.we_wordv[0], "r")) == NULL){ + clicon_log(LOG_DEBUG, "Warning: Could not open CLI history file for reading: %s: %s", + result.we_wordv[0], strerror(errno)); + goto ok; + } + if (cligen_hist_file_load(cli_cligen(h), f) < 0){ + clicon_err(OE_UNIX, errno, "cligen_hist_file_load"); + goto done; + } + ok: + retval = 0; + done: + wordfree(&result); + if (f) + fclose(f); + return retval; +} + +/*! Start CLI history and load from file + * Just log if file does not exist or is not readable + * @param[in] h CLICON handle + */ +static int +cli_history_save(clicon_handle h) +{ + int retval = -1; + char *filename; + FILE *f = NULL; + wordexp_t result = {0,}; /* for tilde expansion */ + + if ((filename = clicon_option_str(h, "CLICON_CLI_HIST_FILE")) == NULL) + goto ok; /* ignore */ + if (wordexp(filename, &result, 0) < 0){ + clicon_err(OE_UNIX, errno, "wordexp"); + goto done; + } + if ((f = fopen(result.we_wordv[0], "w+")) == NULL){ + clicon_log(LOG_DEBUG, "Warning: Could not open CLI history file for writing: %s: %s", + result.we_wordv[0], strerror(errno)); + goto ok; + } + if (cligen_hist_file_save(cli_cligen(h), f) < 0){ + clicon_err(OE_UNIX, errno, "cligen_hist_file_save"); + goto done; + } + ok: + retval = 0; + done: + wordfree(&result); + if (f) + fclose(f); + return retval; +} + + /*! Clean and close all state of cli process (but dont exit). * Cannot use h after this * @param[in] h Clixon handle @@ -90,6 +169,7 @@ cli_terminate(clicon_handle h) if ((x = clicon_conf_xml(h)) != NULL) xml_free(x); cli_plugin_finish(h); + cli_history_save(h); cli_handle_exit(h); clicon_log_exit(); return 0; @@ -134,16 +214,18 @@ cli_interactive(clicon_handle h) new_mode = cli_syntax_mode(h); if ((cmd = clicon_cliread(h)) == NULL) { cligen_exiting_set(cli_cligen(h), 1); /* EOF */ - goto done; + goto ok; /* EOF should not be -1 error? */ } if ((res = clicon_parse(h, cmd, &new_mode, &eval)) < 0) goto done; } + ok: retval = 0; done: return retval; } + static void usage(clicon_handle h, char *argv0) @@ -182,21 +264,21 @@ usage(clicon_handle h, int main(int argc, char **argv) { - int retval = -1; - int c; - int once; - char *tmp; - char *argv0 = argv[0]; - clicon_handle h; - int printgen = 0; - int logclisyntax = 0; - int help = 0; - int logdst = CLICON_LOG_STDERR; - char *restarg = NULL; /* what remains after options */ - yang_spec *yspec; - yang_spec *yspecfg = NULL; /* For config XXX clixon bug */ + int retval = -1; + int c; + int once; + char *tmp; + char *argv0 = argv[0]; + clicon_handle h; + int printgen = 0; + int logclisyntax = 0; + int help = 0; + int logdst = CLICON_LOG_STDERR; + char *restarg = NULL; /* what remains after options */ + yang_spec *yspec; + yang_spec *yspecfg = NULL; /* For config XXX clixon bug */ struct passwd *pw; - char *str; + char *str; /* Defaults */ once = 0; @@ -459,6 +541,10 @@ main(int argc, char **argv) *(argv-1) = tmp; cligen_line_scrolling_set(cli_cligen(h), clicon_option_int(h,"CLICON_CLI_LINESCROLLING")); + /*! Start CLI history and load from file */ + if (cli_history_load(h) < 0) + goto done; + /* Experimental utf8 mode */ cligen_utf8_set(cli_cligen(h), clicon_option_int(h,"CLICON_CLI_UTF8")); /* Launch interfactive event loop, unless -1 */ if (restarg != NULL && strlen(restarg)){ @@ -466,9 +552,8 @@ main(int argc, char **argv) int result; /* */ - if (clicon_parse(h, restarg, &mode, &result) != 1){ + if (clicon_parse(h, restarg, &mode, &result) != 1) goto done; - } if (result < 0) goto done; } diff --git a/doc/CLI.md b/doc/CLI.md index 3af378cd..9275f16e 100644 --- a/doc/CLI.md +++ b/doc/CLI.md @@ -1,7 +1,9 @@ # Clixon CLI * [CLIgen](#cligen) -* [Tricks - eg for large specs](#tricks) +* [Command history](#history) +* [Large spec designs](#large-specs) + ## CLIgen @@ -26,7 +28,18 @@ Clixon adds some features and structure to CLIgen which include: The commands (eg `cli_set`) will be called with the first argument an api-path to the referenced object. * The CLIgen `treename` syntax does not work. -## Tricks +## History + +Clixon CLI supports persistent command history. There are two CLI history related configuration options: `CLICON_CLI_HIST_FILE` with default value `~/.clixon_cli_history` and `CLICON_CLI_HIST_SIZE` with default value 300. + +The design is similar to bash history but is simpler in some respects: + * The CLI loads/saves its complete history to a file on entry and exit, respectively + * The size (number of lines) of the file is the same as the history in memory + * Only the latest session dumping its history will survive (bash merges multiple session history). + +Further, tilde-expansion is supported and if history files are not found or lack appropriate access will not cause an exit but will be logged at debug level + +## Large specs CLIgen is designed to handle large specifications in runtime, but it may be difficult to handle large specifications from a design perspective. @@ -79,3 +92,4 @@ You can also add the C preprocessor as a first step. You can then define macros, %.cli : %.cpp $(CPP) -P -x assembler-with-cpp $(INCLUDES) -o $@ $< ``` + diff --git a/lib/src/clixon_options.c b/lib/src/clixon_options.c index 4253e283..9c81deea 100644 --- a/lib/src/clixon_options.c +++ b/lib/src/clixon_options.c @@ -183,7 +183,7 @@ parse_configfile(clicon_handle h, else #endif { - clicon_err(OE_CFG, 0, "Config file %s: Lacks top-level \"clixon_config\" element\nClixon config files should begin with: ", filename); + clicon_err(OE_CFG, 0, "Config file %s: Lacks top-level \"clixon-config\" element\nClixon config files should begin with: ", filename); goto done; } diff --git a/test/test_cli.sh b/test/test_cli.sh index 7a88c740..fe894be1 100755 --- a/test/test_cli.sh +++ b/test/test_cli.sh @@ -1,5 +1,5 @@ #!/bin/bash -# Test1: backend and cli basic functionality +# Backend and cli basic functionality # Start backend server # Add an ethernet interface and an address # Show configuration diff --git a/test/test_cli_history.sh b/test/test_cli_history.sh new file mode 100755 index 00000000..22591c66 --- /dev/null +++ b/test/test_cli_history.sh @@ -0,0 +1,109 @@ +#!/bin/bash +# Basic CLI history test + +# Magic line must be first in script (see README.md) +s="$_" ; . ./lib.sh || if [ "$s" = $0 ]; then exit 0; else return 0; fi + +APPNAME=example + +# include err() and new() functions and creates $dir + +cfg=$dir/conf_yang.xml +histfile=$dir/histfile + +# Use yang in example + +cat < $cfg + + $cfg + /usr/local/share/clixon + $IETFRFC + clixon-example + /usr/local/lib/$APPNAME/backend + /usr/local/lib/$APPNAME/clispec + /usr/local/lib/$APPNAME/cli + $APPNAME + $histfile + 10 + /usr/local/var/$APPNAME/$APPNAME.sock + /usr/local/var/$APPNAME/$APPNAME.pidfile + 1 + /usr/local/var/$APPNAME + /usr/local/lib/xmldb/text.so + +EOF + +cat < $histfile +first line +EOF + +# NOTE Backend is not really use here +new "test params: -f $cfg" +if [ $BE -ne 0 ]; then + new "kill old backend" + sudo clixon_backend -z -f $cfg + if [ $? -ne 0 ]; then + err + fi + new "start backend -s init -f $cfg" + start_backend -s init -f $cfg + + new "waiting" + sleep $RCWAIT +fi + +new "cli read and add entry to existing history" +expecteof "$clixon_cli -f $cfg" 0 "example 42" "data" + +new "Check histfile exists" +if [ ! -f $histfile ]; then + err "$histfile" "not found" +fi + +new "Check it has two entries" +lines=$(cat $histfile | wc -l) +if [ $lines -ne 2 ]; then + err "Line:$lines" "2" +fi + +new "check it contains first line" +nr=$(grep -c "example 42" $histfile) +if [ $nr -ne 1 ]; then + err "Contains: example 42" "1" +fi + +new "Check it contains example 42" +nr=$(grep -c "example 42" $histfile) +if [ $nr -ne 1 ]; then + err "Contains: example 42" "1" +fi + +new "cli add entry and create newhist file" +expecteof "$clixon_cli -f $cfg -o CLICON_CLI_HIST_FILE=$dir/newhist" 0 "example 43" "data" + +new "Check newhist exists" +if [ ! -f $dir/newhist ]; then + err "$dir/newhist" "not found" +fi + +new "check it contains example 43" +nr=$(grep -c "example 43" $dir/newhist) +if [ $nr -ne 1 ]; then + err "Contains: example 43" "1" +fi + +if [ $BE -eq 0 ]; then + exit # BE +fi + +new "Kill backend" +# Check if premature kill +pid=`pgrep -u root -f clixon_backend` +if [ -z "$pid" ]; then + err "backend already dead" +fi +# kill backend +stop_backend -f $cfg + + +rm -rf $dir diff --git a/yang/clixon/clixon-config@2019-03-05.yang b/yang/clixon/clixon-config@2019-03-05.yang index 85ce39d7..9c666345 100644 --- a/yang/clixon/clixon-config@2019-03-05.yang +++ b/yang/clixon/clixon-config@2019-03-05.yang @@ -297,6 +297,21 @@ module clixon-config { Note that this feature is EXPERIMENTAL and may not properly handle scrolling, control characters, etc"; } + leaf CLICON_CLI_HIST_FILE { + type string; + default "~/.clixon_cli_history"; + description + "Name of CLI history file. If not given, history is not saved. + The number of lines is saved is given by CLICON_CLI_HIST_SIZE."; + } + leaf CLICON_CLI_HIST_SIZE { + type int32; + default 300; + description + "Number of lines to save in CLI history. + Also, if CLICON_CLI_HIST_FILE is set, also the size in lines + of the saved history."; + } leaf CLICON_SOCK_FAMILY { type string; default "UNIX";