Line data Source code
1 : /**
2 : * @file
3 : *
4 : * @brief
5 : *
6 : * @copyright BSD License (see LICENSE.md or https://www.libelektra.org)
7 : */
8 :
9 : #include "complete.hpp"
10 :
11 : #include <functional>
12 : #include <iostream>
13 : #include <limits>
14 : #include <stack>
15 :
16 : #include "cmdline.hpp"
17 : #include <kdb.hpp>
18 : #include <keysetio.hpp>
19 :
20 : using namespace kdb;
21 : using namespace std;
22 :
23 85 : CompleteCommand::CompleteCommand ()
24 : {
25 85 : }
26 :
27 7 : int CompleteCommand::execute (Cmdline const & cl)
28 : {
29 7 : if (cl.maxDepth <= cl.minDepth)
30 : {
31 0 : throw invalid_argument ("the maximum depth has to be larger than the minimum depth");
32 : }
33 7 : if (cl.maxDepth < 0)
34 : {
35 0 : throw invalid_argument ("the maximum depth has to be a positive number");
36 : }
37 7 : if (cl.minDepth < 0)
38 : {
39 0 : throw invalid_argument ("the minimum depth has to be a positive number");
40 : }
41 :
42 7 : cout.setf (ios_base::unitbuf);
43 7 : if (cl.null)
44 : {
45 : cout.unsetf (ios_base::skipws);
46 : }
47 :
48 14 : const bool hasArgument = cl.arguments.size () > 0;
49 22 : const string argument = hasArgument ? cl.arguments[cl.arguments.size () - 1] : "";
50 7 : complete (argument, cl);
51 :
52 14 : return 0;
53 : }
54 :
55 7 : void CompleteCommand::complete (string const & argument, Cmdline const & cl)
56 : {
57 : using namespace std::placeholders; // for bind
58 :
59 7 : if (argument.empty ())
60 : { // No argument, show all completions by analyzing everything including namespaces, so adjust the offset for that
61 2 : const Key root = Key ("/", KEY_END);
62 8 : printResults (root, cl.minDepth, cl.maxDepth, cl, analyze (getKeys (root, false, cl), cl),
63 4 : bind (filterDepth, cl.minDepth, cl.maxDepth, _1), printResult);
64 : }
65 12 : else if (!argument.empty () && argument[0] == '+')
66 : { // is it a bookmark?
67 : // Try to resolve the bookmark
68 0 : const Key resolvedBookmark = cl.resolveBookmark (argument);
69 0 : if (resolvedBookmark.isValid ())
70 : {
71 0 : complete (resolvedBookmark.getFullName (), cl);
72 : }
73 : else
74 : { // Bookmark not resolvable, so try a bookmark completion
75 : // since for legacy reasons its probably /sw/kdb, we use /sw as a root
76 0 : const Key root = Key ("/sw", KEY_END);
77 0 : printResults (root, 0, cl.maxDepth, cl, analyze (getKeys (root, true, cl), cl),
78 0 : bind (filterBookmarks, argument, _1), printBookmarkResult);
79 : }
80 : }
81 : else
82 : {
83 18 : const Key parsedArgument (argument, KEY_END);
84 26 : if ((!parsedArgument.isValid () || !shallShowNextLevel (argument)) && parsedArgument.getBaseName ().empty ())
85 : { // is it a namespace completion?
86 4 : const Key root = Key ("/", KEY_END);
87 200 : const auto filter = [&](const pair<Key, pair<int, int>> & c) {
88 400 : return filterDepth (cl.minDepth, cl.maxDepth, c) && filterName (argument, c);
89 202 : };
90 16 : printResults (root, cl.minDepth, cl.maxDepth, cl, analyze (getKeys (root, false, cl), cl), filter, printResult);
91 : }
92 : else
93 : { // the "normal" completion cases
94 4 : completeNormal (argument, parsedArgument, cl);
95 : }
96 : }
97 7 : }
98 :
99 4 : void CompleteCommand::completeNormal (string const & argument, Key const & parsedArgument, Cmdline const & cl)
100 : {
101 8 : Key root = parsedArgument;
102 8 : const Key parent = getParentKey (root);
103 : // Its important to use the parent element here as using non-existent elements may yield no keys
104 12 : KeySet ks = getKeys (parent, false, cl);
105 : // the namespaces count as existent although not found by lookup
106 20 : const bool isValidNamespace = parsedArgument.isValid () && parsedArgument.getBaseName ().empty ();
107 13 : const bool rootExists = isValidNamespace || ks.lookup (root);
108 4 : if (!rootExists)
109 : {
110 : root = parent;
111 : }
112 8 : const auto result = analyze (ks, cl);
113 :
114 : // we see depth relative to the completion level, if the root exists, distance will be higher so subtract 1
115 : // to add up for the offset added my shallShowNextLevel
116 8 : const int offset = getKeyDepth (root) - rootExists + shallShowNextLevel (argument);
117 4 : if (cl.debug)
118 : {
119 0 : cout << "Root is " << root << " with a key depth of " << getKeyDepth (root) << endl;
120 0 : cout << "The root exists: " << rootExists << "and we shall show the next level: " << shallShowNextLevel (argument) << endl;
121 0 : cout << "Offset relative to completion level is " << offset << endl;
122 : }
123 :
124 4 : const auto nameFilter = root.isCascading () ? filterCascading : filterName;
125 : // Let elektra handle the escaping of the input for us
126 8 : const string argumentEscaped = parsedArgument.getFullName ();
127 72 : const auto filter = [&](const pair<Key, pair<int, int>> & c) {
128 288 : return filterDepth (cl.minDepth + offset,
129 297 : max (cl.maxDepth, cl.maxDepth > INT_MAX - offset ? INT_MAX : cl.maxDepth + offset), c) &&
130 9 : nameFilter (argument, c);
131 76 : };
132 12 : printResults (root, cl.minDepth, cl.maxDepth, cl, result, filter, printResult);
133 4 : }
134 :
135 : /*
136 : * McCabe complexity of 12, 3 caused by debug switches so its ok
137 : */
138 7 : const map<Key, pair<int, int>> CompleteCommand::analyze (KeySet const & ks, Cmdline const & cl)
139 : {
140 7 : map<Key, pair<int, int>> hierarchy;
141 21 : stack<Key> keyStack;
142 14 : Key parent;
143 14 : Key last;
144 7 : addNamespaces (hierarchy, cl);
145 :
146 7 : ks.rewind ();
147 21 : if (!(ks.next ()))
148 : {
149 : return hierarchy;
150 : }
151 :
152 7 : const Key first = ks.current ();
153 7 : int curDepth = getKeyDepth (first) - 1;
154 21 : hierarchy[first] = pair<int, int> (0, curDepth);
155 : keyStack.push (first);
156 :
157 857 : while (!keyStack.empty ())
158 : {
159 1275 : Key current = keyStack.top ();
160 425 : keyStack.pop ();
161 843 : if (last.isValid () && current.isDirectBelow (last))
162 : { // down in the hierarchy, last element is new parent
163 135 : parent = last;
164 135 : curDepth++;
165 : }
166 :
167 425 : if (cl.debug)
168 : {
169 0 : cout << "Analyzing " << current << ", the last processed key is " << last << " and the parent is " << parent
170 0 : << " at depth " << curDepth << endl;
171 : }
172 :
173 425 : if (current.isDirectBelow (parent))
174 : { // hierarchy continues at the current level
175 1113 : increaseCount (hierarchy, parent, [](int p) { return p; });
176 1484 : increaseCount (hierarchy, current, [=](int) { return curDepth; });
177 : }
178 : else
179 : { // hierarchy does not fit the current parent, expand the current key to the stack to find the new parent
180 54 : Key tmp = current;
181 649 : while (!hierarchy[current].first && !(current.getBaseName ().empty () && 1 == getKeyDepth (current)))
182 : { // Go back up in the hierarchy until we encounter a known key or are back at the namespace level
183 119 : if (cl.debug)
184 : {
185 0 : cout << "Expanding " << tmp << endl;
186 : }
187 119 : keyStack.push (tmp);
188 119 : current = tmp;
189 357 : tmp = getParentKey (tmp);
190 : }
191 162 : parent = getParentKey (current);
192 54 : curDepth = hierarchy[current].second;
193 54 : if (cl.debug)
194 : {
195 0 : cout << "Finished expanding, resume at " << current << " with parent " << parent << " and depth "
196 0 : << curDepth << endl;
197 : }
198 : }
199 :
200 1768 : if (keyStack.empty () && (ks.next ()))
201 : { // Current hierarchy processed, we can resume with the next
202 897 : keyStack.push (ks.current ());
203 : }
204 425 : last = current;
205 : }
206 :
207 : return hierarchy;
208 : }
209 :
210 7 : void CompleteCommand::printResults (
211 : Key const & root, const int minDepth, const int maxDepth, Cmdline const & cl, map<Key, pair<int, int>> const & result,
212 : std::function<bool(pair<Key, pair<int, int>> const & current)> const & filter,
213 : std::function<void(pair<Key, pair<int, int>> const & current, const bool verbose)> const & resultPrinter)
214 : {
215 7 : if (cl.verbose)
216 : {
217 0 : cout << "Showing results for a minimum depth of " << minDepth;
218 0 : if (maxDepth != numeric_limits<int>::max ())
219 : {
220 0 : cout << " and a maximum depth of " << maxDepth;
221 : }
222 : else
223 : {
224 0 : cout << " and no maximum depth";
225 : }
226 : cout << endl;
227 : }
228 :
229 393 : for (const auto & it : result)
230 : {
231 1488 : if (cl.debug || filter (it))
232 : {
233 64 : resultPrinter (it, cl.verbose);
234 : }
235 : }
236 :
237 7 : if (cl.debug || cl.verbose)
238 : { // Only print this in debug mode to avoid destroying autocompletions because of warnings
239 0 : printWarnings (cerr, root, cl.verbose, cl.debug);
240 : }
241 7 : }
242 :
243 11 : int CompleteCommand::getKeyDepth (Key const & key)
244 : {
245 22 : return std::distance (key.begin (), key.end ());
246 : }
247 :
248 177 : const Key CompleteCommand::getParentKey (Key const & key)
249 : {
250 354 : Key parentKey = key.dup (); // We can't set baseName on keys in keysets, so duplicate it
251 177 : ckdb::keySetBaseName (parentKey.getKey (), NULL);
252 177 : return parentKey;
253 : }
254 :
255 7 : KeySet CompleteCommand::getKeys (Key root, const bool cutAtRoot, Cmdline const & cl)
256 : {
257 7 : KeySet ks;
258 14 : KDB kdb;
259 7 : kdb.get (ks, root);
260 7 : addMountpoints (ks, root, cl);
261 7 : if (cutAtRoot)
262 : {
263 0 : ks = ks.cut (root);
264 : }
265 7 : return ks;
266 : }
267 :
268 0 : bool CompleteCommand::shallShowNextLevel (string const & argument)
269 : {
270 9 : auto it = argument.rbegin ();
271 : // If the argument ends in / its an indicator to complete the next level (like done by shells), but not if its escaped
272 60 : return it != argument.rend () && (*it) == '/' && ((++it) == argument.rend () || (*it) != '\\');
273 : }
274 :
275 7 : void CompleteCommand::addMountpoints (KeySet & ks, Key const & root, Cmdline const & cl)
276 : {
277 14 : KDB kdb;
278 14 : Key mountpointPath ("system/elektra/mountpoints", KEY_END);
279 14 : KeySet mountpoints;
280 :
281 7 : kdb.get (mountpoints, mountpointPath);
282 35 : mountpoints = mountpoints.cut (mountpointPath);
283 :
284 21 : for (const Key mountpoint : mountpoints)
285 : {
286 0 : if (mountpoint.isDirectBelow (mountpointPath))
287 : {
288 0 : const string actualName = mountpoints.lookup (mountpoint.getFullName () + "/mountpoint").getString ();
289 0 : Key mountpointKey (actualName, KEY_END);
290 : // If the mountpoint already has some contents, its expanded with a namespace, so leave it out then
291 0 : if (mountpointKey.isBelow (root) && !KeySet (ks).cut (mountpointKey).size ())
292 : {
293 : ks.append (mountpointKey);
294 : }
295 : }
296 : }
297 :
298 7 : if (cl.debug || cl.verbose)
299 : { // Only print this in debug mode to avoid destroying autocompletions because of warnings
300 0 : printWarnings (cerr, mountpointPath, cl.verbose, cl.debug);
301 : }
302 7 : }
303 :
304 : /*
305 : * McCabe complexity of 11, 4 caused by debug switches so its ok
306 : */
307 7 : void CompleteCommand::addNamespaces (map<Key, pair<int, int>> & hierarchy, Cmdline const & cl)
308 : {
309 : const string namespaces[] = {
310 : "spec/", "proc/", "dir/", "user/", "system/",
311 119 : };
312 :
313 : // Check for new namespaces, issue a warning in case
314 7 : if (cl.debug || cl.verbose)
315 : {
316 0 : for (elektraNamespace ens = KEY_NS_FIRST; ens <= KEY_NS_LAST; ++ens)
317 : {
318 : // since ens are numbers, there is no way to get a string representation if not found in that case
319 : bool found = false;
320 0 : for (const string ns : namespaces)
321 : {
322 0 : found = found || ckdb::keyGetNamespace (Key (ns, KEY_END).getKey ()) == ens;
323 : }
324 0 : if (!found)
325 : {
326 0 : cerr << "Missing namespace detected:" << ens << ". \nPlease report this issue." << endl;
327 : }
328 : }
329 : }
330 :
331 112 : for (const string ns : namespaces)
332 : {
333 105 : const Key nsKey (ns, KEY_END);
334 35 : if ((cl.debug || cl.verbose) && ckdb::keyGetNamespace (nsKey.getKey ()) == KEY_NS_EMPTY)
335 : { // Check for outdated namespaces, issue a warning in case
336 0 : cerr << "Outdated namespace detected:" << ns << ".\nPlease report this issue." << endl;
337 : }
338 105 : hierarchy[nsKey] = pair<int, int> (1, 0);
339 : }
340 7 : }
341 :
342 742 : void CompleteCommand::increaseCount (map<Key, pair<int, int>> & hierarchy, Key const & key, function<int(int)> const & depthIncreaser)
343 : {
344 742 : const pair<int, int> prev = hierarchy[key];
345 2968 : hierarchy[key] = pair<int, int> (prev.first + 1, depthIncreaser (prev.second));
346 742 : }
347 :
348 100 : bool CompleteCommand::filterDepth (const int minDepth, const int maxDepth, pair<Key, pair<int, int>> const & current)
349 : {
350 372 : return current.second.second >= minDepth && current.second.second < maxDepth;
351 : }
352 :
353 8 : bool CompleteCommand::filterCascading (string const & argument, pair<Key, pair<int, int>> const & current)
354 : {
355 : // For a cascading key completion, ignore the preceding namespace
356 16 : const string test = current.first.getFullName ();
357 8 : size_t cascadationOffset = test.find ("/");
358 8 : if (cascadationOffset == string::npos)
359 : {
360 0 : cascadationOffset = 0;
361 : }
362 24 : return argument.size () <= test.size () - cascadationOffset &&
363 56 : equal (argument.begin (), argument.end (), test.begin () + cascadationOffset);
364 : }
365 :
366 11 : bool CompleteCommand::filterName (string const & argument, pair<Key, pair<int, int>> const & current)
367 : {
368 22 : const string test = current.first.getFullName ();
369 62 : return argument.size () <= test.size () && equal (argument.begin (), argument.end (), test.begin ());
370 : }
371 :
372 : /**
373 : * McCabe Complexity of 15 due to the boolean conjunctions, easy to understand so its ok
374 : */
375 0 : bool CompleteCommand::filterBookmarks (string const & bookmarkName, pair<Key, pair<int, int>> const & current)
376 : {
377 : // For a bookmark completion, ignore everything except the bookmarks by comparing the base name
378 : // as we search in /sw due to legacy reasons, ensure we have an actual bookmark by checking the path
379 0 : bool elektraFound = false;
380 0 : bool kdbFound = false;
381 0 : bool bookmarksFound = false;
382 0 : bool bookmarkFound = false;
383 :
384 0 : for (const string part : current.first)
385 : {
386 : // size 1 -> +, so show all bookmarks, order of these checks is important!
387 0 : bookmarkFound = bookmarksFound && (bookmarkFound || bookmarkName.size () == 1 ||
388 0 : (bookmarkName.size () - 1 <= part.size () &&
389 0 : equal (bookmarkName.begin () + 1, bookmarkName.end (), part.begin ())));
390 0 : bookmarksFound = bookmarksFound || (part == "bookmarks" && !bookmarkFound);
391 0 : kdbFound = kdbFound || (!bookmarksFound && part == "kdb");
392 0 : elektraFound = elektraFound || (!kdbFound && part == "elektra");
393 : }
394 :
395 0 : return (elektraFound || kdbFound) && bookmarksFound && bookmarkFound;
396 : }
397 :
398 0 : void CompleteCommand::printBookmarkResult (pair<Key, pair<int, int>> const & current, const bool verbose)
399 : { // Ignore the path for a bookmark completion
400 0 : cout << "+" << current.first.getBaseName ();
401 0 : if (current.second.first > 1)
402 : {
403 0 : cout << "/";
404 : }
405 0 : if (verbose)
406 : {
407 0 : cout << (current.second.first > 1 ? " node " : " leaf ");
408 0 : cout << (current.second.first - 1) << " " << current.second.second;
409 : }
410 0 : cout << endl;
411 0 : }
412 :
413 16 : void CompleteCommand::printResult (pair<Key, pair<int, int>> const & current, const bool verbose)
414 : {
415 48 : cout << current.first.getFullName ();
416 16 : if (current.second.first > 1)
417 : {
418 9 : cout << "/";
419 : }
420 16 : if (verbose)
421 : {
422 0 : cout << (current.second.first > 1 ? " node " : " leaf ");
423 0 : cout << (current.second.first - 1) << " " << current.second.second;
424 : }
425 16 : cout << endl;
426 16 : }
427 :
428 85 : CompleteCommand::~CompleteCommand ()
429 : {
430 7249 : }
|