blob: b716276c6aaee9388e33332eef10fc56f7b6a02b [file] [log] [blame]
// Copyright 2021 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include <algorithm>
#include <cctype>
#include <functional>
#include <iostream>
#include <span>
#include <string>
#include <string_view>
#include <unordered_map>
#include <vector>
#include "pw_log/log.h"
namespace {
// String used to prompt for user input in the CLI loop.
constexpr char kPrompt[] = ">";
// Convert the provided string to a lowercase equivalent.
std::string ToLower(std::string_view view) {
std::string str{view};
std::transform(str.begin(), str.end(), str.begin(), [](char c) {
return std::tolower(c);
});
return str;
}
// Scan an input line for tokens, returning a vector containing each token.
// Tokens are either whitespace delimited strings or a quoted string which may
// contain spaces and is terminated by another quote. When delimiting by
// whitespace any consecutive sequence of whitespace is treated as a single
// delimiter.
//
// For example, the tokenization of the following line:
//
// The duck said "quack, quack" before eating its bread
//
// Would result in the following tokens:
//
// ["The", "duck", "said", "quack, quack", "before", "eating", "its", "bread"]
//
std::vector<std::string_view> TokenizeLine(std::string_view line) {
size_t token_start = 0;
size_t index = 0;
bool in_quote = false;
std::vector<std::string_view> tokens;
while (index < line.size()) {
// Trim leading/trailing whitespace for each token.
while (index < line.size() && std::isspace(line[index])) {
++index;
}
if (index >= line.size()) {
// Have reached the end and no further tokens remain.
break;
}
token_start = index++;
if (line[token_start] == '"') {
in_quote = true;
// Don't include the quote character.
++token_start;
}
// In a token, scan for the end of the token.
while (index < line.size()) {
if ((in_quote && line[index] == '"') ||
(!in_quote && std::isspace(line[index]))) {
break;
}
++index;
}
if (index >= line.size() && in_quote) {
PW_LOG_WARN("Assuming closing quote at EOL.");
}
tokens.push_back(line.substr(token_start, index - token_start));
in_quote = false;
++index;
}
return tokens;
}
// Context supplied to (and mutable by) each command.
struct CommandContext {
// When set to `true`, the CLI will exit once the active command returns.
bool quit = false;
};
// Commands are given mutable CommandContext and a span tokens in the line of
// the command.
using Command =
std::function<bool(CommandContext*, std::span<std::string_view>)>;
// Echoes all arguments provided to cout.
bool CommandEcho(CommandContext* /*context*/,
std::span<std::string_view> tokens) {
bool first = true;
for (const auto& token : tokens.subspan(1)) {
if (!first) {
std::cout << ' ';
}
std::cout << token;
first = false;
}
std::cout << std::endl;
return true;
}
// Quit the CLI.
bool CommandQuit(CommandContext* context,
std::span<std::string_view> /*tokens*/) {
context->quit = true;
return true;
}
} // namespace
int main(int /*argc*/, char* /*argv*/[]) {
CommandContext context;
std::unordered_map<std::string, Command> commands{
{"echo", CommandEcho},
{"exit", CommandQuit},
{"quit", CommandQuit},
};
// Enter CLI loop.
while (true) {
// Prompt for input.
std::string line;
std::cout << kPrompt << ' ' << std::flush;
std::getline(std::cin, line);
// Tokenize provided line.
auto tokens = TokenizeLine(line);
if (tokens.empty()) {
continue;
}
// Search for provided command.
auto it = commands.find(ToLower(tokens[0]));
if (it == commands.end()) {
PW_LOG_ERROR("Unrecognized command \"%.*s\".",
static_cast<int>(tokens[0].size()),
tokens[0].data());
continue;
}
// Invoke the command.
Command command = it->second;
command(&context, tokens);
if (context.quit) {
break;
}
}
return EXIT_SUCCESS;
}