[storage] Input and cmd line for disk-inspect.

Use the Fuchsia line input editor and command-line parser for the
interactive disk-inspect utility.

The command-line parser gives better help and more flexible parsing. The
line input editor adds history and better editing.

There should be no behavior change.

Change-Id: Ib0a3f2659dc09d6b1d4399a1f6f1333f861698dd
Reviewed-on: https://fuchsia-review.googlesource.com/c/fuchsia/+/405494
Commit-Queue: Brett Wilson <brettw@google.com>
Reviewed-by: Gianfranco Valentino <gevalentino@google.com>
Testability-Review: Gianfranco Valentino <gevalentino@google.com>
diff --git a/src/devices/block/bin/disk-inspect/BUILD.gn b/src/devices/block/bin/disk-inspect/BUILD.gn
index 28eb1dd..b803ba8 100644
--- a/src/devices/block/bin/disk-inspect/BUILD.gn
+++ b/src/devices/block/bin/disk-inspect/BUILD.gn
@@ -8,6 +8,8 @@
   deps = [
     "//sdk/fidl/fuchsia.hardware.block.partition:fuchsia.hardware.block.partition_llcpp",
     "//sdk/lib/fdio",
+    "//src/lib/line_input",
+    "//zircon/public/lib/cmdline",
     "//zircon/system/ulib/disk_inspector",
     "//zircon/system/ulib/minfs",
   ]
diff --git a/src/devices/block/bin/disk-inspect/main.cc b/src/devices/block/bin/disk-inspect/main.cc
index ae30e3a..3d14d12 100644
--- a/src/devices/block/bin/disk-inspect/main.cc
+++ b/src/devices/block/bin/disk-inspect/main.cc
@@ -4,13 +4,14 @@
 
 #include <fcntl.h>
 #include <fuchsia/hardware/block/partition/llcpp/fidl.h>
-#include <getopt.h>
+#include <lib/cmdline/args_parser.h>
 #include <lib/fdio/fdio.h>
 #include <stdio.h>
 #include <zircon/status.h>
 
 #include <iostream>
 #include <memory>
+#include <optional>
 #include <sstream>
 
 #include <block-client/cpp/block-device.h>
@@ -24,54 +25,74 @@
 #include <minfs/command_handler.h>
 #include <minfs/minfs_inspector.h>
 
+#include "src/lib/line_input/modal_line_input.h"
+
 namespace {
 
-constexpr char kUsageMessage[] = R"""(
-Tool for inspecting a block device as a filesystem.
+bool should_quit = false;
 
-disk-inspect --device /dev/class/block/002 --name minfs
+constexpr char kHelpIntro[] =
+    R"(Tool for inspecting a block device as a filesystem. Typical usage:
 
-Options:
-  --device (-d) path : Specifies the block device to use.
-  --name (-n) : What filesystem type to represent the block device. Only
-                supports "minfs" for now.
-)""";
+  disk-inspect --device /dev/class/block/002 --name minfs
+
+Options
+
+)";
+
+const char kDeviceHelp[] =
+    "  --device (-d) <device-name>\n"
+    "      The path to the block device to use. For example,\n"
+    "      \"/dev/class/block/000\".";
+const char kHelpHelp[] =
+    "  --help (-h)\n"
+    "      Prints usage instructions.";
+const char kNameHelp[] =
+    "  --name (-n) <format>\n"
+    "      The filesystem type of the block device. Only \"minfs\" is currently\n"
+    "      supported.";
 
 // Configuration info (what to do).
 struct Config {
-  const char* path;
-  const char* name;
+  std::string path;
+  std::string name;
 };
 
-bool GetOptions(int argc, char** argv, Config* config) {
-  while (true) {
-    struct option options[] = {
-        {"device", required_argument, nullptr, 'd'},
-        {"name", required_argument, nullptr, 'n'},
-        {"help", no_argument, nullptr, 'h'},
-        {nullptr, 0, nullptr, 0},
-    };
-    int opt_index;
-    int c = getopt_long(argc, argv, "d:n:h", options, &opt_index);
-    if (c < 0) {
-      break;
-    }
-    switch (c) {
-      case 'd':
-        config->path = optarg;
-        break;
-      case 'n':
-        config->name = optarg;
-        break;
-      case 'h':
-        std::cout << kUsageMessage << "\n";
-        return false;
-    }
-  }
-  return argc == optind;
-}
+std::optional<Config> GetOptions(int argc, char** argv) {
+  cmdline::ArgsParser<Config> parser;
 
-bool ValidateOptions(const Config& config) { return !(!config.path || !config.name); }
+  bool print_help = argc == 1;  // Default to showing help if nothing is specified.
+  parser.AddSwitch("device", 'd', kDeviceHelp, &Config::path);
+  parser.AddGeneralSwitch("help", 'h', kHelpHelp, [&print_help]() { print_help = true; });
+  parser.AddSwitch("name", 'n', kNameHelp, &Config::name);
+
+  std::vector<std::string> params;
+  Config config;
+  if (auto status = parser.Parse(argc, argv, &config, &params); status.has_error()) {
+    std::cerr << status.error_message() << "\n\n";
+    return std::nullopt;
+  }
+
+  // Check for explicitly-requested help.
+  if (print_help) {
+    std::cout << kHelpIntro << parser.GetHelp();
+    return std::nullopt;
+  }
+
+  // There should be no non-switch args.
+  if (!params.empty()) {
+    std::cerr << "This program takes no non-switch arguments. See --help.\n\n";
+    return std::nullopt;
+  }
+
+  // Validate.
+  if (config.path.empty() || config.name.empty()) {
+    std::cerr << "Both --device and --name are required.\n\n";
+    return std::nullopt;
+  }
+
+  return config;
+}
 
 fit::result<uint32_t, std::string> GetBlockSize(const std::string& name) {
   if (name == "minfs") {
@@ -131,58 +152,72 @@
   return handler;
 }
 
+void OnLineTyped(line_input::ModalLineInputStdout& input, disk_inspector::CommandHandler* handler,
+                 const std::string& line) {
+  if (line.find_first_not_of(' ') == std::string::npos)
+    return;
+
+  input.AddToHistory(line);
+
+  // Hide the input line so output gets appended without the lines of typing.
+  input.Hide();
+
+  std::stringstream ss(line);
+  std::istream_iterator<std::string> begin(ss);
+  std::istream_iterator<std::string> end;
+  std::vector<std::string> command_args(begin, end);
+  if (command_args[0] == "exit" || command_args[0] == "quit" || command_args[0] == "q") {
+    should_quit = true;
+    return;  // Don't unhide when exiting.
+  }
+
+  if (command_args[0] == "help" || command_args[0] == "h" || command_args[0] == "?") {
+    handler->PrintSupportedCommands();
+  } else {
+    if (zx_status_t status = handler->CallCommand(command_args); status != ZX_OK) {
+      switch (status) {
+        case ZX_ERR_NOT_SUPPORTED:
+          std::cerr << "Command not supported.\n";
+          break;
+        default:
+          std::cerr << "Call command failed with error: " << zx_status_get_string(status) << " ("
+                    << status << ")\n";
+          break;
+      }
+    }
+  }
+
+  // Re-show to match Hide() call at the top.
+  input.Show();
+}
+
 }  //  namespace
 
 int main(int argc, char** argv) {
-  Config config = {};
-  if (!GetOptions(argc, argv, &config)) {
-    std::cout << kUsageMessage << "\n";
+  auto option_result = GetOptions(argc, argv);
+  if (!option_result)
     return -1;
-  }
 
-  if (!ValidateOptions(config)) {
-    std::cout << kUsageMessage << "\n";
-    return -1;
-  }
-
-  std::unique_ptr<disk_inspector::CommandHandler> handler = GetHandler(config.path, config.name);
-  if (handler == nullptr) {
+  const Config& config = *option_result;
+  std::unique_ptr<disk_inspector::CommandHandler> handler =
+      GetHandler(config.path.c_str(), config.name.c_str());
+  if (!handler) {
     std::cerr << "Could not get inspector at path. Closing.\n";
     return -1;
   }
 
   std::cout << "Starting " << config.name
             << " inspector. Type \"help\" to get available commands.\n";
-  std::cout << "Type \"exit\" to quit the application.\n";
-  while (true) {
-    std::string command_str;
-    getline(std::cin, command_str);
-    if (command_str.find_first_not_of(' ') == std::string::npos) {
-      continue;
-    }
-    std::stringstream ss(command_str);
-    std::istream_iterator<std::string> begin(ss);
-    std::istream_iterator<std::string> end;
-    std::vector<std::string> command_args(begin, end);
-    if (command_args[0] == "exit") {
-      return 0;
-    }
-    if (command_args[0] == "help") {
-      handler->PrintSupportedCommands();
-      continue;
-    }
-    zx_status_t status = handler->CallCommand(command_args);
-    if (status != ZX_OK) {
-      switch (status) {
-        case ZX_ERR_NOT_SUPPORTED: {
-          std::cerr << "Command not supported.\n";
-          break;
-        }
-        default: {
-          std::cerr << "Call command failed with error: " << status << "\n";
-        }
-      }
-    }
-  }
+  std::cout << "Type \"exit\" or \"quit\" to quit the application.\n";
+
+  line_input::ModalLineInputStdout input;
+  input.Init(
+      [&input, &handler](const std::string& line) { OnLineTyped(input, handler.get(), line); },
+      "[disk-inspect] ");
+
+  input.Show();
+  while (!should_quit)
+    input.OnInput(static_cast<char>(getc(stdin)));
+
   return 0;
 }