1 #include <climits>
2 #include <cstring>
3 #include <exception>
4 #include <iostream>
5 #include <iterator>
6 #include <stdexcept>
7 #include <string>
8 #include <type_traits>
9 #include <unordered_map>
10 #include "argparse.h"
11
12 namespace {
13
14 const ArgparseOption HELP_OPTION_FULL = { OPTION_HELP, "?", "help", 0, nullptr, "print help message" };
15 const ArgparseOption HELP_OPTION_LONG_ONLY = { OPTION_HELP, nullptr, "help", 0, nullptr, "print help message" };
16
17 constexpr int HELP_INDENT = 32;
18
19
get_short_name(const ArgparseOption & opt)20 const char *get_short_name(const ArgparseOption &opt) noexcept
21 {
22 if (opt.short_name)
23 return opt.short_name;
24 else if (opt.long_name)
25 return opt.long_name;
26 else
27 return "";
28 }
29
get_long_name(const ArgparseOption & opt)30 const char *get_long_name(const ArgparseOption &opt) noexcept
31 {
32 if (opt.long_name)
33 return opt.long_name;
34 else if (opt.short_name)
35 return opt.short_name;
36 else
37 return "";
38 }
39
opt_has_param(OptionType type)40 bool opt_has_param(OptionType type)
41 {
42 switch (type) {
43 case OPTION_INT:
44 case OPTION_UINT:
45 case OPTION_LONGLONG:
46 case OPTION_ULONGLONG:
47 case OPTION_FLOAT:
48 case OPTION_STRING:
49 case OPTION_USER1:
50 return true;
51 default:
52 return false;
53 }
54 }
55
56
57 struct Error {
58 int code;
59 };
60
61 class OptionIterator {
62 public:
63 typedef ptrdiff_t difference_type;
64 typedef ArgparseOption value_type;
65 typedef const ArgparseOption *pointer;
66 typedef const ArgparseOption &reference;
67 typedef std::bidirectional_iterator_tag iterator_category;
68 private:
69 pointer m_opt;
70
is_null() const71 bool is_null() const noexcept { return !m_opt || m_opt->type == OPTION_NULL; }
72 public:
OptionIterator(pointer opt=nullptr)73 explicit OptionIterator(pointer opt = nullptr) noexcept : m_opt{ opt } {}
74
get() const75 pointer get() const noexcept { return m_opt; }
76
operator bool() const77 explicit operator bool() const noexcept { return !is_null(); }
78
operator *() const79 reference operator*() const noexcept { return *get(); }
operator ->() const80 pointer operator->() const noexcept { return get(); }
81
operator ++()82 OptionIterator &operator++() noexcept { ++m_opt; return *this; }
operator ++(int)83 OptionIterator operator++(int) noexcept { OptionIterator ret = *this; ++*this; return ret; }
84
operator --()85 OptionIterator &operator--() noexcept { --m_opt; return *this; }
operator --(int)86 OptionIterator operator--(int) noexcept { OptionIterator ret = *this; ++*this; return ret; }
87
operator ==(const OptionIterator & other) const88 bool operator==(const OptionIterator &other) const noexcept
89 {
90 return m_opt == other.m_opt || (is_null() && other.is_null());
91 }
92
operator !=(const OptionIterator & other) const93 bool operator!=(const OptionIterator &other) const noexcept { return !(*this == other); }
94 };
95
96 class OptionRange {
97 const ArgparseOption *m_first;
98 public:
OptionRange(const ArgparseOption * first)99 explicit OptionRange(const ArgparseOption *first) noexcept : m_first{ first } {}
100
begin() const101 OptionIterator begin() const noexcept { return OptionIterator{ m_first }; }
end() const102 OptionIterator end() const noexcept { return OptionIterator{}; }
103 };
104
105 class OptionMap {
106 std::unordered_map<char, const ArgparseOption *> m_short;
107 std::unordered_map<std::string, const ArgparseOption *> m_long;
108 public:
insert_opt(const ArgparseOption * opt)109 void insert_opt(const ArgparseOption *opt)
110 {
111 if (opt->short_name)
112 m_short[opt->short_name[0]] = opt;
113 if (opt->long_name)
114 m_long[opt->long_name] = opt;
115 }
116
find_short(char c) const117 const ArgparseOption *find_short(char c) const noexcept
118 {
119 auto it = m_short.find(c);
120 return it == m_short.end() ? nullptr : it->second;
121 }
122
find_long(const std::string & s) const123 const ArgparseOption *find_long(const std::string &s) const noexcept
124 {
125 auto it = m_long.find(s);
126 return it == m_long.end() ? nullptr : it->second;
127 }
128 };
129
130
131 template <class T>
132 T stox(const char *s, size_t *pos);
133
134 template <>
stox(const char * s,size_t * pos)135 int stox<int>(const char *s, size_t *pos) { return std::stoi(s, pos); }
136
137 template <>
stox(const char * s,size_t * pos)138 unsigned stox<unsigned>(const char *s, size_t *pos)
139 {
140 #if ULONG_MAX > UINT_MAX
141 unsigned long x = std::stoul(s, pos);
142 return x > UINT_MAX ? throw std::out_of_range{ "integer out of range" } : static_cast<unsigned>(x);
143 #else
144 return std::stoul(s, pos);
145 #endif
146 }
147
148 template <>
stox(const char * s,size_t * pos)149 long long stox<long long>(const char *s, size_t *pos) { return std::stoll(s, pos); }
150
151 template <>
stox(const char * s,size_t * pos)152 unsigned long long stox<unsigned long long>(const char *s, size_t *pos) { return std::stoull(s, pos); }
153
154
155 template <class T>
parse_integer(const char * s)156 T parse_integer(const char *s)
157 {
158 try {
159 size_t pos;
160 T x = stox<T>(s, &pos);
161
162 if (s[pos] != '\0')
163 throw std::invalid_argument{ "unparsed characters" };
164
165 return static_cast<T>(x);
166 } catch (const std::exception &e) {
167 std::cerr << "error parsing integer from '" << s << "': " << e.what() << '\n';
168 throw Error{ ARGPARSE_BAD_PARAMETER };
169 }
170 }
171
parse_double(const char * s)172 double parse_double(const char *s)
173 {
174 try {
175 size_t pos;
176 auto x = std::stod(s, &pos);
177
178 if (s[pos] != '\0')
179 throw std::invalid_argument{ "unparsed characters" };
180 return x;
181 } catch (const std::exception &e) {
182 std::cerr << "error parsing float from: '" << s << "': " << e.what() << '\n';
183 throw Error{ ARGPARSE_BAD_PARAMETER };
184 }
185 }
186
handle_switch(const ArgparseOption & opt,void * out,const char * param,bool negated)187 void handle_switch(const ArgparseOption &opt, void *out, const char *param, bool negated)
188 {
189 if (negated && opt.type != OPTION_FLAG && opt.type != OPTION_USER0) {
190 std::cerr << "switch '" << get_long_name(opt) << "' can not be negated\n";
191 throw Error{ ARGPARSE_BAD_PARAMETER };
192 }
193 if (opt_has_param(opt.type) && !param) {
194 std::cerr << "switch '" << get_long_name(opt) << "' missing parameter\n";
195 throw Error{ ARGPARSE_BAD_PARAMETER };
196 }
197
198 void *out_ptr = static_cast<char *>(out) + opt.offset;
199
200 switch (opt.type) {
201 case OPTION_FLAG:
202 *static_cast<char *>(out_ptr) = !negated;
203 break;
204 case OPTION_HELP:
205 throw Error{ ARGPARSE_HELP_MESSAGE };
206 case OPTION_INCREMENT:
207 *static_cast<int *>(out_ptr) += 1;
208 break;
209 case OPTION_DECREMENT:
210 *static_cast<int *>(out_ptr) -= 1;
211 break;
212 case OPTION_INT:
213 *static_cast<int *>(out_ptr) = parse_integer<int>(param);
214 break;
215 case OPTION_UINT:
216 *static_cast<unsigned int *>(out_ptr) = parse_integer<unsigned>(param);
217 break;
218 case OPTION_LONGLONG:
219 *static_cast<long long *>(out_ptr) = parse_integer<long long>(param);
220 break;
221 case OPTION_ULONGLONG:
222 *static_cast<unsigned long long *>(out_ptr) = parse_integer<unsigned long long>(param);
223 break;
224 case OPTION_FLOAT:
225 *static_cast<double *>(out_ptr) = parse_double(param);
226 break;
227 case OPTION_STRING:
228 *static_cast<const char **>(out_ptr) = param;
229 break;
230 case OPTION_USER0:
231 case OPTION_USER1:
232 if (opt.func(&opt, out_ptr, param, negated))
233 throw Error{ ARGPARSE_BAD_PARAMETER };
234 break;
235 case OPTION_NULL:
236 default:
237 throw std::logic_error{ "bad option type" };
238 }
239 }
240
handle_long_switch(const OptionMap & options,void * out,int argc,const char * const * argv,int * pos,const char * s,size_t len)241 void handle_long_switch(const OptionMap &options, void *out, int argc, const char * const *argv, int *pos, const char *s, size_t len)
242 {
243 const char *param = nullptr;
244
245 if (const char *find = std::strchr(s, '=')) {
246 param = find + 1;
247 len = find - s;
248 }
249
250 const ArgparseOption *opt = nullptr;
251 bool negated = false;
252
253 std::string sw{ s, len };
254
255 if ((opt = options.find_long(sw))) {
256 // Ordinary switch: "--abc".
257 negated = false;
258 } else if (sw.find("no-") == 0 && (opt = options.find_long(sw.substr(3)))) {
259 // Negated switch: "--no-abc".
260 negated = true;
261 } else {
262 std::cerr << "unrecognized switch '--" << sw << "'\n";
263 throw Error{ ARGPARSE_INVALID_SWITCH };
264 }
265
266 // Parameter as next argument: "--xyz 123".
267 if (opt_has_param(opt->type) && !param && *pos < argc)
268 param = argv[++*pos];
269
270 handle_switch(*opt, out, param, negated);
271 };
272
handle_short_switch(const OptionMap & options,void * out,int argc,const char * const * argv,int * pos,const char * s,size_t len)273 void handle_short_switch(const OptionMap &options, void *out, int argc, const char * const *argv, int *pos, const char *s, size_t len)
274 {
275 for (size_t i = 0; i < len; ++i) {
276 const ArgparseOption *opt = options.find_short(s[i]);
277 if (!opt) {
278 std::cerr << "unrecognized switch '" << s[i] << "' (-" << s << ")\n";
279 throw Error{ ARGPARSE_INVALID_SWITCH };
280 }
281
282 const char *param = nullptr;
283 if (opt_has_param(opt->type)) {
284 if (i < len - 1) {
285 // Parameter in same argument: "-a3".
286 param = s + i + 1;
287 } else if (*pos < argc) {
288 // Parameter as next argument: "-a 3".
289 param = argv[++*pos];
290 }
291
292 // No more switches exist in the same argument.
293 handle_switch(*opt, out, param, false);
294 break;
295 } else {
296 handle_switch(*opt, out, nullptr, false);
297 }
298 }
299 };
300
301
write(const char * s,size_t * len)302 void write(const char *s, size_t *len)
303 {
304 std::cout << s;
305 *len += std::strlen(s);
306 }
307
print_switch(const ArgparseOption & opt)308 void print_switch(const ArgparseOption &opt)
309 {
310 const char *short_name = opt.short_name;
311 const char *long_name = opt.long_name;
312 size_t len = 0;
313
314 std::cout << '\t';
315
316 if (long_name) {
317 if (opt.type == OPTION_FLAG)
318 write("--[no-]", &len);
319 else
320 write("--", &len);
321
322 write(long_name, &len);
323 }
324 if (short_name) {
325 if (long_name)
326 write(" / ", &len);
327
328 write("-", &len);
329 write(short_name, &len);
330 }
331
332 if (opt.description) {
333 for (; len < HELP_INDENT; ++len) {
334 std::cout << ' ';
335 }
336 std::cout << opt.description;
337 }
338 std::cout << '\n';
339 }
340
print_positional(const ArgparseOption & opt)341 void print_positional(const ArgparseOption &opt)
342 {
343 const char *name = get_long_name(opt);
344 size_t len = std::strlen(name);
345
346 std::cout << '\t' << name;
347 if (opt.description) {
348 for (; len < HELP_INDENT; ++len) {
349 std::cout << ' ';
350 }
351 std::cout << opt.description;
352 }
353 std::cout << '\n';
354 }
355
print_help_message(const ArgparseCommandLine & cmd)356 void print_help_message(const ArgparseCommandLine &cmd)
357 {
358 bool has_help = false;
359 bool has_q = false;
360
361 // Print summary.
362 if (cmd.summary)
363 std::cout << cmd.program_name << ": " << cmd.summary << "\n\n";
364
365 // Print short help.
366 std::cout << "Usage: " << cmd.program_name << " [opts] ";
367 for (const auto &opt : OptionRange{ cmd.positional }) {
368 std::cout << get_short_name(opt) << ' ';
369 }
370 std::cout << '\n';
371
372 // Print help for switches.
373 std::cout << "Options:\n";
374 for (const auto &opt : OptionRange{ cmd.switches }) {
375 if (opt.type == OPTION_HELP || (opt.long_name && !std::strcmp(opt.long_name, "help")))
376 has_help = true;
377 if (opt.short_name && opt.short_name[1] == '?')
378 has_q = true;
379
380 print_switch(opt);
381 }
382 // Print built-in help option.
383 if (!has_help) {
384 if (!has_q)
385 print_switch(HELP_OPTION_FULL);
386 else
387 print_switch(HELP_OPTION_LONG_ONLY);
388 }
389
390 // Print help for positional arguments.
391 std::cout << "Arguments:\n";
392 for (const auto &opt : OptionRange{ cmd.positional }) {
393 print_positional(opt);
394 }
395
396 if (cmd.help_message)
397 std::cout << '\n' << cmd.help_message << '\n';
398 }
399
400 } // namespace
401
402
argparse_parse(const ArgparseCommandLine * cmd,void * out,int argc,char ** argv)403 int argparse_parse(const ArgparseCommandLine *cmd, void *out, int argc, char **argv)
404 {
405 int ret = 0;
406
407 try {
408 OptionMap options;
409 bool has_user_help = false;
410
411 OptionIterator positional_cur{ cmd->positional };
412 bool is_positional = false;
413 int pos;
414
415 for (const auto &opt : OptionRange{ cmd->switches }) {
416 if (opt.type == OPTION_HELP || (opt.long_name && !std::strcmp(opt.long_name, "help")))
417 has_user_help = true;
418
419 options.insert_opt(&opt);
420 }
421 if (!has_user_help) {
422 if (!options.find_short('?'))
423 options.insert_opt(&HELP_OPTION_FULL);
424 else
425 options.insert_opt(&HELP_OPTION_LONG_ONLY);
426 }
427
428 for (pos = 1; pos < argc; ++pos) {
429 const char *s = argv[pos];
430 size_t len = std::strlen(s);
431
432 if (!is_positional) {
433 if (len > 2 && s[0] == '-' && s[1] == '-') {
434 // Long form switch: "--[no-]xyz[=123]".
435 handle_long_switch(options, out, argc, argv, &pos, s + 2, len - 2);
436 } else if (len > 1 && s[0] == '-') {
437 // Special end of switches marker: "--".
438 if (s[1] == '-') {
439 is_positional = true;
440 continue;
441 }
442 // Short switch sequence: "-abc".
443 handle_short_switch(options, out, argc, argv, &pos, s + 1, len - 1);
444 } else {
445 is_positional = true;
446 }
447 }
448 if (is_positional) {
449 // End of positional arguments or possible varargs.
450 if (!positional_cur)
451 break;
452
453 // Next positional argument.
454 handle_switch(*positional_cur++, out, s, false);
455 }
456 }
457 if (positional_cur && positional_cur->type != OPTION_NULL) {
458 // Insufficient positional arguments.
459 std::cerr << "expected argument '" << get_long_name(*positional_cur) << "'\n";
460 throw Error{ ARGPARSE_INSUFFICIENT_ARGS };
461 }
462
463 // Successful parse.
464 ret = pos;
465 } catch (const Error &e) {
466 ret = e.code;
467 } catch (const std::logic_error &e) {
468 std::cerr << "malformed command line definition: " << e.what();
469 std::terminate();
470 } catch (const std::exception &e) {
471 std::cerr << "error: " << e.what();
472 ret = ARGPARSE_FATAL;
473 }
474
475 if (ret < 0 && ret != ARGPARSE_FATAL)
476 print_help_message(*cmd);
477
478 return ret;
479 }
480