X-Git-Url: https://git.dogcows.com/gitweb?a=blobdiff_plain;f=src%2Fstlplus%2Fsubsystems%2Fcli_parser.cpp;fp=src%2Fstlplus%2Fsubsystems%2Fcli_parser.cpp;h=1088f3897fa442cecb548c0488debcb144309d91;hb=6b0a0d0efafe34d48ab344fca3b479553bd4e62c;hp=0000000000000000000000000000000000000000;hpb=85783316365181491a3e3c0c63659972477cebba;p=chaz%2Fyoink diff --git a/src/stlplus/subsystems/cli_parser.cpp b/src/stlplus/subsystems/cli_parser.cpp new file mode 100644 index 0000000..1088f38 --- /dev/null +++ b/src/stlplus/subsystems/cli_parser.cpp @@ -0,0 +1,637 @@ +//////////////////////////////////////////////////////////////////////////////// + +// Author: Andy Rushton +// Copyright: (c) Southampton University 1999-2004 +// (c) Andy Rushton 2004-2009 +// License: BSD License, see ../docs/license.html + +//////////////////////////////////////////////////////////////////////////////// +#include "cli_parser.hpp" +#include "file_system.hpp" +#include "dprintf.hpp" +//////////////////////////////////////////////////////////////////////////////// + +namespace stlplus +{ + + //////////////////////////////////////////////////////////////////////////////// + // cli_definition internals + + const std::string& stlplus::cli_definition::name(void) const + { + return m_name; + } + + stlplus::cli_kind_t stlplus::cli_definition::kind(void) const + { + return m_kind; + } + + stlplus::cli_mode_t stlplus::cli_definition::mode(void) const + { + return m_mode; + } + + const std::string& stlplus::cli_definition::message(void) const + { + return m_message; + } + + const std::string& stlplus::cli_definition::default_value(void) const + { + return m_default; + } + + //////////////////////////////////////////////////////////////////////////////// + // internal data structures + + class cli_value + { + public: + unsigned m_definition; + std::string m_value; + unsigned m_level; + std::string m_source; + + cli_value(unsigned definition, const std::string& value, unsigned level, const std::string& source) : + m_definition(definition), m_value(value), m_level(level), m_source(source) + { + } + }; + + //////////////////////////////////////////////////////////////////////////////// + + class cli_parser_data + { + public: + message_handler& m_messages; + std::string m_executable; + cli_parser::definitions m_definitions; + std::vector m_values; + unsigned m_level; + bool m_valid; + std::vector m_ini_files; + + public: + + cli_parser_data(message_handler& messages) : + m_messages(messages), m_level(1), m_valid(true) + { + // ensure that the CLI's messages are in the message handler - these + // can be overridden from a file later - see message_handler + if (!m_messages.message_present("CLI_VALUE_MISSING")) + m_messages.add_message("CLI_VALUE_MISSING", "option @0 requires a value - end of line was reached instead"); + if (!m_messages.message_present("CLI_UNRECOGNISED_OPTION")) + m_messages.add_message("CLI_UNRECOGNISED_OPTION", "@0 is not a recognised option for this command"); + if (!m_messages.message_present("CLI_NO_VALUES")) + m_messages.add_message("CLI_NO_VALUES", "argument @0 is invalid - this program doesn't take command-line arguments"); + if (!m_messages.message_present("CLI_USAGE_PROGRAM")) + m_messages.add_message("CLI_USAGE_PROGRAM", "usage:\n\t@0 { arguments }"); + if (!m_messages.message_present("CLI_USAGE_DEFINITIONS")) + m_messages.add_message("CLI_USAGE_DEFINITIONS", "arguments:"); + if (!m_messages.message_present("CLI_USAGE_VALUES")) + m_messages.add_message("CLI_USAGE_VALUES", "values:"); + if (!m_messages.message_present("CLI_USAGE_VALUE_RESULT")) + m_messages.add_message("CLI_USAGE_VALUE_RESULT", "\t@0 : from @1"); + if (!m_messages.message_present("CLI_USAGE_SWITCH_RESULT")) + m_messages.add_message("CLI_USAGE_SWITCH_RESULT", "\t-@0 : from @1"); + if (!m_messages.message_present("CLI_USAGE_OPTION_RESULT")) + m_messages.add_message("CLI_USAGE_OPTION_RESULT", "\t-@0 @1 : from @2"); + if (!m_messages.message_present("CLI_INI_HEADER")) + m_messages.add_message("CLI_INI_HEADER", "configuration files:"); + if (!m_messages.message_present("CLI_INI_FILE_PRESENT")) + m_messages.add_message("CLI_INI_FILE_PRESENT", "\t@0"); + if (!m_messages.message_present("CLI_INI_FILE_ABSENT")) + m_messages.add_message("CLI_INI_FILE_ABSENT", "\t@0 (not found)"); + if (!m_messages.message_present("CLI_INI_VARIABLE")) + m_messages.add_message("CLI_INI_VARIABLE", "unknown variable \"@0\" found in Ini file"); + } + + unsigned add_definition(const cli_parser::definition& definition) + { + m_definitions.push_back(definition); + return m_definitions.size()-1; + } + + std::string name(unsigned i) const throw(cli_index_error) + { + if (i >= m_values.size()) throw cli_index_error("Index " + dformat("%u",i) + " out of range"); + return m_definitions[m_values[i].m_definition].name(); + } + + unsigned id(unsigned i) const throw(cli_index_error) + { + if (i >= m_values.size()) throw cli_index_error("Index " + dformat("%u",i) + " out of range"); + return m_values[i].m_definition; + } + + cli_parser::kind_t kind(unsigned i) const throw(cli_index_error) + { + if (i >= m_values.size()) throw cli_index_error("Index " + dformat("%u",i) + " out of range"); + return m_definitions[m_values[i].m_definition].kind(); + } + + cli_parser::mode_t mode(unsigned i) const throw(cli_index_error) + { + if (i >= m_values.size()) throw cli_index_error("Index " + dformat("%u",i) + " out of range"); + return m_definitions[m_values[i].m_definition].mode(); + } + +// unsigned add_definition(const std::string& name, +// cli_parser::kind_t kind, +// cli_parser::mode_t mode, +// const std::string& message) +// { +// return add_definition(cli_parser::definition(name, kind, mode, message)); +// } + + unsigned find_definition(const std::string& name) + { + // this does substring matching on the definitions and returns the first + // match - however, it requires at least one character in the substring so + // that the "" convention for command line arguments doesn't match with + // anything. It returns size() if it fails + for (unsigned i = 0; i < m_definitions.size(); i++) + { + std::string candidate = m_definitions[i].name(); + if ((candidate.empty() && name.empty()) || + (!name.empty() && candidate.size() >= name.size() && candidate.substr(0,name.size()) == name)) + return i; + } + return m_definitions.size(); + } + + void clear_definitions(void) + { + m_definitions.clear(); + m_values.clear(); + reset_level(); + set_valid(); + } + + void reset_level(void) + { + // the starting level is 1 so that later functions can call clear_level with + // a value of m_level-1 without causing underflow + m_level = 1; + } + + void increase_level(void) + { + m_level++; + } + + void clear_level(unsigned definition, unsigned level) + { + // clears all values with this definition at the specified level or below + for (std::vector::iterator i = m_values.begin(); i != m_values.end(); ) + { + if (i->m_definition == definition && i->m_level <= level) + i = m_values.erase(i); + else + i++; + } + } + + void set_valid(void) + { + m_valid = true; + } + + void set_invalid(void) + { + m_valid = false; + } + + bool valid(void) const + { + return m_valid; + } + + unsigned add_value(unsigned definition, const std::string& value, const std::string& source) + { + // behaviour depends on mode: + // - single: erase all previous values + // - multiple: erase values at a lower level than current + // - cumulative: erase no previous values + switch (m_definitions[definition].mode()) + { + case cli_single_mode: + clear_level(definition, m_level); + break; + case cli_multiple_mode: + clear_level(definition, m_level-1); + break; + case cli_cumulative_mode: + break; + } + m_values.push_back(cli_value(definition,value,m_level,source)); + return m_values.size()-1; + } + + unsigned add_switch(unsigned definition, bool value, const std::string& source) + { + return add_value(definition, value ? "on" : "off", source); + } + + void erase_value(unsigned definition) + { + // this simply erases all previous values + clear_level(definition, m_level); + } + + void add_ini_file(const std::string& file) + { + m_ini_files.push_back(file); + } + + unsigned ini_file_size(void) const + { + return m_ini_files.size(); + } + + const std::string& ini_file(unsigned i) const + { + return m_ini_files[i]; + } + + unsigned add_checked_definition(const cli_parser::definition& definition) throw(cli_mode_error) + { + // check for stupid combinations + // at this stage the only really stupid one is to declare command line arguments to be switch mode + if (definition.name().empty() && definition.kind() == cli_switch_kind) + { + set_invalid(); + throw cli_mode_error("CLI arguments cannot be switch kind"); + } + // add the definition to the set of all definitions + unsigned i = add_definition(definition); + // also add it to the list of values, but only if it has a default value + if (!definition.default_value().empty()) + add_value(i, definition.default_value(), "builtin default"); + return i; + } + + bool switch_value(unsigned i) const throw(cli_mode_error,cli_index_error) + { + if (i >= m_values.size()) throw cli_index_error("Index " + dformat("%u",i) + " out of range"); + if (kind(i) != cli_switch_kind) throw cli_mode_error(name(i) + " is not a switch kind"); + std::string value = m_values[i].m_value; + return value == "on" || value == "true" || value == "1"; + } + + std::string string_value(unsigned i) const throw(cli_mode_error,cli_index_error) + { + if (i >= m_values.size()) throw cli_index_error("Index " + dformat("%u",i) + " out of range"); + if (kind(i) != cli_value_kind) throw cli_mode_error(name(i) + " is not a value kind"); + return m_values[i].m_value; + } + + void set_defaults(const ini_manager& defaults, const std::string& ini_section) throw() + { + // import default values from the Ini Manager + increase_level(); + // get the set of all names from the Ini manager so that illegal names generate meaningful error messages + std::vector names = defaults.variable_names(ini_section); + for (unsigned i = 0; i < names.size(); i++) + { + std::string name = names[i]; + unsigned definition = find_definition(name); + if (definition == (unsigned)-1) + { + // not found - give an error report + message_position position(defaults.variable_filename(ini_section,name), + defaults.variable_linenumber(ini_section,name), + 0); + m_messages.error(position,"CLI_INI_VARIABLE", name); + } + else + { + // found - so add the value + // multi-valued variables are entered as a comma-separated list and this is then turned into a vector + // the vector is empty if the value was empty + std::vector values = defaults.variable_values(ini_section, name); + // an empty string is used to negate the value + if (values.empty()) + erase_value(definition); + else + { + std::string source = filespec_to_relative_path(defaults.variable_filename(ini_section, name)); + for (unsigned j = 0; j < values.size(); j++) + add_value(definition, values[j], source); + } + } + } + // add the set of ini files to the list for usage reports + for (unsigned j = 0; j < defaults.size(); j++) + add_ini_file(defaults.filename(j)); + } + + bool parse(char* argv[]) throw(cli_argument_error,message_handler_id_error,message_handler_format_error) + { + bool result = true; + if (!argv) throw cli_argument_error("Argument vector cannot be null"); + increase_level(); + if (argv[0]) + m_executable = argv[0]; + for (unsigned i = 1; argv[i]; i++) + { + std::string name = argv[i]; + if (!name.empty() && name[0] == '-') + { + // we have a command line option + unsigned found = find_definition(name.substr(1, name.size()-1)); + if (found < m_definitions.size()) + { + // found it in its positive form + switch (m_definitions[found].kind()) + { + case cli_switch_kind: + add_switch(found, true, "command line"); + break; + case cli_value_kind: + // get the next argument in argv as the value of this option + // first check that there is a next value + if (!argv[i+1]) + result &= m_messages.error("CLI_VALUE_MISSING", name); + else + add_value(found, argv[++i], "command line"); + break; + } + } + else if (name.size() > 3 && name.substr(1,2) == "no") + { + found = find_definition(name.substr(3, name.size()-3)); + if (found < m_definitions.size()) + { + // found it in its negated form + switch (m_definitions[found].kind()) + { + case cli_switch_kind: + add_switch(found, false, "command line"); + break; + case cli_value_kind: + erase_value(found); + break; + } + } + else + { + // could not find this option in either its true or negated form + result &= m_messages.error("CLI_UNRECOGNISED_OPTION", name); + } + } + else + { + // could not find this option and it could not be negated + result &= m_messages.error("CLI_UNRECOGNISED_OPTION", name); + } + } + else + { + // we have a command-line value which is represented internally as an option with an empty string as its name + // some very obscure commands do not have values - only options, so allow for that case too + unsigned found = find_definition(""); + if (found < m_definitions.size()) + add_value(found, name, "command line"); + else + result &= m_messages.error("CLI_NO_VALUES", name); + } + } + if (!result) set_invalid(); + return result; + } + + void usage(void) const throw(std::runtime_error) + { + m_messages.information("CLI_USAGE_PROGRAM", m_executable); + m_messages.information("CLI_USAGE_DEFINITIONS"); + for (unsigned d = 0; d < m_definitions.size(); d++) + m_messages.information(m_definitions[d].message()); + if (m_values.size() != 0) + { + m_messages.information("CLI_USAGE_VALUES"); + for (unsigned v = 0; v < m_values.size(); v++) + { + std::string source = m_values[v].m_source; + std::string key = name(v); + if (key.empty()) + { + // command-line values + m_messages.information("CLI_USAGE_VALUE_RESULT", string_value(v), source); + } + else if (kind(v) == cli_switch_kind) + { + // a switch + m_messages.information("CLI_USAGE_SWITCH_RESULT", (switch_value(v) ? name(v) : "no" + name(v)), source); + } + else + { + // other values + std::vector args; + args.push_back(name(v)); + args.push_back(string_value(v)); + args.push_back(source); + m_messages.information("CLI_USAGE_OPTION_RESULT", args); + } + } + } + if (ini_file_size() > 0) + { + m_messages.information("CLI_INI_HEADER"); + for (unsigned i = 0; i < ini_file_size(); i++) + { + if (file_exists(ini_file(i))) + m_messages.information("CLI_INI_FILE_PRESENT", filespec_to_relative_path(ini_file(i))); + else + m_messages.information("CLI_INI_FILE_ABSENT", filespec_to_relative_path(ini_file(i))); + } + } + } + + private: + // make this class uncopyable + cli_parser_data(const cli_parser_data&); + cli_parser_data& operator = (const cli_parser_data&); + }; + + //////////////////////////////////////////////////////////////////////////////// + + cli_parser::cli_parser(message_handler& messages) throw() : + m_data(new cli_parser_data(messages)) + { + } + + cli_parser::cli_parser(cli_parser::definitions_t definitions, message_handler& messages) throw(cli_mode_error) : + m_data(new cli_parser_data(messages)) + { + add_definitions(definitions); + } + + cli_parser::cli_parser(cli_parser::definitions_t definitions, const ini_manager& defaults, const std::string& ini_section, message_handler& messages) throw(cli_mode_error) : + m_data(new cli_parser_data(messages)) + { + add_definitions(definitions); + set_defaults(defaults, ini_section); + } + + cli_parser::cli_parser(char* argv[], cli_parser::definitions_t definitions, message_handler& messages) throw(cli_mode_error,message_handler_id_error,message_handler_format_error) : + m_data(new cli_parser_data(messages)) + { + add_definitions(definitions); + parse(argv); + } + + cli_parser::cli_parser(char* argv[], cli_parser::definitions_t definitions, const ini_manager& defaults, const std::string& ini_section, message_handler& messages) throw(cli_mode_error,message_handler_id_error,message_handler_format_error) : + m_data(new cli_parser_data(messages)) + { + add_definitions(definitions); + set_defaults(defaults, ini_section); + parse(argv); + } + + cli_parser::cli_parser(cli_parser::definitions definitions, message_handler& messages) throw(cli_mode_error) : + m_data(new cli_parser_data(messages)) + { + add_definitions(definitions); + } + + cli_parser::cli_parser(cli_parser::definitions definitions, const ini_manager& defaults, const std::string& ini_section, message_handler& messages) throw(cli_mode_error) : + m_data(new cli_parser_data(messages)) + { + add_definitions(definitions); + set_defaults(defaults, ini_section); + } + + cli_parser::cli_parser(char* argv[], cli_parser::definitions definitions, message_handler& messages) throw(cli_mode_error,message_handler_id_error,message_handler_format_error) : + m_data(new cli_parser_data(messages)) + { + add_definitions(definitions); + parse(argv); + } + + cli_parser::cli_parser(char* argv[], cli_parser::definitions definitions, const ini_manager& defaults, const std::string& ini_section, message_handler& messages) throw(cli_mode_error,message_handler_id_error,message_handler_format_error) : + m_data(new cli_parser_data(messages)) + { + add_definitions(definitions); + set_defaults(defaults, ini_section); + parse(argv); + } + + cli_parser::~cli_parser(void) throw() + { + } + + void cli_parser::add_definitions(cli_parser::definitions_t definitions) throw(cli_mode_error) + { + m_data->clear_definitions(); + // the definitions array is terminated by a definition with a null name pointer + for (unsigned i = 0; definitions[i].m_name; i++) + add_definition(definitions[i]); + } + + unsigned cli_parser::add_definition(const cli_parser::definition_t& definition) throw(cli_mode_error,cli_argument_error) + { + std::string name = definition.m_name ? definition.m_name : ""; + std::string message = definition.m_message ? definition.m_message : ""; + std::string value = definition.m_default ? definition.m_default : ""; + return add_definition(cli_parser::definition(name, definition.m_kind, definition.m_mode, message, value)); + } + + void cli_parser::add_definitions(cli_parser::definitions definitions) throw(cli_mode_error) + { + m_data->clear_definitions(); + for (unsigned i = 0; i < definitions.size(); i++) + add_definition(definitions[i]); + } + + unsigned cli_parser::add_definition(const cli_parser::definition& definition) throw(cli_mode_error) + { + return m_data->add_checked_definition(definition); + } + + void cli_parser::set_defaults(const ini_manager& defaults, const std::string& ini_section) throw() + { + m_data->set_defaults(defaults, ini_section); + } + + bool cli_parser::parse(char* argv[]) throw(cli_argument_error,message_handler_id_error,message_handler_format_error) + { + return m_data->parse(argv); + } + + bool cli_parser::valid(void) throw() + { + return m_data->valid(); + } + + unsigned cli_parser::size(void) const throw() + { + return m_data->m_values.size(); + } + + std::string cli_parser::name(unsigned i) const throw(cli_index_error) + { + return m_data->name(i); + } + + unsigned cli_parser::id(unsigned i) const throw(cli_index_error) + { + return m_data->id(i); + } + + cli_parser::kind_t cli_parser::kind(unsigned i) const throw(cli_index_error) + { + return m_data->kind(i); + } + + bool cli_parser::switch_kind(unsigned i) const throw(cli_index_error) + { + return kind(i) == cli_switch_kind; + } + + bool cli_parser::value_kind(unsigned i) const throw(cli_index_error) + { + return kind(i) == cli_value_kind; + } + + cli_parser::mode_t cli_parser::mode(unsigned i) const throw(cli_index_error) + { + return m_data->mode(i); + } + + bool cli_parser::single_mode(unsigned i) const throw(cli_index_error) + { + return mode(i) == cli_single_mode; + } + + bool cli_parser::multiple_mode(unsigned i) const throw(cli_index_error) + { + return mode(i) == cli_multiple_mode; + } + + bool cli_parser::cumulative_mode(unsigned i) const throw(cli_index_error) + { + return mode(i) == cli_cumulative_mode; + } + + bool cli_parser::switch_value(unsigned i) const throw(cli_mode_error,cli_index_error) + { + return m_data->switch_value(i); + } + + std::string cli_parser::string_value(unsigned i) const throw(cli_mode_error,cli_index_error) + { + return m_data->string_value(i); + } + + //////////////////////////////////////////////////////////////////////////////// + + void cli_parser::usage(void) const throw(std::runtime_error) + { + m_data->usage(); + } + + //////////////////////////////////////////////////////////////////////////////// + +} // end namespace stlplus