blob: 2eab01c3b741f03343d653e84035ceb4459396e3 [file] [log] [blame]
Radek Krejcied5acc52019-04-25 15:57:04 +02001/**
2 * @file completion.c
3 * @author Michal Vasko <mvasko@cesnet.cz>
4 * @brief libyang's yanglint tool auto completion
5 *
6 * Copyright (c) 2015 CESNET, z.s.p.o.
7 *
8 * This source code is licensed under BSD 3-Clause License (the "License").
9 * You may not use this file except in compliance with the License.
10 * You may obtain a copy of the License at
11 *
12 * https://opensource.org/licenses/BSD-3-Clause
13 */
14
Radek Krejci535ea9f2020-05-29 16:01:05 +020015#define _GNU_SOURCE
Radek Krejcif8dc59a2020-11-25 13:47:44 +010016#define _POSIX_C_SOURCE 200809L /* strdup */
Radek Krejcied5acc52019-04-25 15:57:04 +020017
Radek Krejcied5acc52019-04-25 15:57:04 +020018#include <errno.h>
Radek Krejci535ea9f2020-05-29 16:01:05 +020019#include <stdint.h>
20#include <stdio.h>
21#include <stdlib.h>
Radek Krejcied5acc52019-04-25 15:57:04 +020022#include <string.h>
23
Radek Krejcied5acc52019-04-25 15:57:04 +020024#include "libyang.h"
25
Radek Krejcie9f13b12020-11-09 17:42:04 +010026#include "cmd.h"
27#include "common.h"
romanc1607ce2022-09-12 10:12:39 +020028#include "compat.h"
Radek Krejci535ea9f2020-05-29 16:01:05 +020029#include "linenoise/linenoise.h"
30
Radek Krejcie9f13b12020-11-09 17:42:04 +010031/* from the main.c */
Radek Krejcied5acc52019-04-25 15:57:04 +020032extern struct ly_ctx *ctx;
33
romanc1607ce2022-09-12 10:12:39 +020034/**
35 * @brief Add a match to the completions array.
36 *
37 * @param[in] match Match to be added.
38 * @param[in,out] matches Matches provided to the user as a completion hint.
39 * @param[in,out] match_count Number of matches.
40 */
41static void
42cmd_completion_add_match(const char *match, char ***matches, unsigned int *match_count)
43{
44 void *p;
45
46 ++(*match_count);
47 p = realloc(*matches, *match_count * sizeof **matches);
48 if (!p) {
49 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
50 return;
51 }
52 *matches = p;
53 (*matches)[*match_count - 1] = strdup(match);
54}
55
56/**
57 * @brief Provides completion for command names.
58 *
59 * @param[in] hint User input.
60 * @param[out] matches Matches provided to the user as a completion hint.
61 * @param[out] match_count Number of matches.
62 */
Radek Krejcied5acc52019-04-25 15:57:04 +020063static void
64get_cmd_completion(const char *hint, char ***matches, unsigned int *match_count)
65{
66 int i;
Radek Krejcied5acc52019-04-25 15:57:04 +020067
68 *match_count = 0;
69 *matches = NULL;
70
71 for (i = 0; commands[i].name; i++) {
72 if (!strncmp(hint, commands[i].name, strlen(hint))) {
romanc1607ce2022-09-12 10:12:39 +020073 cmd_completion_add_match(commands[i].name, matches, match_count);
Radek Krejcied5acc52019-04-25 15:57:04 +020074 }
75 }
76}
77
romanc1607ce2022-09-12 10:12:39 +020078/**
aPiecek15cc4cf2023-04-17 15:23:21 +020079 * @brief Provides completion for arguments.
80 *
81 * @param[in] hint User input.
82 * @param[in] args Array of all possible arguments. The last element must be NULL.
83 * @param[out] matches Matches provided to the user as a completion hint.
84 * @param[out] match_count Number of matches.
85 */
86static void
87get_arg_completion(const char *hint, const char **args, char ***matches, unsigned int *match_count)
88{
89 int i;
90
91 *match_count = 0;
92 *matches = NULL;
93
94 for (i = 0; args[i]; i++) {
95 if (!strncmp(hint, args[i], strlen(hint))) {
96 cmd_completion_add_match(args[i], matches, match_count);
97 }
98 }
99 if (*match_count == 0) {
100 for (i = 0; args[i]; i++) {
101 cmd_completion_add_match(args[i], matches, match_count);
102 }
103 }
104}
105
106/**
romanc1607ce2022-09-12 10:12:39 +0200107 * @brief Provides completion for module names.
108 *
109 * @param[in] hint User input.
110 * @param[out] matches Matches provided to the user as a completion hint.
111 * @param[out] match_count Number of matches.
112 */
Radek Krejcied5acc52019-04-25 15:57:04 +0200113static void
114get_model_completion(const char *hint, char ***matches, unsigned int *match_count)
115{
Michal Vaskofd69e1d2020-07-03 11:57:17 +0200116 LY_ARRAY_COUNT_TYPE u;
Radek Krejcied5acc52019-04-25 15:57:04 +0200117 uint32_t idx = 0;
118 const struct lys_module *module;
Radek Krejcied5acc52019-04-25 15:57:04 +0200119
120 *match_count = 0;
121 *matches = NULL;
122
123 while ((module = ly_ctx_get_module_iter(ctx, &idx))) {
124 if (!strncmp(hint, module->name, strlen(hint))) {
romanc1607ce2022-09-12 10:12:39 +0200125 cmd_completion_add_match(module->name, matches, match_count);
Radek Krejcied5acc52019-04-25 15:57:04 +0200126 }
127
128 LY_ARRAY_FOR(module->parsed->includes, u) {
129 if (!strncmp(hint, module->parsed->includes[u].submodule->name, strlen(hint))) {
romanc1607ce2022-09-12 10:12:39 +0200130 cmd_completion_add_match(module->parsed->includes[u].submodule->name, matches, match_count);
Radek Krejcied5acc52019-04-25 15:57:04 +0200131 }
132 }
133 }
134}
135
romanc1607ce2022-09-12 10:12:39 +0200136/**
137 * @brief Add all child nodes of a single node to the completion hint.
138 *
139 * @param[in] last_node Node of which children will be added to the hint.
140 * @param matches[out] Matches provided to the user as a completion hint.
141 * @param match_count[out] Number of matches.
142 */
143static void
144single_hint_add_children(const struct lysc_node *last_node, char ***matches, unsigned int *match_count)
145{
146 const struct lysc_node *node = NULL;
147 char *match;
148
149 if (!last_node) {
150 return;
151 }
152
romanf00089c2022-10-06 16:01:31 +0200153 while ((node = lys_getnext(node, last_node, NULL, LYS_GETNEXT_WITHCASE | LYS_GETNEXT_WITHCHOICE))) {
154 match = lysc_path(node, LYSC_PATH_LOG, NULL, 0);
romanc1607ce2022-09-12 10:12:39 +0200155 cmd_completion_add_match(match, matches, match_count);
156 free(match);
157 }
158}
159
160/**
161 * @brief Add module and/or node's children names to the hint.
162 *
163 * @param[in] module Compiled schema module.
164 * @param[in] parent Parent node of which children are potential matches.
165 * @param[in] hint_node_name Node name contained within the hint specified by user.
166 * @param[in,out] matches Matches provided to the user as a completion hint.
167 * @param[in,out] match_count Number of matches.
168 * @param[out] last_node Last processed node.
169 */
170static void
171add_all_children_nodes(const struct lysc_module *module, const struct lysc_node *parent,
172 const char *hint_node_name, char ***matches, unsigned int *match_count, const struct lysc_node **last_node)
173{
174 const struct lysc_node *node;
175 char *match, *node_name = NULL;
176
177 *last_node = NULL;
178
179 if (!parent && !module) {
180 return;
181 }
182
183 node = NULL;
romanf00089c2022-10-06 16:01:31 +0200184 while ((node = lys_getnext(node, parent, module, LYS_GETNEXT_WITHCASE | LYS_GETNEXT_WITHCHOICE))) {
romanc1607ce2022-09-12 10:12:39 +0200185 if (parent && (node->module != parent->module)) {
186 /* augmented node */
187 if (asprintf(&node_name, "%s:%s", node->module->name, node->name) == -1) {
188 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
189 break;
190 }
191 } else {
192 node_name = strdup(node->name);
193 if (!node_name) {
194 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
195 break;
196 }
197 }
198 if (!hint_node_name || !strncmp(hint_node_name, node_name, strlen(hint_node_name))) {
199 /* adding just module names + their top level node(s) to the hint */
200 *last_node = node;
romanf00089c2022-10-06 16:01:31 +0200201 match = lysc_path(node, LYSC_PATH_LOG, NULL, 0);
romanc1607ce2022-09-12 10:12:39 +0200202 cmd_completion_add_match(match, matches, match_count);
203 free(match);
204 }
205 free(node_name);
206 }
207}
208
209/**
210 * @brief Provides completion for schemas.
211 *
212 * @param[in] hint User input.
213 * @param[out] matches Matches provided to the user as a completion hint.
214 * @param[out] match_count Number of matches.
215 */
216static void
217get_schema_completion(const char *hint, char ***matches, unsigned int *match_count)
218{
219 const struct lys_module *module;
romanf00089c2022-10-06 16:01:31 +0200220 uint32_t idx;
romanc1607ce2022-09-12 10:12:39 +0200221 const char *start;
222 char *end, *module_name = NULL, *path = NULL;
223 const struct lysc_node *parent, *last_node;
224 int rc = 0;
225 size_t len;
226
227 *match_count = 0;
228 *matches = NULL;
229
230 if (strlen(hint)) {
231 if (hint[0] != '/') {
232 return;
233 }
234 start = hint + 1;
235 } else {
236 start = hint;
237 }
238
239 end = strchr(start, ':');
240 if (!end) {
241 /* no module name */
242 len = strlen(start);
243
244 /* go through all the modules */
245 idx = 0;
246 while ((module = ly_ctx_get_module_iter(ctx, &idx))) {
247 if (!module->implemented) {
248 continue;
249 }
250
251 if (!len || !strncmp(start, module->name, len)) {
252 /* add all their (matching) top level nodes */
253 add_all_children_nodes(module->compiled, NULL, NULL, matches, match_count, &last_node);
254 }
255 }
256 } else {
257 /* module name known */
258 module_name = strndup(start, end - start);
259 if (!module_name) {
260 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
261 rc = 1;
262 goto cleanup;
263 }
264
265 module = ly_ctx_get_module_implemented(ctx, module_name);
266 if (!module) {
267 goto cleanup;
268 }
269
270 /* find the last '/', if it is at the beginning of the hint, only path up to the top level node is known,
271 * else the name of the last node starts after the found '/' */
272 start = strrchr(hint, '/');
273 if (!start) {
274 goto cleanup;
275 }
276
277 if (start == hint) {
278 /* only the (incomplete) top level node path, add all (matching) top level nodes */
279 add_all_children_nodes(module->compiled, NULL, end + 1, matches, match_count, &last_node);
280 goto cleanup;
281 }
282
283 /* get rid of stuff after the last '/' to obtain the parent node */
284 path = strndup(hint, start - hint);
285 if (!path) {
286 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
287 rc = 1;
288 goto cleanup;
289 }
290
romanf00089c2022-10-06 16:01:31 +0200291 /* get the last parent in the hint (it may not exist) */
292 parent = find_schema_path(ctx, path);
romanc1607ce2022-09-12 10:12:39 +0200293
294 /* add all (matching) child nodes of the parent */
295 add_all_children_nodes(NULL, parent, start + 1, matches, match_count, &last_node);
296 }
297
298cleanup:
299 if (!rc && (*match_count == 1)) {
300 /* to avoid a single hint (space at the end), add all children as hints */
301 single_hint_add_children(last_node, matches, match_count);
302 }
303 free(path);
304 free(module_name);
305}
306
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200307/**
aPiecek15cc4cf2023-04-17 15:23:21 +0200308 * @brief Get all possible argument hints for option.
309 *
310 * @param[in] hint User input.
311 * @param[out] matches Matches provided to the user as a completion hint.
312 * @param[out] match_count Number of matches.
313 */
314static void
315get_print_format_arg(const char *hint, char ***matches, unsigned int *match_count)
316{
317 const char *args[] = {"yang", "yin", "tree", "info", NULL};
318
319 get_arg_completion(hint, args, matches, match_count);
320}
321
322/**
323 * @copydoc get_print_format_arg
324 */
325static void
326get_data_type_arg(const char *hint, char ***matches, unsigned int *match_count)
327{
328 const char *args[] = {"data", "config", "get", "getconfig", "edit", "rpc", "reply", "notif", NULL};
329
330 get_arg_completion(hint, args, matches, match_count);
331}
332
333/**
334 * @copydoc get_print_format_arg
335 */
336static void
337get_data_in_format_arg(const char *hint, char ***matches, unsigned int *match_count)
338{
339 const char *args[] = {"xml", "json", "lyb", NULL};
340
341 get_arg_completion(hint, args, matches, match_count);
342}
343
344/**
345 * @copydoc get_print_format_arg
346 */
347static void
348get_data_default_arg(const char *hint, char ***matches, unsigned int *match_count)
349{
350 const char *args[] = {"all", "all-tagged", "trim", "implicit-tagged", NULL};
351
352 get_arg_completion(hint, args, matches, match_count);
353}
354
355/**
356 * @copydoc get_print_format_arg
357 */
358static void
359get_list_format_arg(const char *hint, char ***matches, unsigned int *match_count)
360{
361 const char *args[] = {"xml", "json", NULL};
362
363 get_arg_completion(hint, args, matches, match_count);
364}
365
366/**
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200367 * @brief Get the string before the hint, which autocompletion is for.
368 *
369 * @param[in] buf Complete user input.
370 * @param[in] hint Hint part of the user input.
371 * @return Pointer to the last string.
372 */
373static const char *
374get_last_str(const char *buf, const char *hint)
375{
376 const char *ptr;
377
378 if (buf == hint) {
379 return buf;
380 }
381
382 ptr = hint - 1;
383 while (ptr[0] == ' ') {
384 --ptr;
385 if (buf == ptr) {
386 return buf;
387 }
388 }
389
390 while (ptr[-1] != ' ') {
391 --ptr;
392 if (buf == ptr) {
393 return buf;
394 }
395 }
396
397 return ptr;
398}
399
romanc1607ce2022-09-12 10:12:39 +0200400/* callback */
Radek Krejcied5acc52019-04-25 15:57:04 +0200401void
402complete_cmd(const char *buf, const char *hint, linenoiseCompletions *lc)
403{
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200404 struct autocomplete {
aPiecek15cc4cf2023-04-17 15:23:21 +0200405 enum COMMAND_INDEX ci; /**< command index to global variable 'commands' */
406 const char *opt; /**< optional option */
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200407 void (*ln_cb)(const char *, const char *, linenoiseCompletions *); /**< linenoise callback to call */
408 void (*yl_cb)(const char *, char ***, unsigned int *); /**< yanglint callback to call */
409 } ac[] = {
aPiecek15cc4cf2023-04-17 15:23:21 +0200410 {CMD_ADD, NULL, linenoisePathCompletion, NULL},
411 {CMD_PRINT, "-f", NULL, get_print_format_arg},
412 {CMD_PRINT, "-P", NULL, get_schema_completion},
413 {CMD_PRINT, "-o", linenoisePathCompletion, NULL},
414 {CMD_PRINT, NULL, NULL, get_model_completion},
415 {CMD_SEARCHPATH, NULL, linenoisePathCompletion, NULL},
416 {CMD_DATA, "-t", NULL, get_data_type_arg},
417 {CMD_DATA, "-O", linenoisePathCompletion, NULL},
aPiecek079bcde2023-05-05 11:48:25 +0200418 {CMD_DATA, "-R", linenoisePathCompletion, NULL},
aPiecek15cc4cf2023-04-17 15:23:21 +0200419 {CMD_DATA, "-f", NULL, get_data_in_format_arg},
420 {CMD_DATA, "-F", NULL, get_data_in_format_arg},
421 {CMD_DATA, "-d", NULL, get_data_default_arg},
422 {CMD_DATA, "-o", linenoisePathCompletion, NULL},
423 {CMD_DATA, NULL, linenoisePathCompletion, NULL},
424 {CMD_LIST, NULL, NULL, get_list_format_arg},
425 {CMD_FEATURE, NULL, NULL, get_model_completion},
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200426 };
aPiecek15cc4cf2023-04-17 15:23:21 +0200427 size_t name_len;
428 const char *last, *name, *getoptstr;
429 char opt[3] = {'\0', ':', '\0'};
Radek Krejcied5acc52019-04-25 15:57:04 +0200430 char **matches = NULL;
431 unsigned int match_count = 0, i;
432
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200433 if (buf == hint) {
434 /* command autocomplete */
Radek Krejcied5acc52019-04-25 15:57:04 +0200435 get_cmd_completion(hint, &matches, &match_count);
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200436
437 } else {
438 for (i = 0; i < (sizeof ac / sizeof *ac); ++i) {
aPiecek15cc4cf2023-04-17 15:23:21 +0200439 /* Find the right command. */
440 name = commands[ac[i].ci].name;
441 name_len = strlen(name);
442 if (strncmp(buf, name, name_len) || (buf[name_len] != ' ')) {
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200443 /* not this command */
444 continue;
445 }
446
aPiecek15cc4cf2023-04-17 15:23:21 +0200447 /* Select based on the right option. */
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200448 last = get_last_str(buf, hint);
aPiecek15cc4cf2023-04-17 15:23:21 +0200449 opt[0] = (last[0] == '-') && last[1] ? last[1] : '\0';
450 getoptstr = commands[ac[i].ci].optstring;
451 if (!ac[i].opt && opt[0] && strstr(getoptstr, opt)) {
452 /* completion for the argument must be defined */
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200453 continue;
aPiecek15cc4cf2023-04-17 15:23:21 +0200454 } else if (ac[i].opt && opt[0] && strncmp(ac[i].opt, last, strlen(ac[i].opt))) {
455 /* completion for (another) option */
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200456 continue;
aPiecek15cc4cf2023-04-17 15:23:21 +0200457 } else if (ac[i].opt && !opt[0]) {
458 /* completion is defined for option */
459 continue;
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200460 }
461
462 /* callback */
463 if (ac[i].ln_cb) {
464 ac[i].ln_cb(buf, hint, lc);
465 } else {
466 ac[i].yl_cb(hint, &matches, &match_count);
467 }
468 break;
469 }
Radek Krejcied5acc52019-04-25 15:57:04 +0200470 }
471
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200472 /* transform matches into autocompletion, if needed */
Radek Krejcied5acc52019-04-25 15:57:04 +0200473 for (i = 0; i < match_count; ++i) {
474 linenoiseAddCompletion(lc, matches[i]);
475 free(matches[i]);
476 }
477 free(matches);
478}