My Project
commands.cpp
1 /*
2  * uuid-console - Microcontroller console shell
3  * Copyright 2019,2022 Simon Arlott
4  *
5  * This program is free software: you can redistribute it and/or modify
6  * it under the terms of the GNU General Public License as published by
7  * the Free Software Foundation, either version 3 of the License, or
8  * (at your option) any later version.
9  *
10  * This program is distributed in the hope that it will be useful,
11  * but WITHOUT ANY WARRANTY; without even the implied warranty of
12  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13  * GNU General Public License for more details.
14  *
15  * You should have received a copy of the GNU General Public License
16  * along with this program. If not, see <http://www.gnu.org/licenses/>.
17  */
18 
19 #include <uuid/console.h>
20 
21 #include <Arduino.h>
22 
23 #include <algorithm>
24 #include <memory>
25 #include <map>
26 #include <set>
27 #include <string>
28 #include <utility>
29 #include <vector>
30 
31 #if !defined(__cpp_lib_make_unique) && !defined(DOXYGEN)
32 namespace std {
33 
34 template<typename _Tp, typename... _Args>
35 inline unique_ptr<_Tp> make_unique(_Args&&... __args) {
36  return unique_ptr<_Tp>(new _Tp(std::forward<_Args>(__args)...));
37 }
38 
39 } // namespace std
40 #endif
41 
42 namespace uuid {
43 
44 namespace console {
45 
47  add_command(0, 0, 0, name, flash_string_vector{}, function, nullptr);
48 }
49 
51  command_function function) {
52  add_command(0, 0, 0, name, arguments, function, nullptr);
53 }
54 
56  command_function function, argument_completion_function arg_function) {
57  add_command(0, 0, 0, name, arguments, function, arg_function);
58 }
59 
60 void Commands::add_command(unsigned int context, const flash_string_vector &name,
61  command_function function) {
62  add_command(context, 0, 0, name, flash_string_vector{}, function, nullptr);
63 }
64 
65 void Commands::add_command(unsigned int context, const flash_string_vector &name,
66  const flash_string_vector &arguments, command_function function) {
67  add_command(context, 0, 0, name, arguments, function, nullptr);
68 }
69 
70 void Commands::add_command(unsigned int context, const flash_string_vector &name,
71  const flash_string_vector &arguments, command_function function,
72  argument_completion_function arg_function) {
73  add_command(context, 0, 0, name, arguments, function, arg_function);
74 }
75 
76 void Commands::add_command(unsigned int context, unsigned int flags,
77  const flash_string_vector &name, command_function function) {
78  add_command(context, flags, 0, name, flash_string_vector{}, function, nullptr);
79 }
80 
81 void Commands::add_command(unsigned int context, unsigned int flags,
82  const flash_string_vector &name, const flash_string_vector &arguments,
83  command_function function) {
84  add_command(context, flags, 0, name, arguments, function, nullptr);
85 }
86 
87 void Commands::add_command(unsigned int context, unsigned int flags,
88  const flash_string_vector &name, const flash_string_vector &arguments,
89  command_function function, argument_completion_function arg_function) {
90  add_command(context, flags, 0, name, arguments, function, arg_function);
91 }
92 
93 void Commands::add_command(unsigned int context, unsigned int flags, unsigned int not_flags,
94  const flash_string_vector &name, command_function function) {
95  add_command(context, flags, not_flags, name, flash_string_vector{}, function, nullptr);
96 }
97 
98 void Commands::add_command(unsigned int context, unsigned int flags, unsigned int not_flags,
99  const flash_string_vector &name, const flash_string_vector &arguments,
100  command_function function) {
101  add_command(context, flags, not_flags, name, arguments, function, nullptr);
102 }
103 
104 void Commands::add_command(unsigned int context, unsigned int flags, unsigned int not_flags,
105  const flash_string_vector &name, const flash_string_vector &arguments,
106  command_function function, argument_completion_function arg_function) {
107  commands_.emplace(std::piecewise_construct, std::forward_as_tuple(context),
108  std::forward_as_tuple(flags, not_flags, name, arguments, function, arg_function));
109 }
110 
112  auto commands = find_command(shell, command_line);
113  auto longest = commands.exact.crbegin();
114  Execution result;
115 
116  result.error = nullptr;
117 
118  if (commands.exact.empty()) {
119  result.error = F("Command not found");
120  } else if (commands.exact.count(longest->first) == 1) {
121  auto &command = longest->second;
122  std::vector<std::string> arguments;
123 
124  for (auto it = std::next(command_line->cbegin(), command->name_.size()); it != command_line->cend(); it++) {
125  arguments.push_back(std::move(*it));
126  }
127  command_line.reset();
128 
129  if (commands.partial.upper_bound(longest->first) != commands.partial.end() && !arguments.empty()) {
130  result.error = F("Command not found");
131  } else if (arguments.size() < command->minimum_arguments()) {
132  result.error = F("Not enough arguments for command");
133  } else if (arguments.size() > command->maximum_arguments()) {
134  result.error = F("Too many arguments for command");
135  } else {
136  command->function_(shell, arguments);
137  }
138  } else {
139  result.error = F("Fatal error (multiple commands found)");
140  }
141 
142  return result;
143 }
144 
145 bool Commands::find_longest_common_prefix(const std::multimap<size_t,const Command*> &commands, std::vector<std::string> &longest_name) {
146  size_t component_prefix = 0;
147  size_t shortest_match = commands.begin()->first;
148 
149  longest_name.reserve(shortest_match);
150 
151  {
152  // Check if any of the commands have a common prefix of components
153  auto &first = commands.begin()->second->name_;
154  bool all_match = true;
155 
156  for (size_t length = 0; all_match && length < shortest_match; length++) {
157  for (auto command_it = std::next(commands.begin()); command_it != commands.end(); command_it++) {
158  if (read_flash_string(*std::next(first.begin(), length)) != read_flash_string(*std::next(command_it->second->name_.begin(), length))) {
159  all_match = false;
160  break;
161  }
162  }
163 
164  if (all_match) {
165  component_prefix = length + 1;
166  }
167  }
168 
169  auto name_it = first.begin();
170  for (size_t i = 0; i < component_prefix; i++) {
171  longest_name.push_back(std::move(read_flash_string(*name_it)));
172  name_it++;
173  }
174  }
175 
176  if (component_prefix < shortest_match) {
177  // Check if the next component has a common substring
178  auto first = *std::next(commands.begin()->second->name_.begin(), component_prefix);
179  bool all_match = true;
180  size_t chars_prefix = 0;
181 
182  for (size_t length = 0; all_match; length++) {
183  for (auto command_it = std::next(commands.begin()); command_it != commands.end(); command_it++) {
184  // This relies on the null terminator character limiting the
185  // length before it becomes longer than any of the strings
186  if (pgm_read_byte(reinterpret_cast<PGM_P>(first) + length)
187  != pgm_read_byte(reinterpret_cast<PGM_P>(*std::next(command_it->second->name_.begin(), component_prefix)) + length)) {
188  all_match = false;
189  break;
190  }
191  }
192 
193  if (all_match) {
194  chars_prefix = length + 1;
195  }
196  }
197 
198  if (chars_prefix > 0) {
199  longest_name.push_back(std::move(read_flash_string(first).substr(0, chars_prefix)));
200  return false;
201  }
202  }
203 
204  return true;
205 }
206 
207 std::string Commands::find_longest_common_prefix(const std::vector<std::string> &arguments) {
208  auto& first = *arguments.begin();
209  bool all_match = true;
210  size_t chars_prefix = 0;
211 
212  for (size_t length = 0; all_match; length++) {
213  for (auto argument_it = std::next(arguments.begin()); argument_it != arguments.end(); argument_it++) {
214  // This relies on the null terminator character limiting the
215  // length before it becomes longer than any of the strings
216  if (first[length] != (*argument_it)[length]) {
217  all_match = false;
218  break;
219  }
220  }
221 
222  if (all_match) {
223  chars_prefix = length + 1;
224  }
225  }
226 
227  return arguments.begin()->substr(0, chars_prefix);
228 }
229 
231  auto commands = find_command(shell, command_line);
232  Completion result;
233 
234  auto match = commands.partial.begin();
235  size_t count;
236  bool multiple_matches;
237  if (match != commands.partial.end()) {
238  count = commands.partial.count(match->first);
239  multiple_matches = count > 1 || commands.partial.size() > count
240  || (!commands.exact.empty() && commands.exact.rbegin()->first >= command_line.total_size());
241  } else if (!commands.exact.empty()) {
242  // Use prev() because both iterators must be forwards
243  match = std::prev(commands.exact.end());
244  count = commands.exact.count(match->first);
245  multiple_matches = false;
246  } else {
247  return result;
248  }
249 
250  std::unique_ptr<Command> temp_command;
251  std::vector<std::string> temp_command_name;
252  std::multimap<size_t,const Command*>::iterator temp_command_it;
253 
254  if (multiple_matches && (commands.exact.empty() || command_line.total_size() > commands.exact.begin()->second->name_.size())) {
255  // There are multiple matching commands, find the longest common prefix
256  bool whole_components = find_longest_common_prefix(commands.partial, temp_command_name);
257 
258  // Construct a temporary command with the longest common prefix to use as the replacement
259  if (!temp_command_name.empty() && command_line.total_size() <= temp_command_name.size()) {
260  temp_command = std::make_unique<Command>(0, 0, flash_string_vector{}, flash_string_vector{}, nullptr, nullptr);
261  count = 1;
262  match = commands.partial.end();
263  result.replacement.trailing_space = whole_components;
264 
265  for (auto &name : temp_command_name) {
266  result.replacement->emplace_back(name);
267  }
268  }
269  }
270 
271  if (count == 1 && !multiple_matches) {
272  // Construct a replacement string for a single matching command
273  auto &matching_command = match->second;
274 
275  for (auto &name : matching_command->name_) {
276  result.replacement->push_back(std::move(read_flash_string(name)));
277  }
278 
279  if (command_line.total_size() > result.replacement->size() && command_line.total_size() <= matching_command->name_.size() + matching_command->maximum_arguments()) {
280  // Try to auto-complete arguments
281  std::vector<std::string> arguments{std::next(command_line->cbegin(), result.replacement->size()), command_line->cend()};
282 
283  result.replacement->insert(result.replacement->end(), arguments.cbegin(), arguments.cend());
284  result.replacement.trailing_space = command_line.trailing_space;
285 
286  auto current_args_count = arguments.size();
287  std::string last_argument;
288 
289  if (!command_line.trailing_space) {
290  // Remove the last argument so that it can be auto-completed
291  last_argument = std::move(result.replacement->back());
292  result.replacement->pop_back();
293  if (!arguments.empty()) {
294  arguments.pop_back();
295  current_args_count--;
296  }
297  }
298 
299  auto potential_arguments = matching_command->arg_function_
300  ? matching_command->arg_function_(shell, arguments, last_argument)
301  : std::vector<std::string>{};
302 
303  // Remove arguments that can't match
304  if (!command_line.trailing_space) {
305  for (auto it = potential_arguments.begin(); it != potential_arguments.end(); ) {
306  if (it->rfind(last_argument, 0) == std::string::npos) {
307  it = potential_arguments.erase(it);
308  } else {
309  it++;
310  }
311  }
312  }
313 
314  // Auto-complete if there's something present in the last argument
315  // or the only potential argument is an empty string.
316  if (!command_line.trailing_space) {
317  if (potential_arguments.size() == 1) {
318  if (last_argument == *potential_arguments.begin()) {
319  if (result.replacement->size() + 1 < matching_command->name_.size() + matching_command->maximum_arguments()) {
320  // Add a space because this argument is complete and there are more arguments for this command
321  result.replacement.trailing_space = true;
322  }
323  }
324 
325  last_argument = *potential_arguments.begin();
326  potential_arguments.clear();
327 
328  // Remaining help should skip the replaced argument
329  current_args_count++;
330  } else if (potential_arguments.size() > 1) {
331  last_argument = find_longest_common_prefix(potential_arguments);
332  }
333  }
334 
335  // Put the last argument back
336  if (!command_line.trailing_space) {
337  result.replacement->push_back(std::move(last_argument));
338  }
339 
340  CommandLine remaining_help;
341 
342  if (!potential_arguments.empty()) {
343  // Remaining help should skip the suggested argument
344  current_args_count++;
345  }
346 
347  if (current_args_count < matching_command->maximum_arguments()) {
348  remaining_help.escape_initial_parameters();
349 
350  for (auto it = std::next(matching_command->arguments_.cbegin(), current_args_count); it != matching_command->arguments_.cend(); it++) {
351  remaining_help->push_back(std::move(read_flash_string(*it)));
352  }
353  }
354 
355  if (potential_arguments.empty()) {
356  if (!remaining_help->empty()) {
357  result.help.push_back(std::move(remaining_help));
358  }
359  } else {
360  for (auto potential_argument : potential_arguments) {
361  CommandLine help;
362 
363  help->emplace_back(potential_argument);
364 
365  if (!remaining_help->empty()) {
367  help->insert(help->end(), remaining_help->begin(), remaining_help->end());
368  }
369 
370  result.help.push_back(std::move(help));
371  }
372  }
373  } else if (result.replacement->size() < matching_command->name_.size() + matching_command->maximum_arguments()) {
374  // Add a space because there are more arguments for this command
375  result.replacement.trailing_space = true;
376  }
377  } else if (count != 0) {
378  // Provide help for all of the potential commands
379  for (auto command_it = commands.all.begin(); command_it != commands.all.end(); command_it++) {
380  CommandLine help;
381 
382  auto line_it = command_line->cbegin();
383  auto flash_name_it = (*command_it)->name_.cbegin();
384 
385  if (temp_command) {
386  // Skip parts of the command name/line when the longest common prefix was used
387  size_t skip = temp_command_name.size();
388  if (!result.replacement.trailing_space) {
389  skip--;
390  }
391 
392  // Exact match that is shorter than the replacement
393  if (flash_name_it + skip > (*command_it)->name_.cend()) {
394  continue;
395  }
396 
397  flash_name_it += skip;
398  while (line_it != command_line->cend()) {
399  line_it++;
400  }
401  }
402 
403  for (; flash_name_it != (*command_it)->name_.cend(); flash_name_it++) {
404  std::string name = read_flash_string(*flash_name_it);
405 
406  // Skip parts of the command name that match the command line
407  if (line_it != command_line->cend()) {
408  if (name == *line_it++) {
409  continue;
410  } else {
411  line_it = command_line->cend();
412  }
413  }
414 
415  help->emplace_back(name);
416  }
417 
419 
420  for (auto argument : (*command_it)->arguments_) {
421  // Skip parts of the command arguments that exist in the command line
422  if (line_it != command_line->cend()) {
423  line_it++;
424  continue;
425  }
426 
427  help->push_back(std::move(read_flash_string(argument)));
428  }
429 
430  result.help.push_back(std::move(help));
431  }
432  }
433 
434  if (multiple_matches && !commands.exact.empty() && result.replacement.total_size() == 0) {
435  // Try to add a space to exact matches if there's no other partial match replacement
436  auto longest = commands.exact.crbegin();
437 
438  if (commands.exact.count(longest->first) == 1) {
439  for (auto &name : longest->second->name_) {
440  result.replacement->push_back(std::move(read_flash_string(name)));
441  }
442 
443  // Add a space because there are sub-commands for a command that has matched exactly
444  result.replacement.trailing_space = true;
445  }
446  }
447 
448  // Don't try to shorten the command line or offer an identical replacement
449  if (command_line.total_size() > result.replacement.total_size() || result.replacement == command_line) {
450  result.replacement.reset();
451  }
452 
453  return result;
454 }
455 
457  Match commands;
458  auto context_commands = commands_.equal_range(shell.context());
459 
460  for (auto it = context_commands.first; it != context_commands.second; it++) {
461  auto& command = it->second;
462  bool match = true;
463  bool exact = true;
464 
465  if (!shell.has_flags(command.flags_, command.not_flags_)) {
466  continue;
467  }
468 
469  auto name_it = command.name_.cbegin();
470  auto line_it = command_line->cbegin();
471 
472  for (; name_it != command.name_.cend() && line_it != command_line->cend(); name_it++, line_it++) {
473  std::string name = read_flash_string(*name_it);
474  size_t found = name.rfind(*line_it, 0);
475 
476  if (found == std::string::npos) {
477  match = false;
478  break;
479  } else if (line_it->length() != name.length()) {
480  for (auto line_check_it = std::next(line_it); line_check_it != command_line->cend(); line_check_it++) {
481  if (!line_check_it->empty()) {
482  // If there's more in the command line then this can't match
483  match = false;
484  }
485  }
486 
487  if (command_line.trailing_space) {
488  // If there's a trailing space in the command line then this can't be a partial match
489  match = false;
490  }
491 
492  // Don't check the rest of the command if this is only a partial match
493  break;
494  }
495  }
496 
497  if (name_it != command.name_.cend()) {
498  exact = false;
499  }
500 
501  if (match) {
502  if (exact) {
503  commands.exact.emplace(command.name_.size(), &command);
504  } else {
505  commands.partial.emplace(command.name_.size(), &command);
506  }
507  commands.all.push_back(&command);
508  }
509  }
510 
511  return commands;
512 }
513 
514 Commands::Command::Command(unsigned int flags, unsigned int not_flags,
515  const flash_string_vector name, const flash_string_vector arguments,
516  command_function function, argument_completion_function arg_function)
517  : flags_(flags), not_flags_(not_flags), name_(name), arguments_(arguments),
518  function_(function), arg_function_(arg_function) {
519 
520 }
521 
522 Commands::Command::~Command() {
523 
524 }
525 
527  return std::count_if(arguments_.cbegin(), arguments_.cend(), [] (const __FlashStringHelper *argument) { return pgm_read_byte(argument) == '<'; });
528 }
529 
530 } // namespace console
531 
532 } // namespace uuid
uuid::console::Commands::complete_command
Completion complete_command(Shell &shell, const CommandLine &command_line)
Complete a partial command for a Shell if it exists in the current context and with the current flags...
Definition: commands.cpp:230
uuid::console::CommandLine
Representation of a command line, with parameters separated by spaces and an optional trailing space.
Definition: console.h:1642
uuid::console::Shell
Base class for a command shell.
Definition: console.h:772
uuid::console::Commands::Match::all
std::vector< const Command * > all
Definition: console.h:726
uuid::console::Commands::Completion::replacement
CommandLine replacement
Definition: console.h:1810
uuid::console::CommandLine::reset
void reset()
Reset this command line's data.
Definition: command_line.cpp:158
uuid::console::Commands::Execution::error
const __FlashStringHelper * error
Definition: console.h:103
uuid::console::CommandLine::escape_initial_parameters
void escape_initial_parameters()
Only escape the number of parameters that currently exist when formatting the command line for output...
Definition: console.h:1728
uuid::console::CommandLine::trailing_space
bool trailing_space
Definition: console.h:1794
uuid::console::Commands::Execution
Result of a command execution operation.
Definition: console.h:102
uuid::console::Commands::Command::minimum_arguments
size_t minimum_arguments() const
Determine the minimum number of arguments for this command based on the help text for the arguments t...
Definition: commands.cpp:526
uuid::console::Commands::Command::Command
Command(unsigned int flags, unsigned int not_flags, const flash_string_vector name, const flash_string_vector arguments, command_function function, argument_completion_function arg_function)
Create a command for execution on a Shell.
Definition: commands.cpp:514
uuid::console::CommandLine::total_size
size_t total_size() const
Get the total size of the command line parameters, taking into account the trailing space.
Definition: console.h:1698
uuid::console::Shell::context
unsigned int context() const
Get the context at the top of the stack.
Definition: console.h:979
uuid::console::Commands::add_command
void add_command(const flash_string_vector &name, command_function function)
Add a command with no arguments to the list of commands in this container.
Definition: commands.cpp:46
uuid::flash_string_vector
std::vector< const __FlashStringHelper * > flash_string_vector
Type definition for a std::vector of flash strings.
Definition: common.h:99
uuid::console::Commands::Completion
Result of a command completion operation.
Definition: console.h:1808
uuid::console::Commands::Completion::help
std::list< CommandLine > help
Definition: console.h:1809
uuid::console::Commands::execute_command
Execution execute_command(Shell &shell, CommandLine &&command_line)
Execute a command for a Shell if it exists in the current context and with the current flags.
Definition: commands.cpp:111
uuid::console::Commands::Match::exact
std::multimap< size_t, const Command * > exact
Definition: console.h:724
uuid::console::Shell::has_flags
bool has_flags(unsigned int flags, unsigned int not_flags=0) const
Check if the current flags include all of the specified flags and none of the specified not_flags.
Definition: console.h:1031
uuid::console::Commands::command_function
std::function< void(Shell &shell, std::vector< std::string > &arguments)> command_function
Function to handle a command.
Definition: console.h:113
uuid::read_flash_string
std::string read_flash_string(const __FlashStringHelper *flash_str)
Read a string from flash and convert it to a std::string.
Definition: read_flash_string.cpp:27
uuid::console::Commands::find_command
Match find_command(Shell &shell, const CommandLine &command_line)
Find commands by matching them against the command line.
Definition: commands.cpp:456
uuid
Common utilities.
Definition: get_uptime_ms.cpp:28
uuid::console::Commands::Match
Result of a command find operation.
Definition: console.h:723
uuid::console::Commands::argument_completion_function
std::function< const std::vector< std::string >(Shell &shell, const std::vector< std::string > &current_arguments, const std::string &next_argument)> argument_completion_function
Function to obtain completions for a command line.
Definition: console.h:132
uuid::console::Commands::commands_
std::multimap< unsigned int, Command > commands_
Definition: console.h:762
uuid::console::Commands::find_longest_common_prefix
static bool find_longest_common_prefix(const std::multimap< size_t, const Command * > &commands, std::vector< std::string > &longest_name)
Find the longest common prefix from a shortest match of commands.
Definition: commands.cpp:145
uuid::console::Commands::Match::partial
std::multimap< size_t, const Command * > partial
Definition: console.h:725