[uwptool] Refactor command-handling to a map (#26309)

Extracts each of the commands (listapps, install, uninstall, launch)
into a command class that is registered at startup. This simplifies
adding further commands, and cleans up the code a bit.

This is a refactoring that introduces no changes to existing functionality.
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index ed9fa70..57901f1 100755
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1612,6 +1612,8 @@
 FILE: ../../../flutter/shell/platform/windows/text_input_plugin.h
 FILE: ../../../flutter/shell/platform/windows/text_input_plugin_delegate.h
 FILE: ../../../flutter/shell/platform/windows/text_input_plugin_unittest.cc
+FILE: ../../../flutter/shell/platform/windows/uwptool_commands.cc
+FILE: ../../../flutter/shell/platform/windows/uwptool_commands.h
 FILE: ../../../flutter/shell/platform/windows/uwptool_main.cc
 FILE: ../../../flutter/shell/platform/windows/uwptool_utils.cc
 FILE: ../../../flutter/shell/platform/windows/uwptool_utils.h
diff --git a/shell/platform/windows/BUILD.gn b/shell/platform/windows/BUILD.gn
index 3402ed8..5a8a5d1 100644
--- a/shell/platform/windows/BUILD.gn
+++ b/shell/platform/windows/BUILD.gn
@@ -316,9 +316,13 @@
   }
 
   sources = [
+    "uwptool_commands.cc",
+    "uwptool_commands.h",
     "uwptool_utils.cc",
     "uwptool_utils.h",
   ]
+
+  deps = [ ":string_conversion" ]
 }
 
 # Command-line tool used by the flutter tool that serves a similar purpose to
diff --git a/shell/platform/windows/uwptool_commands.cc b/shell/platform/windows/uwptool_commands.cc
new file mode 100644
index 0000000..8a54ebe
--- /dev/null
+++ b/shell/platform/windows/uwptool_commands.cc
@@ -0,0 +1,112 @@
+#include "flutter/shell/platform/windows/uwptool_commands.h"
+
+#include <iostream>
+#include <sstream>
+
+#include "flutter/shell/platform/windows/string_conversion.h"
+#include "flutter/shell/platform/windows/uwptool_utils.h"
+
+namespace flutter {
+
+bool ListAppsCommand::ValidateArgs(const std::vector<std::string>& args) const {
+  return true;
+}
+
+int ListAppsCommand::Run(const std::vector<std::string>& args) const {
+  flutter::ApplicationStore app_store;
+  for (const flutter::Application& app : app_store.GetApps()) {
+    std::wcout << app.GetPackageFamily() << std::endl;
+  }
+  return 0;
+}
+
+bool InstallCommand::ValidateArgs(const std::vector<std::string>& args) const {
+  return args.size() >= 1;
+}
+
+int InstallCommand::Run(const std::vector<std::string>& args) const {
+  std::wstring package_uri = flutter::Utf16FromUtf8(args[0]);
+  std::vector<std::wstring> dependency_uris;
+  for (int i = 1; i < args.size(); ++i) {
+    dependency_uris.push_back(flutter::Utf16FromUtf8(args[i]));
+  }
+  flutter::ApplicationStore app_store;
+  if (app_store.InstallApp(package_uri, dependency_uris)) {
+    std::wcerr << L"Installed application " << package_uri << std::endl;
+    return 0;
+  }
+  return 1;
+}
+
+bool UninstallCommand::ValidateArgs(
+    const std::vector<std::string>& args) const {
+  return args.size() >= 1;
+}
+
+int UninstallCommand::Run(const std::vector<std::string>& args) const {
+  std::wstring package_family = flutter::Utf16FromUtf8(args[0]);
+  bool success = true;
+  flutter::ApplicationStore app_store;
+  for (flutter::Application& app : app_store.GetApps(package_family)) {
+    if (app.Uninstall()) {
+      std::wcerr << L"Uninstalled application " << app.GetPackageFullName()
+                 << std::endl;
+    } else {
+      std::wcerr << L"error: Failed to uninstall application "
+                 << app.GetPackageFullName() << std::endl;
+      success = false;
+    }
+  }
+  return success ? 0 : 1;
+}
+
+bool LaunchCommand::ValidateArgs(const std::vector<std::string>& args) const {
+  return args.size() >= 1;
+}
+
+int LaunchCommand::Run(const std::vector<std::string>& args) const {
+  // Get the package family name.
+  std::string package_family = args[0];
+
+  // Concatenate the remaining args, comma-separated.
+  std::ostringstream app_args;
+  for (int i = 1; i < args.size(); ++i) {
+    app_args << args[i];
+    if (i < args.size() - 1) {
+      app_args << ",";
+    }
+  }
+  int process_id = LaunchApp(flutter::Utf16FromUtf8(package_family),
+                             flutter::Utf16FromUtf8(app_args.str()));
+  if (process_id == -1) {
+    std::cerr << "error: Failed to launch app with package family "
+              << package_family << std::endl;
+    return 1;
+  }
+
+  // Write an informative message for the user to stderr.
+  std::cerr << "Launched app with package family " << package_family
+            << ". PID: " << std::endl;
+  // Write the PID to stdout. The flutter tool reads this value in.
+  std::cout << process_id << std::endl;
+  return 0;
+}
+
+// Launches the app installed on the system with the specified package.
+//
+// Returns -1 if no matching app, or multiple matching apps are found, or if
+// the app fails to launch. Otherwise, the process ID of the launched app is
+// returned.
+int LaunchCommand::LaunchApp(const std::wstring_view package_family,
+                             const std::wstring_view args) const {
+  flutter::ApplicationStore app_store;
+  for (flutter::Application& app : app_store.GetApps(package_family)) {
+    int process_id = app.Launch(args);
+    if (process_id != -1) {
+      return process_id;
+    }
+  }
+  return -1;
+}
+
+}  // namespace flutter
diff --git a/shell/platform/windows/uwptool_commands.h b/shell/platform/windows/uwptool_commands.h
new file mode 100644
index 0000000..2917c63
--- /dev/null
+++ b/shell/platform/windows/uwptool_commands.h
@@ -0,0 +1,94 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+#ifndef FLUTTER_SHELL_PLATFORM_WINDOWS_UWPTOOL_COMMANDS_H_
+#define FLUTTER_SHELL_PLATFORM_WINDOWS_UWPTOOL_COMMANDS_H_
+
+#include <string>
+#include <vector>
+
+namespace flutter {
+
+// A uwptool command that can be invoked as the first argument of the uwptool
+// arguments list.
+class Command {
+ public:
+  Command(const std::string_view name,
+          const std::string_view usage,
+          const std::string_view description)
+      : name_(name), usage_(usage), description_(description) {}
+  virtual ~Command() {}
+
+  std::string GetCommandName() const { return name_; }
+  std::string GetUsage() const { return usage_; }
+  std::string GetDescription() const { return description_; }
+
+  // Returns true if the arguments list constitute valid arguments for this
+  // command.
+  virtual bool ValidateArgs(const std::vector<std::string>& args) const = 0;
+
+  // Invokes the command with the specified arguments list.
+  virtual int Run(const std::vector<std::string>& args) const = 0;
+
+ private:
+  std::string name_;
+  std::string usage_;
+  std::string description_;
+};
+
+// Command that prints a list of all installed applications on the system.
+class ListAppsCommand : public Command {
+ public:
+  ListAppsCommand()
+      : Command("listapps",
+                "listapps",
+                "List installed apps by package family name") {}
+
+  bool ValidateArgs(const std::vector<std::string>& args) const override;
+  int Run(const std::vector<std::string>& args) const override;
+};
+
+// Command that installs the specified package and dependencies.
+class InstallCommand : public Command {
+ public:
+  InstallCommand()
+      : Command("install",
+                "install PACKAGE_URI DEPENDENCY_URI...",
+                "Install the specified package with all listed dependencies") {}
+
+  bool ValidateArgs(const std::vector<std::string>& args) const override;
+  int Run(const std::vector<std::string>& args) const override;
+};
+
+// Command that uninstalls the specified package.
+class UninstallCommand : public Command {
+ public:
+  UninstallCommand()
+      : Command("uninstall",
+                "uninstall PACKAGE_FAMILY_NAME",
+                "Uninstall the specified package") {}
+
+  bool ValidateArgs(const std::vector<std::string>& args) const override;
+  int Run(const std::vector<std::string>& args) const override;
+};
+
+// Command that launches the specified application package.
+class LaunchCommand : public Command {
+ public:
+  LaunchCommand()
+      : Command("launch",
+                "launch PACKAGE_FAMILY_NAME",
+                "Launch the specified package") {}
+
+  bool ValidateArgs(const std::vector<std::string>& args) const override;
+  int Run(const std::vector<std::string>& args) const override;
+
+ private:
+  int LaunchApp(const std::wstring_view package_family,
+                const std::wstring_view args) const;
+};
+
+}  // namespace flutter
+
+#endif  // FLUTTER_SHELL_PLATFORM_WINDOWS_UWPTOOL_COMMANDS_H_
diff --git a/shell/platform/windows/uwptool_main.cc b/shell/platform/windows/uwptool_main.cc
index 0183716..eedc33c 100644
--- a/shell/platform/windows/uwptool_main.cc
+++ b/shell/platform/windows/uwptool_main.cc
@@ -5,83 +5,33 @@
 #include <Windows.h>
 #include <winrt/base.h>
 
-#include <algorithm>
+#include <iomanip>
 #include <iostream>
-#include <sstream>
+#include <map>
+#include <memory>
 #include <string>
 #include <vector>
 
 #include "flutter/fml/command_line.h"
-#include "flutter/shell/platform/windows/string_conversion.h"
-#include "flutter/shell/platform/windows/uwptool_utils.h"
+#include "flutter/shell/platform/windows/uwptool_commands.h"
 
 namespace {
 
-// Prints a list of installed UWP apps to stdout.
-void PrintInstalledApps() {
-  flutter::ApplicationStore app_store;
-  for (const flutter::Application& app : app_store.GetApps()) {
-    std::wcout << app.GetPackageFamily() << std::endl;
-  }
-}
-
-// Launches the app installed on the system with the specified package.
-//
-// Returns -1 if no matching app, or multiple matching apps are found, or if
-// the app fails to launch. Otherwise, the process ID of the launched app is
-// returned.
-int LaunchApp(const std::wstring_view package_family,
-              const std::wstring_view args) {
-  flutter::ApplicationStore app_store;
-  for (flutter::Application& app : app_store.GetApps(package_family)) {
-    int process_id = app.Launch(args);
-    if (process_id != -1) {
-      return process_id;
-    }
-  }
-  return -1;
-}
-
-// Installs the app in the specified build output directory.
-//
-// Returns true on success.
-bool InstallApp(const std::wstring_view package_uri,
-                const std::vector<std::wstring>& dependency_uris) {
-  flutter::ApplicationStore app_store;
-  if (app_store.InstallApp(package_uri, dependency_uris)) {
-    std::wcerr << L"Installed application " << package_uri << std::endl;
-    return true;
-  }
-  return false;
-}
-
-// Uninstalls the app with the specified package.
-//
-// Returns true on success.
-bool UninstallApp(const std::wstring_view package_family) {
-  bool success = true;
-  flutter::ApplicationStore app_store;
-  for (flutter::Application& app : app_store.GetApps(package_family)) {
-    if (app.Uninstall()) {
-      std::wcerr << L"Uninstalled application " << app.GetPackageFullName()
-                 << std::endl;
-    } else {
-      std::wcerr << L"error: Failed to uninstall application "
-                 << app.GetPackageFullName() << std::endl;
-      success = false;
-    }
-  }
-  return success;
-}
+using CommandMap = std::map<std::string, std::unique_ptr<flutter::Command>>;
 
 // Prints the command usage to stderr.
-void PrintUsage() {
-  std::cerr << "usage: uwptool COMMAND [ARGUMENTS]" << std::endl
-            << "commands:" << std::endl
-            << "  listapps                       list all apps" << std::endl
-            << "  launch PACKAGE_FAMILY          launch an app" << std::endl
-            << "  install PACKAGE_URI DEP_URI... install an app" << std::endl
-            << "  uninstall PACKAGE_FAMILY       uninstall an app" << std::endl;
+void PrintUsage(const CommandMap& commands) {
+  std::cerr << "usage: uwptool COMMAND [ARGUMENTS]" << std::endl;
+  std::cerr << std::endl;
+  std::cerr << "Available commands:" << std::endl;
+  for (const auto& [command_name, command] : commands) {
+    std::cerr << "  " << std::left << std::setw(15) << command_name
+              << command->GetDescription() << std::endl;
+  }
+}
+
+void PrintCommandUsage(const flutter::Command& command) {
+  std::cerr << "usage: uwptool " << command.GetUsage() << std::endl;
 }
 
 }  // namespace
@@ -89,69 +39,37 @@
 int main(int argc, char** argv) {
   winrt::init_apartment();
 
+  // Register commands alphabetically, to make usage string clearer.
+  CommandMap commands;
+  commands.emplace("install", std::make_unique<flutter::InstallCommand>());
+  commands.emplace("launch", std::make_unique<flutter::LaunchCommand>());
+  commands.emplace("listapps", std::make_unique<flutter::ListAppsCommand>());
+  commands.emplace("uninstall", std::make_unique<flutter::UninstallCommand>());
+
+  // Parse command line arguments.
   auto command_line = fml::CommandLineFromArgcArgv(argc, argv);
   if (command_line.positional_args().size() < 1) {
-    PrintUsage();
+    PrintUsage(commands);
+    return 1;
+  }
+  std::vector<std::string> command_args(
+      command_line.positional_args().begin() + 1,
+      command_line.positional_args().end());
+
+  // Determine the command.
+  const std::string& command_name = command_line.positional_args()[0];
+  const auto& it = commands.find(command_name);
+  if (it == commands.end()) {
+    std::cerr << "Unknown command: " << command_name << std::endl;
+    PrintUsage(commands);
     return 1;
   }
 
-  const std::vector<std::string>& args = command_line.positional_args();
-  std::string command = args[0];
-  if (command == "listapps") {
-    PrintInstalledApps();
-    return 0;
-  } else if (command == "launch") {
-    if (args.size() < 2) {
-      PrintUsage();
-      return 1;
-    }
-
-    // Get the package family name.
-    std::string package_family = args[1];
-
-    // Concatenate the remaining args, comma-separated.
-    std::ostringstream app_args;
-    for (int i = 2; i < args.size(); ++i) {
-      app_args << args[i];
-      if (i < args.size() - 1) {
-        app_args << ",";
-      }
-    }
-    int process_id = LaunchApp(flutter::Utf16FromUtf8(package_family),
-                               flutter::Utf16FromUtf8(app_args.str()));
-    if (process_id == -1) {
-      std::cerr << "error: Failed to launch app with package family "
-                << package_family << std::endl;
-      return 1;
-    }
-
-    // Write an informative message for the user to stderr.
-    std::cerr << "Launched app with package family " << package_family
-              << ". PID: " << std::endl;
-    // Write the PID to stdout. The flutter tool reads this value in.
-    std::cout << process_id << std::endl;
-    return 0;
-  } else if (command == "install") {
-    if (args.size() < 2) {
-      PrintUsage();
-      return 1;
-    }
-    std::wstring package_uri = flutter::Utf16FromUtf8(args[1]);
-    std::vector<std::wstring> dependency_uris;
-    for (int i = 2; i < args.size(); ++i) {
-      dependency_uris.push_back(flutter::Utf16FromUtf8(args[i]));
-    }
-    return InstallApp(package_uri, dependency_uris) ? 0 : 1;
-  } else if (command == "uninstall") {
-    if (args.size() < 2) {
-      PrintUsage();
-      return 1;
-    }
-    std::string package_family = args[1];
-    return UninstallApp(flutter::Utf16FromUtf8(package_family)) ? 0 : 1;
+  // Run the command.
+  auto& command = it->second;
+  if (!command->ValidateArgs(command_args)) {
+    PrintCommandUsage(*command);
+    return 1;
   }
-
-  std::cerr << "Unknown command: " << command << std::endl;
-  PrintUsage();
-  return 1;
+  return command->Run(command_args);
 }