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