Sfoglia il codice sorgente

Merge branch 'test' into main

* test:
  Fix exception thrower for parsing
  Test repeatable arguments.
  Cover the rest of the usecases for arguments.
  Add a readme and a way to get argument overflow.
  Adding more tests.
  Fix bug in flag checking for --no-*.
  Add flag tests.
  Fix two bugs in arguments.
  Write the first batch of tests.
  Perform some cleanup, add some test skeletons.
Sam Jaffe 4 anni fa
parent
commit
a38407f988

+ 56 - 0
README.md

@@ -0,0 +1,56 @@
+#  Fluent Argument Parser
+A tool for the processing of command-line arguments to a C++ program using a fluent interface. No need for writing a constructor or having to create a long list of bindings in main.
+
+## Installation
+
+## Usage
+
+
+### Arguments
+Positional arguments are provided with their zero-based indices. Arguments can be declared as optional, but you cannot include required arguments with a higher index than any optional positional arg.
+
+Arguments always have an arity of 1, and so cannot store information into a container. Arguments past the end requested will be accessible in a public member function called `args()`.
+
+### Options
+Options are provided as a long-form string name, and input into the command line in the form `--name value`. Options can map to either singular values (e.g. std::string, int) or containers (e.g. std::vector<T>). Options mapped to containers are repeatable, so you can provide the flag more than one time. Those mapped to singular values will generate an error if repeated.  
+In either case, options with an arity greater than one are not allowed.
+
+Abbreviated options support key-value concatenation, such as how you can do `-I/path/to/my/include/dir` in gcc/clang.
+
+#### Snippets
+
+Singular option storage cannot be repeated
+```c++
+std::string directory = option("logdir");
+
+$ ./a.out --logdir /var/log --logdir /usr/local/var/log
+Error in program argument handling: Repeated option not allowed for argument logdir
+```
+
+Abbreviations are not automatically generated
+```c++
+std::string directory = option("logdir");
+
+$ ./a.out -l /var/log
+Error in program argument handling: Unknown argument provided: -l
+```
+
+Pairs/Tuples don't get to use increased arity
+```c++
+std::pair<int, int> bounds = options("bounds");
+std::cout << args() << std::endl;
+
+$ ./a.out --bounds 1920 1080
+[ "1080" ]
+```
+
+### Flags
+Flags are a sub-class of options who do not have a follow-on value, but instead change the state of the given object through being called.
+
+Flags are supported for the types of `bool` and `int` only.  
+With boolean flags, in addition to the `--name` argument created and the abbreviated form, a `--no-name` argument will be registered that sets the option value to false (in case the default is set to true).  
+Integer flags cannot have default values, and do not have inverted forms. Instead, it is possible to repeat an integer flag endlessly, incrementing it by one with each appearance.
+
+Abbreviated flags have the additional feature that they can be concatenated, and integer flags can be number-repeated.  
+For example, suppose that both `-v` and `-k` are valid flags, representing `int verbosity` and `bool dry_run` respectively. Then the following are all valid input tokens: `-vk`, `-vv`, `-vvvk`, and `-v5`. The argument `-v5k` will still generate a parse error, however.
+

+ 6 - 1
include/program_args/arguments.h

@@ -19,6 +19,10 @@ public:
   Arguments() = default;
   Arguments(int argc, char const * const * const argv);
 
+  std::vector<std::string> args() const {
+    return {arguments.begin() + argument_names.size(), arguments.end()};
+  }
+
 protected:
   auto argument(size_t index, std::string const & name,
                 std::string const & description = "");
@@ -52,7 +56,8 @@ private:
 
   // Data/Output variables
   std::string program;
-  size_t optional_from{std::numeric_limits<size_t>::max()};
+  constexpr static size_t const no_optional_args{~0ul};
+  size_t optional_from{no_optional_args};
   std::vector<std::string> arguments;
   std::map<std::string, std::vector<std::string>> options;
   std::map<std::string, int> flags;

+ 7 - 5
include/program_args/arguments_impl.hpp

@@ -1,5 +1,6 @@
 #pragma once
 
+#include <iostream>
 #include <utility>
 
 #include "arguments.h"
@@ -14,7 +15,7 @@ Arguments<Impl>::Argument::operator T() const {
   if (!primed()) {
     return T();
   } else if (self->arguments.size() > index) {
-    return convert<T>(self->arguments.at(index));
+    return convert<T>(name, self->arguments.at(index));
   }
   throw IllegalPositionError("No argument provided", index);
 }
@@ -77,7 +78,7 @@ namespace program {
 template <typename Impl>
 template <typename T>
 Arguments<Impl>::Option::operator T() const {
-  return (*this) ? convert<T>(self->options.at(name)) : T();
+  return (*this) ? convert<T>(name, self->options.at(name)) : T();
 }
 
 template <typename Impl> Arguments<Impl>::Option::operator bool() const {
@@ -104,7 +105,8 @@ template <typename Impl>
 Arguments<Impl>::Arguments(int argc, char const * const * const argv) {
   Impl generator;
   *this = static_cast<Arguments const &>(generator);
-  if (argument_names.rbegin()->first != argument_names.size() - 1) {
+  if (argument_names.size() &&
+      argument_names.rbegin()->first != argument_names.size() - 1) {
     throw IllegalPositionError("Higher positional than number recorded",
                                argument_names.rbegin()->first);
   }
@@ -123,7 +125,7 @@ Arguments<Impl>::Arguments(int argc, char const * const * const argv) {
     } else if (arg[0] != '-') {
       arguments.emplace_back(arg);
     } else if (is_flag(arg)) {
-      if (arg.substr(0, 4) == "--no-") {
+      if (arg.substr(0, 5) == "--no-") {
         flags[id(arg)] = 0;
       } else {
         ++flags[id(arg)];
@@ -150,7 +152,7 @@ template <typename Impl> void Arguments<Impl>::usage() const {
   for (auto & [index, name] : argument_names) {
     std::cout << " " << (index == optional_from ? "[" : "") << name;
   }
-  if (optional_from != std::numeric_limits<size_t>::max()) { std::cout << "]"; }
+  if (optional_from != no_optional_args) { std::cout << "]"; }
   std::cout << "\nArgument Arguments:\n";
   for (auto & [name, desc] : argument_descriptions) {
     std::cout << "  " << name << ": " << desc << "\n";

+ 48 - 7
include/program_args/utilities.h

@@ -4,6 +4,19 @@
 #include <type_traits>
 #include <vector>
 
+namespace program::traits {
+template <typename T, typename = void>
+struct is_repeatable : std::false_type {};
+
+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 {
 inline std::string join(std::string const & tok,
                         std::vector<std::string> const & data) {
@@ -17,28 +30,56 @@ inline std::string join(std::string const & tok,
 
 template <typename T, typename = void> struct conversion_helper;
 
-template <typename T, typename D> T convert(D const & data) {
+/**
+ * \brief Conversion method for positional arguments. Positional arguments are
+ * always represented with a singular string.
+ * \param name The name of the argument being parsed, for logging purposes
+ * \param data A string containing the value to be processed.
+ * \return An object of the given type
+ */
+template <typename T>
+T convert(std::string const & name, std::string const & data) {
   return conversion_helper<T>{}(data);
 }
 
+/**
+ * \brief Conversion method for command-line options. Because some options are
+ * repeatable, we need to pass in a vector of objects that might be used.
+ * \param name The name of the option being parsed, for logging purposes
+ * \param data A vector of arguments assigned to this option.
+ * \invariant data.size() > 0
+ * \return An object of the given type
+ * \throws ArgumentStructureError if the argument has been repeated but is
+ * not a repeatable type.
+ */
+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());
+    }
+  } 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); }
-  T operator()(std::vector<std::string> const & data) const {
-    return operator()(data.front());
-  }
 };
 
 template <> struct conversion_helper<int> {
   int operator()(std::string const & str) const { return std::stoi(str); }
-  int operator()(std::vector<std::string> const & data) const {
-    return operator()(data.front());
-  }
 };
 
 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) {

+ 22 - 0
program_args-test/Info.plist

@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+	<key>CFBundleDevelopmentRegion</key>
+	<string>$(DEVELOPMENT_LANGUAGE)</string>
+	<key>CFBundleExecutable</key>
+	<string>$(EXECUTABLE_NAME)</string>
+	<key>CFBundleIdentifier</key>
+	<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
+	<key>CFBundleInfoDictionaryVersion</key>
+	<string>6.0</string>
+	<key>CFBundleName</key>
+	<string>$(PRODUCT_NAME)</string>
+	<key>CFBundlePackageType</key>
+	<string>$(PRODUCT_BUNDLE_PACKAGE_TYPE)</string>
+	<key>CFBundleShortVersionString</key>
+	<string>1.0</string>
+	<key>CFBundleVersion</key>
+	<string>1</string>
+</dict>
+</plist>

+ 171 - 2
program_args.xcodeproj/project.pbxproj

@@ -6,7 +6,21 @@
 	objectVersion = 50;
 	objects = {
 
+/* Begin PBXBuildFile section */
+		CD8C5A8925D057900004A6D9 /* GoogleMock.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = CDE4F79B25CF316A009E4EC1 /* GoogleMock.framework */; };
+		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 */; };
+/* End PBXBuildFile section */
+
 /* Begin PBXContainerItemProxy section */
+		CD8C5A7B25D0577E0004A6D9 /* PBXContainerItemProxy */ = {
+			isa = PBXContainerItemProxy;
+			containerPortal = CDE4F78225CF309E009E4EC1 /* Project object */;
+			proxyType = 1;
+			remoteGlobalIDString = CDE4F78925CF309E009E4EC1;
+			remoteInfo = program_args;
+		};
 		CDE4F79A25CF316A009E4EC1 /* PBXContainerItemProxy */ = {
 			isa = PBXContainerItemProxy;
 			containerPortal = CDE4F79325CF316A009E4EC1 /* GoogleMock.xcodeproj */;
@@ -38,12 +52,31 @@
 /* End PBXContainerItemProxy section */
 
 /* Begin PBXFileReference section */
+		CD8C5A7525D0577E0004A6D9 /* program_args-test.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = "program_args-test.xctest"; sourceTree = BUILT_PRODUCTS_DIR; };
+		CD8C5A7925D0577E0004A6D9 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
+		CD8C5A8A25D057AA0004A6D9 /* options_test.cpp */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.cpp; path = options_test.cpp; sourceTree = "<group>"; };
+		CD8C5A9025D057C00004A6D9 /* arguments.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = arguments.h; sourceTree = "<group>"; };
+		CD8C5A9125D057C00004A6D9 /* utilities.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = utilities.h; sourceTree = "<group>"; };
+		CD8C5A9225D057C00004A6D9 /* arguments_impl.hpp */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.h; path = arguments_impl.hpp; sourceTree = "<group>"; };
+		CD8C5A9325D057C00004A6D9 /* exception.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = exception.h; sourceTree = "<group>"; };
+		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>"; };
+		CDD334A025D200AB008540EE /* README.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = README.md; 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>"; };
 /* End PBXFileReference section */
 
 /* Begin PBXFrameworksBuildPhase section */
+		CD8C5A7225D0577E0004A6D9 /* Frameworks */ = {
+			isa = PBXFrameworksBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CD8C5A8925D057900004A6D9 /* GoogleMock.framework in Frameworks */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		CDE4F78825CF309E009E4EC1 /* Frameworks */ = {
 			isa = PBXFrameworksBuildPhase;
 			buildActionMask = 2147483647;
@@ -54,13 +87,51 @@
 /* End PBXFrameworksBuildPhase section */
 
 /* Begin PBXGroup section */
+		CD8C5A7625D0577E0004A6D9 /* program_args-test */ = {
+			isa = PBXGroup;
+			children = (
+				CD8C5A7925D0577E0004A6D9 /* Info.plist */,
+			);
+			path = "program_args-test";
+			sourceTree = "<group>";
+		};
+		CD8C5A8825D057900004A6D9 /* Frameworks */ = {
+			isa = PBXGroup;
+			children = (
+			);
+			name = Frameworks;
+			sourceTree = "<group>";
+		};
+		CD8C5A8E25D057C00004A6D9 /* include */ = {
+			isa = PBXGroup;
+			children = (
+				CD8C5A8F25D057C00004A6D9 /* program_args */,
+			);
+			path = include;
+			sourceTree = "<group>";
+		};
+		CD8C5A8F25D057C00004A6D9 /* program_args */ = {
+			isa = PBXGroup;
+			children = (
+				CD8C5A9025D057C00004A6D9 /* arguments.h */,
+				CD8C5A9125D057C00004A6D9 /* utilities.h */,
+				CD8C5A9225D057C00004A6D9 /* arguments_impl.hpp */,
+				CD8C5A9325D057C00004A6D9 /* exception.h */,
+			);
+			path = program_args;
+			sourceTree = "<group>";
+		};
 		CDE4F78125CF309E009E4EC1 = {
 			isa = PBXGroup;
 			children = (
+				CDD334A025D200AB008540EE /* README.md */,
 				CDE4F79325CF316A009E4EC1 /* GoogleMock.xcodeproj */,
+				CD8C5A8E25D057C00004A6D9 /* include */,
 				CDE4F7A225CF317C009E4EC1 /* program_args */,
 				CDE4F79125CF30BA009E4EC1 /* test */,
+				CD8C5A7625D0577E0004A6D9 /* program_args-test */,
 				CDE4F78B25CF309E009E4EC1 /* Products */,
+				CD8C5A8825D057900004A6D9 /* Frameworks */,
 			);
 			sourceTree = "<group>";
 		};
@@ -68,6 +139,7 @@
 			isa = PBXGroup;
 			children = (
 				CDE4F78A25CF309E009E4EC1 /* libprogram_args.a */,
+				CD8C5A7525D0577E0004A6D9 /* program_args-test.xctest */,
 			);
 			name = Products;
 			sourceTree = "<group>";
@@ -75,6 +147,10 @@
 		CDE4F79125CF30BA009E4EC1 /* test */ = {
 			isa = PBXGroup;
 			children = (
+				CD8C5A9625D058470004A6D9 /* xcode_gtest_helper.h */,
+				CD8C5AA325D072F50004A6D9 /* argument_test.cpp */,
+				CD8C5A8A25D057AA0004A6D9 /* options_test.cpp */,
+				CD8C5A9F25D06D0B0004A6D9 /* flag_test.cpp */,
 			);
 			path = test;
 			sourceTree = "<group>";
@@ -103,6 +179,24 @@
 /* End PBXHeadersBuildPhase section */
 
 /* Begin PBXNativeTarget section */
+		CD8C5A7425D0577E0004A6D9 /* program_args-test */ = {
+			isa = PBXNativeTarget;
+			buildConfigurationList = CD8C5A8325D0577E0004A6D9 /* Build configuration list for PBXNativeTarget "program_args-test" */;
+			buildPhases = (
+				CD8C5A7125D0577E0004A6D9 /* Sources */,
+				CD8C5A7225D0577E0004A6D9 /* Frameworks */,
+				CD8C5A7325D0577E0004A6D9 /* Resources */,
+			);
+			buildRules = (
+			);
+			dependencies = (
+				CD8C5A7C25D0577E0004A6D9 /* PBXTargetDependency */,
+			);
+			name = "program_args-test";
+			productName = "program_args-test";
+			productReference = CD8C5A7525D0577E0004A6D9 /* program_args-test.xctest */;
+			productType = "com.apple.product-type.bundle.unit-test";
+		};
 		CDE4F78925CF309E009E4EC1 /* program_args */ = {
 			isa = PBXNativeTarget;
 			buildConfigurationList = CDE4F78E25CF309E009E4EC1 /* Build configuration list for PBXNativeTarget "program_args" */;
@@ -128,6 +222,9 @@
 			attributes = {
 				LastUpgradeCheck = 1240;
 				TargetAttributes = {
+					CD8C5A7425D0577E0004A6D9 = {
+						CreatedOnToolsVersion = 12.4;
+					};
 					CDE4F78925CF309E009E4EC1 = {
 						CreatedOnToolsVersion = 12.4;
 					};
@@ -153,6 +250,7 @@
 			projectRoot = "";
 			targets = (
 				CDE4F78925CF309E009E4EC1 /* program_args */,
+				CD8C5A7425D0577E0004A6D9 /* program_args-test */,
 			);
 		};
 /* End PBXProject section */
@@ -188,7 +286,27 @@
 		};
 /* End PBXReferenceProxy section */
 
+/* Begin PBXResourcesBuildPhase section */
+		CD8C5A7325D0577E0004A6D9 /* Resources */ = {
+			isa = PBXResourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
+/* End PBXResourcesBuildPhase section */
+
 /* Begin PBXSourcesBuildPhase section */
+		CD8C5A7125D0577E0004A6D9 /* Sources */ = {
+			isa = PBXSourcesBuildPhase;
+			buildActionMask = 2147483647;
+			files = (
+				CD8C5A8B25D057AA0004A6D9 /* options_test.cpp in Sources */,
+				CD8C5AA025D06D0B0004A6D9 /* flag_test.cpp in Sources */,
+				CD8C5AA425D072F50004A6D9 /* argument_test.cpp in Sources */,
+			);
+			runOnlyForDeploymentPostprocessing = 0;
+		};
 		CDE4F78725CF309E009E4EC1 /* Sources */ = {
 			isa = PBXSourcesBuildPhase;
 			buildActionMask = 2147483647;
@@ -198,14 +316,54 @@
 		};
 /* End PBXSourcesBuildPhase section */
 
+/* Begin PBXTargetDependency section */
+		CD8C5A7C25D0577E0004A6D9 /* PBXTargetDependency */ = {
+			isa = PBXTargetDependency;
+			target = CDE4F78925CF309E009E4EC1 /* program_args */;
+			targetProxy = CD8C5A7B25D0577E0004A6D9 /* PBXContainerItemProxy */;
+		};
+/* End PBXTargetDependency section */
+
 /* Begin XCBuildConfiguration section */
+		CD8C5A7D25D0577E0004A6D9 /* Debug */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = "program_args-test/Info.plist";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/../Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.program-args-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Debug;
+		};
+		CD8C5A7E25D0577E0004A6D9 /* Release */ = {
+			isa = XCBuildConfiguration;
+			buildSettings = {
+				CODE_SIGN_STYLE = Automatic;
+				COMBINE_HIDPI_IMAGES = YES;
+				INFOPLIST_FILE = "program_args-test/Info.plist";
+				LD_RUNPATH_SEARCH_PATHS = (
+					"$(inherited)",
+					"@executable_path/../Frameworks",
+					"@loader_path/../Frameworks",
+				);
+				PRODUCT_BUNDLE_IDENTIFIER = "leumasjaffe.program-args-test";
+				PRODUCT_NAME = "$(TARGET_NAME)";
+			};
+			name = Release;
+		};
 		CDE4F78C25CF309E009E4EC1 /* Debug */ = {
 			isa = XCBuildConfiguration;
 			buildSettings = {
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LANGUAGE_STANDARD = "c++17";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
 				CLANG_ENABLE_OBJC_ARC = YES;
@@ -255,6 +413,7 @@
 				MTL_FAST_MATH = YES;
 				ONLY_ACTIVE_ARCH = YES;
 				SDKROOT = macosx;
+				USER_HEADER_SEARCH_PATHS = include/;
 			};
 			name = Debug;
 		};
@@ -264,7 +423,7 @@
 				ALWAYS_SEARCH_USER_PATHS = NO;
 				CLANG_ANALYZER_NONNULL = YES;
 				CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE;
-				CLANG_CXX_LANGUAGE_STANDARD = "gnu++14";
+				CLANG_CXX_LANGUAGE_STANDARD = "c++17";
 				CLANG_CXX_LIBRARY = "libc++";
 				CLANG_ENABLE_MODULES = YES;
 				CLANG_ENABLE_OBJC_ARC = YES;
@@ -307,6 +466,7 @@
 				MTL_ENABLE_DEBUG_INFO = NO;
 				MTL_FAST_MATH = YES;
 				SDKROOT = macosx;
+				USER_HEADER_SEARCH_PATHS = include/;
 			};
 			name = Release;
 		};
@@ -333,6 +493,15 @@
 /* End XCBuildConfiguration section */
 
 /* Begin XCConfigurationList section */
+		CD8C5A8325D0577E0004A6D9 /* Build configuration list for PBXNativeTarget "program_args-test" */ = {
+			isa = XCConfigurationList;
+			buildConfigurations = (
+				CD8C5A7D25D0577E0004A6D9 /* Debug */,
+				CD8C5A7E25D0577E0004A6D9 /* Release */,
+			);
+			defaultConfigurationIsVisible = 0;
+			defaultConfigurationName = Release;
+		};
 		CDE4F78525CF309E009E4EC1 /* Build configuration list for PBXProject "program_args" */ = {
 			isa = XCConfigurationList;
 			buildConfigurations = (

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

@@ -0,0 +1,52 @@
+<?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>

+ 78 - 0
program_args.xcodeproj/xcshareddata/xcschemes/program_args.xcscheme

@@ -0,0 +1,78 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<Scheme
+   LastUpgradeVersion = "1240"
+   version = "1.3">
+   <BuildAction
+      parallelizeBuildables = "YES"
+      buildImplicitDependencies = "YES">
+      <BuildActionEntries>
+         <BuildActionEntry
+            buildForTesting = "YES"
+            buildForRunning = "YES"
+            buildForProfiling = "YES"
+            buildForArchiving = "YES"
+            buildForAnalyzing = "YES">
+            <BuildableReference
+               BuildableIdentifier = "primary"
+               BlueprintIdentifier = "CDE4F78925CF309E009E4EC1"
+               BuildableName = "libprogram_args.a"
+               BlueprintName = "program_args"
+               ReferencedContainer = "container:program_args.xcodeproj">
+            </BuildableReference>
+         </BuildActionEntry>
+      </BuildActionEntries>
+   </BuildAction>
+   <TestAction
+      buildConfiguration = "Debug"
+      selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
+      selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
+      shouldUseLaunchSchemeArgsEnv = "YES"
+      codeCoverageEnabled = "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">
+      <MacroExpansion>
+         <BuildableReference
+            BuildableIdentifier = "primary"
+            BlueprintIdentifier = "CDE4F78925CF309E009E4EC1"
+            BuildableName = "libprogram_args.a"
+            BlueprintName = "program_args"
+            ReferencedContainer = "container:program_args.xcodeproj">
+         </BuildableReference>
+      </MacroExpansion>
+   </ProfileAction>
+   <AnalyzeAction
+      buildConfiguration = "Debug">
+   </AnalyzeAction>
+   <ArchiveAction
+      buildConfiguration = "Release"
+      revealArchiveInOrganizer = "YES">
+   </ArchiveAction>
+</Scheme>

+ 95 - 0
test/argument_test.cpp

@@ -0,0 +1,95 @@
+//
+//  argument_test.cpp
+//  program_args-test
+//
+//  Created by Sam Jaffe on 2/7/21.
+//
+
+#include "program_args/arguments.h"
+
+#include "xcode_gtest_helper.h"
+
+using testing::ElementsAre;
+using testing::Eq;
+
+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;
+
+  std::string arg0 = argument(0, "arg0");
+};
+
+TEST(ArgumentTest, ThrowsIfMissingArg) {
+  EXPECT_THROW(parse<ArgumentTest>({""}), program::IllegalPositionError);
+}
+
+TEST(ArgumentTest, ProcessesArgument) {
+  auto const options = parse<ArgumentTest>({"", "value"});
+  EXPECT_THAT(options.arg0, Eq("value"));
+}
+
+TEST(ArgumentTest, HasNoIssueWithOverflowArg) {
+  auto const options = parse<ArgumentTest>({"", "value", "extra"});
+  EXPECT_THAT(options.arg0, Eq("value"));
+  EXPECT_THAT(options.args(), ElementsAre("extra"));
+}
+
+struct OptionalArgumentTest : program::Arguments<OptionalArgumentTest> {
+  using program::Arguments<OptionalArgumentTest>::Arguments;
+
+  std::string arg0 = argument(0, "arg0");
+  std::string arg1 = argument(1, "arg1") = "second";
+};
+
+TEST(OptionalArgumentTest, DoesNotRequireOptionalArg) {
+  EXPECT_NO_THROW(parse<OptionalArgumentTest>({"", "value"}));
+}
+
+TEST(OptionalArgumentTest, DefaultsOutValueAsExpected) {
+  auto const options = parse<OptionalArgumentTest>({"", "value"});
+  EXPECT_THAT(options.arg1, Eq("second"));
+}
+
+TEST(OptionalArgumentTest, DoesNotOverflowSecondArg) {
+  auto const options = parse<OptionalArgumentTest>({"", "value", "extra"});
+  EXPECT_THAT(options.arg0, Eq("value"));
+  EXPECT_THAT(options.arg1, Eq("extra"));
+  EXPECT_THAT(options.args(), ElementsAre());
+}
+
+struct DuplicateArgumentTest : program::Arguments<DuplicateArgumentTest> {
+  using program::Arguments<DuplicateArgumentTest>::Arguments;
+
+  std::string arg0 = argument(0, "arg0");
+  std::string arg1 = argument(0, "arg1");
+};
+
+TEST(DuplicateArgumentTest, ThrowsOnConstruction) {
+  EXPECT_THROW(DuplicateArgumentTest(), program::IllegalPositionError);
+}
+
+struct SkippedArgumentTest : program::Arguments<SkippedArgumentTest> {
+  using program::Arguments<SkippedArgumentTest>::Arguments;
+
+  std::string arg0 = argument(1, "arg0");
+};
+
+TEST(SkippedArgumentTest, ThrowsOnParsing) {
+  EXPECT_NO_THROW(SkippedArgumentTest());
+  EXPECT_THROW(parse<SkippedArgumentTest>({"", "", ""}),
+               program::IllegalPositionError);
+}
+
+struct OOOArgumentTest : program::Arguments<OOOArgumentTest> {
+  using program::Arguments<OOOArgumentTest>::Arguments;
+
+  std::string arg0 = argument(0, "arg0") = "";
+  std::string arg1 = argument(1, "arg1");
+};
+
+TEST(OOOArgumentTest, ThrowsOnConstruction) {
+  EXPECT_THROW(OOOArgumentTest(), program::ArgumentStructureError);
+}

+ 111 - 0
test/flag_test.cpp

@@ -0,0 +1,111 @@
+//
+//  flag_test.cpp
+//  program_args-test
+//
+//  Created by Sam Jaffe on 2/7/21.
+//
+
+#include "program_args/arguments.h"
+
+#include "xcode_gtest_helper.h"
+
+using testing::Eq;
+
+template <typename T, size_t N> static T parse(char const * const (&argv)[N]) {
+  return T(N, argv);
+}
+
+struct FlagTest : program::Arguments<FlagTest> {
+  using program::Arguments<FlagTest>::Arguments;
+
+  bool dry_run = flag("dry-run");
+  int verbosity = flag("verbose");
+};
+
+TEST(FlagTest, ArgumentsAreDefaultedNaturally) {
+  auto const options = parse<FlagTest>({""});
+  EXPECT_THAT(options.dry_run, Eq(false));
+  EXPECT_THAT(options.verbosity, Eq(0));
+}
+
+TEST(FlagTest, FlagsDoNotRequireAdditionalArguments) {
+  EXPECT_NO_THROW(parse<FlagTest>({"", "--dry-run"}));
+}
+
+TEST(FlagTest, BooleanFlagSetsState) {
+  auto const options = parse<FlagTest>({"", "--dry-run"});
+  EXPECT_THAT(options.dry_run, Eq(true));
+}
+
+TEST(FlagTest, BooleanFlagIsIdempotent) {
+  EXPECT_NO_THROW(parse<FlagTest>({"", "--dry-run", "--dry-run"}));
+}
+
+TEST(FlagTest, BooleanFlagCanBeInverted) {
+  auto const options = parse<FlagTest>({"", "--dry-run", "--no-dry-run"});
+  EXPECT_THAT(options.dry_run, Eq(false));
+}
+
+TEST(FlagTest, IntegerFlagIncrementsState) {
+  auto const options = parse<FlagTest>({"", "--verbose"});
+  EXPECT_THAT(options.verbosity, Eq(1));
+}
+
+TEST(FlagTest, IntegerFlagCanBeRepeated) {
+  auto const options = parse<FlagTest>({"", "--verbose", "--verbose"});
+  EXPECT_THAT(options.verbosity, Eq(2));
+}
+
+struct FlagWithDefaultTest : program::Arguments<FlagWithDefaultTest> {
+  using program::Arguments<FlagWithDefaultTest>::Arguments;
+
+  bool dry_run = flag("dry-run") = true;
+};
+
+TEST(FlagWithDefaultTest, ArgumentsAreDefaultedToValue) {
+  auto const options = parse<FlagWithDefaultTest>({""});
+  EXPECT_THAT(options.dry_run, Eq(true));
+}
+
+TEST(FlagWithDefaultTest, BooleanFlagCanBeInverted) {
+  auto const options = parse<FlagWithDefaultTest>({"", "--no-dry-run"});
+  EXPECT_THAT(options.dry_run, Eq(false));
+}
+
+struct FlagWithAbbrevTest : program::Arguments<FlagWithAbbrevTest> {
+  using program::Arguments<FlagWithAbbrevTest>::Arguments;
+
+  bool dry_run = flag("dry-run", 'k');
+  int verbosity = flag("verbose", 'v');
+};
+
+TEST(FlagWithAbbrevTest, BooleanFlagSetsState) {
+  auto const options = parse<FlagWithAbbrevTest>({"", "-k"});
+  EXPECT_THAT(options.dry_run, Eq(true));
+}
+
+TEST(FlagWithAbbrevTest, IntegerFlagIncrementsState) {
+  auto const options = parse<FlagWithAbbrevTest>({"", "-v"});
+  EXPECT_THAT(options.verbosity, Eq(1));
+}
+
+TEST(FlagWithAbbrevTest, CanComposeTogetherAbbreviatedFlags) {
+  auto const options = parse<FlagWithAbbrevTest>({"", "-vk"});
+  EXPECT_THAT(options.dry_run, Eq(true));
+  EXPECT_THAT(options.verbosity, Eq(1));
+}
+
+TEST(FlagWithAbbrevTest, CanComposeTogetherSameAbbrevFlags) {
+  auto const options = parse<FlagWithAbbrevTest>({"", "-vv"});
+  EXPECT_THAT(options.verbosity, Eq(2));
+}
+
+TEST(FlagWithAbbrevTest, CanNumberRepeatAbbrevFlags) {
+  auto const options = parse<FlagWithAbbrevTest>({"", "-v3"});
+  EXPECT_THAT(options.verbosity, Eq(3));
+}
+
+TEST(FlagWithAbbrevTest, CannotNumberRepeatAndConcatAbbrevFlags) {
+  EXPECT_THROW(parse<FlagWithAbbrevTest>({"", "-v3k"}),
+               program::NotAnArgumentError);
+}

+ 166 - 0
test/options_test.cpp

@@ -0,0 +1,166 @@
+//
+//  options_test.cpp
+//  program_args-test
+//
+//  Created by Sam Jaffe on 2/7/21.
+//
+
+#include "program_args/arguments.h"
+
+#include "xcode_gtest_helper.h"
+
+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 LongOptionTest : program::Arguments<LongOptionTest> {
+  using program::Arguments<LongOptionTest>::Arguments;
+
+  std::string endpoint = option("endpoint");
+  int port = option("port");
+};
+
+TEST(LongOptionTest, ArgumentsAreDefaultedNaturally) {
+  auto const options = parse<LongOptionTest>({""});
+  EXPECT_THAT(options.endpoint, Eq(""));
+  EXPECT_THAT(options.port, Eq(0));
+}
+
+TEST(LongOptionTest, CanProvideStringArgument) {
+  auto const options = parse<LongOptionTest>({"", "--endpoint", "/null"});
+  EXPECT_THAT(options.endpoint, Eq("/null"));
+}
+
+TEST(LongOptionTest, CanProvideNumericArgument) {
+  auto const options = parse<LongOptionTest>({"", "--port", "443"});
+  EXPECT_THAT(options.port, Eq(443));
+}
+
+TEST(LongOptionTest, WillThrowOnInvalidNumericArgument) {
+  EXPECT_THROW(parse<LongOptionTest>({"", "--port", "one"}),
+               program::ProgramArgumentsError);
+}
+
+TEST(LongOptionTest, WillThrowOnRepeatedArgument) {
+  EXPECT_THROW(parse<LongOptionTest>({"", "--port", "8080", "--port", "443"}),
+               program::ArgumentStructureError);
+}
+
+TEST(LongOptionTest, WillThrowOnUnknownArgument) {
+  EXPECT_THROW(parse<LongOptionTest>({"", "--path", "/null"}),
+               program::NotAnArgumentError);
+}
+
+TEST(LongOptionTest, DoesNotImplicitlyShortenArgs) {
+  EXPECT_THROW(parse<LongOptionTest>({"", "-p", "443"}),
+               program::NotAnArgumentError);
+}
+
+struct LongOptionWithDefaultTest
+    : program::Arguments<LongOptionWithDefaultTest> {
+  using program::Arguments<LongOptionWithDefaultTest>::Arguments;
+
+  std::string endpoint = option("endpoint") = "/default";
+  int port = option("port") = 8080;
+};
+
+TEST(LongOptionWithDefaultTest, ArgumentsAreDefaultedNaturally) {
+  auto const options = parse<LongOptionWithDefaultTest>({""});
+  EXPECT_THAT(options.endpoint, Eq("/default"));
+  EXPECT_THAT(options.port, Eq(8080));
+}
+
+TEST(LongOptionWithDefaultTest, CanProvideStringArgument) {
+  auto const options =
+      parse<LongOptionWithDefaultTest>({"", "--endpoint", "/null"});
+  EXPECT_THAT(options.endpoint, Eq("/null"));
+}
+
+TEST(LongOptionWithDefaultTest, CanProvideNumericArgument) {
+  auto const options = parse<LongOptionWithDefaultTest>({"", "--port", "443"});
+  EXPECT_THAT(options.port, Eq(443));
+}
+
+struct LongOptionWithAbbrevTest : program::Arguments<LongOptionWithAbbrevTest> {
+  using program::Arguments<LongOptionWithAbbrevTest>::Arguments;
+
+  std::string endpoint = option("endpoint", 'e');
+  int port = option("port", 'p');
+};
+
+TEST(LongOptionWithAbbrevTest, CanProvideStringArgument) {
+  auto const options =
+      parse<LongOptionWithAbbrevTest>({"", "--endpoint", "/null"});
+  EXPECT_THAT(options.endpoint, Eq("/null"));
+}
+
+TEST(LongOptionWithAbbrevTest, CanProvideAbbrevStringArgument) {
+  auto const options = parse<LongOptionWithAbbrevTest>({"", "-e", "/null"});
+  EXPECT_THAT(options.endpoint, Eq("/null"));
+}
+
+TEST(LongOptionWithAbbrevTest, CanProvideNumericArgument) {
+  auto const options = parse<LongOptionWithAbbrevTest>({"", "--port", "443"});
+  EXPECT_THAT(options.port, Eq(443));
+}
+
+TEST(LongOptionWithAbbrevTest, CanProvideAbbrevNumericArgument) {
+  auto const options = parse<LongOptionWithAbbrevTest>({"", "-p", "443"});
+  EXPECT_THAT(options.port, Eq(443));
+}
+
+TEST(LongOptionWithAbbrevTest, ShortAndLongArgsGoToSamePool) {
+  EXPECT_THROW(
+      parse<LongOptionWithAbbrevTest>({"", "--port", "8080", "-p", "443"}),
+      program::ArgumentStructureError);
+}
+
+TEST(LongOptionWithAbbrevTest, CanPutAbbrevArgAndValueInSameToken) {
+  EXPECT_NO_THROW(parse<LongOptionWithAbbrevTest>({"", "-p443"}));
+  auto const options = parse<LongOptionWithAbbrevTest>({"", "-p443"});
+  EXPECT_THAT(options.port, Eq(443));
+}
+
+struct LongOptionRepeatTest : program::Arguments<LongOptionRepeatTest> {
+  using program::Arguments<LongOptionRepeatTest>::Arguments;
+
+  std::vector<int> port = option("port");
+};
+
+TEST(LongOptionRepeatTest, DefaultIsEmpty) {
+  auto const options = parse<LongOptionRepeatTest>({""});
+  EXPECT_THAT(options.port, IsEmpty());
+}
+
+TEST(LongOptionRepeatTest, CanProvideArgument) {
+  auto const options = parse<LongOptionRepeatTest>({"", "--port", "443"});
+  EXPECT_THAT(options.port, ElementsAre(443));
+}
+
+TEST(LongOptionRepeatTest, RepeatingArgumentsAppends) {
+  auto const options =
+      parse<LongOptionRepeatTest>({"", "--port", "443", "--port", "8080"});
+  EXPECT_THAT(options.port, ElementsAre(443, 8080));
+}
+
+struct LongOptionRepeatWithDefaultTest
+    : program::Arguments<LongOptionRepeatWithDefaultTest> {
+  using program::Arguments<LongOptionRepeatWithDefaultTest>::Arguments;
+
+  std::vector<int> port = option("port") = std::vector{8080};
+};
+
+TEST(LongOptionRepeatWithDefaultTest, DefaultIsProvided) {
+  auto const options = parse<LongOptionRepeatWithDefaultTest>({""});
+  EXPECT_THAT(options.port, ElementsAre(8080));
+}
+
+TEST(LongOptionRepeatWithDefaultTest, ArgumentOverwritesDefault) {
+  auto const options =
+      parse<LongOptionRepeatWithDefaultTest>({"", "--port", "443"});
+  EXPECT_THAT(options.port, ElementsAre(443));
+}

+ 38 - 0
test/xcode_gtest_helper.h

@@ -0,0 +1,38 @@
+//
+//  xcode_gtest_helper.h
+//  tax-calculator-test
+//
+//  Created by Sam Jaffe on 11/25/20.
+//  Copyright © 2020 Sam Jaffe. All rights reserved.
+//
+
+#pragma once
+
+#if defined(__APPLE__)
+
+#pragma clang diagnostic push
+#pragma clang diagnostic ignored "-Wquoted-include-in-framework-header"
+
+#include <gmock/gmock.h>
+#include <gtest/gtest.h>
+
+#pragma clang diagnostic pop
+
+#if defined(TARGET_OS_OSX)
+// This is a hack to allow XCode to properly display failures when running
+// unit tests.
+#undef EXPECT_THAT
+#define EXPECT_THAT ASSERT_THAT
+#undef EXPECT_THROW
+#define EXPECT_THROW ASSERT_THROW
+#undef EXPECT_ANY_THROW
+#define EXPECT_ANY_THROW ASSERT_ANY_THROW
+#undef EXPECT_NO_THROW
+#define EXPECT_NO_THROW ASSERT_NO_THROW
+#undef EXPECT_TRUE
+#define EXPECT_TRUE ASSERT_TRUE
+#undef EXPECT_FALSE
+#define EXPECT_FALSE ASSERT_FALSE
+
+#endif
+#endif