/* 88888888 888888888888 88888888888888 8888888888888888 888888888888888888 888888 8888 888888 88888 88 88888 888888 8888 888888 88888888888888888888 88888888888888888888 8888888888888888888888 8888888888888888888888888888 88888888888888888888888888888888 88888888888888888888 888888888888888888888888 888888 8888888888 888888 888 8888 8888 888 888 888 OCTOBANANA Licensed under the MIT License Copyright (c) 2018-2020 Brett Robinson Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ #ifndef OB_PARG_HH #define OB_PARG_HH #include "ob/algorithm.hh" #include "ob/string.hh" #include "ob/term.hh" namespace iom = OB::Term::iomanip; namespace aec = OB::Term::ANSI_Escape_Codes; #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace OB { class Parg { public: struct Style { std::string h1 {aec::fg_magenta + aec::underline}; std::string h2 {aec::fg_green}; std::string p {aec::fg_white_bright}; std::string pa {aec::fg_white}; std::string opt {aec::fg_green}; std::string arg {aec::fg_yellow}; std::string val {aec::fg_cyan}; std::string success {aec::fg_green}; std::string error {aec::fg_red}; } style; struct Section { std::string head; std::vector> body; }; struct Section_Command { std::string head; std::vector>>> body; }; using Output = std::function; Parg() { } Parg(int _argc, char** _argv) { argc_ = _argc; argvf(_argv); } bool color() { return color_; } Parg& color(bool const val) { color_ = val; return *this; } Parg& name(std::string const& _name) { name_ = _name; return *this; } std::string name() const { return name_; } Parg& version(std::string const& _version) { version_ = _version; return *this; } std::string version() const { if (version_.empty()) { return {}; } std::ostringstream out; OB::Term::ostream os {out}; ostream_init(os); os << aec::wrap(name_, style.h2) << " " << aec::wrap(version_, style.p) #ifdef DEBUG << " " << aec::wrap("DEBUG", style.error) #endif << "\n"; return out.str(); } Parg& usage(std::string const& _usage) { usage_.emplace_back(name_ + " " + _usage); return *this; } std::string usage() const { if (usage_.empty()) { return {}; } std::ostringstream out; OB::Term::ostream os {out}; ostream_init(os); os << aec::wrap("Usage", style.h1) << iom::push(); bool alt {false}; for (auto const& e : usage_) { os << aec::wrap(e, (alt ? style.pa : style.p)) << "\n"; alt = ! alt; } os << iom::pop(); return out.str(); } Parg& description(std::string const& _description) { description_ = _description; return *this; } std::string description() const { return description_; } Parg& info(Output const& out) { info_.emplace_back(out); return *this; } Parg& info(Section const& s) { info([s](auto& os, auto const& st) { os << aec::wrap(s.head, st.h1) << iom::push(); bool alt {false}; for (auto const& [k, v] : s.body) { if (k.empty()) { os << aec::wrap(v, (alt ? st.pa : st.p)) << "\n"; } else { OB::Algorithm::for_each(OB::String::split_view(k, ", "), [&os, &st](auto const& obj) { os << st.h2 << obj << st.p << ", "; }, [&os, &st](auto const& obj) { os << st.h2 << obj; }); os << aec::clear << iom::push() << aec::wrap(v, (alt ? st.pa : st.p)) << iom::pop(); } alt = ! alt; } os << "\n" << iom::pop(); }); return *this; } Parg& info(Section_Command const& s) { info([s](auto& os, auto const& st) { os << aec::wrap(s.head, st.h1) << iom::push(); bool alt {false}; for (auto const& [k, v] : s.body) { auto const cmd = OB::String::split_view(k, " ", 2); os << iom::word_break(false) << aec::wrap(cmd.at(0), st.h2); if (cmd.size() == 2) { os << " " << aec::wrap(cmd.at(1), (v.at(0).first.empty() ? st.arg : st.val)); } else if (cmd.size() >= 3) { os << " " << aec::wrap(cmd.at(1), st.val) << " " << aec::wrap(cmd.at(2), st.arg); } os << iom::word_break(true) << iom::push(); for (auto const& [k1, v1] : v) { if (k1.empty()) { os << aec::wrap(v1, (alt ? st.pa : st.p)) << iom::pop(); } else { os << aec::wrap(k1, st.val) << iom::push() << aec::wrap(v1, (alt ? st.pa : st.p)) << iom::pop(); } alt = ! alt; } if (! v.at(0).first.empty()) { os << iom::pop(); } } os << "\n" << iom::pop(); }); return *this; } Parg& author(std::string const& _author) { author_ = _author; return *this; } std::string author() const { return author_; } std::string license() const { std::ostringstream out; OB::Term::ostream os {out}; ostream_init(os); os << aec::wrap("MIT License", style.h1) << "\n\n" << aec::wrap("Copyright (c) 2019 Brett Robinson", style.h2) << "\n\n" << iom::first_wrap(true) << style.p << R"(Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.)" << "\n"; return out.str(); } void ostream_init(OB::Term::ostream& os) const { if (OB::Term::is_term(STDOUT_FILENO)) { std::size_t width {0}; OB::Term::width(width, STDOUT_FILENO); os.width(width); os.line_wrap(true); } else { os.line_wrap(false); } // uncomment when generating a new help.txt file // os.width(80); // os.line_wrap(true); os.escape_codes(color_); os.indent(2); os.first_wrap(false); os.white_space(false); } std::string help() const { std::ostringstream out; OB::Term::ostream os {out}; ostream_init(os); if (! description_.empty()) { os << aec::wrap(name_, style.h1) << iom::push() << aec::wrap(description_, style.p) << iom::pop(); } if (! usage_.empty()) { os << "\n" << aec::wrap("Usage", style.h1) << iom::push(); bool alt {false}; for (auto const& e : usage_) { os << aec::wrap(e, (alt ? style.pa : style.p)) << "\n"; alt = ! alt; } os << "\n" << iom::pop(); } // options if (! data_.empty()) { os << aec::wrap("Options", style.h1) << iom::push(); bool alt {false}; for (auto const& v : data_) { auto const& e = v.second; if (! e.short_.empty()) { os << aec::wrap("-" + e.short_, style.opt); } if (! e.short_.empty() && ! e.long_.empty()) { os << aec::wrap(", ", style.p); } if (! e.long_.empty()) { os << aec::wrap("--" + e.long_, style.opt); } if (! e.mode_) { os << aec::wrap("=", style.p) << aec::wrap("<" + e.arg_ + ">", style.arg) << aec::wrap(" [" + e.value_ + "]", style.val); } os << iom::push() << aec::wrap(e.info_, (alt ? style.pa : style.p)) << iom::pop(); alt = ! alt; } os << "\n" << iom::pop(); } if (! info_.empty()) { for (auto const& e : info_) { e(os, style); } } if (! author_.empty()) { os << aec::wrap("Author", style.h1) << iom::push() << aec::wrap(author_, style.p) << iom::pop(); } return out.str(); } int parse() { if (is_stdin_) { pipe_stdin(); } status_ = parse_args(argc_, argv_); return status_; } int parse(int argc, char** argv) { if (is_stdin_) { pipe_stdin(); } argc_ = argc; argvf(argv); status_ = parse_args(argc_, argv_); return status_; } int parse(std::string const& str) { auto args = str_to_args(str); status_ = parse_args(args.size(), args); return status_; } std::vector str_to_args(std::string const& str) { std::vector args; std::string const backslash {"\\"}; // parse str into arg vector as if it was parsed by the shell for (std::size_t i = 0; i < str.size(); ++i) { std::string e {str.at(i)}; // default if (e.find_first_not_of(" \n\t\"'") != std::string::npos) { bool escaped {false}; args.emplace_back(""); for (;i < str.size(); ++i) { e = str.at(i); if (! escaped && e.find_first_of(" \n\t") != std::string::npos) { --i; // put back unmatched char break; } else if (e == backslash) { escaped = true; } else if (escaped) { args.back() += e; escaped = false; } else { args.back() += e; } } continue; } // whitespace else if (e.find_first_of(" \n\t") != std::string::npos) { for (;i < str.size(); ++i) { e = str.at(i); if (e.find_first_not_of(" \n\t") != std::string::npos) { --i; // put back unmatched char break; } } continue; } // string else if (e.find_first_of("\"'") != std::string::npos) { std::string quote {e}; bool escaped {false}; ++i; // skip start quote args.emplace_back(""); for (;i < str.size(); ++i) { e = str.at(i); if (! escaped && e == quote) { break; // skip end quote } else if (e == backslash) { escaped = true; } else if (escaped) { args.back() += e; escaped = false; } else { args.back() += e; } } } } return args; } void set(std::string const& _name, std::string const& _info) { // sets a flag std::string delim {","}; if (_name.find(delim) != std::string::npos) { // short and long bool has_short {false}; std::string _long; std::string _short; auto const names = delimit(_name, delim); assert(names.size() >= 1 && names.size() <= 2); _long = names.at(0); if (names.size() == 2) has_short = true; if (has_short) { // short name must be one char assert(names.at(1).size() == 1); _short = names.at(1); } flags_[_short] = _long; data_[_long].long_ = _long; data_[_long].short_ = _short; data_[_long].mode_ = true; data_[_long].value_ = "0"; data_[_long].info_ = _info; } else { if (_name.size() == 1) { // short flags_[_name] = _name; data_[_name].long_ = _name; data_[_name].short_ = _name; data_[_name].mode_ = true; data_[_name].value_ = "0"; data_[_name].info_ = _info; } else { // long data_[_name].long_ = _name; data_[_name].mode_ = true; data_[_name].value_ = "0"; data_[_name].info_ = _info; } } } void set(std::string const& _name, std::string const& _default, std::string const& _arg, std::string const& _info) { // sets an option std::string delim {","}; if (_name.find(delim) != std::string::npos) { bool has_short {false}; std::string _long; std::string _short; auto const names = delimit(_name, delim); assert(names.size() >= 1 && names.size() <= 2); _long = names.at(0); if (names.size() == 2) has_short = true; if (has_short) { // short name must be one char assert(names.at(1).size() == 1); _short = names.at(1); } flags_[_short] = _long; data_[_long].long_ = _long; data_[_long].short_ = _short; data_[_long].mode_ = false; data_[_long].value_ = _default; data_[_long].arg_ = _arg; data_[_long].info_ = _info; } else { if (_name.size() == 1) { // short flags_[_name] = _name; data_[_name].long_ = _name; data_[_name].short_ = _name; data_[_name].mode_ = false; data_[_name].value_ = _default; data_[_name].arg_ = _arg; data_[_name].info_ = _info; } else { // long data_[_name].long_ = _name; data_[_name].mode_ = false; data_[_name].value_ = _default; data_[_name].arg_ = _arg; data_[_name].info_ = _info; } } } template || std::is_same_v, int> = 0> T get(std::string const& _key) { if (data_.find(_key) == data_.end()) { throw std::logic_error("parg get '" + _key + "' is not defined"); } return data_[_key].value_; } template, std::is_arithmetic_v, int> = 0> T get(std::string const& _key) { if (data_.find(_key) == data_.end()) { throw std::logic_error("parg get '" + _key + "' is not defined"); } std::stringstream ss; ss << data_[_key].value_; T val; ss >> val; return val; } bool find(std::string const& _key) const { // key must exist if (data_.find(_key) == data_.end()) return false; return data_.at(_key).seen_; } Parg& set_pos(bool const _positional = true) { is_positional_ = _positional; return *this; } std::string get_pos() const { std::string str; if (positional_vec_.empty()) { return str; } for (auto const& e : positional_vec_) { str += e + " "; } str.pop_back(); return str; } std::vector get_pos_vec() const { return positional_vec_; } Parg& set_stdin(bool const _stdin = true) { is_stdin_ = _stdin; return *this; } std::string get_stdin() const { return stdin_; } int status() const { return status_; } std::string error() const { if (error_.empty()) { return {}; } std::ostringstream out; OB::Term::ostream os {out}; ostream_init(os); os << iom::first_wrap(false) << aec::wrap("Error: ", style.error) << error_ << "\n" << iom::first_wrap(true); auto const similar_names = similar(); if (similar_names.size() > 0) { os << "\nDid you mean:" << style.opt << iom::push(); OB::Algorithm::for_each(similar_names, [&os](auto const& e) { os << "--" << e << "\n"; }, [&os](auto const& e) { os << "--" << e; }); os << aec::clear << iom::pop(); } return out.str(); } std::vector const& similar() const { return similar_; } std::size_t flags_found() const { std::size_t count {0}; for (auto const& e : data_) { if (e.second.mode_ && e.second.seen_) { ++count; } } return count; } std::size_t options_found() const { std::size_t count {0}; for (auto const& e : data_) { if (! e.second.mode_ && e.second.seen_) { ++count; } } return count; } struct Option { std::string arg_; std::string info_; std::string short_; std::string long_; bool mode_; std::string value_; bool seen_ {false}; }; private: int argc_ {0}; std::vector argv_; bool color_ {true}; std::string name_; std::string version_; std::vector usage_; std::string description_; std::vector info_; std::string author_; std::map data_; std::map flags_; bool is_positional_ {false}; std::string positional_; std::vector positional_vec_; std::string stdin_; bool is_stdin_ {false}; int status_ {0}; std::string error_; std::vector similar_; void argvf(char** _argv) { // removes first arg if (argc_ < 1) return; for (int i = 1; i < argc_; ++i) { argv_.emplace_back(_argv[i]); } // std::cerr << "argv: " << i << " -> " << argv_.at(i) << std::endl; --argc_; } int pipe_stdin() { if (! isatty(STDIN_FILENO)) { stdin_.assign((std::istreambuf_iterator(std::cin)), (std::istreambuf_iterator())); return 0; } stdin_ = ""; return -1; } std::vector delimit(std::string const& str, std::string const& delim) const { std::vector vtok; std::size_t start {0}; std::size_t end = str.find(delim); while (end != std::string::npos) { vtok.emplace_back(str.substr(start, end - start)); start = end + delim.length(); end = str.find(delim, start); } vtok.emplace_back(str.substr(start, end)); return vtok; } int parse_args(int _argc, std::vector const& _argv) { if (_argc < 1) return 1; bool dashdash {false}; // loop through arg vector for (int i = 0; i < _argc; ++i) { std::string const& tmp {_argv.at(static_cast(i))}; // std::cerr << "ARG: " << i << " -> " << tmp << std::endl; if (dashdash) { positional_vec_.emplace_back(tmp); continue; } if (tmp.size() > 1 && tmp.at(0) == '-' && tmp.at(1) != '-') { // short // std::cerr << "SHORT: " << tmp << std::endl; std::string c {tmp.at(1)}; if (flags_.find(c) != flags_.end() && !(data_.at(flags_.at(c)).mode_)) { // short arg // std::cerr << "SHORT: arg -> " << c << std::endl; if (data_.at(flags_.at(c)).seen_) { // error error_ = "flag '-" + c + "' has already been seen"; return -1; } if (tmp.size() > 2 && tmp.at(2) != '=') { data_.at(flags_.at(c)).value_ = tmp.substr(2, tmp.size() - 1); data_.at(flags_.at(c)).seen_ = true; } else if (tmp.size() > 3 && tmp.at(2) == '=') { data_.at(flags_.at(c)).value_ = tmp.substr(3, tmp.size() - 1); data_.at(flags_.at(c)).seen_ = true; } else if (i + 1 < _argc) { data_.at(flags_.at(c)).value_ = _argv.at(static_cast(i + 1)); data_.at(flags_.at(c)).seen_ = true; ++i; } else { // error error_ = "flag '-" + c + "' requires an arg"; return -1; } } else { // short mode for (std::size_t j = 1; j < tmp.size(); ++j) { std::string s {tmp.at(j)}; if (flags_.find(s) != flags_.end() && data_.at(flags_.at(s)).mode_) { // std::cerr << "SHORT: mode -> " << s << std::endl; if (data_.at(flags_.at(s)).seen_) { // error error_ = "flag '-" + s + "' has already been seen"; return -1; } data_.at(flags_.at(s)).value_ = "1"; data_.at(flags_.at(s)).seen_ = true; } else { // error error_ = "invalid flag '" + tmp + "'"; // find_similar(s); return -1; } } } } else if (tmp.size() > 2 && tmp.at(0) == '-' && tmp.at(1) == '-') { // long || -- // std::cerr << "LONG: " << tmp << std::endl; std::string c {tmp.substr(2, tmp.size() - 1)}; std::string a; auto const delim = c.find("="); if (delim != std::string::npos) { c = tmp.substr(2, delim); a = tmp.substr(3 + delim, tmp.size() - 1); } if (data_.find(c) != data_.end()) { if (data_.at(c).seen_) { // error error_ = "option '--" + c + "' has already been seen"; return -1; } if (data_.at(c).mode_ && a.size() == 0) { // std::cerr << "LONG: mode -> " << c << std::endl; data_.at(c).value_ = "1"; data_.at(c).seen_ = true; } else { // std::cerr << "LONG: arg -> " << c << std::endl; if (a.size() > 0) { data_.at(c).value_ = a; data_.at(c).seen_ = true; } else if (i + 1 < _argc) { data_.at(c).value_ = _argv.at(static_cast(i + 1)); data_.at(c).seen_ = true; ++i; } else { // error error_ = "option '--" + c + "' requires an arg"; return -1; } } } else { // error error_ = "invalid option '" + tmp + "'"; find_similar(c); return -1; } } else if (tmp.size() > 0 && is_positional_) { // positional // std::cerr << "POS: " << tmp << std::endl; if (tmp == "--") { dashdash = true; } else { positional_vec_.emplace_back(tmp); } } else { // error error_ = "no match for '" + tmp + "'"; find_similar(tmp); return -1; } } return 0; } void find_similar(std::string const& name) { int const weight_max {8}; std::vector> dist; for (auto const& [key, val] : data_) { int weight {0}; if (OB::String::starts_with(val.long_, name)) { weight = 0; } else { weight = OB::String::damerau_levenshtein(name, val.long_, 1, 2, 3, 0); } if (weight < weight_max) { dist.emplace_back(weight, val.long_); } } std::sort(dist.begin(), dist.end(), [](auto const& lhs, auto const& rhs) { return (lhs.first == rhs.first) ? (lhs.second.size() < rhs.second.size()) : (lhs.first < rhs.first); }); for (auto const& [key, val] : dist) { similar_.emplace_back(val); } size_t const similar_max {3}; if (similar_.size() > similar_max) { similar_.erase(similar_.begin() + similar_max, similar_.end()); } } }; // class Parg } // namespace OB #endif // OB_PARG_HH