Pārlūkot izejas kodu

Merge branch 'feat/multi' into main

* feat/multi:
  chore: make the xcscheme cleanable
  refactor: implement program args string parse in terms of string-utils
  refactor: clean up the implementation of container parsing by using traits
  feat: add handling for map<string, V>
  bugfix: don't allow mixing of rest() and action() docs: add doxygen comments to various functions
  feat: add initializer_list based construction for default arguments
  cleanup: use name importing
  docs: add example demonstrating new features in a partial implementation of a git command spec
  feat: pass through rest() to an argument handler.
  feat: specify options/flags that are only one letter.
  feat: add binding for deferred default values
Sam Jaffe 3 gadi atpakaļ
vecāks
revīzija
378d34581e

+ 3 - 0
.gitmodules

@@ -1,3 +1,6 @@
 [submodule "external/scoped_buffer_capture"]
 	path = external/scoped_buffer_capture
 	url = ssh://git@gogs.sjaffe.name:3000/sjjaffe/cpp-scoped-buffer-capture.git
+[submodule "external/string-utils"]
+	path = external/string-utils
+	url = ssh://git@gogs.sjaffe.name:3000/sjjaffe/cpp-string-utils.git

+ 53 - 0
README.md

@@ -19,6 +19,59 @@ Abbreviated options support key-value concatenation, such as how you can do `-I/
 
 #### Snippets
 
+A partial git command arguments:
+```c++
+struct Push : program::Arguments<Push> {
+  using Arguments::Arguments;
+  bool atomic = flag("atomic");
+  bool dry_run = flag("dry-run", 'n') = false;
+  bool no_verify = flag("no-verify", "skip push hooks") = false;
+  bool quiet = flag("quiet", 'q') = false;
+  bool verbose = flag("verbose", 'v') = false;
+  // ...more options
+  // TODO: Fetch Upstream Implicitly
+  std::string repository = argument(0, "repository") = "";
+  // TODO: Implement
+  std::vector<std::string> refspec = rest("refspec");
+};
+
+struct Pull : program::Arguments<Pull> {
+  using Arguments::Arguments;
+  bool quiet = flag("quiet", 'q') = false;
+  bool verbose = flag("verbose", 'v') = false;
+  bool commit = flag("commit", "immediately commit the merged result") = true;
+  bool edit = flag("edit", "edit the commit message before merging") = true;
+  bool fastforward = flag("ff", "resolve the merge with a fast-forward if possible") = true;
+  bool only_ff = flag("ff-only", "fail if fast-forwarding will not succeed");
+  // ...more options
+  // TODO: Fetch Upstream Implicitly
+  std::string repository = argument(0, "repository") = "";
+  // TODO: Implement
+  std::vector<std::string> refspec = arguments("refspec");
+};
+
+struct Commit : program::Arguments<Commit> {
+  using Arguments::Arguments;
+};
+
+struct Git : program::Arguments<Git> {
+public:
+  using Arguments::Arguments;
+  std::string pwd = option('C');
+  // TODO: Generator?
+  std::string git_dir = option("git-dir") = PROGRAM_DEFER(locate_git_dir(pwd));
+  std::map<std::string, std::string> config_env = option("config-env");
+  // ...more options
+private:
+  friend int main(int, char const * const*);
+  Commit commit = action("commit");
+  Push push = action("push");
+  Pull pull = action("pull");
+};
+
+TYPED_MAIN(Git)
+```
+
 Singular option storage cannot be repeated
 ```c++
 std::string directory = option("logdir");

+ 1 - 0
external/string-utils

@@ -0,0 +1 @@
+Subproject commit 32b877b3ab7aedd6178ea3a8a206555cba9b6c9f

+ 94 - 8
include/program_args/arguments.h

@@ -7,6 +7,17 @@
 
 namespace program {
 
+/**
+ * @brief A string-literal object that requires input arguments to be two or more characters
+ */
+struct LongArg {
+  template <size_t N> constexpr LongArg(char const (&str)[N]) : str(str) {
+    static_assert(N > 1, "cannot create longargs with 0 or 1 character");
+  }
+  operator std::string const &() const { return str; }
+  std::string str;
+};
+
 template <typename Impl> class Arguments {
 private:
   using option_id = std::string;
@@ -25,29 +36,94 @@ public:
   }
 
 protected:
-  auto action(std::string const &name, std::string const &description = "");
-  auto argument(size_t index, std::string const & name,
+  /**
+   * @brief Define an "action"/"subcommand"/"verb" to be executed. An example of this pattern
+   * would be how git visualizes sub-commands in its interface, even though in implementation
+   * it appears different.
+   * @invariant Will fail to compile if bound to something that isn't a subtype of Arguments
+   * @param name A name of the action for usage messages
+   * @param description An optional description object
+   * @return A binding object that can implicitly cast to a subtype of Arguments
+   *
+   * @throws ArgumentMixingError if both argument() and action() have been invoked in this
+   */
+  auto action(LongArg name, std::string const &description = "");
+  
+  /**
+   * @brief Define an argument to be passed in to this program
+   *
+   * @param index The 0-based index of this argument in the program args array.
+   * @param name A name of the argument for usage messages
+   * @param description An optional description object
+   * @return A binding object that will write its data in to an object of arity 1
+   *
+   * @throws ArgumentMixingError if both argument() and action() have been invoked in this
+   * @throws ArgumentStructureError if this argument is required, but a prior argument was optional
+   * @throws IllegalPositionError if index is already in use in this
+   * @throws IllegalPositionError if no argument is available at index
+   */
+  auto argument(size_t index, LongArg name,
                 std::string const & description = "");
-  auto flag(std::string const & name, std::string const & description = "");
-  auto flag(std::string const & name, char abbrev,
-            std::string const & description = "");
-  auto option(std::string const & name, std::string const & description = "");
-  auto option(std::string const & name, char abbrev,
+  /**
+   * @brief Return all unbound arguments
+   * @param name An optional name to describe these arguments
+   * @param description An optional description object
+   * @returns All unbound arguments in {@sa Arguments::arguments}
+
+   * @throws ArgumentMixingError if both argument() and action() have been invoked in this
+   * @throws ArgumentStructureError if rest is called with multiple names
+   */
+  std::vector<std::string> rest(LongArg name = "args",
+                                std::string const & description = "");
+  
+  /**
+   * @brief Define a flag (option with an arity of zero).
+   * If a flag binds to a boolean and has a name, then t will be invertable with "--no-" prefix.
+   * If a flag binds to an integer, then it increments the value with each repetition.
+   * @invariant One or both of name and abbrev must be provided
+   * @param name The name of the flag, to be parsed from the commandline with a "--" prefix
+   * @param abbrev A single-character version of a flag, to be parsed from the commandline
+   * with a '-' prefix
+   * @param description An optional description object
+   */
+  auto flag(LongArg name, char abbrev, std::string const & description = "");
+  auto flag(LongArg name, std::string const & description = "");
+  auto flag(char abbrev, std::string const & description = "");
+  
+  /**
+   * @brief Define an option. If an option is repeated, then it can be used to fill out multiple
+   * entries in a map or vector.
+   * @invariant One or both of name and abbrev must be provided
+   * @param name The name of the option, to be parsed from the commandline with a "--" prefix
+   * @param abbrev A single-character version of an option, to be parsed from the commandline
+   * with a '-' prefix
+   * @param description An optional description object
+   */
+  auto option(LongArg name, char abbrev,
               std::string const & description = "");
+  auto option(LongArg name, std::string const & description = "");
+  auto option(char abbrev, std::string const & description = "");
 
 private:
   void usage() const;
   void add_options(std::string const & name, char abbrev,
                    std::string const & description,
                    std::vector<std::string> aliases = {});
+  
+  bool has_actions() const { return !action_descriptions.empty(); }
+  bool has_arguments() const { return !(rest_name.empty() && argument_names.empty()); }
+  
   option_id id(std::string const & arg) const;
   option_id id(char arg) const { return id({'-', arg}); }
+  
   bool is_option(std::string const & arg) const;
   bool is_option(char arg) const { return is_option({'-', arg}); }
+  
   bool is_flag(std::string const & arg) const;
   bool is_flag(char arg) const { return is_flag({'-', arg}); }
 
 private:
+  // A helper for what limited introspection is required for handling actions in test
   friend struct ArgumentTestHelper;
   friend int main(int, char const * const *);
   
@@ -56,6 +132,7 @@ private:
   using make_hook_t = std::function<main_callback_t(size_t, char const * const *)>;
   // Metadata variables
   bool primed_{false};
+  std::string rest_name;
   std::map<option_id, std::string> argument_descriptions;
   std::map<size_t, option_id> argument_names;
   std::map<option_id, std::string> option_descriptions;
@@ -116,6 +193,7 @@ template <typename Impl> struct Arguments<Impl>::Option {
   char abbrev;
   std::string description;
 
+  template <typename T> auto operator=(std::initializer_list<T> value);
   template <typename T> auto operator=(T && value);
   template <typename T> operator T() const;
   bool primed() const;
@@ -127,7 +205,14 @@ template <typename B, typename V>
 struct Arguments<Impl>::WithDefault {
   B impl;
   V default_value;
-  template <typename T> operator T() const { return impl ?: T(default_value); }
+  template <typename T> operator T() const {
+    if (impl) { return impl; }
+    if constexpr (std::is_invocable_r<T, V>{}) {
+      return T(default_value());
+    } else {
+      return T{default_value};
+    }
+  }
 };
 
 }
@@ -148,3 +233,4 @@ int typed_main(Args const &, Action const &);
   }                                                                            \
   int typed_main(tname const & args)
 
+#define PROGRAM_DEFER(...) [this]() { return __VA_ARGS__; }

+ 55 - 13
include/program_args/arguments_impl.hpp

@@ -36,11 +36,15 @@ template <typename Impl> Arguments<Impl>::Action::operator bool() const {
 
 template <typename Impl> bool Arguments<Impl>::Action::primed() const {
   if (self->primed_) { return true; }
-  if (!self->argument_names.empty()) { throw ArgumentMixingError(); }
+  if (self->has_arguments()) { throw ArgumentMixingError(); }
   self->action_descriptions.emplace(name, description);
   return false;
 }
 
+}
+
+namespace program {
+
 template <typename Impl>
 template <typename T>
 Arguments<Impl>::Argument::operator T() const {
@@ -69,7 +73,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 (self->has_actions()) { throw ArgumentMixingError(); }
   if (is_optional) {
     self->optional_from = std::min(self->optional_from, index);
   } else if (self->optional_from < index) {
@@ -107,7 +111,11 @@ template <typename Impl> auto Arguments<Impl>::Flag::operator=(bool && value) {
 
 template <typename Impl> bool Arguments<Impl>::Flag::primed(bool inv) const {
   if (self->primed_) { return self->flags.count(name); }
-  self->add_options(name, abbrev, description, std::vector(inv, "no-" + name));
+  std::vector<std::string> aliases;
+  if (name.size() > 1 && inv) {
+    aliases.push_back("no-" + name);
+  }
+  self->add_options(name, abbrev, description, aliases);
   self->flag_names.emplace(name);
   return false;
 }
@@ -136,6 +144,17 @@ auto Arguments<Impl>::Option::operator=(T && value) {
   return WithDefault<Option, T>{*this, std::forward<T>(value)};
 }
 
+
+template <typename Impl>
+template <typename T>
+auto Arguments<Impl>::Option::operator=(std::initializer_list<T> value) {
+  if (description.size()) {
+    using ::program::to_string;
+    description += "\n    Default Value: " + to_string(value);
+  }
+  return WithDefault<Option, std::initializer_list<T>>{*this, value};
+}
+
 template <typename Impl> bool Arguments<Impl>::Option::primed() const {
   if (self->primed_) { return true; }
   self->add_options(name, abbrev, description);
@@ -205,6 +224,9 @@ template <typename Impl> void Arguments<Impl>::usage() const {
   for (auto & [index, name] : argument_names) {
     std::cout << " " << (index == optional_from ? "[" : "") << name;
   }
+  if (rest_name.size()) {
+    std::cout << " [" << rest_name << "...]";
+  }
   if (optional_from != no_optional_args) { std::cout << "]"; }
   std::cout << "\nArgument Arguments:\n";
   for (auto & [name, desc] : argument_descriptions) {
@@ -223,7 +245,7 @@ void Arguments<Impl>::add_options(std::string const & name, char abbrev,
   for (auto & str : aliases) {
     str = "--" + str;
   }
-  aliases.push_back("--" + name);
+  if (name.size() > 1) { aliases.push_back("--" + name); }
   if (abbrev) { aliases.push_back(std::string{'-', abbrev}); }
   for (auto & str : aliases) {
     if (!option_names.emplace(str, name).second) {
@@ -250,39 +272,59 @@ bool Arguments<Impl>::is_flag(std::string const & arg) const {
 }
 
 template <typename Impl>
-auto Arguments<Impl>::action(std::string const & name,
-                             std::string const & description) {
+auto Arguments<Impl>::action(LongArg name, std::string const & description) {
   return Action{this, name, description};
 }
 
 template <typename Impl>
-auto Arguments<Impl>::argument(size_t index, std::string const & name,
+auto Arguments<Impl>::argument(size_t index, LongArg name,
                                std::string const & description) {
   return Argument{this, index, false, name, description};
 }
 
 template <typename Impl>
-auto Arguments<Impl>::flag(std::string const & name,
-                           std::string const & description) {
+std::vector<std::string> Arguments<Impl>::rest(LongArg name,
+                                               std::string const & description) {
+  if (has_actions()) { throw ArgumentMixingError(); }
+  if (!rest_name.empty() && rest_name != name.str) {
+    throw ArgumentStructureError("duplicate rest() parameter", name);
+  }
+  rest_name = name;
+  argument_descriptions.emplace(name, description);
+  size_t const i = std::min(arguments.size(), argument_names.size());
+  return {arguments.begin() + i, arguments.end()};
+}
+
+template <typename Impl>
+auto Arguments<Impl>::flag(LongArg name, std::string const & description) {
   return Flag{this, name, 0, description, false};
 }
 
 template <typename Impl>
-auto Arguments<Impl>::flag(std::string const & name, char abbrev,
+auto Arguments<Impl>::flag(LongArg name, char abbrev,
                            std::string const & description) {
   return Flag{this, name, abbrev, description, false};
 }
 
 template <typename Impl>
-auto Arguments<Impl>::option(std::string const & name,
-                             std::string const & description) {
+auto Arguments<Impl>::flag(char abbrev, std::string const & description) {
+  return Flag{this, {abbrev}, abbrev, description, false};
+}
+
+template <typename Impl>
+auto Arguments<Impl>::option(LongArg name, std::string const & description) {
   return Option{this, name, 0, description};
 }
 
 template <typename Impl>
-auto Arguments<Impl>::option(std::string const & name, char abbrev,
+auto Arguments<Impl>::option(LongArg name, char abbrev,
                              std::string const & description) {
   return Option{this, name, abbrev, description};
 }
 
+template <typename Impl>
+auto Arguments<Impl>::option(char abbrev, std::string const & description) {
+  return Option{this, {abbrev}, abbrev, description};
+}
+
 }

+ 21 - 42
include/program_args/utilities.h

@@ -4,17 +4,12 @@
 #include <type_traits>
 #include <vector>
 
-namespace program::traits {
-template <typename T, typename = void>
-struct is_repeatable : std::false_type {};
+#include <string_utils/cast.h>
 
-template <typename F>
-struct is_repeatable<F, std::enable_if_t<!std::is_void_v<
-                            std::result_of_t<F(std::vector<std::string>)>>>>
-    : std::true_type {};
-
-template <typename T>
-constexpr bool const is_repeatable_v = is_repeatable<T>::value;
+namespace program::detail {
+template <typename, typename, typename = void> struct has_cast : std::false_type {};
+template <typename T, typename S>
+struct has_cast<T, S, std::void_t<decltype(string_utils::cast(S(), std::declval<T&>()))>> : std::true_type {};
 }
 
 namespace program {
@@ -39,7 +34,11 @@ template <typename T, typename = void> struct conversion_helper;
  */
 template <typename T>
 T convert(std::string const & name, std::string const & data) {
-  return conversion_helper<T>{}(data);
+  if (auto [rval, success] = string_utils::cast<T>(data); success) {
+    return rval;
+  } else {
+    throw ArgumentStructureError("unable to parse", name);
+  }
 }
 
 /**
@@ -54,40 +53,20 @@ T convert(std::string const & name, std::string const & data) {
  */
 template <typename T>
 T convert(std::string const & name, std::vector<std::string> const & data) {
-  conversion_helper<T> helper;
-  try {
-    if constexpr (traits::is_repeatable_v<decltype(helper)>) {
-      return helper(data);
-    } else if (data.size() == 1) {
-      return helper(data.front());
+  if constexpr (detail::has_cast<T, std::string>{}) {
+    if (data.size() != 1) {
+      throw ArgumentStructureError("repeated option not allowed", name);
+    } else {
+      return convert<T>(name, data.front());
     }
-  } catch (std::exception const & ex) {
-    throw ArgumentStructureError(ex.what(), name);
-  }
-  throw ArgumentStructureError("Repeated option not allowed", name);
-}
-
-template <typename T>
-struct conversion_helper<
-    T, std::enable_if_t<std::is_convertible_v<std::string, T>>> {
-  T operator()(std::string const & str) const { return T(str); }
-};
-
-template <> struct conversion_helper<int> {
-  int operator()(std::string const & str) const { return std::stoi(str); }
-};
-
-template <typename T>
-struct conversion_helper<std::vector<T>> : conversion_helper<T> {
-  using conversion_helper<T>::operator();
-  std::vector<T> operator()(std::vector<std::string> const & data) const {
-    std::vector<T> rval;
-    for (auto & str : data) {
-      rval.emplace_back((*this)(str));
+  } else {
+    if (auto [rval, success] = string_utils::cast<T>(data); success) {
+      return rval;
+    } else {
+      throw ArgumentStructureError("unable to parse", name);
     }
-    return rval;
   }
-};
+}
 
 using std::to_string;
 template <typename T> std::string to_string(T const &) { return "?"; }

+ 62 - 4
program_args.xcodeproj/project.pbxproj

@@ -30,6 +30,27 @@
 			remoteGlobalIDString = CDE4F78925CF309E009E4EC1;
 			remoteInfo = program_args;
 		};
+		CDCB1D8328567635001C406D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDCB1D7D28567635001C406D /* string-utils.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = CD266862252FF4B600B3E667;
+			remoteInfo = "string-utils";
+		};
+		CDCB1D8528567635001C406D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDCB1D7D28567635001C406D /* string-utils.xcodeproj */;
+			proxyType = 2;
+			remoteGlobalIDString = CD266886252FFAAE00B3E667;
+			remoteInfo = "string_utils-test";
+		};
+		CDCB1D892856793F001C406D /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDCB1D7D28567635001C406D /* string-utils.xcodeproj */;
+			proxyType = 1;
+			remoteGlobalIDString = CD266861252FF4B600B3E667;
+			remoteInfo = "string-utils";
+		};
 		CDE4F79A25CF316A009E4EC1 /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = CDE4F79325CF316A009E4EC1 /* GoogleMock.xcodeproj */;
@@ -73,6 +94,7 @@
 		CD8C5A9625D058470004A6D9 /* xcode_gtest_helper.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = xcode_gtest_helper.h; sourceTree = "<group>"; };
 		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>"; };
+		CDCB1D7D28567635001C406D /* string-utils.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = "string-utils.xcodeproj"; path = "external/string-utils/string-utils.xcodeproj"; 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; };
@@ -141,10 +163,20 @@
 			path = program_args;
 			sourceTree = "<group>";
 		};
+		CDCB1D7E28567635001C406D /* Products */ = {
+			isa = PBXGroup;
+			children = (
+				CDCB1D8428567635001C406D /* libstring-utils.a */,
+				CDCB1D8628567635001C406D /* string_utils-test.xctest */,
+			);
+			name = Products;
+			sourceTree = "<group>";
+		};
 		CDE4F78125CF309E009E4EC1 = {
 			isa = PBXGroup;
 			children = (
 				CD2F0C5325DC9AE000CB394A /* scoped_buffer_capture.xcodeproj */,
+				CDCB1D7D28567635001C406D /* string-utils.xcodeproj */,
 				CDD334A025D200AB008540EE /* README.md */,
 				CDE4F79325CF316A009E4EC1 /* GoogleMock.xcodeproj */,
 				CD8C5A8E25D057C00004A6D9 /* include */,
@@ -213,6 +245,7 @@
 			buildRules = (
 			);
 			dependencies = (
+				CDCB1D8A2856793F001C406D /* PBXTargetDependency */,
 				CD8C5A7C25D0577E0004A6D9 /* PBXTargetDependency */,
 			);
 			name = "program_args-test";
@@ -273,6 +306,10 @@
 					ProductGroup = CD2F0C5425DC9AE000CB394A /* Products */;
 					ProjectRef = CD2F0C5325DC9AE000CB394A /* scoped_buffer_capture.xcodeproj */;
 				},
+				{
+					ProductGroup = CDCB1D7E28567635001C406D /* Products */;
+					ProjectRef = CDCB1D7D28567635001C406D /* string-utils.xcodeproj */;
+				},
 			);
 			projectRoot = "";
 			targets = (
@@ -290,6 +327,20 @@
 			remoteRef = CD2F0C5B25DC9AE000CB394A /* PBXContainerItemProxy */;
 			sourceTree = BUILT_PRODUCTS_DIR;
 		};
+		CDCB1D8428567635001C406D /* libstring-utils.a */ = {
+			isa = PBXReferenceProxy;
+			fileType = archive.ar;
+			path = "libstring-utils.a";
+			remoteRef = CDCB1D8328567635001C406D /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
+		CDCB1D8628567635001C406D /* string_utils-test.xctest */ = {
+			isa = PBXReferenceProxy;
+			fileType = wrapper.cfbundle;
+			path = "string_utils-test.xctest";
+			remoteRef = CDCB1D8528567635001C406D /* PBXContainerItemProxy */;
+			sourceTree = BUILT_PRODUCTS_DIR;
+		};
 		CDE4F79B25CF316A009E4EC1 /* GoogleMock.framework */ = {
 			isa = PBXReferenceProxy;
 			fileType = wrapper.framework;
@@ -358,6 +409,11 @@
 			target = CDE4F78925CF309E009E4EC1 /* program_args */;
 			targetProxy = CD8C5A7B25D0577E0004A6D9 /* PBXContainerItemProxy */;
 		};
+		CDCB1D8A2856793F001C406D /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			name = "string-utils";
+			targetProxy = CDCB1D892856793F001C406D /* PBXContainerItemProxy */;
+		};
 /* End PBXTargetDependency section */
 
 /* Begin XCBuildConfiguration section */
@@ -449,10 +505,11 @@
 				MTL_FAST_MATH = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
-				USER_HEADER_SEARCH_PATHS = (
-					include/,
+				SYSTEM_HEADER_SEARCH_PATHS = (
+					"external/string-utils/include/",
 					external/scoped_buffer_capture/include/,
 				);
+				USER_HEADER_SEARCH_PATHS = include/;
 			};
 			name = Debug;
 		};
@@ -505,10 +562,11 @@
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
 				SDKROOT = macosx;
-				USER_HEADER_SEARCH_PATHS = (
-					include/,
+				SYSTEM_HEADER_SEARCH_PATHS = (
+					"external/string-utils/include/",
 					external/scoped_buffer_capture/include/,
 				);
+				USER_HEADER_SEARCH_PATHS = include/;
 			};
 			name = Release;
 		};

+ 0 - 52
program_args.xcodeproj/xcshareddata/xcschemes/program_args-test.xcscheme

@@ -1,52 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<Scheme
-   LastUpgradeVersion = "1240"
-   version = "1.3">
-   <BuildAction
-      parallelizeBuildables = "YES"
-      buildImplicitDependencies = "YES">
-   </BuildAction>
-   <TestAction
-      buildConfiguration = "Debug"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      shouldUseLaunchSchemeArgsEnv = "YES">
-      <Testables>
-         <TestableReference
-            skipped = "NO">
-            <BuildableReference
-               BuildableIdentifier = "primary"
-               BlueprintIdentifier = "CD8C5A7425D0577E0004A6D9"
-               BuildableName = "program_args-test.xctest"
-               BlueprintName = "program_args-test"
-               ReferencedContainer = "container:program_args.xcodeproj">
-            </BuildableReference>
-         </TestableReference>
-      </Testables>
-   </TestAction>
-   <LaunchAction
-      buildConfiguration = "Debug"
-      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
-      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
-      launchStyle = "0"
-      useCustomWorkingDirectory = "NO"
-      ignoresPersistentStateOnLaunch = "NO"
-      debugDocumentVersioning = "YES"
-      debugServiceExtension = "internal"
-      allowLocationSimulation = "YES">
-   </LaunchAction>
-   <ProfileAction
-      buildConfiguration = "Release"
-      shouldUseLaunchSchemeArgsEnv = "YES"
-      savedToolIdentifier = ""
-      useCustomWorkingDirectory = "NO"
-      debugDocumentVersioning = "YES">
-   </ProfileAction>
-   <AnalyzeAction
-      buildConfiguration = "Debug">
-   </AnalyzeAction>
-   <ArchiveAction
-      buildConfiguration = "Release"
-      revealArchiveInOrganizer = "YES">
-   </ArchiveAction>
-</Scheme>

+ 1 - 1
program_args.xcodeproj/xcshareddata/xcschemes/program_args.xcscheme

@@ -1,6 +1,6 @@
 <?xml version="1.0" encoding="UTF-8"?>
 <Scheme
-   LastUpgradeVersion = "1240"
+   LastUpgradeVersion = "1340"
    version = "1.3">
    <BuildAction
       parallelizeBuildables = "YES"

+ 8 - 8
test/action_test.cpp

@@ -33,32 +33,32 @@ int typed_main(Args const &, Action const &) {
 }
 
 struct Checkout : program::Arguments<Checkout> {
-  using program::Arguments<Checkout>::Arguments;
+  using Arguments::Arguments;
   std::string commitish = argument(0, "commit-ish");
 };
 
 struct Commit : program::Arguments<Commit> {
-  using program::Arguments<Commit>::Arguments;
+  using Arguments::Arguments;
   std::string message = option("message", 'm');
 };
 
 struct Bad1 : program::Arguments<Bad1> {
-  using program::Arguments<Bad1>::Arguments;
-  
+  using Arguments::Arguments;
+
   Commit commit = action("commit");
   std::string arg0 = argument(0, "arg0");
 };
 
 struct Bad2 : program::Arguments<Bad2> {
-  using program::Arguments<Bad2>::Arguments;
-  
+  using Arguments::Arguments;
+
   std::string arg0 = argument(0, "arg0");
   Commit commit = action("commit");
 };
 
 struct Git : program::Arguments<Git> {
-  using program::Arguments<Git>::Arguments;
-  
+  using Arguments::Arguments;
+
   bool verbose = flag("verbose", 'v');
   
   Commit commit = action("commit");

+ 25 - 5
test/argument_test.cpp

@@ -11,13 +11,14 @@
 
 using testing::ElementsAre;
 using testing::Eq;
+using testing::IsEmpty;
 
 template <typename T, size_t N> static T parse(char const * const (&argv)[N]) {
   return T(N, argv);
 }
 
 struct ArgumentTest : program::Arguments<ArgumentTest> {
-  using program::Arguments<ArgumentTest>::Arguments;
+  using Arguments::Arguments;
 
   std::string arg0 = argument(0, "arg0");
 };
@@ -38,7 +39,7 @@ TEST(ArgumentTest, HasNoIssueWithOverflowArg) {
 }
 
 struct OptionalArgumentTest : program::Arguments<OptionalArgumentTest> {
-  using program::Arguments<OptionalArgumentTest>::Arguments;
+  using Arguments::Arguments;
 
   std::string arg0 = argument(0, "arg0");
   std::string arg1 = argument(1, "arg1") = "second";
@@ -61,7 +62,7 @@ TEST(OptionalArgumentTest, DoesNotOverflowSecondArg) {
 }
 
 struct DuplicateArgumentTest : program::Arguments<DuplicateArgumentTest> {
-  using program::Arguments<DuplicateArgumentTest>::Arguments;
+  using Arguments::Arguments;
 
   std::string arg0 = argument(0, "arg0");
   std::string arg1 = argument(0, "arg1");
@@ -72,7 +73,7 @@ TEST(DuplicateArgumentTest, ThrowsOnConstruction) {
 }
 
 struct SkippedArgumentTest : program::Arguments<SkippedArgumentTest> {
-  using program::Arguments<SkippedArgumentTest>::Arguments;
+  using Arguments::Arguments;
 
   std::string arg0 = argument(1, "arg0");
 };
@@ -84,7 +85,7 @@ TEST(SkippedArgumentTest, ThrowsOnParsing) {
 }
 
 struct OOOArgumentTest : program::Arguments<OOOArgumentTest> {
-  using program::Arguments<OOOArgumentTest>::Arguments;
+  using Arguments::Arguments;
 
   std::string arg0 = argument(0, "arg0") = "";
   std::string arg1 = argument(1, "arg1");
@@ -93,3 +94,22 @@ struct OOOArgumentTest : program::Arguments<OOOArgumentTest> {
 TEST(OOOArgumentTest, ThrowsOnConstruction) {
   EXPECT_THROW(OOOArgumentTest(), program::ArgumentStructureError);
 }
+
+struct RestArgsTest : program::Arguments<RestArgsTest> {
+  using Arguments::Arguments;
+  
+  std::string arg0 = argument(0, "arg0") = "";
+  std::vector<std::string> args = rest();
+};
+
+TEST(RestArgsTest, SetsOptionalArgFirst) {
+  auto const options = parse<RestArgsTest>({"", "A"});
+  EXPECT_THAT(options.arg0, "A");
+  EXPECT_THAT(options.args, IsEmpty());
+}
+
+TEST(RestArgsTest, ForwardsOverflowIntoRest) {
+  auto const options = parse<RestArgsTest>({"", "A", "B", "C"});
+  EXPECT_THAT(options.arg0, "A");
+  EXPECT_THAT(options.args, ElementsAre("B", "C"));
+}

+ 17 - 3
test/flag_test.cpp

@@ -16,7 +16,7 @@ template <typename T, size_t N> static T parse(char const * const (&argv)[N]) {
 }
 
 struct FlagTest : program::Arguments<FlagTest> {
-  using program::Arguments<FlagTest>::Arguments;
+  using Arguments::Arguments;
 
   bool dry_run = flag("dry-run");
   int verbosity = flag("verbose");
@@ -57,7 +57,7 @@ TEST(FlagTest, IntegerFlagCanBeRepeated) {
 }
 
 struct FlagWithDefaultTest : program::Arguments<FlagWithDefaultTest> {
-  using program::Arguments<FlagWithDefaultTest>::Arguments;
+  using Arguments::Arguments;
 
   bool dry_run = flag("dry-run") = true;
 };
@@ -73,7 +73,7 @@ TEST(FlagWithDefaultTest, BooleanFlagCanBeInverted) {
 }
 
 struct FlagWithAbbrevTest : program::Arguments<FlagWithAbbrevTest> {
-  using program::Arguments<FlagWithAbbrevTest>::Arguments;
+  using Arguments::Arguments;
 
   bool dry_run = flag("dry-run", 'k');
   int verbosity = flag("verbose", 'v');
@@ -109,3 +109,17 @@ TEST(FlagWithAbbrevTest, CannotNumberRepeatAndConcatAbbrevFlags) {
   EXPECT_THROW(parse<FlagWithAbbrevTest>({"", "-v3k"}),
                program::NotAnArgumentError);
 }
+
+struct ShortFlagTest : program::Arguments<ShortFlagTest> {
+  using Arguments::Arguments;
+  int arg = flag('n');
+};
+
+TEST(ShortFlagTest, DoesNotAllowDoubleDash) {
+  EXPECT_THROW(parse<ShortFlagTest>({"", "--n"}), program::NotAnArgumentError);
+}
+
+TEST(ShortFlagTest, CanConstructWithOnlyAbbrev) {
+  auto const options = parse<ShortFlagTest>({"", "-n"});
+  EXPECT_THAT(options.arg, 1);
+}

+ 47 - 7
test/options_test.cpp

@@ -7,6 +7,8 @@
 
 #include "program_args/arguments.h"
 
+#include <filesystem>
+
 #include "xcode_gtest_helper.h"
 
 using testing::ElementsAre;
@@ -18,7 +20,7 @@ template <typename T, size_t N> static T parse(char const * const (&argv)[N]) {
 }
 
 struct LongOptionTest : program::Arguments<LongOptionTest> {
-  using program::Arguments<LongOptionTest>::Arguments;
+  using Arguments::Arguments;
 
   std::string endpoint = option("endpoint");
   int port = option("port");
@@ -62,7 +64,7 @@ TEST(LongOptionTest, DoesNotImplicitlyShortenArgs) {
 
 struct LongOptionWithDefaultTest
     : program::Arguments<LongOptionWithDefaultTest> {
-  using program::Arguments<LongOptionWithDefaultTest>::Arguments;
+  using Arguments::Arguments;
 
   std::string endpoint = option("endpoint") = "/default";
   int port = option("port") = 8080;
@@ -86,7 +88,7 @@ TEST(LongOptionWithDefaultTest, CanProvideNumericArgument) {
 }
 
 struct LongOptionWithAbbrevTest : program::Arguments<LongOptionWithAbbrevTest> {
-  using program::Arguments<LongOptionWithAbbrevTest>::Arguments;
+  using Arguments::Arguments;
 
   std::string endpoint = option("endpoint", 'e');
   int port = option("port", 'p');
@@ -126,7 +128,7 @@ TEST(LongOptionWithAbbrevTest, CanPutAbbrevArgAndValueInSameToken) {
 }
 
 struct LongOptionRepeatTest : program::Arguments<LongOptionRepeatTest> {
-  using program::Arguments<LongOptionRepeatTest>::Arguments;
+  using Arguments::Arguments;
 
   std::vector<int> port = option("port");
 };
@@ -149,14 +151,16 @@ TEST(LongOptionRepeatTest, RepeatingArgumentsAppends) {
 
 struct LongOptionRepeatWithDefaultTest
     : program::Arguments<LongOptionRepeatWithDefaultTest> {
-  using program::Arguments<LongOptionRepeatWithDefaultTest>::Arguments;
-
-  std::vector<int> port = option("port") = std::vector{8080};
+  using Arguments::Arguments;
+  
+  std::vector<int> port = option("port") = 8080;
+  std::vector<int> default_ports = option("default_ports") = {80, 443};
 };
 
 TEST(LongOptionRepeatWithDefaultTest, DefaultIsProvided) {
   auto const options = parse<LongOptionRepeatWithDefaultTest>({""});
   EXPECT_THAT(options.port, ElementsAre(8080));
+  EXPECT_THAT(options.default_ports, ElementsAre(80, 443));
 }
 
 TEST(LongOptionRepeatWithDefaultTest, ArgumentOverwritesDefault) {
@@ -164,3 +168,39 @@ TEST(LongOptionRepeatWithDefaultTest, ArgumentOverwritesDefault) {
       parse<LongOptionRepeatWithDefaultTest>({"", "--port", "443"});
   EXPECT_THAT(options.port, ElementsAre(443));
 }
+
+namespace fs = std::filesystem;
+struct DeferOptionTest : program::Arguments<DeferOptionTest> {
+  using Arguments::Arguments;
+  fs::path path = option("path") = ".";
+  fs::path log = option("log") = PROGRAM_DEFER(path / "test.log");
+};
+
+TEST(DeferOptionTest, DefaultResolves) {
+  auto const options = parse<DeferOptionTest>({""});
+  EXPECT_THAT(options.log.string(), "./test.log");
+}
+
+TEST(DeferOptionTest, CapturesUpdateToDependent) {
+  auto const options = parse<DeferOptionTest>({"", "--path", "/var/log"});
+  EXPECT_THAT(options.log.string(), "/var/log/test.log");
+}
+
+TEST(DeferOptionTest, SettingActualArgWillOverwrite) {
+  auto const options = parse<DeferOptionTest>({"", "--path", "/var/log", "--log", "test.log"});
+  EXPECT_THAT(options.log.string(), "test.log");
+}
+
+struct ShortOptionTest : program::Arguments<ShortOptionTest> {
+  using Arguments::Arguments;
+  int arg = option('n');
+};
+
+TEST(ShortOptionTest, DoesNotAllowDoubleDash) {
+  EXPECT_THROW(parse<ShortOptionTest>({"", "--n", "5"}), program::NotAnArgumentError);
+}
+
+TEST(ShortOptionTest, CanConstructWithOnlyAbbrev) {
+  auto const options = parse<ShortOptionTest>({"", "-n", "5"});
+  EXPECT_THAT(options.arg, 5);
+}

+ 3 - 3
test/usage_test.cpp

@@ -25,7 +25,7 @@ template <typename T> auto usage() {
 }
 
 struct ArgNamesTest : program::Arguments<ArgNamesTest> {
-  using program::Arguments<ArgNamesTest>::Arguments;
+  using Arguments::Arguments;
   bool dry_run = flag("dry-run");
   int level = option("level");
   int port = option("port", 'p');
@@ -47,7 +47,7 @@ TEST(ArgNamesTest, PrintsOutBothSingleNameWithoutAbbrev) {
 }
 
 struct ArgDescTest : program::Arguments<ArgDescTest> {
-  using program::Arguments<ArgDescTest>::Arguments;
+  using Arguments::Arguments;
   int port = option("port", "The host port to listen on for web traffic");
 };
 
@@ -57,7 +57,7 @@ TEST(ArgDescTest, PrintsOutBothArgsWhenRelevant) {
 }
 
 struct ArgDefValTest : program::Arguments<ArgDefValTest> {
-  using program::Arguments<ArgDefValTest>::Arguments;
+  using Arguments::Arguments;
   int level = option("level") = 5;
   int port = option("port", "The host port to listen on for web traffic") =
       8080;