* 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.
This commit is contained in:
Olof hagsand 2019-03-08 11:26:07 +01:00
parent 5602d3e987
commit b03f8332e1
8 changed files with 255 additions and 23 deletions

View file

@ -5,6 +5,15 @@
## 3.10.0 (Upcoming) ## 3.10.0 (Upcoming)
### Major New features ### 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 * New backend startup and upgrade support, see [doc/startup.md] for details
* Enable with CLICON_XMLDB_MODSTATE config option * Enable with CLICON_XMLDB_MODSTATE config option
* Check modules-state tags when loading a datastore at startup * Check modules-state tags when loading a datastore at startup

View file

@ -114,7 +114,7 @@ cli_handle_init(void)
int int
cli_handle_exit(clicon_handle h) cli_handle_exit(clicon_handle h)
{ {
cligen_handle ch = cligen(h); cligen_handle ch = cligen(h);
struct cli_handle *cl = handle(h); struct cli_handle *cl = handle(h);
if (cl->cl_stx) if (cl->cl_stx)

View file

@ -55,6 +55,7 @@
#include <pwd.h> #include <pwd.h>
#include <assert.h> #include <assert.h>
#include <libgen.h> #include <libgen.h>
#include <wordexp.h>
/* cligen */ /* cligen */
#include <cligen/cligen.h> #include <cligen/cligen.h>
@ -72,6 +73,84 @@
/* Command line options to be passed to getopt(3) */ /* 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:" #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). /*! Clean and close all state of cli process (but dont exit).
* Cannot use h after this * Cannot use h after this
* @param[in] h Clixon handle * @param[in] h Clixon handle
@ -90,6 +169,7 @@ cli_terminate(clicon_handle h)
if ((x = clicon_conf_xml(h)) != NULL) if ((x = clicon_conf_xml(h)) != NULL)
xml_free(x); xml_free(x);
cli_plugin_finish(h); cli_plugin_finish(h);
cli_history_save(h);
cli_handle_exit(h); cli_handle_exit(h);
clicon_log_exit(); clicon_log_exit();
return 0; return 0;
@ -134,16 +214,18 @@ cli_interactive(clicon_handle h)
new_mode = cli_syntax_mode(h); new_mode = cli_syntax_mode(h);
if ((cmd = clicon_cliread(h)) == NULL) { if ((cmd = clicon_cliread(h)) == NULL) {
cligen_exiting_set(cli_cligen(h), 1); /* EOF */ 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) if ((res = clicon_parse(h, cmd, &new_mode, &eval)) < 0)
goto done; goto done;
} }
ok:
retval = 0; retval = 0;
done: done:
return retval; return retval;
} }
static void static void
usage(clicon_handle h, usage(clicon_handle h,
char *argv0) char *argv0)
@ -182,21 +264,21 @@ usage(clicon_handle h,
int int
main(int argc, char **argv) main(int argc, char **argv)
{ {
int retval = -1; int retval = -1;
int c; int c;
int once; int once;
char *tmp; char *tmp;
char *argv0 = argv[0]; char *argv0 = argv[0];
clicon_handle h; clicon_handle h;
int printgen = 0; int printgen = 0;
int logclisyntax = 0; int logclisyntax = 0;
int help = 0; int help = 0;
int logdst = CLICON_LOG_STDERR; int logdst = CLICON_LOG_STDERR;
char *restarg = NULL; /* what remains after options */ char *restarg = NULL; /* what remains after options */
yang_spec *yspec; yang_spec *yspec;
yang_spec *yspecfg = NULL; /* For config XXX clixon bug */ yang_spec *yspecfg = NULL; /* For config XXX clixon bug */
struct passwd *pw; struct passwd *pw;
char *str; char *str;
/* Defaults */ /* Defaults */
once = 0; once = 0;
@ -459,6 +541,10 @@ main(int argc, char **argv)
*(argv-1) = tmp; *(argv-1) = tmp;
cligen_line_scrolling_set(cli_cligen(h), clicon_option_int(h,"CLICON_CLI_LINESCROLLING")); 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")); cligen_utf8_set(cli_cligen(h), clicon_option_int(h,"CLICON_CLI_UTF8"));
/* Launch interfactive event loop, unless -1 */ /* Launch interfactive event loop, unless -1 */
if (restarg != NULL && strlen(restarg)){ if (restarg != NULL && strlen(restarg)){
@ -466,9 +552,8 @@ main(int argc, char **argv)
int result; int result;
/* */ /* */
if (clicon_parse(h, restarg, &mode, &result) != 1){ if (clicon_parse(h, restarg, &mode, &result) != 1)
goto done; goto done;
}
if (result < 0) if (result < 0)
goto done; goto done;
} }

View file

@ -1,7 +1,9 @@
# Clixon CLI # Clixon CLI
* [CLIgen](#cligen) * [CLIgen](#cligen)
* [Tricks - eg for large specs](#tricks) * [Command history](#history)
* [Large spec designs](#large-specs)
## CLIgen ## 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 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. * 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 CLIgen is designed to handle large specifications in runtime, but it may be
difficult to handle large specifications from a design perspective. 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 %.cli : %.cpp
$(CPP) -P -x assembler-with-cpp $(INCLUDES) -o $@ $< $(CPP) -P -x assembler-with-cpp $(INCLUDES) -o $@ $<
``` ```

View file

@ -183,7 +183,7 @@ parse_configfile(clicon_handle h,
else else
#endif #endif
{ {
clicon_err(OE_CFG, 0, "Config file %s: Lacks top-level \"clixon_config\" element\nClixon config files should begin with: <clixon-config xmlns=\"http://clicon.org/config\" (See Changelog in Clixon 3.10)>", filename); clicon_err(OE_CFG, 0, "Config file %s: Lacks top-level \"clixon-config\" element\nClixon config files should begin with: <clixon-config xmlns=\"http://clicon.org/config\" (See Changelog in Clixon 3.10)>", filename);
goto done; goto done;
} }

View file

@ -1,5 +1,5 @@
#!/bin/bash #!/bin/bash
# Test1: backend and cli basic functionality # Backend and cli basic functionality
# Start backend server # Start backend server
# Add an ethernet interface and an address # Add an ethernet interface and an address
# Show configuration # Show configuration

109
test/test_cli_history.sh Executable file
View file

@ -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 <<EOF > $cfg
<clixon-config xmlns="http://clicon.org/config">
<CLICON_CONFIGFILE>$cfg</CLICON_CONFIGFILE>
<CLICON_YANG_DIR>/usr/local/share/clixon</CLICON_YANG_DIR>
<CLICON_YANG_DIR>$IETFRFC</CLICON_YANG_DIR>
<CLICON_YANG_MODULE_MAIN>clixon-example</CLICON_YANG_MODULE_MAIN>
<CLICON_BACKEND_DIR>/usr/local/lib/$APPNAME/backend</CLICON_BACKEND_DIR>
<CLICON_CLISPEC_DIR>/usr/local/lib/$APPNAME/clispec</CLICON_CLISPEC_DIR>
<CLICON_CLI_DIR>/usr/local/lib/$APPNAME/cli</CLICON_CLI_DIR>
<CLICON_CLI_MODE>$APPNAME</CLICON_CLI_MODE>
<CLICON_CLI_HIST_FILE>$histfile</CLICON_CLI_HIST_FILE>
<CLICON_CLI_HIST_SIZE>10</CLICON_CLI_HIST_SIZE>
<CLICON_SOCK>/usr/local/var/$APPNAME/$APPNAME.sock</CLICON_SOCK>
<CLICON_BACKEND_PIDFILE>/usr/local/var/$APPNAME/$APPNAME.pidfile</CLICON_BACKEND_PIDFILE>
<CLICON_CLI_GENMODEL_COMPLETION>1</CLICON_CLI_GENMODEL_COMPLETION>
<CLICON_XMLDB_DIR>/usr/local/var/$APPNAME</CLICON_XMLDB_DIR>
<CLICON_XMLDB_PLUGIN>/usr/local/lib/xmldb/text.so</CLICON_XMLDB_PLUGIN>
</clixon-config>
EOF
cat <<EOF > $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

View file

@ -297,6 +297,21 @@ module clixon-config {
Note that this feature is EXPERIMENTAL and may not properly handle Note that this feature is EXPERIMENTAL and may not properly handle
scrolling, control characters, etc"; 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 { leaf CLICON_SOCK_FAMILY {
type string; type string;
default "UNIX"; default "UNIX";