├── .gitignore ├── .travis.yml ├── CMakeLists.txt ├── LICENSE ├── README.md ├── include └── optionparser.h └── tests ├── CMakeLists.txt ├── include └── doctest.h └── test_parser.cc /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | .vscode/ 35 | run-test 36 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | - osx 4 | compiler: 5 | - clang 6 | - gcc 7 | language: cpp 8 | dist: xenial 9 | script: 10 | - mkdir build && cd build && cmake .. && make 11 | - CTEST_OUTPUT_ON_FAILURE=1 make test 12 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.12) 2 | 3 | project("optionparser" 4 | VERSION 0.2.0 5 | DESCRIPTION "A lightweight header-only option parser designed for headache-minimization in C++." 6 | HOMEPAGE_URL "https://github.com/lukedeo/option-parser") 7 | 8 | include(CTest) 9 | include(GNUInstallDirs) 10 | 11 | add_library(${PROJECT_NAME} INTERFACE) 12 | add_library(${PROJECT_NAME}::${PROJECT_NAME} ALIAS ${PROJECT_NAME}) 13 | 14 | target_include_directories( 15 | ${PROJECT_NAME} 16 | INTERFACE 17 | include/ 18 | ) 19 | 20 | target_compile_features(${PROJECT_NAME} INTERFACE cxx_std_11) 21 | 22 | if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) 23 | add_subdirectory(tests) 24 | add_test(NAME test-optionparser COMMAND tests/test-optionparser) 25 | endif() -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 Luke de Oliveira 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `option-parser` 💥 2 | 3 | [![Build Status](https://travis-ci.org/lukedeo/option-parser.svg?branch=master)](https://travis-ci.org/lukedeo/option-parser) 4 | 5 | A lightweight header-only option parser designed for headache-minimization in C++. Just copy-and-paste into your project and go! 6 | 7 | ## Example Usage 8 | 9 | ```c++ 10 | #include "include/optionparser.h" 11 | 12 | ... 13 | 14 | 15 | int main(int argc, char const *argv[]) { 16 | optionparser::OptionParser p; 17 | 18 | p.add_option("--number", "-n") 19 | .help("A number to do something with") 20 | .default_value(42) 21 | .mode(optionparser::StorageMode::STORE_VALUE); 22 | 23 | p.add_option("--file") 24 | .help("pass a list of files to load.") 25 | .mode(optionparser::StorageMode::STORE_MULT_VALUES) 26 | .required(true); 27 | 28 | p.add_option("--save", "-s") 29 | .help("Pass a file to save.") 30 | .dest("") 31 | .mode(optionparser::StorageMode::STORE_VALUE); 32 | 33 | p.eat_arguments(argc, argv); 34 | 35 | if (p.get_value("number")) { 36 | auto number_passed = p.get_value("number"); 37 | } 38 | 39 | if (p.get_value("file")) { 40 | auto filenames = p.get_value>("file"); 41 | } 42 | 43 | return 0; 44 | } 45 | ``` 46 | 47 | After you `p.add_option("--foo", "-f")`, you can chain additional statements. These include: 48 | 49 | * `.default_value(...)`, to set a sensible default. 50 | * `.dest(...)`, to set the metavar (i.e., the key to retrieve the value) 51 | * `.help(...)`, to set a help string for that argument. 52 | * `.mode(...)`, can pass one of `optionparser::StorageMode::STORE_VALUE`, `optionparser::StorageMode::STORE_MULT_VALUES`, or `optionparser::StorageMode::STORE_TRUE`. 53 | * `.required(...)`, which can make a specific command line flag required for valid invocation. 54 | 55 | # 🚧 HELP! 56 | 57 | Some things I'd love to have but don't have the time to do (in order of priority): 58 | 59 | * Documentation! The library has a small surface area, but people shouldn't have to dig through a header file to find out how to do things... 60 | * A proper `CMakeLists.txt` file. 61 | * Subparsers, though nice, might not be possible given the library structure. 62 | 63 | -------------------------------------------------------------------------------- /include/optionparser.h: -------------------------------------------------------------------------------- 1 | //----------------------------------------------------------------------------- 2 | // optionparser.h -- A Header-Only commandline argument parser 3 | // Author: Luke de Oliveira 4 | // License: MIT 5 | //----------------------------------------------------------------------------- 6 | 7 | #ifndef OPTIONPARSER_H_ 8 | #define OPTIONPARSER_H_ 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace optionparser { 18 | 19 | // The utils::* namespace contains general utilities not necessarily useful 20 | // outside the main scope of the library 21 | namespace utils { 22 | 23 | std::vector split_str(std::string s, 24 | const std::string &delimiter = " ") { 25 | size_t pos = 0; 26 | size_t delimiter_length = delimiter.length(); 27 | std::vector vals; 28 | while ((pos = s.find(delimiter)) != std::string::npos) { 29 | vals.push_back(s.substr(0, pos)); 30 | s.erase(0, pos + delimiter_length); 31 | } 32 | vals.push_back(s); 33 | return vals; 34 | } 35 | 36 | std::string stitch_str(const std::vector &text, 37 | unsigned max_per_line = 80, 38 | const std::string &leading_str = "") { 39 | std::vector result; 40 | 41 | std::string line_value; 42 | for (const auto &token : text) { 43 | if (line_value.empty()) { 44 | line_value = (leading_str + token); 45 | continue; 46 | } 47 | 48 | auto hypothetical_line = line_value; 49 | hypothetical_line.append(" " + token); 50 | 51 | if (hypothetical_line.size() > max_per_line) { 52 | // In this case, we were better off before 53 | result.emplace_back(line_value); 54 | line_value = (leading_str + token); 55 | } else { 56 | line_value = hypothetical_line; 57 | } 58 | } 59 | // Collect the last line since we don't track indices in the loop proper. 60 | result.emplace_back(line_value); 61 | return std::accumulate( 62 | result.begin() + 1, result.end(), result.at(0), 63 | [](std::string &s, const std::string &piece) -> std::string { 64 | return s + "\n" + piece; 65 | }); 66 | } 67 | 68 | } // end namespace utils 69 | 70 | // Define a thin error for any sort of parser error that arises 71 | class ParserError : public std::runtime_error { 72 | using std::runtime_error::runtime_error; 73 | }; 74 | 75 | // Enums for Option config 76 | enum StorageMode { STORE_TRUE = 0, STORE_VALUE, STORE_MULT_VALUES }; 77 | enum OptionType { LONG_OPT = 0, SHORT_OPT, POSITIONAL_OPT, EMPTY_OPT }; 78 | 79 | // Option class definition 80 | class Option { 81 | public: 82 | Option() = default; 83 | 84 | std::string help_doc(); 85 | 86 | std::string &short_flag() { return m_short_flag; } 87 | std::string &long_flag() { return m_long_flag; } 88 | std::string &pos_flag() { return m_pos_flag; } 89 | 90 | bool found() { return m_found; } 91 | Option &found(bool found) { 92 | m_found = found; 93 | return *this; 94 | } 95 | 96 | StorageMode mode() { return m_mode; } 97 | Option &mode(const StorageMode &mode) { 98 | m_mode = mode; 99 | return *this; 100 | } 101 | 102 | bool required() { return m_required; } 103 | Option &required(bool req) { 104 | m_required = req; 105 | return *this; 106 | } 107 | 108 | std::string metavar() { 109 | std::string formatted_metavar; 110 | if (!m_metavar.empty()) { 111 | if (m_mode == STORE_TRUE) { 112 | return ""; 113 | } 114 | formatted_metavar = m_metavar; 115 | } else { 116 | for (const auto &cand : 117 | {m_dest, m_pos_flag, m_long_flag, std::string("ARG")}) { 118 | if (!cand.empty()) { 119 | formatted_metavar = cand.substr(cand.find_first_not_of('-')); 120 | std::transform(formatted_metavar.begin(), formatted_metavar.end(), 121 | formatted_metavar.begin(), ::toupper); 122 | break; 123 | } 124 | } 125 | } 126 | if (m_mode == STORE_MULT_VALUES) { 127 | formatted_metavar = (formatted_metavar + "1 [" + formatted_metavar + 128 | "2, " + formatted_metavar + "3, ...]"); 129 | } 130 | return formatted_metavar; 131 | } 132 | 133 | Option &metavar(const std::string &mvar) { 134 | m_metavar = mvar; 135 | return *this; 136 | } 137 | 138 | std::string help() { return m_help; } 139 | Option &help(const std::string &help) { 140 | m_help = help; 141 | return *this; 142 | } 143 | 144 | std::string dest() { return m_dest; } 145 | Option &dest(const std::string &dest) { 146 | m_dest = dest; 147 | return *this; 148 | } 149 | 150 | std::string default_value() { return m_default_value; } 151 | 152 | Option &default_value(const std::string &default_value) { 153 | m_default_value = default_value; 154 | return *this; 155 | } 156 | 157 | Option &default_value(const char *default_value) { 158 | m_default_value = std::string(default_value); 159 | return *this; 160 | } 161 | 162 | template Option &default_value(const T &default_value) { 163 | m_default_value = std::to_string(default_value); 164 | return *this; 165 | } 166 | 167 | static OptionType get_type(std::string opt); 168 | static std::string get_destination(const std::string &first_option, 169 | const std::string &second_option); 170 | static void validate_option_types(const OptionType &first_option_type, 171 | const OptionType &second_option_type); 172 | 173 | private: 174 | bool m_found = false; 175 | bool m_required = false; 176 | StorageMode m_mode = STORE_TRUE; 177 | std::string m_help = ""; 178 | std::string m_dest = ""; 179 | std::string m_default_value = ""; 180 | std::string m_metavar = ""; 181 | 182 | std::string m_short_flag = ""; 183 | std::string m_long_flag = ""; 184 | std::string m_pos_flag = ""; 185 | }; 186 | 187 | // Non-inline definitions for Option methods 188 | std::string Option::help_doc() { 189 | std::string h = " "; 190 | if (!m_long_flag.empty()) { 191 | h += m_long_flag; 192 | if (!m_short_flag.empty()) { 193 | h += ", "; 194 | } 195 | } 196 | if (!m_short_flag.empty()) { 197 | h += m_short_flag; 198 | } 199 | if (!m_pos_flag.empty()) { 200 | h += m_pos_flag; 201 | } 202 | 203 | auto arg_buf = std::max(h.length() + 1, static_cast(25)); 204 | auto help_str = utils::stitch_str(utils::split_str(m_help), arg_buf + 50, 205 | std::string(arg_buf, ' ')); 206 | char char_buf[h.length() + help_str.length() + 100]; 207 | sprintf(char_buf, ("%-" + std::to_string(arg_buf) + "s%s\n").c_str(), 208 | h.c_str(), help_str.substr(arg_buf).c_str()); 209 | return std::string(char_buf); 210 | } 211 | 212 | OptionType Option::get_type(std::string opt) { 213 | if (opt.empty()) { 214 | return OptionType::EMPTY_OPT; 215 | } 216 | if (opt.size() == 2) { 217 | if (opt[0] == '-') { 218 | return OptionType::SHORT_OPT; 219 | } 220 | } 221 | 222 | if (opt.size() > 2) { 223 | if (opt[0] == '-' && opt[1] == '-') { 224 | return OptionType::LONG_OPT; 225 | } 226 | } 227 | 228 | return OptionType::POSITIONAL_OPT; 229 | } 230 | 231 | void Option::validate_option_types(const OptionType &first_option_type, 232 | const OptionType &second_option_type) { 233 | 234 | auto err = [](const std::string &msg) { 235 | throw std::runtime_error("Parser inconsistency: " + msg); 236 | }; 237 | if (first_option_type == OptionType::EMPTY_OPT) { 238 | err("Cannot have first option be empty."); 239 | } 240 | if (first_option_type == OptionType::POSITIONAL_OPT && 241 | second_option_type != OptionType::EMPTY_OPT) { 242 | err("Positional arguments can only have one option, found non-empty second " 243 | "option."); 244 | } 245 | if (second_option_type == OptionType::POSITIONAL_OPT) { 246 | err("Cannot have second option be a positional option."); 247 | } 248 | } 249 | 250 | std::string Option::get_destination(const std::string &first_option, 251 | const std::string &second_option) { 252 | std::string dest; 253 | 254 | auto first_opt_type = Option::get_type(first_option); 255 | auto second_opt_type = Option::get_type(second_option); 256 | 257 | validate_option_types(first_opt_type, second_opt_type); 258 | 259 | if (first_opt_type == OptionType::LONG_OPT) { 260 | dest = first_option.substr(2); 261 | } else if (second_opt_type == OptionType::LONG_OPT) { 262 | dest = second_option.substr(2); 263 | } else { 264 | if (first_opt_type == OptionType::SHORT_OPT) { 265 | dest = first_option.substr(1) + "_option"; 266 | } else if (second_opt_type == OptionType::SHORT_OPT) { 267 | dest = second_option.substr(1) + "_option"; 268 | } else { 269 | if (first_opt_type == OptionType::POSITIONAL_OPT && 270 | second_opt_type == OptionType::EMPTY_OPT) { 271 | dest = first_option; 272 | } else { 273 | std::string msg = "Parser inconsistency error."; 274 | throw std::runtime_error(msg); 275 | } 276 | } 277 | } 278 | 279 | return dest; 280 | } 281 | 282 | // OptionParser class definition 283 | class OptionParser { 284 | public: 285 | explicit OptionParser(std::string description = "", bool create_help = true) 286 | : m_options(0), m_description(std::move(description)), 287 | m_pos_args_count(1), m_exit_on_failure(true) { 288 | if (create_help) { 289 | add_option("--help", "-h").help("Display this help message and exit."); 290 | } 291 | } 292 | 293 | ~OptionParser() = default; 294 | 295 | void eat_arguments(unsigned int argc, char const *argv[]); 296 | 297 | Option &add_option(const std::string &first_option, 298 | const std::string &second_option = ""); 299 | 300 | // We template-specialize these later 301 | template T get_value(const std::string &key); 302 | 303 | void help(); 304 | 305 | OptionParser &exit_on_failure(bool exit = true); 306 | 307 | OptionParser &throw_on_failure(bool throw_ = true); 308 | 309 | private: 310 | Option &add_option_internal(const std::string &first_option, 311 | const std::string &second_option); 312 | 313 | void try_to_exit_with_message(const std::string &e); 314 | 315 | ParserError fail_for_missing_key(const std::string &key); 316 | 317 | ParserError fail_unrecognized_argument(const std::string &arg); 318 | 319 | ParserError 320 | fail_for_missing_arguments(const std::vector &missing_flags); 321 | 322 | bool get_value_arg(std::vector &arguments, unsigned int &arg, 323 | Option &opt, std::string &flag); 324 | 325 | bool try_to_get_opt(std::vector &arguments, unsigned int &arg, 326 | Option &option, std::string &flag); 327 | 328 | void check_for_missing_args(); 329 | 330 | std::map> m_values; 331 | int m_pos_args_count; 332 | std::vector