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>
diff --git a/BUILD.gn b/BUILD.gn
index 1d1e617..7cd01b9 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -185,6 +185,12 @@
# Add target-specific images.
deps += pw_TARGET_APPLICATIONS
+
+ # Add the pw_tool target to be built on host.
+ if (defined(pw_toolchain_SCOPE.is_host_toolchain) &&
+ pw_toolchain_SCOPE.is_host_toolchain) {
+ deps += [ "$dir_pw_tool" ]
+ }
}
group("host_tools") {
@@ -218,6 +224,7 @@
"$dir_pw_sync",
"$dir_pw_sys_io",
"$dir_pw_thread",
+ "$dir_pw_tool",
"$dir_pw_trace",
"$dir_pw_unit_test",
"$dir_pw_varint",
diff --git a/modules.gni b/modules.gni
index 54c92a3..341dbb1 100644
--- a/modules.gni
+++ b/modules.gni
@@ -97,6 +97,7 @@
dir_pw_thread_threadx = get_path_info("pw_thread_threadx", "abspath")
dir_pw_third_party = get_path_info("third_party", "abspath")
dir_pw_tokenizer = get_path_info("pw_tokenizer", "abspath")
+ dir_pw_tool = get_path_info("pw_tool", "abspath")
dir_pw_toolchain = get_path_info("pw_toolchain", "abspath")
dir_pw_trace = get_path_info("pw_trace", "abspath")
dir_pw_trace_tokenized = get_path_info("pw_trace_tokenized", "abspath")
diff --git a/pw_tool/BUILD b/pw_tool/BUILD
new file mode 100644
index 0000000..304ec5b
--- /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 0000000..522ae3a
--- /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 0000000..b716276
--- /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;
+}