/* * \brief Line editor for command-line interfaces * \author Norman Feske * \date 2013-03-18 */ /* * Copyright (C) 2013-2017 Genode Labs GmbH * * This file is part of the Genode OS framework, which is distributed * under the terms of the GNU Affero General Public License version 3. */ #ifndef _LINE_EDITOR_H_ #define _LINE_EDITOR_H_ /* Genode includes */ #include #include #include #include #include #include namespace Cli_monitor { using Genode::List; using Genode::max; using Genode::strlen; using Genode::strncpy; using Genode::snprintf; using Genode::strcmp; using Genode::size_t; using Genode::off_t; using Genode::Interface; struct Completable; struct Argument; struct Parameter; struct Command_line; struct Command; struct Command_registry; struct Scanner_policy; struct Argument_tracker; struct Line_editor; typedef Genode::Token Token; } struct Cli_monitor::Completable { typedef Genode::String<64> Name; typedef Genode::String<160> Short_help; Name const _name; Short_help const _short_help; Name name() const { return _name; } Short_help short_help() const { return _short_help; } Completable(char const *name, char const *short_help) : _name(name), _short_help(short_help) { } }; /** * Representation of normal command-line argument */ struct Cli_monitor::Argument : Completable { Argument(char const *name, char const *short_help) : Completable(name, short_help) { } char const *name_suffix() const { return ""; } }; /** * Representation of a parameter of the form '--tag value' */ struct Cli_monitor::Parameter : List::Element, Completable { enum Type { IDENT, NUMBER, VOID }; Type const _type; Parameter(char const *name, Type type, char const *short_help) : Completable(name, short_help), _type(type) { } bool needs_value() const { return _type != VOID; } char const *name_suffix() const { switch (_type) { case IDENT: return ""; case NUMBER: return ""; case VOID: return ""; } return ""; } }; /** * Representation of a command that can have arguments and parameters */ struct Cli_monitor::Command : private List::Element, private Completable { List _parameters { }; friend class List; using List::Element::next; using Completable::name; using Completable::short_help; /** * Functor that takes a command 'Argument' object as argument */ struct Argument_fn : Interface { virtual void operator () (Argument const &) const = 0; }; Command(char const *name, char const *short_help) : Completable(name, short_help) { } virtual ~Command() { } void add_parameter(Parameter &par) { _parameters.insert(&par); } char const *name_suffix() const { return ""; } List ¶meters() { return _parameters; } virtual void execute(Command_line &, Terminal::Session &terminal) = 0; /** * Command-specific support for 'for_each_argument' */ virtual void _for_each_argument(Argument_fn const &) const { }; /** * Execute functor 'fn' for each command argument */ template void for_each_argument(FN const &fn) const { struct _Fn : Argument_fn { FN const &fn; void operator () (Argument const &arg) const override { fn(arg); } _Fn(FN const &fn) : fn(fn) { } } _fn(fn); _for_each_argument(_fn); } }; struct Cli_monitor::Command_registry : List { }; /** * Scanner policy that accepts '-', '.' and '_' as valid identifier characters */ struct Cli_monitor::Scanner_policy { static bool identifier_char(char c, unsigned i) { return Genode::is_letter(c) || (c == '_') || (c == '-') || (c == '.') || (i && Genode::is_digit(c)); } }; /** * State machine used for sequentially parsing command-line arguments */ struct Cli_monitor::Argument_tracker { private: Command &_command; enum State { EXPECT_COMMAND, EXPECT_SPACE_BEFORE_ARG, EXPECT_ARG, EXPECT_SPACE_BEFORE_VAL, EXPECT_VAL, INVALID }; State _state; /** * Return true if there is exactly one complete match and no additional * partial matches */ static bool _one_matching_argument(char const *str, size_t str_len, Command const &command) { unsigned complete_cnt = 0, partial_cnt = 0; auto argument_fn = [&] (Argument const &arg) { if (strcmp(arg.name().string(), str, str_len) == 0) { partial_cnt++; if (strlen(arg.name().string()) == str_len) complete_cnt++; } }; command.for_each_argument(argument_fn); return partial_cnt == 1 && complete_cnt == 1; } public: Argument_tracker(Command &command) : _command(command), _state(EXPECT_COMMAND) { } template static T *lookup(char const *str, size_t str_len, List &list) { Token tag(str, str_len); for (T *curr = list.first(); curr; curr = curr->next()) if (strcmp(tag.start(), curr->name().string(), tag.len()) == 0 && strlen(curr->name().string()) == tag.len()) return curr; return 0; } template static T *lookup(Token token, List &list) { return lookup(token.start(), token.len(), list); } void supply_token(Token token, bool token_may_be_incomplete = false) { switch (_state) { case INVALID: break; case EXPECT_COMMAND: if (token.type() == Token::IDENT) { _state = EXPECT_SPACE_BEFORE_ARG; break; } _state = INVALID; break; case EXPECT_SPACE_BEFORE_ARG: if (token.type() == Token::WHITESPACE) _state = EXPECT_ARG; break; case EXPECT_ARG: if (token.type() == Token::IDENT) { Parameter *parameter = lookup(token.start(), token.len(), _command.parameters()); if (parameter && parameter->needs_value()) { _state = EXPECT_SPACE_BEFORE_VAL; break; } if (!token_may_be_incomplete || _one_matching_argument(token.start(), token.len(), _command)) _state = EXPECT_SPACE_BEFORE_ARG; } break; case EXPECT_SPACE_BEFORE_VAL: if (token.type() == Token::WHITESPACE) _state = EXPECT_VAL; break; case EXPECT_VAL: if (token.type() == Token::IDENT || token.type() == Token::NUMBER) { _state = EXPECT_SPACE_BEFORE_ARG; } break; } } bool valid() const { return _state != INVALID; } bool expect_arg() const { return _state == EXPECT_ARG; } bool expect_space() const { return _state == EXPECT_SPACE_BEFORE_ARG || _state == EXPECT_SPACE_BEFORE_VAL; } }; /** * Editing and completion logic */ class Cli_monitor::Line_editor { private: char const *_prompt; size_t const _prompt_len; char * const _buf; size_t const _buf_size; unsigned _cursor_pos = 0; Terminal::Session &_terminal; Command_registry &_commands; bool _complete = false; /** * State tracker for escape sequences within user input * * This tracker is used to decode special keys (e.g., cursor keys). */ struct Seq_tracker { enum State { INIT, GOT_ESC, GOT_FIRST } _state = INIT; char _normal = 0, _first = 0, _second = 0; bool _sequence_complete { false }; Seq_tracker() { } void input(char c) { switch (_state) { case INIT: if (c == 27) _state = GOT_ESC; else _normal = c; _sequence_complete = false; break; case GOT_ESC: _first = c; _state = GOT_FIRST; break; case GOT_FIRST: _second = c; _state = INIT; _sequence_complete = true; break; } } bool normal() const { return _state == INIT && !_sequence_complete; } char normal_char() const { return _normal; } bool _fn_complete(char match_first, char match_second) const { return _sequence_complete && _first == match_first && _second == match_second; } bool key_up() const { return _fn_complete(91, 65); } bool key_down() const { return _fn_complete(91, 66); } bool key_right() const { return _fn_complete(91, 67); } bool key_left() const { return _fn_complete(91, 68); } bool key_delete() const { return _fn_complete(91, 51); } }; Seq_tracker _seq_tracker { }; void _write(char c) { _terminal.write(&c, sizeof(c)); } void _write(char const *s) { _terminal.write(s, strlen(s)); } void _write_spaces(unsigned num) { for (unsigned i = 0; i < num; i++) _write(' '); } void _write_newline() { _write(10); } void _clear_until_end_of_line() { _write("\e[K "); } void _move_cursor_to(unsigned pos) { char seq[10]; snprintf(seq, sizeof(seq), "\e[%ldG", pos + _prompt_len); _write(seq); } void _delete_character() { strncpy(&_buf[_cursor_pos], &_buf[_cursor_pos + 1], _buf_size); _move_cursor_to(_cursor_pos); _write(&_buf[_cursor_pos]); _clear_until_end_of_line(); _move_cursor_to(_cursor_pos); } void _insert_character(char c) { /* insert regular character */ if (_cursor_pos >= _buf_size - 1) return; /* make room in the buffer */ for (unsigned i = _buf_size - 1; i > _cursor_pos; i--) _buf[i] = _buf[i - 1]; _buf[_cursor_pos] = c; /* update terminal */ _write(&_buf[_cursor_pos]); _cursor_pos++; _move_cursor_to(_cursor_pos); } void _fresh_prompt() { _write(_prompt); _write(_buf); _move_cursor_to(_cursor_pos); } void _handle_key() { enum { BACKSPACE = 8, TAB = 9, LINE_FEED = 10, CARRIAGE_RETURN = 13 }; if (_seq_tracker.key_left()) { if (_cursor_pos > 0) { _cursor_pos--; _write(BACKSPACE); } return; } if (_seq_tracker.key_right()) { if (_cursor_pos < strlen(_buf)) { _cursor_pos++; _move_cursor_to(_cursor_pos); } return; } if (_seq_tracker.key_delete()) _delete_character(); if (!_seq_tracker.normal()) return; char const c = _seq_tracker.normal_char(); if (c == TAB) { _perform_completion(); return; } if (c == CARRIAGE_RETURN || c == LINE_FEED) { if (strlen(_buf) > 0) { _write(LINE_FEED); _complete = true; } return; } if (c == BACKSPACE) { if (_cursor_pos > 0) { _cursor_pos--; _delete_character(); } return; } if (c == 126) return; _insert_character(c); } template COMPLETABLE *_lookup_matching(char const *str, size_t str_len, List &list) { Token tag(str, str_len); COMPLETABLE *curr = list.first(); for (; curr; curr = curr->next()) { if (strcmp(tag.start(), curr->name(), tag.len()) == 0 && strlen(curr->name()) == tag.len()) return curr; } return nullptr; } Command *_lookup_matching_command() { Token cmd(_buf, _cursor_pos); for (Command *curr = _commands.first(); curr; curr = curr->next()) if (strcmp(cmd.start(), curr->name().string(), cmd.len()) == 0 && _cursor_pos > cmd.len()) return curr; return nullptr; } template unsigned _num_partial_matches(char const *str, size_t str_len, List &list) { Token token(str, str_len); unsigned num_partial_matches = 0; for (T *curr = list.first(); curr; curr = curr->next()) { if (strcmp(token.start(), curr->name().string(), token.len()) != 0) continue; num_partial_matches++; } return num_partial_matches; } unsigned _num_matching_arguments(char const *str, size_t str_len, Command const &command) const { unsigned num_matches = 0; auto argument_fn = [&] (Argument const &arg) { if (strcmp(arg.name().string(), str, str_len) == 0) num_matches++; }; command.for_each_argument(argument_fn); return num_matches; } /** * Determine the name-column width of list of partial matches */ template size_t _width_of_partial_matches(char const *str, size_t str_len, List &list) { Token token(str, str_len); size_t max_name_len = 0; for (T *curr = list.first(); curr; curr = curr->next()) { if (strcmp(token.start(), curr->name().string(), token.len()) != 0) continue; size_t const name_len = strlen(curr->name().string()) + strlen(curr->name_suffix()); max_name_len = max(max_name_len, name_len); } return max_name_len; } unsigned _width_of_matching_arguments(char const *str, size_t str_len, Command const &command) const { size_t max_name_len = 0; auto argument_fn = [&] (Argument const &arg) { if (strcmp(arg.name().string(), str, str_len) == 0) { size_t const name_len = strlen(arg.name().string()); if (name_len > max_name_len) max_name_len = name_len; } }; command.for_each_argument(argument_fn); return max_name_len; } template char const *_any_partial_match_name(char const *str, size_t str_len, List &list) { Token token(str, str_len); for (T *curr = list.first(); curr; curr = curr->next()) if (strcmp(token.start(), curr->name().string(), token.len()) == 0) return curr->name().string(); return 0; } Argument::Name _any_matching_argument(char const *str, size_t str_len, Command const &command) const { Argument::Name name; auto argument_fn = [&] (Argument const &arg) { if (strcmp(arg.name().string(), str, str_len) == 0) name = arg.name(); }; command.for_each_argument(argument_fn); return name; } template void _list_partial_matches(char const *str, size_t str_len, unsigned pad, List &list) { Token token(str, str_len); for (T *curr = list.first(); curr; curr = curr->next()) { if (strcmp(token.start(), curr->name().string(), token.len()) != 0) continue; _write_newline(); _write_spaces(2); _write(curr->name().string()); _write_spaces(1); _write(curr->name_suffix()); /* pad short help with whitespaces */ size_t const name_len = strlen(curr->name().string()) + strlen(curr->name_suffix()); _write_spaces(pad + 3 - name_len); _write(curr->short_help().string()); } } void _list_matching_arguments(char const *str, size_t str_len, unsigned pad, Command const &command) { auto argument_fn = [&] (Argument const &arg) { if (strcmp(arg.name().string(), str, str_len) == 0) { _write_newline(); _write_spaces(2); _write(arg.name().string()); _write_spaces(1); _write(arg.name_suffix()); /* pad short help with whitespaces */ size_t const name_len = strlen(arg.name().string()) + strlen(arg.name_suffix()); _write_spaces(pad + 3 - name_len); _write(arg.short_help().string()); } }; command.for_each_argument(argument_fn); } template void _do_completion(char const *str, size_t str_len, List &list) { Token token(str, str_len); /* look up completable token */ T *partial_match = 0; for (T *curr = list.first(); curr; curr = curr->next()) { if (strcmp(token.start(), curr->name().string(), token.len()) == 0) { partial_match = curr; break; } } if (!partial_match) return; for (unsigned i = token.len(); i < strlen(partial_match->name().string()); i++) _insert_character(partial_match->name().string()[i]); _insert_character(' '); } void _do_argument_completion(char const *str, size_t str_len, Command const &command) { Argument::Name partial_match; auto argument_fn = [&] (Argument const &arg) { if (strcmp(arg.name().string(), str, str_len) == 0) partial_match = arg.name(); }; command.for_each_argument(argument_fn); for (unsigned i = str_len; i < strlen(partial_match.string()); i++) _insert_character(partial_match.string()[i]); _insert_character(' '); } void _complete_argument(char const *str, size_t str_len, Command &command) { unsigned const matching_parameters = _num_partial_matches(str, str_len, command.parameters()); unsigned const matching_arguments = _num_matching_arguments(str, str_len, command); /* matches are ambiguous */ if (matching_arguments + matching_parameters > 1) { /* * Try to complete additional characters that are common among * all matches. */ char buf[Completable::Name::size()]; strncpy(buf, str, Genode::min(sizeof(buf), str_len + 1)); /* pick any representative as a template to take characters from */ char const *name = _any_partial_match_name(str, str_len, command.parameters()); Argument::Name arg_name; if (!name) { arg_name = _any_matching_argument(str, str_len, command); if (strlen(arg_name.string())) name = arg_name.string(); } size_t i = str_len; for (; (i < sizeof(buf) - 1) && (i < strlen(name)); i++) { buf[i + 0] = name[i]; buf[i + 1] = 0; if (matching_parameters != _num_partial_matches(buf, i + 1, command.parameters())) break; if (matching_arguments != _num_matching_arguments(buf, i + 1, command)) break; _insert_character(buf[i]); } /* * If we managed to do a partial completion, let's yield * control to the user. */ if (i > str_len) return; /* * No automatic completion was possible, print list of possible * parameters and arguments */ size_t const pad = max(_width_of_partial_matches(str, str_len, command.parameters()), _width_of_matching_arguments(str, str_len, command)); _list_partial_matches(str, str_len, pad, command.parameters()); _list_matching_arguments(str, str_len, pad, command); _write_newline(); _fresh_prompt(); return; } if (matching_parameters == 1) _do_completion(str, str_len, command.parameters()); if (matching_arguments == 1) _do_argument_completion(str, str_len, command); } void _perform_completion() { Command *command = _lookup_matching_command(); if (!command) { unsigned const matches = _num_partial_matches(_buf, _cursor_pos, _commands); if (matches == 1) _do_completion(_buf, _cursor_pos, _commands); if (matches > 1) { unsigned const pad = _width_of_partial_matches(_buf, _cursor_pos, _commands); _list_partial_matches(_buf, _cursor_pos, pad, _commands); _write_newline(); _fresh_prompt(); } return; } /* * We hava a valid command, now try to complete the parameters... */ /* determine token under the cursor */ Argument_tracker argument_tracker(*command); Token token(_buf, _buf_size); for (; token; token = token.next()) { argument_tracker.supply_token(token, true); if (!argument_tracker.valid()) return; unsigned long const token_pos = (unsigned long)(token.start() - _buf); /* we have reached the token under the cursor */ if (token.type() == Token::IDENT && _cursor_pos >= token_pos && _cursor_pos <= token_pos + token.len()) { if (argument_tracker.expect_arg()) { char const *start = token.start(); size_t const len = _cursor_pos - token_pos; _complete_argument(start, len, *command); return; } } } /* the cursor is positioned at beginning of new argument */ if (argument_tracker.expect_arg()) _complete_argument("", 0, *command); if (argument_tracker.expect_space()) _insert_character(' '); } public: /** * Constructor * * \param prompt string to be printed at the beginning of the line * \param buf destination buffer * \param buf_size destination buffer size * \param terminal terminal used as output device * \param commands meta information about commands and their arguments */ Line_editor(char const *prompt, char *buf, size_t buf_size, Terminal::Session &terminal, Command_registry &commands) : _prompt(prompt), _prompt_len(strlen(prompt)), _buf(buf), _buf_size(buf_size), _terminal(terminal), _commands(commands) { reset(); } /** * Reset prompt to initial state after construction */ void reset() { _buf[0] = 0; _complete = false; _cursor_pos = 0; _seq_tracker = Seq_tracker(); _fresh_prompt(); } /** * Supply a character of user input */ void submit_input(char c) { _seq_tracker.input(c); _handle_key(); } /** * Returns true if the editing is complete, i.e., the user pressed the * return key. */ bool completed() const { return _complete; } }; #endif /* _LINE_EDITOR_H_ */