* 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)
|
||||
|
||||
### 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
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@
|
|||
#include <pwd.h>
|
||||
#include <assert.h>
|
||||
#include <libgen.h>
|
||||
#include <wordexp.h>
|
||||
|
||||
/* cligen */
|
||||
#include <cligen/cligen.h>
|
||||
|
|
@ -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)
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
18
doc/CLI.md
18
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 $@ $<
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -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: <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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
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
|
||||
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";
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue