diff --git a/include/blt/parse/argparse.h b/include/blt/parse/argparse.h index 5d72f3b..73f30c3 100644 --- a/include/blt/parse/argparse.h +++ b/include/blt/parse/argparse.h @@ -57,6 +57,7 @@ namespace blt } arg_vector_t(const char* str); + arg_vector_t(const std::string& str); [[nodiscard]] inline bool isFlag() const @@ -72,6 +73,9 @@ namespace blt } ) || str == name; } + + // returns the first flag that starts with '--' otherwise return the first '-' flag + [[nodiscard]] std::string getFirstFullFlag(); }; class arg_nargs_t @@ -100,6 +104,11 @@ namespace blt arg_nargs_t(std::string s); arg_nargs_t(const char* s); + + [[nodiscard]] bool takesArgs() const + { + return args > 0 || flags > 0; + } }; struct arg_properties_t @@ -239,6 +248,11 @@ namespace blt friend arg_parse; private: std::vector arg_properties_storage; + size_t max_line_length = 80; + // TODO: grouping like git's help + // pre/postfix applied to the help message + std::string prefix; + std::string postfix; public: std::vector name_associations; HASHMAP flag_associations; @@ -278,34 +292,50 @@ namespace blt private: static std::string filename(const std::string& path); + static std::string getMetavar(const arg_properties_t* const& arg); + static std::string getFlagHelp(const arg_properties_t* const& arg); + + static bool takesArgs(const arg_properties_t* const& arg); + + /** + * prints out a new line if current line length is greater than max line length, using spacing to generate the next line's + * beginning spaces. + */ + void checkAndPrintFormattingLine(size_t& current_line_length, size_t spacing) const; // expects that the current flag has already been consumed (advanced past), leaves tokenizer in a state where the next element is 'current' - bool consumeArguments(arg_tokenizer& tokenizer, const std::string& flag, const arg_properties_t& properties, std::vector& v_out) const; + bool consumeArguments( + arg_tokenizer& tokenizer, const std::string& flag, const arg_properties_t& properties, std::vector& v_out + ) const; void handlePositionalArgument(arg_tokenizer& tokenizer, size_t& last_pos); void handleFlagArgument(arg_tokenizer& tokenizer); void processFlag(arg_tokenizer& tokenizer, const std::string& flag); - + template - static inline bool holds_alternative(const arg_data_t& v){ + static inline bool holds_alternative(const arg_data_t& v) + { if constexpr (std::is_same_v) return std::holds_alternative(v); else return std::holds_alternative(v) && std::holds_alternative(std::get(v)); } + template - static inline T& get(arg_data_t& v){ + static inline T& get(arg_data_t& v) + { if constexpr (std::is_same_v) return std::get(v); else return std::get(std::get(v)); } + public: - arg_parse() + arg_parse(const std::string& helpMessage = "show this help menu and exit") { - addArgument(arg_builder({"--help", "-h"}).setAction(arg_action_t::HELP).setHelp("Show this help menu").build()); + addArgument(arg_builder({"--help", "-h"}).setAction(arg_action_t::HELP).setHelp(helpMessage).build()); }; void addArgument(const arg_properties_t& args); @@ -315,8 +345,24 @@ namespace blt arg_results parse_args(const std::vector& args); void printUsage() const; + void printHelp() const; + inline void setHelpPrefix(const std::string& str) + { + user_args.prefix = str; + } + + inline void setHelpPostfix(const std::string& str) + { + user_args.postfix = str; + } + + inline void setMaxLineLength(size_t size) + { + user_args.max_line_length = size; + } + ~arg_parse() { for (auto* p : user_args.arg_properties_storage) @@ -325,6 +371,7 @@ namespace blt }; std::string to_string(const blt::arg_data_t& v); + std::string to_string(const blt::arg_data_internal_t& v); } diff --git a/src/blt/parse/argparse.cpp b/src/blt/parse/argparse.cpp index 00c4ff1..02dbd00 100644 --- a/src/blt/parse/argparse.cpp +++ b/src/blt/parse/argparse.cpp @@ -68,6 +68,20 @@ namespace blt name = str; } + std::string arg_vector_t::getFirstFullFlag() + { + // assign flag so it always exists, will be first '-' flag if we fail to find a '--' flag + std::string flag = flags[0]; + // search for first '--' flag + for (const auto& f : flags) + if (f.starts_with("--")) + { + flag = f; + break; + } + return flag; + } + std::string to_string(const arg_data_t& v) { if (holds_alternative(v)) @@ -119,19 +133,8 @@ namespace blt if (properties->a_dest.empty()) { if (properties->a_flags.isFlag()) - { - // take first arg so a_dest exists, could be - or -- - properties->a_dest = properties->a_flags.flags[0]; - // look for a -- arg (python's behaviour) - for (const auto& flag : properties->a_flags.flags) - { - if (flag.starts_with("--")) - { - properties->a_dest = flag; - break; - } - } - } else + properties->a_dest = properties->a_flags.getFirstFullFlag(); + else properties->a_dest = properties->a_flags.name; } @@ -291,6 +294,7 @@ namespace blt switch (properties->a_action) { case arg_action_t::HELP: + printUsage(); printHelp(); break; case arg_action_t::STORE: @@ -409,20 +413,150 @@ namespace blt unrec = unrec.substr(0, unrec.size() - 1); printUsage(); std::cout << loaded_args.program_name << ": error: unrecognized args: " << unrec << "\n"; - printHelp(); - - return loaded_args; + std::exit(0); + } + + bool arg_parse::takesArgs(const arg_properties_t* const& arg) + { + switch (arg->a_action) + { + case arg_action_t::STORE_CONST: + case arg_action_t::STORE_TRUE: + case arg_action_t::STORE_FALSE: + case arg_action_t::APPEND_CONST: + case arg_action_t::COUNT: + case arg_action_t::HELP: + case arg_action_t::VERSION: + return false; + case arg_action_t::STORE: + case arg_action_t::APPEND: + case arg_action_t::EXTEND: + return arg->a_nargs.takesArgs(); + } + return false; } void arg_parse::printHelp() const { - std::cout << ("I am helpful!\n"); + std::cout << "\npositional arguments:\n"; + // spaces per tab + const size_t tab_size = 8; + size_t max_length = 0; + // search for longest pos arg length + for (const auto& arg : user_args.arg_properties_storage) + { + if (!arg->a_flags.isFlag()) + max_length = std::max(arg->a_flags.name.size(), max_length); + else { + auto tmp = getFlagHelp(arg); + max_length = std::max(tmp.size(), max_length); + } + } + for (const auto& arg : user_args.arg_properties_storage) + { + if (!arg->a_flags.isFlag()) + { + const auto& name = arg->a_flags.name; + std::cout << name; + auto size = std::max(static_cast(max_length) - static_cast(name.size()), 0l); + size += tab_size; + for (int64_t i = 0; i < size; i++) + std::cout << " "; + std::cout << arg->a_help << "\n"; + } + } + std::cout << "\noptions:\n"; + for (const auto& arg : user_args.arg_properties_storage) + { + if (arg->a_flags.isFlag()) + { + const auto& name = getFlagHelp(arg); + std::cout << name; + auto size = std::max(static_cast(max_length) - static_cast(name.size()), 0l); + size += tab_size; + for (int64_t i = 0; i < size; i++) + std::cout << " "; + std::cout << arg->a_help << "\n"; + } + } + std::exit(0); } void arg_parse::printUsage() const { + std::string usage = "Usage: " + loaded_args.program_name + " "; + std::cout << usage; + size_t current_line_length = 0; + + for (const auto& arg : user_args.arg_properties_storage) + { + auto meta = getMetavar(arg); + + std::string str = "["; + if (arg->a_flags.isFlag()) + { + str += arg->a_flags.getFirstFullFlag(); + if (takesArgs(arg)) + { + str += " "; + str += meta; + str += ""; + } + str += "]"; + str += ' '; + } else + { + str += "<"; + str += arg->a_flags.name; + str += ">] "; + } + + current_line_length += str.size(); + checkAndPrintFormattingLine(current_line_length, usage.size()); + + std::cout << str; + } + std::cout << "\n"; + } + void arg_parse::checkAndPrintFormattingLine(size_t& current_line_length, size_t spacing) const + { + if (current_line_length > user_args.max_line_length) + { + std::cout << "\n"; + for (size_t i = 0; i < spacing; i++) + std::cout << " "; + current_line_length = 0; + } + } + std::string arg_parse::getMetavar(const arg_properties_t* const& arg) + { + auto meta = arg->a_metavar; + + if (meta.empty()) + meta = blt::string::toUpperCase(arg->a_dest); + + return meta; + } + + std::string arg_parse::getFlagHelp(const arg_properties_t* const& arg) + { + auto meta = getMetavar(arg); + // bad that we have to create this twice! + std::string tmp; + for (const auto& flag : arg->a_flags.flags) + { + tmp += flag; + if (takesArgs(arg)) + { + tmp += ' '; + tmp += meta; + } + tmp += ", "; + } + tmp = tmp.substr(0, tmp.size()-2); + return tmp; } } \ No newline at end of file diff --git a/src/tests/main.cpp b/src/tests/main.cpp index 43955fe..371b801 100755 --- a/src/tests/main.cpp +++ b/src/tests/main.cpp @@ -73,14 +73,14 @@ int main(int argc, const char** argv) { parser.addArgument(blt::arg_builder("--foo").setAction(blt::arg_action_t::STORE_TRUE).setDefault(false).build()); parser.addArgument(blt::arg_builder({"--goo", "-g"}).build()); parser.addArgument(blt::arg_builder({"--oop", "-o"}).build()); - parser.addArgument(blt::arg_builder("Sexy_pos").build()); + parser.addArgument(blt::arg_builder("Sexy_pos").setHelp("I am helpful!").build()); auto args = parser.parse_args(argc, argv); std::vector superArgs { "BLT_TESTS", "Sexy", "-p", "I have poop", - "-f" + "--help" }; auto args2 = parser.parse_args(superArgs);