blob: b716276c6aaee9388e33332eef10fc56f7b6a02b [file] [log] [blame]
Jason Graffius4d13de92021-01-16 20:26:21 -08001// Copyright 2021 The Pigweed Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License"); you may not
4// use this file except in compliance with the License. You may obtain a copy of
5// the License at
6//
7// https://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
12// License for the specific language governing permissions and limitations under
13// the License.
14
15#include <algorithm>
16#include <cctype>
17#include <functional>
18#include <iostream>
19#include <span>
20#include <string>
21#include <string_view>
22#include <unordered_map>
23#include <vector>
24
25#include "pw_log/log.h"
26
27namespace {
28
29// String used to prompt for user input in the CLI loop.
30constexpr char kPrompt[] = ">";
31
32// Convert the provided string to a lowercase equivalent.
33std::string ToLower(std::string_view view) {
34 std::string str{view};
35 std::transform(str.begin(), str.end(), str.begin(), [](char c) {
36 return std::tolower(c);
37 });
38 return str;
39}
40
41// Scan an input line for tokens, returning a vector containing each token.
42// Tokens are either whitespace delimited strings or a quoted string which may
43// contain spaces and is terminated by another quote. When delimiting by
44// whitespace any consecutive sequence of whitespace is treated as a single
45// delimiter.
46//
47// For example, the tokenization of the following line:
48//
49// The duck said "quack, quack" before eating its bread
50//
51// Would result in the following tokens:
52//
53// ["The", "duck", "said", "quack, quack", "before", "eating", "its", "bread"]
54//
55std::vector<std::string_view> TokenizeLine(std::string_view line) {
56 size_t token_start = 0;
57 size_t index = 0;
58 bool in_quote = false;
59 std::vector<std::string_view> tokens;
60
61 while (index < line.size()) {
62 // Trim leading/trailing whitespace for each token.
63 while (index < line.size() && std::isspace(line[index])) {
64 ++index;
65 }
66
67 if (index >= line.size()) {
68 // Have reached the end and no further tokens remain.
69 break;
70 }
71
72 token_start = index++;
73 if (line[token_start] == '"') {
74 in_quote = true;
75 // Don't include the quote character.
76 ++token_start;
77 }
78
79 // In a token, scan for the end of the token.
80 while (index < line.size()) {
81 if ((in_quote && line[index] == '"') ||
82 (!in_quote && std::isspace(line[index]))) {
83 break;
84 }
85 ++index;
86 }
87
88 if (index >= line.size() && in_quote) {
89 PW_LOG_WARN("Assuming closing quote at EOL.");
90 }
91
92 tokens.push_back(line.substr(token_start, index - token_start));
93 in_quote = false;
94 ++index;
95 }
96
97 return tokens;
98}
99
100// Context supplied to (and mutable by) each command.
101struct CommandContext {
102 // When set to `true`, the CLI will exit once the active command returns.
103 bool quit = false;
104};
105
106// Commands are given mutable CommandContext and a span tokens in the line of
107// the command.
108using Command =
109 std::function<bool(CommandContext*, std::span<std::string_view>)>;
110
111// Echoes all arguments provided to cout.
112bool CommandEcho(CommandContext* /*context*/,
113 std::span<std::string_view> tokens) {
114 bool first = true;
115 for (const auto& token : tokens.subspan(1)) {
116 if (!first) {
117 std::cout << ' ';
118 }
119
120 std::cout << token;
121 first = false;
122 }
123 std::cout << std::endl;
124
125 return true;
126}
127
128// Quit the CLI.
129bool CommandQuit(CommandContext* context,
130 std::span<std::string_view> /*tokens*/) {
131 context->quit = true;
132 return true;
133}
134
135} // namespace
136
137int main(int /*argc*/, char* /*argv*/[]) {
138 CommandContext context;
139 std::unordered_map<std::string, Command> commands{
140 {"echo", CommandEcho},
141 {"exit", CommandQuit},
142 {"quit", CommandQuit},
143 };
144
145 // Enter CLI loop.
146 while (true) {
147 // Prompt for input.
148 std::string line;
149 std::cout << kPrompt << ' ' << std::flush;
150 std::getline(std::cin, line);
151
152 // Tokenize provided line.
153 auto tokens = TokenizeLine(line);
154 if (tokens.empty()) {
155 continue;
156 }
157
158 // Search for provided command.
159 auto it = commands.find(ToLower(tokens[0]));
160 if (it == commands.end()) {
161 PW_LOG_ERROR("Unrecognized command \"%.*s\".",
162 static_cast<int>(tokens[0].size()),
163 tokens[0].data());
164 continue;
165 }
166
167 // Invoke the command.
168 Command command = it->second;
169 command(&context, tokens);
170 if (context.quit) {
171 break;
172 }
173 }
174
175 return EXIT_SUCCESS;
176}