poafloc

poafloc - Parser Of Arguments For Lines Of Commands
git clone git://git.dimitrijedobrota.com/poafloc.git
Log | Files | Refs | README | LICENSE

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++--------
ACMakeLists.txt | 16++++++++++++++++
DMakefile | 11-----------
Dargs.hpp | 493-------------------------------------------------------------------------------
Ddemo.cpp | 84-------------------------------------------------------------------------------
Ademo/CMakeLists.txt | 13+++++++++++++
Ademo/main.cpp | 85+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Ainclude/args.hpp | 99+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/CMakeLists.txt | 13+++++++++++++
Asrc/args.cpp | 199+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/help.cpp | 182+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/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; +}