CLI.cpp 6.43 KB
/*
 * CLI.cpp - CLI library for Arduino/ESP8266 and others implementation
 *
 * Version 1.0, latest version, documentation and bugtracker available at:
 *              https://gitlab.lindenaar.net/arduino/CLI
 *
 * Copyright (c) 2019 Frederik Lindenaar
 *
 * This library is free software: you can redistribute it and/or modify it under
 * the terms of version 3 of the GNU General Public License as published by the
 * Free Software Foundation, or (at your option) a later version of the license.
 *
 * This code is distributed in the hope that it will be useful but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE.  See the GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License along with
 * this program.  If not, visit <http://www.gnu.org/licenses/> to download it.
 */

#include <CLI.h>

#define CHAR_TAB  '\t'
#define CHAR_BS   '\b'
#define CHAR_DELETE char(127)
#define CHAR_CR   '\r'
#define CHAR_LF   '\n'

const char CLI_DEFAULT_PROMPT[]         PROGMEM = "\n> ";
const char CLI_PREFIX_PARAMETER_ERROR[] PROGMEM = "Invalid parameters for ";
const char CLI_PREFIX_UNKNOWN_COMMAND[] PROGMEM = "Unknown command ";

size_t CLI::print_P (const char *str PROGMEM) {
  size_t len = 0;
  if (str) {
    while (char c = pgm_read_byte(str++)) {
      write(c);
      len++;
    }
  }
  return len;
}

void CLI::print2digits(uint8_t num, char filler, uint8_t base) {
  if (num < base) print(filler);
  print(num, base);
}

void CLI::print_mem(uint16_t addr, const uint8_t buff[], uint8_t len, uint8_t width) {
  if (addr < 0x1000) print(' ');
  if (addr < 0x100) print(' ');
  if (addr < 0x10) print(' ');
  print(addr, HEX);
  print(':');

  for (uint8_t i = 0; i < len; i++) {
    print(' ');
    if (buff[i] < 0x10) print(0);
    print(buff[i], HEX);
  }
  while (width > len++) print_P(PSTR("   "));

  print('\t');
  for (uint8_t i = 0; i < len; i++)
    write(isprint(buff[i]) ? buff[i] : '.');

  println();
}

void CLI::printtab() {
  print(CHAR_TAB);
};

CLI::CLI(Stream &stream, const char *banner PROGMEM, const char *prompt PROGMEM) : _stream(&stream), _banner(banner) {
  _state = STATE_INIT;
  _command_count = 0;
  _prompt = (prompt) ? prompt : CLI_DEFAULT_PROMPT;
};

void CLI::add_command(CLI_Command *cmd) {
  if (_command_count < CLI_MAX_CMDS)
    _commands[_command_count++] = cmd;
};

CLI_Command *CLI::get_command(int index) {
  return (index < _command_count) ? _commands[index] : NULL;
}

CLI_Command *CLI::get_command(const char *name, int namelen) {
  for (int i = 0; i < _command_count; i++) {
    if (_commands[i]->matches(name, namelen)) return _commands[i];
  }
  return NULL;
}

#if CLI_MAX_CMDS < 255
uint8_t CLI::find_command(const char *name, int namelen) {
  uint8_t idx = _command_count;
#else
uint16_t CLI::find_command(const char *name, int namelen) {
  uint16_t idx = _command_count;
#endif
  while (idx-- && !_commands[idx]->matches(name, namelen));
  return idx;
}

bool CLI::process() {                       // Workhorse of the CLI
  if (_state == STATE_INIT) {                   // INIT: print banner (if set)
    if (_banner) {
      print_P(_banner);
      println();
    }
    _state = STATE_READY;
  } else if (_state == STATE_READY) {           // READY: print prompt & init
    print_P(_prompt);
    memset(_cmdbuffer, 0, _line_len);
    _line_len = 0;
    _current_cmd = NULL;
    _state = STATE_INPUT;
  } else if (_state == STATE_INPUT) {           // INPUT: process user input
    while (available()) {                           // loop while stream input
      int c = read();                               // get next char
      if (c == CHAR_DELETE) {                   // Process BACKSPACE key
        print_P(PSTR("\b \b"));
        if (_line_len) _cmdbuffer[_line_len--] = 0;
      } else if (c == CHAR_CR) {                // Process ENTER key
        println();
        _state = STATE_PARSE;                       // Change state to PARSE
        return false;                               // and stop processing input
      } else if (c != CHAR_LF && isprint(c)) {  // Process other valid input
        if (_line_len < CLI_MAX_LINE - 1) {         // add to line if space left
          _cmdbuffer[_line_len++] = c;
          write(c);                                 // print char to echo input
        } else {                                    // if not, beep & ignore
          write(char(7));
        }
      }
    }
  } else if (_state == STATE_PARSE) {           // PARSE: parse user command
    const char *cmd = CLI_STR_skipSep(_cmdbuffer);  // skip initial whitespace
    if (*cmd && cmd - _cmdbuffer < _line_len) {     // do we have a command?
      for (char *p = _cmdbuffer + _line_len - 1; *p == ' '; p--, _line_len--)
        *p = 0;                                     // clear trailing whitespace
      const char *sep = CLI_STR_findSep(cmd);       // find end of command
      if (_current_cmd = get_command(cmd, sep - cmd)) { // known command?
        sep = CLI_STR_skipSep(sep);                     // get start of params
        if (_current_cmd->setparams((*sep) ? sep : 0)) {// parse params
          _state = STATE_EXEC;                          // all OK, change state
          return true;                                  // and stop processing
        } else print_P(CLI_PREFIX_PARAMETER_ERROR); // print invalid parameters
      } else print_P(CLI_PREFIX_UNKNOWN_COMMAND) ;  // print unknown command
      _stream->write(cmd, sep - cmd);               // print command name
      println();                                    // and add newline
    }
    _state = STATE_READY;                           // if we get here: READY
  } else if (_state == STATE_EXEC) {            // EXEC: execute the command
    if (_current_cmd->execute(*this)) {             // Execute, false when done
      return true;                                  // not yet done, return true
    } else {
      _state = STATE_READY;                         // done, READY
    }
  }
  return false;                                 // default result false (done)
}


CLI_Command::CLI_Command(CLI &cli, const char *command, const char *description, const char *usagehelp) : command(command), description(description), usagehelp(usagehelp) {
  cli.add_command(this);
};

bool CLI_Command::matches(const char *cmd, uint8_t cmdlen) {
  return pgm_read_byte(&command[cmdlen]) == 0 && strncmp_P(cmd, command, cmdlen) == 0;
};

bool CLI_Command::setparams(const char *params) {
  return !params;
}