BLT/src/blt/parse/argparse.cpp

404 lines
13 KiB
C++
Raw Normal View History

/*
* Created by Brett on 06/08/23.
* Licensed under GNU General Public License V3.0
* See LICENSE file for license detail
*/
#include <blt/parse/argparse.h>
#include <blt/std/logging.h>
#include <blt/std/string.h>
namespace blt
{
void arg_nargs_t::decode(char c)
{
if (c == '?')
flags = UNKNOWN;
else if (c == '+')
flags = ALL_REQUIRED;
else if (c == '*')
flags = ALL;
else
flags = 0;
}
arg_nargs_t::arg_nargs_t(char c)
{
decode(c);
}
arg_nargs_t::arg_nargs_t(std::string s)
{
decode(s[0]);
}
arg_nargs_t::arg_nargs_t(const char* s)
{
decode(*s);
}
class invalid_argument_exception : public std::runtime_error
{
public:
invalid_argument_exception(const std::string& str): std::runtime_error(str)
{}
};
void arg_vector_t::validateFlags()
{
for (const auto& flag : flags)
if (!flag.starts_with('-'))
throw invalid_argument_exception("Flag '" + flag + "' must start with - or --");
}
arg_vector_t::arg_vector_t(const char* str) {
std::string as_string(str);
if (as_string.starts_with('-'))
flags.emplace_back(as_string);
else
name = as_string;
}
arg_vector_t::arg_vector_t(const std::string& str) {
if (str.starts_with('-'))
flags.emplace_back(str);
else
name = str;
}
std::string to_string(const arg_data_t& v)
{
if (holds_alternative<arg_data_internal_t>(v))
return to_string(std::get<arg_data_internal_t>(v));
else if (std::holds_alternative<arg_data_vec_t>(v))
{
const auto& vec = std::get<arg_data_vec_t>(v);
if (vec.size() == 1)
return to_string(vec[0]);
if (vec.empty())
return "Empty Vector";
std::string str;
for (const auto& r : vec)
{
str += to_string(r);
str += ' ';
}
return "Vector of contents: " + str;
}
return "Empty";
}
std::string to_string(const arg_data_internal_t& v)
{
if (std::holds_alternative<std::string>(v)){
return std::get<std::string>(v);
} else if (std::holds_alternative<bool>(v)) {
return std::get<bool>(v) ? "True" : "False";
}
return std::to_string(std::get<int32_t>(v));
}
std::string arg_parse::filename(const std::string& path)
{
auto paths = blt::string::split(path, "/");
auto final = paths[paths.size() - 1];
if (final == "/")
return paths[paths.size() - 2];
return final;
}
void arg_parse::addArgument(const arg_properties_t& args)
{
auto properties = new arg_properties_t(args);
// determine where to store the arg when parsing
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.name;
}
if (properties->a_dest.starts_with("--"))
properties->a_dest = properties->a_dest.substr(2);
else if (properties->a_dest.starts_with('-'))
properties->a_dest = properties->a_dest.substr(1);
// associate flags with their properties
for (const auto& flag : properties->a_flags.flags)
user_args.flag_associations[flag] = properties;
// positional args uses index (vector) to store the properties
if (!properties->a_flags.isFlag())
user_args.name_associations.push_back(properties);
user_args.arg_properties_storage.push_back(properties);
}
bool arg_parse::consumeArguments(arg_tokenizer& tokenizer, const arg_properties_t& properties, std::vector<arg_data_internal_t>& v_out)
{
switch (properties.a_nargs.flags)
{
case 0:
for (int i = 0; i < properties.a_nargs.args; i++)
{
// if we don't have another arg to consume we have a problem!
if (!tokenizer.hasCurrent())
{
BLT_WARN("Expected %d arguments got %d instead!", properties.a_nargs.args, i);
return false;
}
// if we do have one, but it is a flag then we also have a problem!
if (tokenizer.isFlag())
{
BLT_WARN("Expected %d arguments, found flag instead!", properties.a_nargs.args);
return false;
}
// get the value and advance
v_out.emplace_back(tokenizer.get());
tokenizer.advance();
}
return true;
case arg_nargs_t::UNKNOWN:
// no arg next
if (!tokenizer.hasCurrent() || tokenizer.isFlag())
{
// python's default is to store const if around otherwise store default
if (!properties.a_const.empty())
v_out.emplace_back(properties.a_const);
else // this should no longer be required with the changes to how defaults are handled
v_out.emplace_back(properties.a_default);
return true;
}
v_out.emplace_back(tokenizer.get());
tokenizer.advance();
return true;
case arg_nargs_t::ALL:
while (tokenizer.hasCurrent() && !tokenizer.isFlag())
{
v_out.emplace_back(tokenizer.get());
tokenizer.advance();
}
return true;
case arg_nargs_t::ALL_REQUIRED:
if (tokenizer.hasCurrent() && tokenizer.isFlag())
{
BLT_WARN("At least one argument is required!");
return false;
}
while (tokenizer.hasCurrent() && !tokenizer.isFlag())
{
v_out.emplace_back(tokenizer.get());
tokenizer.advance();
}
return true;
}
return false;
}
void arg_parse::handlePositionalArgument(arg_tokenizer& tokenizer, size_t& last_pos)
{
auto index = last_pos++;
if (index >= user_args.name_associations.size())
loaded_args.unrecognized_args.push_back(tokenizer.get());
else
{
loaded_args.data[user_args.name_associations[index]->a_dest] = tokenizer.get();
loaded_args.found_args.insert(user_args.name_associations[index]->a_dest);
}
tokenizer.advance();
}
void arg_parse::handleFlagArgument(arg_tokenizer& tokenizer)
{
auto flag = tokenizer.get();
tokenizer.advance();
// token is a flag, figure out how to handle it
if (flag.starts_with("--"))
processFlag(tokenizer, flag);
else
{
// handle special args like -vvv
if (!flag.starts_with('-'))
BLT_ERROR("Flag processed but does not start with '-'");
// make sure the flag only contains the same character
auto type = flag[1];
for (char c : flag.substr(1))
{
if (c != type)
{
BLT_ERROR("Processed flag '%s' expected %c found %c", flag.c_str(), type, c);
return;
}
}
// size without -
auto len = flag.size() - 1;
// get flag type
std::string str = "- ";
str[1] = flag[1];
for (size_t i = 0; i < len; i++)
processFlag(tokenizer, str);
}
}
void arg_parse::processFlag(arg_tokenizer& tokenizer, const std::string& flag)
{
auto flag_itr = user_args.flag_associations.find(flag);
if (flag_itr == user_args.flag_associations.end())
{
loaded_args.unrecognized_args.push_back(flag);
return;
}
const auto* const properties = user_args.flag_associations.at(flag);
if (properties->a_dest.empty())
{
loaded_args.unrecognized_args.push_back(flag);
return;
}
auto dest = properties->a_dest;
loaded_args.found_args.insert(dest);
switch (properties->a_action)
{
case arg_action_t::HELP:
printHelp();
break;
case arg_action_t::STORE:
{
arg_data_t& data = loaded_args.data[dest];
arg_data_vec_t v;
if (!consumeArguments(tokenizer, *properties, v))
{
printHelp();
return;
}
if (v.size() == 1)
data = v[0];
else
data = v;
break;
}
case arg_action_t::STORE_CONST:
loaded_args.data[dest] = properties->a_const;
break;
case arg_action_t::STORE_FALSE:
loaded_args.data[dest] = false;
break;
case arg_action_t::STORE_TRUE:
loaded_args.data[dest] = true;
break;
case arg_action_t::COUNT:
{
auto& data = loaded_args.data[dest];
if (!holds_alternative<int32_t>(data))
data = 0;
data = get<int32_t>(data) + 1;
break;
}
case arg_action_t::EXTEND:
{
break;
}
case arg_action_t::VERSION:
{
auto file = filename(loaded_args.program_name);
BLT_INFO("%s, %s", file.c_str(), properties->a_version.c_str());
break;
}
case arg_action_t::APPEND_CONST:
{
auto& data = loaded_args.data[dest];
if (!std::holds_alternative<arg_data_vec_t>(data))
{
data = arg_data_vec_t();
}
auto& l = get<arg_data_vec_t>(data);
l.emplace_back(properties->a_const);
break;
}
case arg_action_t::APPEND:
{
auto& data = loaded_args.data[dest];
if (!holds_alternative<arg_data_vec_t>(data))
data = arg_data_vec_t();
auto& l = get<arg_data_vec_t>(data);
consumeArguments(tokenizer, *properties, l);
break;
}
}
}
arg_parse::arg_results arg_parse::parse_args(int argc, const char** argv)
{
std::vector<std::string> args;
args.reserve(argc);
for (int i = 0; i < argc; i++)
args.emplace_back(argv[i]);
return parse_args(args);
}
arg_parse::arg_results arg_parse::parse_args(const std::vector<std::string>& args)
{
arg_tokenizer tokenizer(args);
loaded_args.program_name = tokenizer.get();
tokenizer.advance();
size_t last_positional = 0;
while (tokenizer.hasCurrent())
{
if (tokenizer.isFlag())
handleFlagArgument(tokenizer);
else
handlePositionalArgument(tokenizer, last_positional);
}
// load defaults for args which were not found
for (const auto* arg : user_args.arg_properties_storage) {
if (!to_string(arg->a_default).empty() && !loaded_args.contains(arg->a_dest))
loaded_args.data[arg->a_dest] = arg->a_default;
}
// if there was no problems processing then return the loaded args
if (loaded_args.unrecognized_args.empty())
return loaded_args;
// otherwise construct a string detailing the unrecognized args
std::string unrec;
for (const auto& r : loaded_args.unrecognized_args)
{
unrec += '\'';
unrec += r;
unrec += '\'';
unrec += ' ';
}
// remove the last space caused by the for loop
unrec = unrec.substr(0, unrec.size() - 1);
// TODO: use exceptions?
BLT_WARN("Unrecognized args: %s", unrec.c_str());
printHelp();
return loaded_args;
}
void arg_parse::printHelp()
{
BLT_TRACE("I am helpful!");
std::exit(0);
}
}