commit 130362e87a569b184f3dfef590569354de92397a
parent aee752599f659cfc997c7b10dd8f54fc63c30eed
Author: Dimitrije Dobrota <mail@dimitrijedobrota.com>
Date: Mon, 10 Jun 2024 21:35:47 +0200
Switch to CMake and restructure the project
Diffstat:
M | .gitignore | | | 10 | ++-------- |
A | CMakeLists.txt | | | 16 | ++++++++++++++++ |
D | Makefile | | | 11 | ----------- |
D | args.hpp | | | 493 | ------------------------------------------------------------------------------- |
D | demo.cpp | | | 84 | ------------------------------------------------------------------------------- |
A | demo/CMakeLists.txt | | | 13 | +++++++++++++ |
A | demo/main.cpp | | | 85 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | include/args.hpp | | | 99 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/CMakeLists.txt | | | 13 | +++++++++++++ |
A | src/args.cpp | | | 199 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/help.cpp | | | 182 | +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ |
A | src/trie.cpp | | | 38 | ++++++++++++++++++++++++++++++++++++++ |
12 files changed, 647 insertions(+), 596 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,8 +1,2 @@
-*
-
-!Makefile
-!args.hpp
-!demo.cpp
-!.clang-format
-!.gitignore
-
+build
+.cache
diff --git a/CMakeLists.txt b/CMakeLists.txt
@@ -0,0 +1,16 @@
+cmake_minimum_required(VERSION 3.25.2)
+set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
+
+project(
+ args
+ VERSION 0.0.1
+ DESCRIPTION "Command Line Argument Parser"
+ LANGUAGES CXX
+)
+
+set(CMAKE_CXX_STANDARD 20)
+set(CMAKE_CXX_STANDARD_REQUIRED ON)
+set(CMAKE_CXX_EXTENSIONS OFF)
+
+add_subdirectory(src)
+add_subdirectory(demo)
diff --git a/Makefile b/Makefile
@@ -1,11 +0,0 @@
-
-all: demo
-
-demo: demo.cpp args.hpp
- g++ -o $@ $< -std=c++20 -Wall -Werror -ggdb
-
-clean:
- rm -rf demo
-
-
-.PHONY: clean
diff --git a/args.hpp b/args.hpp
@@ -1,493 +0,0 @@
-#ifndef ARGS_HPP
-#define ARGS_HPP
-
-#include <algorithm>
-#include <cstdint>
-#include <cstring>
-#include <exception>
-#include <format>
-#include <iostream>
-#include <sstream>
-#include <unordered_map>
-#include <vector>
-
-class Parser {
- public:
- struct option_t {
- const char *name;
- const int key;
- const char *arg;
- const uint8_t flags;
- const char *message;
- const int group;
- };
-
- enum Option {
- ARG_OPTIONAL = 0x1,
- HIDDEN = 0x2,
- ALIAS = 0x4,
- };
-
- enum Key {
- ARG = 0,
- END = 0x1000001,
- NO_ARGS = 0x1000002,
- INIT = 0x1000003,
- SUCCESS = 0x1000004,
- ERROR = 0x1000005,
- };
-
- struct argp_t {
- using parse_f = int (*)(int key, const char *arg, Parser *parser);
-
- const option_t *options;
- const parse_f parse;
- const char *doc;
- const char *message;
- };
-
- static int parse(argp_t *argp, int argc, char *argv[], void *input) {
- Parser parser(input, argp);
- return parser.parse(argc, argv, &parser);
- }
-
- void *input;
-
- private:
- Parser(void *input, argp_t *argp) : input(input), argp(argp) {
- int group = 0, key_last = 0;
- bool hidden = false;
-
- for (int i = 0; true; i++) {
- const auto &opt = argp->options[i];
- if (!opt.name && !opt.key && !opt.message) break;
-
- if (!opt.name && !opt.key) {
- group = opt.group ? opt.group : group + 1;
- help_entries.emplace_back(nullptr, opt.message, group);
- continue;
- }
-
- if (!opt.key) {
- if ((opt.flags & ALIAS) == 0) {
- std::cerr << "non alias without a key\n";
- throw new std::runtime_error("no key");
- }
-
- if (!key_last) {
- std::cerr << "no option to alias\n";
- throw new std::runtime_error("no alias");
- }
-
- trie.insert(opt.name, key_last);
-
- if (hidden) continue;
- if (opt.flags & Option::HIDDEN) continue;
-
- help_entries.back().push(opt.name);
- } else {
- if (options.count(opt.key)) {
- std::cerr << std::format("duplicate key {}\n", opt.key);
- throw new std::runtime_error("duplicate key");
- }
-
- if (opt.name) trie.insert(opt.name, opt.key);
- options[key_last = opt.key] = &opt;
-
- bool arg_opt = opt.flags & Option::ARG_OPTIONAL;
-
- if ((opt.flags & ALIAS) == 0) {
- if ((hidden = opt.flags & Option::HIDDEN)) continue;
-
- help_entries.emplace_back(opt.arg, opt.message, group,
- arg_opt);
-
- if (opt.name) help_entries.back().push(opt.name);
- if (std::isprint(opt.key)) {
- help_entries.back().push(opt.key);
- }
- } else {
- if (!key_last) {
- std::cerr << "no option to alias\n";
- throw new std::runtime_error("no alias");
- }
-
- if (hidden) continue;
- if (opt.flags & Option::HIDDEN) continue;
-
- if (opt.name) help_entries.back().push(opt.name);
- if (std::isprint(opt.key)) {
- help_entries.back().push(opt.key);
- }
- }
- }
- }
-
- help_entries.emplace_back(nullptr, "Give this help list", -1);
- help_entries.back().push("help");
- help_entries.back().push('?');
-
- help_entries.emplace_back(nullptr, "Give a short usage message", -1);
- help_entries.back().push("usage");
-
- std::sort(begin(help_entries), end(help_entries));
- }
-
- int parse(int argc, char *argv[], void *input) {
- int args = 0, i;
-
- argp->parse(Key::INIT, 0, this);
-
- for (i = 1; i < argc; i++) {
- if (argv[i][0] != '-') {
- argp->parse(Key::ARG, argv[i], this);
- args++;
- continue;
- }
-
- // stop parsing options, rest are normal arguments
- if (!std::strcmp(argv[i], "--")) break;
-
- if (argv[i][1] != '-') { // short option
- const char *opt = argv[i] + 1;
-
- // loop over ganged options
- for (int j = 0; opt[j]; j++) {
- const char key = opt[j];
-
- if (key == '?') help(argv[0]);
-
- if (!options.count(key)) goto unknown;
- const auto *option = options[key];
-
- const char *arg = nullptr;
- if (option->arg) {
- if (opt[j + 1] != 0) {
- // rest of the line is option argument
- arg = opt + j + 1;
- } else if ((option->flags & ARG_OPTIONAL) == 0) {
- // next argv is option argument
- if (i == argc) goto missing;
- arg = argv[++i];
- }
- }
-
- argp->parse(key, arg, this);
-
- // if last option required argument we are done
- if (arg) break;
- }
- } else { // long option
- const char *opt = argv[i] + 2;
- const auto eq = std::strchr(opt, '=');
-
- std::string opt_s = !eq ? opt : std::string(opt, eq - opt);
-
- if (opt_s == "help") {
- if (eq) goto excess;
- help(argv[0]);
- }
-
- if (opt_s == "usage") {
- if (eq) goto excess;
- usage(argv[0]);
- }
-
- const int key = trie.get(opt_s);
-
- if (!key) goto unknown;
-
- const auto *option = options[key];
- const char *arg = nullptr;
-
- if (!option->arg && eq) goto excess;
- if (option->arg) {
- if (eq) {
- // everything after = is option argument
- arg = eq + 1;
- } else if ((option->flags & ARG_OPTIONAL) == 0) {
- // next argv is option argument
- if (i == argc) goto missing;
- arg = argv[++i];
- }
- }
-
- argp->parse(key, arg, this);
- }
- }
-
- // parse rest argv as normal arguments
- for (i = i + 1; i < argc; i++) {
- argp->parse(Key::ARG, argv[i], this);
- args++;
- }
-
- if (!args) argp->parse(Key::NO_ARGS, 0, this);
-
- argp->parse(Key::END, 0, this);
- argp->parse(Key::SUCCESS, 0, this);
-
- return 0;
-
- unknown:
- std::cerr << std::format("unknown option {}\n", argv[i]);
- argp->parse(Key::ERROR, 0, this);
- return 1;
-
- missing:
- std::cerr << std::format("option {} missing a value\n", argv[i]);
- argp->parse(Key::ERROR, 0, this);
- return 2;
-
- excess:
- std::cerr << std::format("option {} don't require a value\n", argv[i]);
- argp->parse(Key::ERROR, 0, this);
- return 3;
- }
-
- struct help_entry_t {
- std::vector<const char *> opt_long;
- std::vector<char> opt_short;
-
- const char *arg;
- const char *message;
- int group;
- bool opt;
-
- help_entry_t(const char *arg, const char *message, int group,
- bool opt = false)
- : arg(arg), message(message), group(group), opt(opt) {}
-
- void push(char sh) { opt_short.push_back(sh); }
- void push(const char *lg) { opt_long.push_back(lg); }
-
- bool operator<(const help_entry_t &rhs) const {
- if (group != rhs.group) {
- if (group && rhs.group) {
- if (group < 0 && rhs.group < 0) return group < rhs.group;
- if (group < 0 || rhs.group < 0) return rhs.group < 0;
- return group < rhs.group;
- }
-
- return !group;
- }
-
- const char l1 = !opt_long.empty() ? opt_long.front()[0]
- : !opt_short.empty() ? opt_short.front()
- : '0';
-
- const char l2 = !rhs.opt_long.empty() ? rhs.opt_long.front()[0]
- : !rhs.opt_short.empty() ? rhs.opt_short.front()
- : '0';
-
- if (l1 != l2) return l1 < l2;
-
- return std::strcmp(opt_long.front(), rhs.opt_long.front()) < 0;
- }
- };
-
- void print_usage(const char *name) const {
- if (argp->doc) {
- std::istringstream iss(argp->doc);
- std::string s;
-
- std::getline(iss, s, '\n');
- std::cout << " " << s;
-
- while (std::getline(iss, s, '\n')) {
- std::cout << std::format("\n or: {} [OPTIONS...] {}", name,
- s);
- }
- }
- }
-
- void help(const char *name) const {
- std::string m1, m2;
- if (argp->message) {
- std::istringstream iss(argp->message);
- std::getline(iss, m1, '\v');
- std::getline(iss, m2, '\v');
- }
-
- std::cout << std::format("Usage: {} [OPTIONS...]", name);
- print_usage(name);
- if (!m1.empty()) std::cout << "\n" << m1;
- std::cout << "\n\n";
-
- bool first = true;
- for (const auto &entry : help_entries) {
- bool prev = false;
-
- if (entry.opt_short.empty() && entry.opt_long.empty()) {
- if (!first) std::cout << "\n";
- if (entry.message) std::cout << " " << entry.message << ":\n";
- continue;
- }
-
- first = false;
-
- std::string message = " ";
- for (const char c : entry.opt_short) {
- if (!prev) prev = true;
- else message += ", ";
-
- message += std::format("-{}", c);
-
- if (!entry.arg || !entry.opt_long.empty()) continue;
-
- if (entry.opt) message += std::format("[{}]", entry.arg);
- else message += std::format(" {}", entry.arg);
- }
-
- if (!prev) message += " ";
-
- for (const auto l : entry.opt_long) {
- if (!prev) prev = true;
- else message += ", ";
-
- message += std::format("--{}", l);
-
- if (!entry.arg) continue;
-
- if (entry.opt) message += std::format("[={}]", entry.arg);
- else message += std::format("={}", entry.arg);
- }
-
- static const std::size_t limit = 30;
- if (size(message) < limit) {
- message += std::string(limit - size(message), ' ');
- }
-
- std::cout << message;
-
- if (entry.message) {
- std::istringstream iss(entry.message);
- std::size_t count = 0;
- std::string s;
-
- std::cout << " ";
- while (iss >> s) {
- count += size(s);
- if (count > limit) {
- std::cout << std::endl << std::string(limit + 5, ' ');
- count = size(s);
- }
- std::cout << s << " ";
- }
- }
- std::cout << std::endl;
- }
-
- if (!m2.empty()) std::cout << "\n" << m2 << "\n";
-
- exit(0);
- }
-
- void usage(const char *name) const {
- static const std::size_t limit = 60;
- static std::size_t count = 0;
-
- static const auto print = [](const std::string &message) {
- if (count + size(message) > limit) {
- std::cout << "\n ";
- count = 6;
- }
- std::cout << message;
- count += size(message);
- };
-
- std::string message = std::format("Usage: {}", name);
-
- message += " [-";
- for (const auto &entry : help_entries) {
- if (entry.arg) continue;
- for (const char c : entry.opt_short) {
- message += c;
- }
- }
- message += "]";
-
- std::cout << message;
- count = size(message);
-
- for (const auto &entry : help_entries) {
- if (!entry.arg) continue;
- for (const char c : entry.opt_short) {
- if (entry.opt) print(std::format(" [-{}[{}]]", c, entry.arg));
- else print(std::format(" [-{} {}]", c, entry.arg));
- }
- }
-
- for (const auto &entry : help_entries) {
- for (const char *name : entry.opt_long) {
- if (!entry.arg) {
- print(std::format(" [--{}]", name));
- continue;
- }
-
- if (entry.opt) {
- print(std::format(" [--{}[={}]]", name, entry.arg));
- } else {
- print(std::format(" [--{}={}]", name, entry.arg));
- }
- }
- }
-
- print_usage(name);
- std::cout << std::endl;
-
- exit(0);
- }
-
- class trie_t {
- public:
- ~trie_t() noexcept {
- for (uint8_t i = 0; i < 26; i++) {
- delete children[i];
- }
- }
-
- void insert(const std::string &option, int key) {
- trie_t *crnt = this;
-
- for (const char c : option) {
- if (!crnt->terminal) crnt->key = key;
- crnt->count++;
-
- const uint8_t idx = c - 'a';
- if (!crnt->children[idx]) crnt->children[idx] = new trie_t();
- crnt = crnt->children[idx];
- }
-
- crnt->terminal = true;
- crnt->key = key;
- }
-
- int get(const std::string &option) const {
- const trie_t *crnt = this;
-
- for (const char c : option) {
- const uint8_t idx = c - 'a';
- if (!crnt->children[idx]) return 0;
- crnt = crnt->children[idx];
- }
-
- if (!crnt->terminal && crnt->count > 1) return 0;
- return crnt->key;
- }
-
- private:
- trie_t *children[26] = {0};
- uint8_t count = 0;
- int key = 0;
- bool terminal = false;
- };
-
- const argp_t *argp;
-
- std::unordered_map<int, const option_t *> options;
- std::vector<help_entry_t> help_entries;
- trie_t trie;
-};
-
-#endif
diff --git a/demo.cpp b/demo.cpp
@@ -1,84 +0,0 @@
-#include "args.hpp"
-
-#include <cstdint>
-#include <vector>
-
-void error(const std::string &message) { std::cerr << message << std::endl; }
-struct arguments_t {
- const char *output_file = "";
- const char *input_file = "";
-
- bool debug = 0;
- bool hex = 0;
- bool relocatable = 0;
-
- std::vector<const char *> args;
-};
-
-int parse_opt(int key, const char *arg, Parser *parser) {
- auto arguments = (arguments_t *)parser->input;
-
- switch (key) {
- case 777: arguments->debug = true; break;
- case 'h':
- if (arguments->relocatable) error("cannot mix -hex and -relocatable");
- arguments->hex = true;
- break;
- case 'r':
- if (arguments->hex) error("cannot mix -hex and -relocatable");
- arguments->relocatable = true;
- break;
- case 'o': arguments->output_file = arg ? arg : "stdout"; break;
- case 'i': arguments->input_file = arg; break;
- case Parser::Key::ARG: arguments->args.push_back(arg); break;
- case Parser::Key::ERROR: std::cerr << "handled error\n";
- }
-
- return 0;
-}
-
-using enum Parser::Option;
-
-// clang-format off
-static const Parser::option_t options[] = {
- { 0, 'R', 0, 0, "random 0-group option"},
- { 0, 0, 0, 0, "Program mode", 1},
- {"relocatable", 'r', 0, 0, "Output in relocatable format"},
- { "hex", 'h', 0, 0, "Output in hex format"},
- {"hexadecimal", 0, 0, ALIAS | HIDDEN},
- { 0, 0, 0, 0, "For developers", 4},
- { "debug", 777, 0, 0, "Enable debugging mode"},
- { 0, 0, 0, 0, "Input/output", 3},
- { "output", 'o', "file", ARG_OPTIONAL, "Output file, default stdout"},
- { 0, 'i', "file", 0, "Input file"},
- { 0, 0, 0, 0, "Informational Options", -1},
- {0},
-};
-// clang-format on
-
-int main(int argc, char *argv[]) {
- arguments_t arguments;
- Parser::argp_t argp = {
- options, parse_opt, "doc string\nother usage",
- "First half of the message\vsecond half of the message"};
-
- if (Parser::parse(&argp, argc, argv, &arguments)) {
- error("There was an error while parsing arguments");
- return 1;
- }
-
- std::cout << "Command line options: " << std::endl;
-
- std::cout << "\t input: " << arguments.input_file << std::endl;
- std::cout << "\t output: " << arguments.output_file << std::endl;
- std::cout << "\t hex: " << arguments.hex << std::endl;
- std::cout << "\t debug: " << arguments.debug << std::endl;
- std::cout << "\t relocatable: " << arguments.relocatable << std::endl;
-
- std::cout << "\t args: ";
- for (const auto &arg : arguments.args)
- std::cout << arg << " ";
- std::cout << std::endl;
-
- return 0;
-}
diff --git a/demo/CMakeLists.txt b/demo/CMakeLists.txt
@@ -0,0 +1,13 @@
+set(GENERATE_OUT "${CMAKE_BINARY_DIR}/bin")
+
+add_executable(demo
+ main.cpp
+)
+
+target_link_libraries(demo PRIVATE args)
+
+set_target_properties(demo PROPERTIES
+ VERSION ${PROJECT_VERSION}
+ SOVERSION ${PROJECT_VERSION_MAJOR}
+ RUNTIME_OUTPUT_DIRECTORY "${GENERATE_OUT}"
+)
diff --git a/demo/main.cpp b/demo/main.cpp
@@ -0,0 +1,85 @@
+#include "args.hpp"
+
+#include <cstdint>
+#include <iostream>
+#include <vector>
+
+void error(const std::string &message) { std::cerr << message << std::endl; }
+struct arguments_t {
+ const char *output_file = "";
+ const char *input_file = "";
+
+ bool debug = 0;
+ bool hex = 0;
+ bool relocatable = 0;
+
+ std::vector<const char *> args;
+};
+
+int parse_opt(int key, const char *arg, Parser *parser) {
+ auto arguments = (arguments_t *)parser->input;
+
+ switch (key) {
+ case 777: arguments->debug = true; break;
+ case 'h':
+ if (arguments->relocatable) error("cannot mix -hex and -relocatable");
+ arguments->hex = true;
+ break;
+ case 'r':
+ if (arguments->hex) error("cannot mix -hex and -relocatable");
+ arguments->relocatable = true;
+ break;
+ case 'o': arguments->output_file = arg ? arg : "stdout"; break;
+ case 'i': arguments->input_file = arg; break;
+ case Parser::Key::ARG: arguments->args.push_back(arg); break;
+ case Parser::Key::ERROR: std::cerr << "handled error\n";
+ }
+
+ return 0;
+}
+
+using enum Parser::Option;
+
+// clang-format off
+static const Parser::option_t options[] = {
+ { 0, 'R', 0, 0, "random 0-group option"},
+ { 0, 0, 0, 0, "Program mode", 1},
+ {"relocatable", 'r', 0, 0, "Output in relocatable format"},
+ { "hex", 'h', 0, 0, "Output in hex format"},
+ {"hexadecimal", 0, 0, ALIAS | HIDDEN},
+ { 0, 0, 0, 0, "For developers", 4},
+ { "debug", 777, 0, 0, "Enable debugging mode"},
+ { 0, 0, 0, 0, "Input/output", 3},
+ { "output", 'o', "file", ARG_OPTIONAL, "Output file, default stdout"},
+ { 0, 'i', "file", 0, "Input file"},
+ { 0, 0, 0, 0, "Informational Options", -1},
+ {0},
+};
+// clang-format on
+
+int main(int argc, char *argv[]) {
+ arguments_t arguments;
+ Parser::argp_t argp = {
+ options, parse_opt, "doc string\nother usage",
+ "First half of the message\vsecond half of the message"};
+
+ if (Parser::parse(&argp, argc, argv, &arguments)) {
+ error("There was an error while parsing arguments");
+ return 1;
+ }
+
+ std::cout << "Command line options: " << std::endl;
+
+ std::cout << "\t input: " << arguments.input_file << std::endl;
+ std::cout << "\t output: " << arguments.output_file << std::endl;
+ std::cout << "\t hex: " << arguments.hex << std::endl;
+ std::cout << "\t debug: " << arguments.debug << std::endl;
+ std::cout << "\t relocatable: " << arguments.relocatable << std::endl;
+
+ std::cout << "\t args: ";
+ for (const auto &arg : arguments.args)
+ std::cout << arg << " ";
+ std::cout << std::endl;
+
+ return 0;
+}
diff --git a/include/args.hpp b/include/args.hpp
@@ -0,0 +1,99 @@
+#ifndef ARGS_HPP
+#define ARGS_HPP
+
+#include <string>
+#include <unordered_map>
+#include <vector>
+
+class Parser {
+ public:
+ struct option_t {
+ const char *name;
+ const int key;
+ const char *arg;
+ const int flags;
+ const char *message;
+ const int group;
+ };
+
+ enum Option {
+ ARG_OPTIONAL = 0x1,
+ HIDDEN = 0x2,
+ ALIAS = 0x4,
+ };
+
+ enum Key {
+ ARG = 0,
+ END = 0x1000001,
+ NO_ARGS = 0x1000002,
+ INIT = 0x1000003,
+ SUCCESS = 0x1000004,
+ ERROR = 0x1000005,
+ };
+
+ struct argp_t {
+ using parse_f = int (*)(int key, const char *arg, Parser *parser);
+
+ const option_t *options;
+ const parse_f parse;
+ const char *doc;
+ const char *message;
+ };
+
+ static int parse(argp_t *argp, int argc, char *argv[], void *input) {
+ Parser parser(input, argp);
+ return parser.parse(argc, argv, &parser);
+ }
+
+ void *input;
+
+ private:
+ Parser(void *input, argp_t *argp);
+
+ int parse(int argc, char *argv[], void *input);
+
+ void print_usage(const char *name) const;
+ void help(const char *name) const;
+ void usage(const char *name) const;
+
+ struct help_entry_t {
+ help_entry_t(const char *arg, const char *message, int group,
+ bool opt = false)
+ : arg(arg), message(message), group(group), opt(opt) {}
+
+ void push(char sh) { opt_short.push_back(sh); }
+ void push(const char *lg) { opt_long.push_back(lg); }
+
+ bool operator<(const help_entry_t &rhs) const;
+
+ const char *arg;
+ const char *message;
+ int group;
+ bool opt;
+
+ std::vector<const char *> opt_long;
+ std::vector<char> opt_short;
+ };
+
+ class trie_t {
+ public:
+ ~trie_t() noexcept;
+
+ void insert(const std::string &option, int key);
+ int get(const std::string &option) const;
+
+ private:
+ trie_t *children[26] = {0};
+ int count = 0;
+ int key = 0;
+ bool terminal = false;
+ };
+
+ const argp_t *argp;
+
+ std::unordered_map<int, const option_t *> options;
+ std::vector<help_entry_t> help_entries;
+ trie_t trie;
+};
+
+#endif
diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt
@@ -0,0 +1,13 @@
+add_library(args args.cpp help.cpp trie.cpp)
+target_include_directories(args PUBLIC ../include)
+
+set_target_properties(args PROPERTIES
+ VERSION ${PROJECT_VERSION}
+ SOVERSION ${PROJECT_VERSION_MAJOR}
+ PUBLIC_HEADER ../include/args.hpp
+)
+
+install(TARGETS args
+ LIBRARY DESTINATION lib
+ PUBLIC_HEADER DESTINATION include
+)
diff --git a/src/args.cpp b/src/args.cpp
@@ -0,0 +1,199 @@
+#include "args.hpp"
+
+#include <algorithm>
+#include <cstring>
+#include <exception>
+#include <format>
+#include <iostream>
+#include <sstream>
+
+Parser::Parser(void *input, argp_t *argp) : input(input), argp(argp) {
+ int group = 0, key_last = 0;
+ bool hidden = false;
+
+ for (int i = 0; true; i++) {
+ const auto &opt = argp->options[i];
+ if (!opt.name && !opt.key && !opt.message) break;
+
+ if (!opt.name && !opt.key) {
+ group = opt.group ? opt.group : group + 1;
+ help_entries.emplace_back(nullptr, opt.message, group);
+ continue;
+ }
+
+ if (!opt.key) {
+ if ((opt.flags & ALIAS) == 0) {
+ std::cerr << "non alias without a key\n";
+ throw new std::runtime_error("no key");
+ }
+
+ if (!key_last) {
+ std::cerr << "no option to alias\n";
+ throw new std::runtime_error("no alias");
+ }
+
+ trie.insert(opt.name, key_last);
+
+ if (hidden) continue;
+ if (opt.flags & Option::HIDDEN) continue;
+
+ help_entries.back().push(opt.name);
+ } else {
+ if (options.count(opt.key)) {
+ std::cerr << std::format("duplicate key {}\n", opt.key);
+ throw new std::runtime_error("duplicate key");
+ }
+
+ if (opt.name) trie.insert(opt.name, opt.key);
+ options[key_last = opt.key] = &opt;
+
+ bool arg_opt = opt.flags & Option::ARG_OPTIONAL;
+
+ if ((opt.flags & ALIAS) == 0) {
+ if ((hidden = opt.flags & Option::HIDDEN)) continue;
+
+ help_entries.emplace_back(opt.arg, opt.message, group,
+ arg_opt);
+
+ if (opt.name) help_entries.back().push(opt.name);
+ if (std::isprint(opt.key)) {
+ help_entries.back().push(opt.key);
+ }
+ } else {
+ if (!key_last) {
+ std::cerr << "no option to alias\n";
+ throw new std::runtime_error("no alias");
+ }
+
+ if (hidden) continue;
+ if (opt.flags & Option::HIDDEN) continue;
+
+ if (opt.name) help_entries.back().push(opt.name);
+ if (std::isprint(opt.key)) {
+ help_entries.back().push(opt.key);
+ }
+ }
+ }
+ }
+
+ help_entries.emplace_back(nullptr, "Give this help list", -1);
+ help_entries.back().push("help");
+ help_entries.back().push('?');
+
+ help_entries.emplace_back(nullptr, "Give a short usage message", -1);
+ help_entries.back().push("usage");
+
+ std::sort(begin(help_entries), end(help_entries));
+}
+
+int Parser::parse(int argc, char *argv[], void *input) {
+ int args = 0, i;
+
+ argp->parse(Key::INIT, 0, this);
+
+ for (i = 1; i < argc; i++) {
+ if (argv[i][0] != '-') {
+ argp->parse(Key::ARG, argv[i], this);
+ args++;
+ continue;
+ }
+
+ // stop parsing options, rest are normal arguments
+ if (!std::strcmp(argv[i], "--")) break;
+
+ if (argv[i][1] != '-') { // short option
+ const char *opt = argv[i] + 1;
+
+ // loop over ganged options
+ for (int j = 0; opt[j]; j++) {
+ const char key = opt[j];
+
+ if (key == '?') help(argv[0]);
+
+ if (!options.count(key)) goto unknown;
+ const auto *option = options[key];
+
+ const char *arg = nullptr;
+ if (option->arg) {
+ if (opt[j + 1] != 0) {
+ // rest of the line is option argument
+ arg = opt + j + 1;
+ } else if ((option->flags & ARG_OPTIONAL) == 0) {
+ // next argv is option argument
+ if (i == argc) goto missing;
+ arg = argv[++i];
+ }
+ }
+
+ argp->parse(key, arg, this);
+
+ // if last option required argument we are done
+ if (arg) break;
+ }
+ } else { // long option
+ const char *opt = argv[i] + 2;
+ const auto eq = std::strchr(opt, '=');
+
+ std::string opt_s = !eq ? opt : std::string(opt, eq - opt);
+
+ if (opt_s == "help") {
+ if (eq) goto excess;
+ help(argv[0]);
+ }
+
+ if (opt_s == "usage") {
+ if (eq) goto excess;
+ usage(argv[0]);
+ }
+
+ const int key = trie.get(opt_s);
+
+ if (!key) goto unknown;
+
+ const auto *option = options[key];
+ const char *arg = nullptr;
+
+ if (!option->arg && eq) goto excess;
+ if (option->arg) {
+ if (eq) {
+ // everything after = is option argument
+ arg = eq + 1;
+ } else if ((option->flags & ARG_OPTIONAL) == 0) {
+ // next argv is option argument
+ if (i == argc) goto missing;
+ arg = argv[++i];
+ }
+ }
+
+ argp->parse(key, arg, this);
+ }
+ }
+
+ // parse rest argv as normal arguments
+ for (i = i + 1; i < argc; i++) {
+ argp->parse(Key::ARG, argv[i], this);
+ args++;
+ }
+
+ if (!args) argp->parse(Key::NO_ARGS, 0, this);
+
+ argp->parse(Key::END, 0, this);
+ argp->parse(Key::SUCCESS, 0, this);
+
+ return 0;
+
+unknown:
+ std::cerr << std::format("unknown option {}\n", argv[i]);
+ argp->parse(Key::ERROR, 0, this);
+ return 1;
+
+missing:
+ std::cerr << std::format("option {} missing a value\n", argv[i]);
+ argp->parse(Key::ERROR, 0, this);
+ return 2;
+
+excess:
+ std::cerr << std::format("option {} don't require a value\n", argv[i]);
+ argp->parse(Key::ERROR, 0, this);
+ return 3;
+}
diff --git a/src/help.cpp b/src/help.cpp
@@ -0,0 +1,182 @@
+#include "args.hpp"
+
+#include <cstring>
+#include <format>
+#include <iostream>
+#include <sstream>
+
+bool Parser::help_entry_t::operator<(const help_entry_t &rhs) const {
+ if (group != rhs.group) {
+ if (group && rhs.group) {
+ if (group < 0 && rhs.group < 0) return group < rhs.group;
+ if (group < 0 || rhs.group < 0) return rhs.group < 0;
+ return group < rhs.group;
+ }
+
+ return !group;
+ }
+
+ const char l1 = !opt_long.empty() ? opt_long.front()[0]
+ : !opt_short.empty() ? opt_short.front()
+ : '0';
+
+ const char l2 = !rhs.opt_long.empty() ? rhs.opt_long.front()[0]
+ : !rhs.opt_short.empty() ? rhs.opt_short.front()
+ : '0';
+
+ if (l1 != l2) return l1 < l2;
+
+ return std::strcmp(opt_long.front(), rhs.opt_long.front()) < 0;
+}
+
+void Parser::print_usage(const char *name) const {
+ if (argp->doc) {
+ std::istringstream iss(argp->doc);
+ std::string s;
+
+ std::getline(iss, s, '\n');
+ std::cout << " " << s;
+
+ while (std::getline(iss, s, '\n')) {
+ std::cout << std::format("\n or: {} [OPTIONS...] {}", name, s);
+ }
+ }
+}
+
+void Parser::help(const char *name) const {
+ std::string m1, m2;
+ if (argp->message) {
+ std::istringstream iss(argp->message);
+ std::getline(iss, m1, '\v');
+ std::getline(iss, m2, '\v');
+ }
+
+ std::cout << std::format("Usage: {} [OPTIONS...]", name);
+ print_usage(name);
+ if (!m1.empty()) std::cout << "\n" << m1;
+ std::cout << "\n\n";
+
+ bool first = true;
+ for (const auto &entry : help_entries) {
+ bool prev = false;
+
+ if (entry.opt_short.empty() && entry.opt_long.empty()) {
+ if (!first) std::cout << "\n";
+ if (entry.message) std::cout << " " << entry.message << ":\n";
+ continue;
+ }
+
+ first = false;
+
+ std::string message = " ";
+ for (const char c : entry.opt_short) {
+ if (!prev) prev = true;
+ else message += ", ";
+
+ message += std::format("-{}", c);
+
+ if (!entry.arg || !entry.opt_long.empty()) continue;
+
+ if (entry.opt) message += std::format("[{}]", entry.arg);
+ else message += std::format(" {}", entry.arg);
+ }
+
+ if (!prev) message += " ";
+
+ for (const auto l : entry.opt_long) {
+ if (!prev) prev = true;
+ else message += ", ";
+
+ message += std::format("--{}", l);
+
+ if (!entry.arg) continue;
+
+ if (entry.opt) message += std::format("[={}]", entry.arg);
+ else message += std::format("={}", entry.arg);
+ }
+
+ static const std::size_t limit = 30;
+ if (size(message) < limit) {
+ message += std::string(limit - size(message), ' ');
+ }
+
+ std::cout << message;
+
+ if (entry.message) {
+ std::istringstream iss(entry.message);
+ std::size_t count = 0;
+ std::string s;
+
+ std::cout << " ";
+ while (iss >> s) {
+ count += size(s);
+ if (count > limit) {
+ std::cout << std::endl << std::string(limit + 5, ' ');
+ count = size(s);
+ }
+ std::cout << s << " ";
+ }
+ }
+ std::cout << std::endl;
+ }
+
+ if (!m2.empty()) std::cout << "\n" << m2 << "\n";
+
+ exit(0);
+}
+
+void Parser::usage(const char *name) const {
+ static const std::size_t limit = 60;
+ static std::size_t count = 0;
+
+ static const auto print = [](const std::string &message) {
+ if (count + size(message) > limit) {
+ std::cout << "\n ";
+ count = 6;
+ }
+ std::cout << message;
+ count += size(message);
+ };
+
+ std::string message = std::format("Usage: {}", name);
+
+ message += " [-";
+ for (const auto &entry : help_entries) {
+ if (entry.arg) continue;
+ for (const char c : entry.opt_short) {
+ message += c;
+ }
+ }
+ message += "]";
+
+ std::cout << message;
+ count = size(message);
+
+ for (const auto &entry : help_entries) {
+ if (!entry.arg) continue;
+ for (const char c : entry.opt_short) {
+ if (entry.opt) print(std::format(" [-{}[{}]]", c, entry.arg));
+ else print(std::format(" [-{} {}]", c, entry.arg));
+ }
+ }
+
+ for (const auto &entry : help_entries) {
+ for (const char *name : entry.opt_long) {
+ if (!entry.arg) {
+ print(std::format(" [--{}]", name));
+ continue;
+ }
+
+ if (entry.opt) {
+ print(std::format(" [--{}[={}]]", name, entry.arg));
+ } else {
+ print(std::format(" [--{}={}]", name, entry.arg));
+ }
+ }
+ }
+
+ print_usage(name);
+ std::cout << std::endl;
+
+ exit(0);
+}
diff --git a/src/trie.cpp b/src/trie.cpp
@@ -0,0 +1,38 @@
+#include "args.hpp"
+
+#include <cstdint>
+
+Parser::trie_t::~trie_t() noexcept {
+ for (uint8_t i = 0; i < 26; i++) {
+ delete children[i];
+ }
+}
+
+void Parser::trie_t::insert(const std::string &option, int key) {
+ trie_t *crnt = this;
+
+ for (const char c : option) {
+ if (!crnt->terminal) crnt->key = key;
+ crnt->count++;
+
+ const uint8_t idx = c - 'a';
+ if (!crnt->children[idx]) crnt->children[idx] = new trie_t();
+ crnt = crnt->children[idx];
+ }
+
+ crnt->terminal = true;
+ crnt->key = key;
+}
+
+int Parser::trie_t::get(const std::string &option) const {
+ const trie_t *crnt = this;
+
+ for (const char c : option) {
+ const uint8_t idx = c - 'a';
+ if (!crnt->children[idx]) return 0;
+ crnt = crnt->children[idx];
+ }
+
+ if (!crnt->terminal && crnt->count > 1) return 0;
+ return crnt->key;
+}