blob: 9843816f752ab7402b3d64ef0c7366e761d4aaad [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/**
romanc1607ce2022-09-12 10:12:39 +020079 * @brief Provides completion for module names.
80 *
81 * @param[in] hint User input.
82 * @param[out] matches Matches provided to the user as a completion hint.
83 * @param[out] match_count Number of matches.
84 */
Radek Krejcied5acc52019-04-25 15:57:04 +020085static void
86get_model_completion(const char *hint, char ***matches, unsigned int *match_count)
87{
Michal Vaskofd69e1d2020-07-03 11:57:17 +020088 LY_ARRAY_COUNT_TYPE u;
Radek Krejcied5acc52019-04-25 15:57:04 +020089 uint32_t idx = 0;
90 const struct lys_module *module;
Radek Krejcied5acc52019-04-25 15:57:04 +020091
92 *match_count = 0;
93 *matches = NULL;
94
95 while ((module = ly_ctx_get_module_iter(ctx, &idx))) {
96 if (!strncmp(hint, module->name, strlen(hint))) {
romanc1607ce2022-09-12 10:12:39 +020097 cmd_completion_add_match(module->name, matches, match_count);
Radek Krejcied5acc52019-04-25 15:57:04 +020098 }
99
100 LY_ARRAY_FOR(module->parsed->includes, u) {
101 if (!strncmp(hint, module->parsed->includes[u].submodule->name, strlen(hint))) {
romanc1607ce2022-09-12 10:12:39 +0200102 cmd_completion_add_match(module->parsed->includes[u].submodule->name, matches, match_count);
Radek Krejcied5acc52019-04-25 15:57:04 +0200103 }
104 }
105 }
106}
107
romanc1607ce2022-09-12 10:12:39 +0200108/**
109 * @brief Add all child nodes of a single node to the completion hint.
110 *
111 * @param[in] last_node Node of which children will be added to the hint.
112 * @param matches[out] Matches provided to the user as a completion hint.
113 * @param match_count[out] Number of matches.
114 */
115static void
116single_hint_add_children(const struct lysc_node *last_node, char ***matches, unsigned int *match_count)
117{
118 const struct lysc_node *node = NULL;
119 char *match;
120
121 if (!last_node) {
122 return;
123 }
124
romanf00089c2022-10-06 16:01:31 +0200125 while ((node = lys_getnext(node, last_node, NULL, LYS_GETNEXT_WITHCASE | LYS_GETNEXT_WITHCHOICE))) {
126 match = lysc_path(node, LYSC_PATH_LOG, NULL, 0);
romanc1607ce2022-09-12 10:12:39 +0200127 cmd_completion_add_match(match, matches, match_count);
128 free(match);
129 }
130}
131
132/**
133 * @brief Add module and/or node's children names to the hint.
134 *
135 * @param[in] module Compiled schema module.
136 * @param[in] parent Parent node of which children are potential matches.
137 * @param[in] hint_node_name Node name contained within the hint specified by user.
138 * @param[in,out] matches Matches provided to the user as a completion hint.
139 * @param[in,out] match_count Number of matches.
140 * @param[out] last_node Last processed node.
141 */
142static void
143add_all_children_nodes(const struct lysc_module *module, const struct lysc_node *parent,
144 const char *hint_node_name, char ***matches, unsigned int *match_count, const struct lysc_node **last_node)
145{
146 const struct lysc_node *node;
147 char *match, *node_name = NULL;
148
149 *last_node = NULL;
150
151 if (!parent && !module) {
152 return;
153 }
154
155 node = NULL;
romanf00089c2022-10-06 16:01:31 +0200156 while ((node = lys_getnext(node, parent, module, LYS_GETNEXT_WITHCASE | LYS_GETNEXT_WITHCHOICE))) {
romanc1607ce2022-09-12 10:12:39 +0200157 if (parent && (node->module != parent->module)) {
158 /* augmented node */
159 if (asprintf(&node_name, "%s:%s", node->module->name, node->name) == -1) {
160 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
161 break;
162 }
163 } else {
164 node_name = strdup(node->name);
165 if (!node_name) {
166 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
167 break;
168 }
169 }
170 if (!hint_node_name || !strncmp(hint_node_name, node_name, strlen(hint_node_name))) {
171 /* adding just module names + their top level node(s) to the hint */
172 *last_node = node;
romanf00089c2022-10-06 16:01:31 +0200173 match = lysc_path(node, LYSC_PATH_LOG, NULL, 0);
romanc1607ce2022-09-12 10:12:39 +0200174 cmd_completion_add_match(match, matches, match_count);
175 free(match);
176 }
177 free(node_name);
178 }
179}
180
181/**
182 * @brief Provides completion for schemas.
183 *
184 * @param[in] hint User input.
185 * @param[out] matches Matches provided to the user as a completion hint.
186 * @param[out] match_count Number of matches.
187 */
188static void
189get_schema_completion(const char *hint, char ***matches, unsigned int *match_count)
190{
191 const struct lys_module *module;
romanf00089c2022-10-06 16:01:31 +0200192 uint32_t idx;
romanc1607ce2022-09-12 10:12:39 +0200193 const char *start;
194 char *end, *module_name = NULL, *path = NULL;
195 const struct lysc_node *parent, *last_node;
196 int rc = 0;
197 size_t len;
198
199 *match_count = 0;
200 *matches = NULL;
201
202 if (strlen(hint)) {
203 if (hint[0] != '/') {
204 return;
205 }
206 start = hint + 1;
207 } else {
208 start = hint;
209 }
210
211 end = strchr(start, ':');
212 if (!end) {
213 /* no module name */
214 len = strlen(start);
215
216 /* go through all the modules */
217 idx = 0;
218 while ((module = ly_ctx_get_module_iter(ctx, &idx))) {
219 if (!module->implemented) {
220 continue;
221 }
222
223 if (!len || !strncmp(start, module->name, len)) {
224 /* add all their (matching) top level nodes */
225 add_all_children_nodes(module->compiled, NULL, NULL, matches, match_count, &last_node);
226 }
227 }
228 } else {
229 /* module name known */
230 module_name = strndup(start, end - start);
231 if (!module_name) {
232 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
233 rc = 1;
234 goto cleanup;
235 }
236
237 module = ly_ctx_get_module_implemented(ctx, module_name);
238 if (!module) {
239 goto cleanup;
240 }
241
242 /* find the last '/', if it is at the beginning of the hint, only path up to the top level node is known,
243 * else the name of the last node starts after the found '/' */
244 start = strrchr(hint, '/');
245 if (!start) {
246 goto cleanup;
247 }
248
249 if (start == hint) {
250 /* only the (incomplete) top level node path, add all (matching) top level nodes */
251 add_all_children_nodes(module->compiled, NULL, end + 1, matches, match_count, &last_node);
252 goto cleanup;
253 }
254
255 /* get rid of stuff after the last '/' to obtain the parent node */
256 path = strndup(hint, start - hint);
257 if (!path) {
258 YLMSG_E("Memory allocation failed (%s:%d, %s)", __FILE__, __LINE__, strerror(errno));
259 rc = 1;
260 goto cleanup;
261 }
262
romanf00089c2022-10-06 16:01:31 +0200263 /* get the last parent in the hint (it may not exist) */
264 parent = find_schema_path(ctx, path);
romanc1607ce2022-09-12 10:12:39 +0200265
266 /* add all (matching) child nodes of the parent */
267 add_all_children_nodes(NULL, parent, start + 1, matches, match_count, &last_node);
268 }
269
270cleanup:
271 if (!rc && (*match_count == 1)) {
272 /* to avoid a single hint (space at the end), add all children as hints */
273 single_hint_add_children(last_node, matches, match_count);
274 }
275 free(path);
276 free(module_name);
277}
278
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200279/**
280 * @brief Get the string before the hint, which autocompletion is for.
281 *
282 * @param[in] buf Complete user input.
283 * @param[in] hint Hint part of the user input.
284 * @return Pointer to the last string.
285 */
286static const char *
287get_last_str(const char *buf, const char *hint)
288{
289 const char *ptr;
290
291 if (buf == hint) {
292 return buf;
293 }
294
295 ptr = hint - 1;
296 while (ptr[0] == ' ') {
297 --ptr;
298 if (buf == ptr) {
299 return buf;
300 }
301 }
302
303 while (ptr[-1] != ' ') {
304 --ptr;
305 if (buf == ptr) {
306 return buf;
307 }
308 }
309
310 return ptr;
311}
312
romanc1607ce2022-09-12 10:12:39 +0200313/* callback */
Radek Krejcied5acc52019-04-25 15:57:04 +0200314void
315complete_cmd(const char *buf, const char *hint, linenoiseCompletions *lc)
316{
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200317 struct autocomplete {
318 const char *cmd; /**< command */
319 const char *opt; /**< optional option */
320 int last_opt; /**< whether to autocomplete even if an option is last in the hint */
321
322 void (*ln_cb)(const char *, const char *, linenoiseCompletions *); /**< linenoise callback to call */
323 void (*yl_cb)(const char *, char ***, unsigned int *); /**< yanglint callback to call */
324 } ac[] = {
325 {"add", NULL, 1, linenoisePathCompletion, NULL},
326 {"searchpath", NULL, 0, linenoisePathCompletion, NULL},
327 {"data", NULL, 0, linenoisePathCompletion, NULL},
328 {"print", NULL, 0, NULL, get_model_completion},
329 {"feature", NULL, 0, NULL, get_model_completion},
330 {"print", "-P", 1, NULL, get_schema_completion},
331 };
332 size_t cmd_len;
333 const char *last;
Radek Krejcied5acc52019-04-25 15:57:04 +0200334 char **matches = NULL;
335 unsigned int match_count = 0, i;
336
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200337 if (buf == hint) {
338 /* command autocomplete */
Radek Krejcied5acc52019-04-25 15:57:04 +0200339 get_cmd_completion(hint, &matches, &match_count);
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200340
341 } else {
342 for (i = 0; i < (sizeof ac / sizeof *ac); ++i) {
343 cmd_len = strlen(ac[i].cmd);
344 if (strncmp(buf, ac[i].cmd, cmd_len) || (buf[cmd_len] != ' ')) {
345 /* not this command */
346 continue;
347 }
348
349 last = get_last_str(buf, hint);
350 if (ac[i].opt && strncmp(ac[i].opt, last, strlen(ac[i].opt))) {
351 /* autocompletion for (another) option */
352 continue;
353 }
354 if (!ac[i].last_opt && (last[0] == '-')) {
355 /* autocompletion for the command, not an option */
356 continue;
357 }
358 if ((last != buf) && (last[0] != '-')) {
359 /* autocompleted */
360 return;
361 }
362
363 /* callback */
364 if (ac[i].ln_cb) {
365 ac[i].ln_cb(buf, hint, lc);
366 } else {
367 ac[i].yl_cb(hint, &matches, &match_count);
368 }
369 break;
370 }
Radek Krejcied5acc52019-04-25 15:57:04 +0200371 }
372
Michal Vaskoa1c986f2022-09-14 12:01:48 +0200373 /* transform matches into autocompletion, if needed */
Radek Krejcied5acc52019-04-25 15:57:04 +0200374 for (i = 0; i < match_count; ++i) {
375 linenoiseAddCompletion(lc, matches[i]);
376 free(matches[i]);
377 }
378 free(matches);
379}