| // 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; | 
 | } |