* 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:
parent
5602d3e987
commit
b03f8332e1
8 changed files with 255 additions and 23 deletions
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
18
doc/CLI.md
18
doc/CLI.md
|
|
@ -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 $@ $<
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
109
test/test_cli_history.sh
Executable 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
|
||||||
|
|
@ -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";
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue