aboutsummaryrefslogtreecommitdiff
path: root/pw_tool
diff options
context:
space:
mode:
authorJason Graffius <jgraff@google.com>2021-01-16 20:26:21 -0800
committerCQ Bot Account <pigweed-scoped@luci-project-accounts.iam.gserviceaccount.com>2021-03-11 20:04:51 +0000
commit4d13de92f08fc03d160a6f9252602efe69be10ac (patch)
treeee8d4409c5ca2c7c7aea32f8ef4b4bcb5f77e992 /pw_tool
parente7367be3d1f1e868bd37b909024d430662c9f6ee (diff)
downloadpigweed-4d13de92f08fc03d160a6f9252602efe69be10ac.tar.gz
pw_tool: Create a basic CLI tool framework
Adds a basic framework for a CLI tool for future features. At the time of this commit, this only includes basic "proof of concept" style commands, but can and will be extended to more functionality. Change-Id: I0445384bae7b763d1f1301e427dbb6316faf9ec2 Reviewed-on: https://pigweed-review.googlesource.com/c/pigweed/pigweed/+/29880 Commit-Queue: Jason Graffius <jgraff@google.com> Reviewed-by: Alexei Frolov <frolv@google.com> Reviewed-by: Keir Mierle <keir@google.com>
Diffstat (limited to 'pw_tool')
-rw-r--r--pw_tool/BUILD30
-rw-r--r--pw_tool/BUILD.gn25
-rw-r--r--pw_tool/main.cc176
3 files changed, 231 insertions, 0 deletions
diff --git a/pw_tool/BUILD b/pw_tool/BUILD
new file mode 100644
index 000000000..304ec5b94
--- /dev/null
+++ b/pw_tool/BUILD
@@ -0,0 +1,30 @@
+# 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.
+
+load(
+ "//pw_build:pigweed.bzl",
+ "pw_cc_binary",
+)
+
+licenses(["notice"]) # Apache License 2.0
+
+pw_cc_binary(
+ name = "pw_tool",
+ srcs = [ "main.cc" ],
+ deps = [
+ "//pw_log",
+ "//pw_polyfill",
+ ]
+)
+
diff --git a/pw_tool/BUILD.gn b/pw_tool/BUILD.gn
new file mode 100644
index 000000000..522ae3a83
--- /dev/null
+++ b/pw_tool/BUILD.gn
@@ -0,0 +1,25 @@
+# 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.
+
+import("//build_overrides/pigweed.gni")
+import("$dir_pw_build/target_types.gni")
+
+pw_executable("pw_tool") {
+ output_name = "pw_tool"
+ deps = [
+ "$dir_pw_log",
+ "$dir_pw_polyfill",
+ ]
+ sources = [ "main.cc" ]
+}
diff --git a/pw_tool/main.cc b/pw_tool/main.cc
new file mode 100644
index 000000000..b716276c6
--- /dev/null
+++ b/pw_tool/main.cc
@@ -0,0 +1,176 @@
+// 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;
+}