瀏覽代碼

feat: add support for actions, allowing the construction of hierarchical design in argparsing

Sam Jaffe 3 年之前
父節點
當前提交
10594bc883

+ 1 - 1
external/scoped_buffer_capture

@@ -1 +1 @@
-Subproject commit 00e6ddb03d3c2f9fb71ccfee2162401d70b56a6e
+Subproject commit 1647156e0a9ce2cda113a7e8dba62882da5b8362

+ 28 - 1
include/program_args/arguments.h

@@ -10,6 +10,7 @@ namespace program {
 template <typename Impl> class Arguments {
 private:
   using option_id = std::string;
+  struct Action;
   struct Argument;
   struct Flag;
   struct Option;
@@ -24,6 +25,7 @@ public:
   }
 
 protected:
+  auto action(std::string const &name, std::string const &description = "");
   auto argument(size_t index, std::string const & name,
                 std::string const & description = "");
   auto flag(std::string const & name, std::string const & description = "");
@@ -46,6 +48,12 @@ private:
   bool is_flag(char arg) const { return is_flag({'-', arg}); }
 
 private:
+  friend struct ArgumentTestHelper;
+  friend int main(int, char const * const *);
+  
+private:
+  using main_callback_t = std::function<int(Arguments const &)>;
+  using make_hook_t = std::function<main_callback_t(size_t, char const * const *)>;
   // Metadata variables
   bool primed_{false};
   std::map<option_id, std::string> argument_descriptions;
@@ -53,9 +61,12 @@ private:
   std::map<option_id, std::string> option_descriptions;
   std::map<std::string, option_id> option_names;
   std::set<option_id> flag_names;
+  std::map<std::string, make_hook_t> actions;
+  std::map<std::string, std::string> action_descriptions;
 
   // Data/Output variables
   std::string program;
+  main_callback_t main_callback{nullptr};
   constexpr static size_t const no_optional_args{~0ul};
   size_t optional_from{no_optional_args};
   std::vector<std::string> arguments;
@@ -63,6 +74,16 @@ private:
   std::map<std::string, int> flags;
 };
 
+template <typename Impl> struct Arguments<Impl>::Action {
+  Arguments<Impl> * self;
+  std::string name;
+  std::string description;
+
+  template <typename T> operator T() const;
+  bool primed() const;
+  explicit operator bool() const;
+};
+
 template <typename Impl> struct Arguments<Impl>::Argument {
   Arguments<Impl> * self;
   size_t index;
@@ -113,11 +134,17 @@ struct Arguments<Impl>::WithDefault {
 
 #include "arguments_impl.hpp"
 
+
+template <typename Args, typename Action>
+int typed_main(Args const &, Action const &);
+
 #define TYPED_MAIN(tname)                                                      \
   int typed_main(tname const & args);                                          \
   int main(int argc, char const * const * const argv) try {                    \
-    return typed_main(tname(argc, argv));                                      \
+    tname args(argc, argv);                                                    \
+    return (args.main_callback) ? args.main_callback(args) : typed_main(args); \
   } catch (program::ProgramArgumentsError const & pae) {                       \
     std::cerr << "Error in program argument handling: " << pae.what() << "\n"; \
   }                                                                            \
   int typed_main(tname const & args)
+

+ 42 - 1
include/program_args/arguments_impl.hpp

@@ -15,6 +15,32 @@
 
 namespace program {
 
+template <typename Impl>
+template <typename T>
+Arguments<Impl>::Action::operator T() const {
+  static_assert(std::is_base_of_v<Arguments<T>, T>,
+                "Action can only bind to Arguments");
+  if (!primed()) {
+    self->actions.emplace(name, [](int argc, char const * const *argv) {
+      return [action = T(argc, argv)](Arguments<Impl> const &args){
+        return typed_main(args, action);
+      };
+    });
+  }
+  return T();
+}
+
+template <typename Impl> Arguments<Impl>::Action::operator bool() const {
+  return primed() && self->arguments.size() > 0;
+}
+
+template <typename Impl> bool Arguments<Impl>::Action::primed() const {
+  if (self->primed_) { return true; }
+  if (!self->argument_names.empty()) { throw ArgumentMixingError(); }
+  self->action_descriptions.emplace(name, description);
+  return false;
+}
+
 template <typename Impl>
 template <typename T>
 Arguments<Impl>::Argument::operator T() const {
@@ -43,6 +69,7 @@ auto Arguments<Impl>::Argument::operator=(T && value) {
 
 template <typename Impl> bool Arguments<Impl>::Argument::primed() const {
   if (self->primed_) { return true; }
+  if (!self->actions.empty()) { throw ArgumentMixingError(); }
   if (is_optional) {
     self->optional_from = std::min(self->optional_from, index);
   } else if (self->optional_from < index) {
@@ -133,13 +160,21 @@ Arguments<Impl>::Arguments(int argc, char const * const * const argv) {
   program = argv[0];
   for (size_t i = 1; i < argc; ++i) {
     std::string arg = argv[i];
+    char const * const next = argv[i + 1];
     char abbrev = arg[1];
     if (arg == "--help") {
       usage();
       PROGRAM_ARGS_EXIT(0);
     } else if (arg == "--") {
-      arguments.insert(arguments.end(), &argv[i + 1], &argv[argc]);
+      arguments.insert(arguments.end(), &next, &argv[argc]);
       break;
+    } else if (auto hook = actions[arg]) {
+      main_callback = hook(argc - i, argv + i);
+      return;
+    } else if (arg == "help" && next && actions[next]) {
+      char const * const help[] = {next, "--help"};
+      main_callback = actions[next](2, help);
+      return;
     } else if (arg[0] != '-') {
       arguments.emplace_back(arg);
     } else if (is_flag(arg)) {
@@ -214,6 +249,12 @@ bool Arguments<Impl>::is_flag(std::string const & arg) const {
   return is_option(arg) && flag_names.count(id(arg));
 }
 
+template <typename Impl>
+auto Arguments<Impl>::action(std::string const & name,
+                             std::string const & description) {
+  return Action{this, name, description};
+}
+
 template <typename Impl>
 auto Arguments<Impl>::argument(size_t index, std::string const & name,
                                std::string const & description) {

+ 6 - 0
include/program_args/exception.h

@@ -8,6 +8,12 @@ class ProgramArgumentsError : public std::logic_error {
   using std::logic_error::logic_error;
 };
 
+class ArgumentMixingError : public ProgramArgumentsError {
+public:
+  ArgumentMixingError()
+      : ProgramArgumentsError("Cannot mix together argument() and action() clauses in a program") {}
+};
+
 class ArgumentStructureError : public ProgramArgumentsError {
 public:
   ArgumentStructureError(std::string const & desc, std::string const & name)

+ 4 - 0
program_args.xcodeproj/project.pbxproj

@@ -12,6 +12,7 @@
 		CD8C5A8B25D057AA0004A6D9 /* options_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD8C5A8A25D057AA0004A6D9 /* options_test.cpp */; };
 		CD8C5AA025D06D0B0004A6D9 /* flag_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD8C5A9F25D06D0B0004A6D9 /* flag_test.cpp */; };
 		CD8C5AA425D072F50004A6D9 /* argument_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CD8C5AA325D072F50004A6D9 /* argument_test.cpp */; };
+		CDE4AAB927B9EBF000543450 /* action_test.cpp in Sources */ = {isa = PBXBuildFile; fileRef = CDE4AAB827B9EBF000543450 /* action_test.cpp */; };
 /* End PBXBuildFile section */
 
 /* Begin PBXContainerItemProxy section */
@@ -73,6 +74,7 @@
 		CD8C5A9F25D06D0B0004A6D9 /* flag_test.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = flag_test.cpp; sourceTree = "<group>"; };
 		CD8C5AA325D072F50004A6D9 /* argument_test.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = argument_test.cpp; sourceTree = "<group>"; };
 		CDD334A025D200AB008540EE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; sourceTree = "<group>"; };
+		CDE4AAB827B9EBF000543450 /* action_test.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = action_test.cpp; sourceTree = "<group>"; };
 		CDE4F78A25CF309E009E4EC1 /* libprogram_args.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libprogram_args.a; sourceTree = BUILT_PRODUCTS_DIR; };
 		CDE4F79325CF316A009E4EC1 /* GoogleMock.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = GoogleMock.xcodeproj; path = "../../../gmock-xcode-master/GoogleMock.xcodeproj"; sourceTree = "<group>"; };
 		CDE4F7A225CF317C009E4EC1 /* program_args */ = {isa = PBXFileReference; lastKnownFileType = folder; name = program_args; path = include/program_args; sourceTree = "<group>"; };
@@ -168,6 +170,7 @@
 			children = (
 				CD8C5A9625D058470004A6D9 /* xcode_gtest_helper.h */,
 				CD8C5AA325D072F50004A6D9 /* argument_test.cpp */,
+				CDE4AAB827B9EBF000543450 /* action_test.cpp */,
 				CD8C5A8A25D057AA0004A6D9 /* options_test.cpp */,
 				CD8C5A9F25D06D0B0004A6D9 /* flag_test.cpp */,
 				CD2F0C6325DC9B3500CB394A /* usage_test.cpp */,
@@ -335,6 +338,7 @@
 				CD8C5A8B25D057AA0004A6D9 /* options_test.cpp in Sources */,
 				CD8C5AA025D06D0B0004A6D9 /* flag_test.cpp in Sources */,
 				CD8C5AA425D072F50004A6D9 /* argument_test.cpp in Sources */,
+				CDE4AAB927B9EBF000543450 /* action_test.cpp in Sources */,
 				CD2F0C6425DC9B3500CB394A /* usage_test.cpp in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;

+ 83 - 0
test/action_test.cpp

@@ -0,0 +1,83 @@
+//
+//  action_test.cpp
+//  program_args-test
+//
+//  Created by Sam Jaffe on 2/13/22.
+//
+
+#include "program_args/arguments.h"
+
+#include "xcode_gtest_helper.h"
+
+namespace program {
+struct ArgumentTestHelper {
+  template <typename Impl> bool has_main(Arguments<Impl> const &args) const {
+    return static_cast<bool>(args.main_callback);
+  }
+  template <typename Impl> int main(Arguments<Impl> const &args) const {
+    return args.main_callback(args);
+  }
+};
+}
+
+template <typename T, size_t N> static T parse(char const * const (&argv)[N]) {
+  return T(N, argv);
+}
+
+std::string g_type_name;
+
+template <typename Args, typename Action>
+int typed_main(Args const &, Action const &) {
+  g_type_name = typeid(Action).name();
+  return 0;
+}
+
+struct Checkout : program::Arguments<Checkout> {
+  using program::Arguments<Checkout>::Arguments;
+  std::string commitish = argument(0, "commit-ish");
+};
+
+struct Commit : program::Arguments<Commit> {
+  using program::Arguments<Commit>::Arguments;
+  std::string message = option("message", 'm');
+};
+
+struct Bad1 : program::Arguments<Bad1> {
+  using program::Arguments<Bad1>::Arguments;
+  
+  Commit commit = action("commit");
+  std::string arg0 = argument(0, "arg0");
+};
+
+struct Bad2 : program::Arguments<Bad2> {
+  using program::Arguments<Bad2>::Arguments;
+  
+  std::string arg0 = argument(0, "arg0");
+  Commit commit = action("commit");
+};
+
+struct Git : program::Arguments<Git> {
+  using program::Arguments<Git>::Arguments;
+  
+  bool verbose = flag("verbose", 'v');
+  
+  Commit commit = action("commit");
+  Checkout checkout = action("checkout");
+};
+
+TEST(ActionTest, CannotMixActionAndArgument) {
+  EXPECT_THROW(Bad1(), program::ArgumentMixingError);
+  EXPECT_THROW(Bad2(), program::ArgumentMixingError);
+}
+
+TEST(ActionTest, CanRunWithMultipleActions) {
+  EXPECT_NO_THROW(Git());
+}
+
+TEST(ActionTest, ActionIsRouted) {
+  Git git = parse<Git>({"", "-v", "commit", "-m", "this is a message"});
+  program::ArgumentTestHelper helper;
+  EXPECT_TRUE(helper.has_main(git));
+  helper.main(git);
+  EXPECT_THAT(g_type_name, typeid(Commit).name());
+}

+ 1 - 0
test/xcode_gtest_helper.h

@@ -12,6 +12,7 @@
 
 #pragma clang diagnostic push
 #pragma clang diagnostic ignored "-Wquoted-include-in-framework-header"
+#pragma clang diagnostic ignored "-Wcomma"
 
 #include <gmock/gmock.h>
 #include <gtest/gtest.h>