blob: 67c6b687612bfd87b4d6f21d5703e6879d7fb222 [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
aPiecekae7eae12023-06-22 09:12:39 +020046 p = realloc(*matches, (*match_count + 1) * sizeof **matches);
romanc1607ce2022-09-12 10:12:39 +020047 if (!p) {
Michal Vasko1407d7d2023-08-21 09:57:54 +020048 YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
romanc1607ce2022-09-12 10:12:39 +020049 return;
50 }
51 *matches = p;
aPiecekae7eae12023-06-22 09:12:39 +020052 (*matches)[*match_count] = strdup(match);
53 if (!((*matches)[*match_count])) {
Michal Vasko1407d7d2023-08-21 09:57:54 +020054 YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
aPiecekae7eae12023-06-22 09:12:39 +020055 return;
56 }
57 ++(*match_count);
romanc1607ce2022-09-12 10:12:39 +020058}
59
60/**
61 * @brief Provides completion for command names.
62 *
63 * @param[in] hint User input.
64 * @param[out] matches Matches provided to the user as a completion hint.
65 * @param[out] match_count Number of matches.
66 */
Radek Krejcied5acc52019-04-25 15:57:04 +020067static void
68get_cmd_completion(const char *hint, char ***matches, unsigned int *match_count)
69{
70 int i;
Radek Krejcied5acc52019-04-25 15:57:04 +020071
72 *match_count = 0;
73 *matches = NULL;
74
75 for (i = 0; commands[i].name; i++) {
76 if (!strncmp(hint, commands[i].name, strlen(hint))) {
romanc1607ce2022-09-12 10:12:39 +020077 cmd_completion_add_match(commands[i].name, matches, match_count);
Radek Krejcied5acc52019-04-25 15:57:04 +020078 }
79 }
80}
81
romanc1607ce2022-09-12 10:12:39 +020082/**
aPiecek15cc4cf2023-04-17 15:23:21 +020083 * @brief Provides completion for arguments.
84 *
85 * @param[in] hint User input.
86 * @param[in] args Array of all possible arguments. The last element must be NULL.
87 * @param[out] matches Matches provided to the user as a completion hint.
88 * @param[out] match_count Number of matches.
89 */
90static void
91get_arg_completion(const char *hint, const char **args, char ***matches, unsigned int *match_count)
92{
93 int i;
94
95 *match_count = 0;
96 *matches = NULL;
97
98 for (i = 0; args[i]; i++) {
99 if (!strncmp(hint, args[i], strlen(hint))) {
100 cmd_completion_add_match(args[i], matches, match_count);
101 }
102 }
103 if (*match_count == 0) {
104 for (i = 0; args[i]; i++) {
105 cmd_completion_add_match(args[i], matches, match_count);
106 }
107 }
108}
109
110/**
romanc1607ce2022-09-12 10:12:39 +0200111 * @brief Provides completion for module names.
112 *
113 * @param[in] hint User input.
114 * @param[out] matches Matches provided to the user as a completion hint.
115 * @param[out] match_count Number of matches.
116 */
Radek Krejcied5acc52019-04-25 15:57:04 +0200117static void
118get_model_completion(const char *hint, char ***matches, unsigned int *match_count)
119{
Michal Vaskofd69e1d2020-07-03 11:57:17 +0200120 LY_ARRAY_COUNT_TYPE u;
Radek Krejcied5acc52019-04-25 15:57:04 +0200121 uint32_t idx = 0;
122 const struct lys_module *module;
Radek Krejcied5acc52019-04-25 15:57:04 +0200123
124 *match_count = 0;
125 *matches = NULL;
126
127 while ((module = ly_ctx_get_module_iter(ctx, &idx))) {
128 if (!strncmp(hint, module->name, strlen(hint))) {
romanc1607ce2022-09-12 10:12:39 +0200129 cmd_completion_add_match(module->name, matches, match_count);
Radek Krejcied5acc52019-04-25 15:57:04 +0200130 }
131
132 LY_ARRAY_FOR(module->parsed->includes, u) {
133 if (!strncmp(hint, module->parsed->includes[u].submodule->name, strlen(hint))) {
romanc1607ce2022-09-12 10:12:39 +0200134 cmd_completion_add_match(module->parsed->includes[u].submodule->name, matches, match_count);
Radek Krejcied5acc52019-04-25 15:57:04 +0200135 }
136 }
137 }
138}
139
romanc1607ce2022-09-12 10:12:39 +0200140/**
141 * @brief Add all child nodes of a single node to the completion hint.
142 *
aPiecek40351e02023-06-22 09:11:18 +0200143 * @param[in] parent Node of which children will be added to the hint.
aPiecek27337da2023-06-19 14:49:29 +0200144 * @param[out] matches Matches provided to the user as a completion hint.
145 * @param[out] match_count Number of matches.
romanc1607ce2022-09-12 10:12:39 +0200146 */
147static void
aPiecek40351e02023-06-22 09:11:18 +0200148single_hint_add_children(const struct lysc_node *parent, char ***matches, unsigned int *match_count)
romanc1607ce2022-09-12 10:12:39 +0200149{
150 const struct lysc_node *node = NULL;
151 char *match;
152
aPiecek40351e02023-06-22 09:11:18 +0200153 if (!parent) {
romanc1607ce2022-09-12 10:12:39 +0200154 return;
155 }
156
aPiecek40351e02023-06-22 09:11:18 +0200157 while ((node = lys_getnext(node, parent, NULL, LYS_GETNEXT_WITHCASE | LYS_GETNEXT_WITHCHOICE))) {
romanf00089c2022-10-06 16:01:31 +0200158 match = lysc_path(node, LYSC_PATH_LOG, NULL, 0);
romanc1607ce2022-09-12 10:12:39 +0200159 cmd_completion_add_match(match, matches, match_count);
160 free(match);
161 }
162}
163
164/**
165 * @brief Add module and/or node's children names to the hint.
166 *
167 * @param[in] module Compiled schema module.
168 * @param[in] parent Parent node of which children are potential matches.
169 * @param[in] hint_node_name Node name contained within the hint specified by user.
170 * @param[in,out] matches Matches provided to the user as a completion hint.
171 * @param[in,out] match_count Number of matches.
172 * @param[out] last_node Last processed node.
173 */
174static void
175add_all_children_nodes(const struct lysc_module *module, const struct lysc_node *parent,
176 const char *hint_node_name, char ***matches, unsigned int *match_count, const struct lysc_node **last_node)
177{
178 const struct lysc_node *node;
179 char *match, *node_name = NULL;
180
181 *last_node = NULL;
182
183 if (!parent && !module) {
184 return;
185 }
186
187 node = NULL;
romanf00089c2022-10-06 16:01:31 +0200188 while ((node = lys_getnext(node, parent, module, LYS_GETNEXT_WITHCASE | LYS_GETNEXT_WITHCHOICE))) {
romanc1607ce2022-09-12 10:12:39 +0200189 if (parent && (node->module != parent->module)) {
190 /* augmented node */
191 if (asprintf(&node_name, "%s:%s", node->module->name, node->name) == -1) {
Michal Vasko1407d7d2023-08-21 09:57:54 +0200192 YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
romanc1607ce2022-09-12 10:12:39 +0200193 break;
194 }
195 } else {
196 node_name = strdup(node->name);
197 if (!node_name) {
Michal Vasko1407d7d2023-08-21 09:57:54 +0200198 YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
romanc1607ce2022-09-12 10:12:39 +0200199 break;
200 }
201 }
202 if (!hint_node_name || !strncmp(hint_node_name, node_name, strlen(hint_node_name))) {
203 /* adding just module names + their top level node(s) to the hint */
204 *last_node = node;
romanf00089c2022-10-06 16:01:31 +0200205 match = lysc_path(node, LYSC_PATH_LOG, NULL, 0);
romanc1607ce2022-09-12 10:12:39 +0200206 cmd_completion_add_match(match, matches, match_count);
207 free(match);
208 }
209 free(node_name);
210 }
211}
212
213/**
214 * @brief Provides completion for schemas.
215 *
216 * @param[in] hint User input.
217 * @param[out] matches Matches provided to the user as a completion hint.
218 * @param[out] match_count Number of matches.
219 */
220static void
221get_schema_completion(const char *hint, char ***matches, unsigned int *match_count)
222{
223 const struct lys_module *module;
romanf00089c2022-10-06 16:01:31 +0200224 uint32_t idx;
romanc1607ce2022-09-12 10:12:39 +0200225 const char *start;
226 char *end, *module_name = NULL, *path = NULL;
227 const struct lysc_node *parent, *last_node;
228 int rc = 0;
229 size_t len;
230
231 *match_count = 0;
232 *matches = NULL;
233
234 if (strlen(hint)) {
235 if (hint[0] != '/') {
236 return;
237 }
238 start = hint + 1;
239 } else {
240 start = hint;
241 }
242
243 end = strchr(start, ':');
244 if (!end) {
245 /* no module name */
246 len = strlen(start);
247
248 /* go through all the modules */
249 idx = 0;
250 while ((module = ly_ctx_get_module_iter(ctx, &idx))) {
251 if (!module->implemented) {
252 continue;
253 }
254
255 if (!len || !strncmp(start, module->name, len)) {
256 /* add all their (matching) top level nodes */
257 add_all_children_nodes(module->compiled, NULL, NULL, matches, match_count, &last_node);
258 }
259 }
260 } else {
261 /* module name known */
262 module_name = strndup(start, end - start);
263 if (!module_name) {
Michal Vasko1407d7d2023-08-21 09:57:54 +0200264 YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
romanc1607ce2022-09-12 10:12:39 +0200265 rc = 1;
266 goto cleanup;
267 }
268
269 module = ly_ctx_get_module_implemented(ctx, module_name);
270 if (!module) {
271 goto cleanup;
272 }
273
274 /* find the last '/', if it is at the beginning of the hint, only path up to the top level node is known,
275 * else the name of the last node starts after the found '/' */
276 start = strrchr(hint, '/');
277 if (!start) {
278 goto cleanup;
279 }
280
281 if (start == hint) {
282 /* only the (incomplete) top level node path, add all (matching) top level nodes */
283 add_all_children_nodes(module->compiled, NULL, end + 1, matches, match_count, &last_node);
284 goto cleanup;
285 }
286
287 /* get rid of stuff after the last '/' to obtain the parent node */
288 path = strndup(hint, start - hint);
289 if (!path) {
Michal Vasko1407d7d2023-08-21 09:57:54 +0200290 YLMSG_E("Memory allocation failed (%s:%d, %s).", __FILE__, __LINE__, strerror(errno));
romanc1607ce2022-09-12 10:12:39 +0200291 rc = 1;
292 goto cleanup;
293 }
294
romanf00089c2022-10-06 16:01:31 +0200295 /* get the last parent in the hint (it may not exist) */
296 parent = find_schema_path(ctx, path);
romanc1607ce2022-09-12 10:12:39 +0200297
298 /* add all (matching) child nodes of the parent */
299 add_all_children_nodes(NULL, parent, start + 1, matches, match_count, &last_node);
300 }
301
302cleanup:
303 if (!rc && (*match_count == 1)) {
304 /* to avoid a single hint (space at the end), add all children as hints */
305 single_hint_add_children(last_node, matches, match_count);
306 }
307 free(path);
308 free(module_name);
309}
310
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200311/**
aPiecek15cc4cf2023-04-17 15:23:21 +0200312 * @brief Get all possible argument hints for option.
313 *
314 * @param[in] hint User input.
315 * @param[out] matches Matches provided to the user as a completion hint.
316 * @param[out] match_count Number of matches.
317 */
318static void
319get_print_format_arg(const char *hint, char ***matches, unsigned int *match_count)
320{
321 const char *args[] = {"yang", "yin", "tree", "info", NULL};
322
323 get_arg_completion(hint, args, matches, match_count);
324}
325
326/**
327 * @copydoc get_print_format_arg
328 */
329static void
330get_data_type_arg(const char *hint, char ***matches, unsigned int *match_count)
331{
332 const char *args[] = {"data", "config", "get", "getconfig", "edit", "rpc", "reply", "notif", NULL};
333
334 get_arg_completion(hint, args, matches, match_count);
335}
336
337/**
338 * @copydoc get_print_format_arg
339 */
340static void
341get_data_in_format_arg(const char *hint, char ***matches, unsigned int *match_count)
342{
343 const char *args[] = {"xml", "json", "lyb", NULL};
344
345 get_arg_completion(hint, args, matches, match_count);
346}
347
348/**
349 * @copydoc get_print_format_arg
350 */
351static void
352get_data_default_arg(const char *hint, char ***matches, unsigned int *match_count)
353{
354 const char *args[] = {"all", "all-tagged", "trim", "implicit-tagged", NULL};
355
356 get_arg_completion(hint, args, matches, match_count);
357}
358
359/**
360 * @copydoc get_print_format_arg
361 */
362static void
363get_list_format_arg(const char *hint, char ***matches, unsigned int *match_count)
364{
365 const char *args[] = {"xml", "json", NULL};
366
367 get_arg_completion(hint, args, matches, match_count);
368}
369
aPiecek207e5e42023-05-22 09:01:33 +0200370/**
371 * @copydoc get_print_format_arg
372 */
373static void
374get_verb_arg(const char *hint, char ***matches, unsigned int *match_count)
375{
376 const char *args[] = {"error", "warning", "verbose", "debug", NULL};
377
378 get_arg_completion(hint, args, matches, match_count);
379}
380
aPiecek9e28e382023-05-22 08:40:25 +0200381#ifndef NDEBUG
382/**
383 * @copydoc get_print_format_arg
384 */
385static void
386get_debug_arg(const char *hint, char ***matches, unsigned int *match_count)
387{
388 const char *args[] = {"dict", "xpath", "dep-sets", NULL};
389
390 get_arg_completion(hint, args, matches, match_count);
391}
392
393#endif
394
aPiecek15cc4cf2023-04-17 15:23:21 +0200395/**
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200396 * @brief Get the string before the hint, which autocompletion is for.
397 *
398 * @param[in] buf Complete user input.
399 * @param[in] hint Hint part of the user input.
400 * @return Pointer to the last string.
401 */
402static const char *
403get_last_str(const char *buf, const char *hint)
404{
405 const char *ptr;
406
407 if (buf == hint) {
408 return buf;
409 }
410
411 ptr = hint - 1;
412 while (ptr[0] == ' ') {
413 --ptr;
414 if (buf == ptr) {
415 return buf;
416 }
417 }
418
419 while (ptr[-1] != ' ') {
420 --ptr;
421 if (buf == ptr) {
422 return buf;
423 }
424 }
425
426 return ptr;
427}
428
romanc1607ce2022-09-12 10:12:39 +0200429/* callback */
Radek Krejcied5acc52019-04-25 15:57:04 +0200430void
431complete_cmd(const char *buf, const char *hint, linenoiseCompletions *lc)
432{
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200433 struct autocomplete {
aPiecek15cc4cf2023-04-17 15:23:21 +0200434 enum COMMAND_INDEX ci; /**< command index to global variable 'commands' */
435 const char *opt; /**< optional option */
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200436 void (*ln_cb)(const char *, const char *, linenoiseCompletions *); /**< linenoise callback to call */
437 void (*yl_cb)(const char *, char ***, unsigned int *); /**< yanglint callback to call */
438 } ac[] = {
aPiecek15cc4cf2023-04-17 15:23:21 +0200439 {CMD_ADD, NULL, linenoisePathCompletion, NULL},
440 {CMD_PRINT, "-f", NULL, get_print_format_arg},
441 {CMD_PRINT, "-P", NULL, get_schema_completion},
442 {CMD_PRINT, "-o", linenoisePathCompletion, NULL},
443 {CMD_PRINT, NULL, NULL, get_model_completion},
444 {CMD_SEARCHPATH, NULL, linenoisePathCompletion, NULL},
aPiecek647f62e2023-05-18 10:55:58 +0200445 {CMD_EXTDATA, NULL, linenoisePathCompletion, NULL},
aPiecek21c1bc82023-05-18 15:38:31 +0200446 {CMD_CLEAR, "-Y", linenoisePathCompletion, NULL},
aPiecek15cc4cf2023-04-17 15:23:21 +0200447 {CMD_DATA, "-t", NULL, get_data_type_arg},
448 {CMD_DATA, "-O", linenoisePathCompletion, NULL},
aPiecek079bcde2023-05-05 11:48:25 +0200449 {CMD_DATA, "-R", linenoisePathCompletion, NULL},
aPiecek15cc4cf2023-04-17 15:23:21 +0200450 {CMD_DATA, "-f", NULL, get_data_in_format_arg},
451 {CMD_DATA, "-F", NULL, get_data_in_format_arg},
452 {CMD_DATA, "-d", NULL, get_data_default_arg},
453 {CMD_DATA, "-o", linenoisePathCompletion, NULL},
454 {CMD_DATA, NULL, linenoisePathCompletion, NULL},
455 {CMD_LIST, NULL, NULL, get_list_format_arg},
456 {CMD_FEATURE, NULL, NULL, get_model_completion},
aPiecek207e5e42023-05-22 09:01:33 +0200457 {CMD_VERB, NULL, NULL, get_verb_arg},
aPiecek9e28e382023-05-22 08:40:25 +0200458#ifndef NDEBUG
459 {CMD_DEBUG, NULL, NULL, get_debug_arg},
460#endif
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200461 };
aPiecek15cc4cf2023-04-17 15:23:21 +0200462 size_t name_len;
463 const char *last, *name, *getoptstr;
464 char opt[3] = {'\0', ':', '\0'};
Radek Krejcied5acc52019-04-25 15:57:04 +0200465 char **matches = NULL;
466 unsigned int match_count = 0, i;
467
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200468 if (buf == hint) {
469 /* command autocomplete */
Radek Krejcied5acc52019-04-25 15:57:04 +0200470 get_cmd_completion(hint, &matches, &match_count);
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200471
472 } else {
473 for (i = 0; i < (sizeof ac / sizeof *ac); ++i) {
aPiecek15cc4cf2023-04-17 15:23:21 +0200474 /* Find the right command. */
475 name = commands[ac[i].ci].name;
476 name_len = strlen(name);
477 if (strncmp(buf, name, name_len) || (buf[name_len] != ' ')) {
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200478 /* not this command */
479 continue;
480 }
481
aPiecek15cc4cf2023-04-17 15:23:21 +0200482 /* Select based on the right option. */
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200483 last = get_last_str(buf, hint);
aPiecek15cc4cf2023-04-17 15:23:21 +0200484 opt[0] = (last[0] == '-') && last[1] ? last[1] : '\0';
485 getoptstr = commands[ac[i].ci].optstring;
486 if (!ac[i].opt && opt[0] && strstr(getoptstr, opt)) {
487 /* completion for the argument must be defined */
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200488 continue;
aPiecek15cc4cf2023-04-17 15:23:21 +0200489 } else if (ac[i].opt && opt[0] && strncmp(ac[i].opt, last, strlen(ac[i].opt))) {
490 /* completion for (another) option */
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200491 continue;
aPiecek15cc4cf2023-04-17 15:23:21 +0200492 } else if (ac[i].opt && !opt[0]) {
493 /* completion is defined for option */
494 continue;
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200495 }
496
497 /* callback */
498 if (ac[i].ln_cb) {
499 ac[i].ln_cb(buf, hint, lc);
500 } else {
501 ac[i].yl_cb(hint, &matches, &match_count);
502 }
503 break;
504 }
Radek Krejcied5acc52019-04-25 15:57:04 +0200505 }
506
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200507 /* transform matches into autocompletion, if needed */
Radek Krejcied5acc52019-04-25 15:57:04 +0200508 for (i = 0; i < match_count; ++i) {
509 linenoiseAddCompletion(lc, matches[i]);
510 free(matches[i]);
511 }
512 free(matches);
513}