Skip to content

Commit

Permalink
* CLI history: [Preserve CLI command history across sessions. The up/…
Browse files Browse the repository at this point in the history
…down arrows](#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.
  • Loading branch information
olofhagsand committed Mar 8, 2019
1 parent 5602d3e commit b03f833
Show file tree
Hide file tree
Showing 8 changed files with 255 additions and 23 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
4 changes: 2 additions & 2 deletions apps/cli/cli_handle.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
119 changes: 102 additions & 17 deletions apps/cli/cli_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
#include <pwd.h>
#include <assert.h>
#include <libgen.h>
#include <wordexp.h>

/* cligen */
#include <cligen/cligen.h>
Expand All @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -459,16 +541,19 @@ 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)){
char *mode = cli_syntax_mode(h);
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;
}
Expand Down
18 changes: 16 additions & 2 deletions doc/CLI.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# Clixon CLI

* [CLIgen](#cligen)
* [Tricks - eg for large specs](#tricks)
* [Command history](#history)
* [Large spec designs](#large-specs)


## CLIgen

Expand All @@ -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.
Expand Down Expand Up @@ -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 $@ $<
```

2 changes: 1 addition & 1 deletion lib/src/clixon_options.c
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
2 changes: 1 addition & 1 deletion test/test_cli.sh
Original file line number Diff line number Diff line change
@@ -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
Expand Down
109 changes: 109 additions & 0 deletions test/test_cli_history.sh
Original file line number Diff line number Diff line change
@@ -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
15 changes: 15 additions & 0 deletions yang/clixon/clixon-config@2019-03-05.yang
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down

0 comments on commit b03f833

Please sign in to comment.