The lack for autocomplete in interactive mode is a bit annoying, and there doesn't seem to be an easy way to support it using bash completion. Here is a proposal to use bison to guess the next tokens. This patch adds support for keyword autocomplete for libreadline CLI only, it uses the bison push parser to step through each token and lists all all possible next tokens. Some important notes: - First word autocomplete doesn't work, since the last token flex passes is TOKEN_EOF, and the parser is default state - Number of special tokens are specifically skipped, and only symbols that are escaped by \" or \' are considered valid keywords Signed-off-by: Sriram Yagnaraman <sriram.yagnaraman@xxxxxxxx> --- Make_global.am | 2 +- include/nftables/libnftables.h | 2 + include/parser.h | 3 + src/cli.c | 5 +- src/libnftables.c | 13 ++++ src/libnftables.map | 4 ++ src/parser_bison.y | 107 +++++++++++++++++++++++++++++++++ 7 files changed, 134 insertions(+), 2 deletions(-) diff --git a/Make_global.am b/Make_global.am index 5bb541f6..35c81ee0 100644 --- a/Make_global.am +++ b/Make_global.am @@ -18,4 +18,4 @@ # set age to 0. # </snippet> # -libnftables_LIBVERSION=2:0:1 +libnftables_LIBVERSION=3:0:1 diff --git a/include/nftables/libnftables.h b/include/nftables/libnftables.h index 85e08c9b..57f8be64 100644 --- a/include/nftables/libnftables.h +++ b/include/nftables/libnftables.h @@ -92,6 +92,8 @@ void nft_ctx_clear_vars(struct nft_ctx *ctx); int nft_run_cmd_from_buffer(struct nft_ctx *nft, const char *buf); int nft_run_cmd_from_filename(struct nft_ctx *nft, const char *filename); +char **nft_get_expected_tokens(struct nft_ctx *nft, const char *line, const char *text); + #ifdef __cplusplus } /* extern "C" */ #endif diff --git a/include/parser.h b/include/parser.h index f79a22f3..cec3a8f8 100644 --- a/include/parser.h +++ b/include/parser.h @@ -98,6 +98,9 @@ extern void parser_init(struct nft_ctx *nft, struct parser_state *state, struct scope *top_scope); extern int nft_parse(struct nft_ctx *ctx, void *, struct parser_state *state); +extern char **expected_matches (struct nft_ctx *nft, struct parser_state *state, + const char *text); + extern void *scanner_init(struct parser_state *state); extern void scanner_destroy(struct nft_ctx *nft); diff --git a/src/cli.c b/src/cli.c index 11fc85ab..ffd7bf61 100644 --- a/src/cli.c +++ b/src/cli.c @@ -158,7 +158,10 @@ static void cli_complete(char *line) static char **cli_completion(const char *text, int start, int end) { - return NULL; + char *line = strndup(rl_line_buffer, (size_t)start); + + rl_attempted_completion_over = 1; + return nft_get_expected_tokens(cli_nft, line, text); } int cli_init(struct nft_ctx *nft) diff --git a/src/libnftables.c b/src/libnftables.c index 4f538c44..a85cb155 100644 --- a/src/libnftables.c +++ b/src/libnftables.c @@ -555,6 +555,19 @@ static int nft_evaluate(struct nft_ctx *nft, struct list_head *msgs, return 0; } +EXPORT_SYMBOL(nft_get_expected_tokens); +char **nft_get_expected_tokens(struct nft_ctx *nft, const char *line, const char *text) +{ + LIST_HEAD(msgs); + LIST_HEAD(cmds); + + parser_init(nft, nft->state, &msgs, &cmds, nft->top_scope); + nft->scanner = scanner_init(nft->state); + scanner_push_buffer(nft->scanner, &indesc_cmdline, line); + + return expected_matches(nft, nft->state, text); +} + EXPORT_SYMBOL(nft_run_cmd_from_buffer); int nft_run_cmd_from_buffer(struct nft_ctx *nft, const char *buf) { diff --git a/src/libnftables.map b/src/libnftables.map index a46a3ad5..9e4d8fcd 100644 --- a/src/libnftables.map +++ b/src/libnftables.map @@ -33,3 +33,7 @@ LIBNFTABLES_3 { nft_ctx_set_optimize; nft_ctx_get_optimize; } LIBNFTABLES_2; + +LIBNFTABLES_4 { + nft_get_expected_tokens; +} LIBNFTABLES_3; \ No newline at end of file diff --git a/src/parser_bison.y b/src/parser_bison.y index e4f21ca1..d466628d 100644 --- a/src/parser_bison.y +++ b/src/parser_bison.y @@ -13,6 +13,7 @@ #include <ctype.h> #include <stddef.h> #include <stdio.h> +#include <stdlib.h> #include <inttypes.h> #include <syslog.h> #include <netinet/ip.h> @@ -160,6 +161,7 @@ int nft_lex(void *, void *, void *); %name-prefix "nft_" %debug %define api.pure +%define api.push-pull both %parse-param { struct nft_ctx *nft } %parse-param { void *scanner } %parse-param { struct parser_state *state } @@ -6215,3 +6217,108 @@ exthdr_key : HBH close_scope_hbh { $$ = IPPROTO_HOPOPTS; } ; %% + +static int expected_tokens(struct nft_ctx *nft, struct parser_state *state, + int *tokens, int ntokens) +{ + int res = 0; + yypstate *pstate = yypstate_new(); + YYLTYPE yylloc; + int yystatus; + + location_init(nft->scanner, state, &yylloc); + do { + YYSTYPE yylval; + int token = nft_lex(&yylval, &yylloc, nft->scanner); + // Don't let the parse know when we reach the end of input. + if (token == TOKEN_EOF) + break; + yystatus = nft_push_parse(pstate, token, &yylval, &yylloc, nft, nft->scanner, state); + } while (yystatus == YYPUSH_MORE); + + if (!pstate->yynerrs) + { + res = yypstate_expected_tokens(pstate, tokens, ntokens); + } + + yypstate_delete(pstate); + return res; +} + +char **expected_matches (struct nft_ctx *nft, struct parser_state *state, + const char *text) +{ + const size_t len = strlen(text); + int match = 1; + char **matches = NULL; + bool special_token = false; + const int special_tokens[] = { + YYSYMBOL_YYEOF, + YYSYMBOL_YYUNDEF, + YYSYMBOL_JUNK, + YYSYMBOL_NEWLINE, + YYSYMBOL_COLON, + YYSYMBOL_SEMICOLON, + YYSYMBOL_COMMA, + YYSYMBOL_STRING, + YYSYMBOL_QUOTED_STRING, + YYSYMBOL_ASTERISK_STRING, + YYSYMBOL_LAST + }; + int tokens[YYNTOKENS]; + int ntokens = expected_tokens(nft, state, tokens, YYNTOKENS); + + if (ntokens == 0) + goto out; + + // Need initial prefix and final NULL. + matches = calloc((size_t)ntokens + 2, sizeof(*matches)); + if (!matches) + goto out; + + for (int i = 0; i < ntokens; ++i) { + const char* token = yysymbol_name(tokens[i]); + const char* unescaped_token = NULL; + + for (size_t j = 0; j < sizeof(special_tokens)/sizeof(special_tokens[0]); ++j) { + if (tokens[i] == special_tokens[j]) { + special_token = true; + break; + } + } + + if (special_token) { + special_token = false; + continue; + } + + /* Only match tokens that are escaped by \" or \' */ + if (token[0] == '"' || token[0] == '\'') { + unescaped_token = strndup(token + 1, strlen(token) - 2); + } + + + if (unescaped_token && (!len || + strncmp(text, unescaped_token, len) == 0)) { + matches[match++] = strdup(unescaped_token); + } + } + + // Find the longest common prefix, and install it in matches[0], as + // required by readline. + if (match == 1) { + free(matches); + return NULL; + } else { + size_t lcplen = strlen(matches[1]); + for (int i = 2; i < match && lcplen; ++i) + for (size_t j = 0; j < lcplen; ++j) + if (matches[1][j] != matches[i][j]) + lcplen = j; + matches[0] = strdup(matches[1]); + matches[0][lcplen] = '\0'; + } + +out: + return matches; +} -- 2.34.1