diff --git a/src/base/CMakeLists.txt b/src/base/CMakeLists.txt index f06b1985b..6de95e1b6 100644 --- a/src/base/CMakeLists.txt +++ b/src/base/CMakeLists.txt @@ -26,6 +26,7 @@ add_library(base-lib memory_dump.cpp mutex.cpp path.cpp + program_options.cpp serialization.cpp sha1.cpp sha1_rfc3174.c diff --git a/src/base/program_options.cpp b/src/base/program_options.cpp new file mode 100644 index 000000000..3383ef6fa --- /dev/null +++ b/src/base/program_options.cpp @@ -0,0 +1,213 @@ +// ASEPRITE base library +// Copyright (C) 2001-2012 David Capello +// +// This source file is ditributed under a BSD-like license, please +// read LICENSE.txt for more information. + +#include "config.h" + +#include "base/program_options.h" + +#include +#include +#include + +using namespace std; + +namespace base { + +struct same_name { + const string& name; + same_name(const string& name) : name(name) { } + bool operator()(const ProgramOptions::Option* a) { + return a->name() == name; + } +}; + +struct same_mnemonic { + char mnemonic; + same_mnemonic(char mnemonic) : mnemonic(mnemonic) { } + bool operator()(const ProgramOptions::Option* a) { + return a->mnemonic() == mnemonic; + } +}; + +ProgramOptions::ProgramOptions() +{ +} + +ProgramOptions::~ProgramOptions() +{ + for (OptionList::const_iterator + it=m_options.begin(), end=m_options.end(); it != end; ++it) + delete *it; +} + +ProgramOptions::Option& ProgramOptions::add(const string& name) +{ + Option* option = new Option(name); + m_options.push_back(option); + return *option; +} + +void ProgramOptions::parse(int argc, const char* argv[]) +{ + for (int i=1; i 0) && (len > 0)) { + // Use mnemonics + if (n == 1) { + char usedBy = 0; + + for (size_t j=1; jsetEnabled(true); + + if (option->doesRequireValue()) { + if (usedBy != 0) { + stringstream msg; + msg << "You cannot use '-" << option->mnemonic() + << "' and '-" << usedBy << "' " + << "together, both options need one extra argument"; + throw InvalidProgramOptionsCombination(msg.str()); + } + + if (i+1 >= argc) { + stringstream msg; + msg << "Option '-" << option->mnemonic() + << "' needs one extra argument"; + throw ProgramOptionNeedsValue(msg.str()); + } + + // Set the value specified for this argument + option->setValue(argv[++i]); + usedBy = option->mnemonic(); + } + } + } + // Use name + else { + string optionName; + string optionValue; + size_t equalSignPos = arg.find('=', n); + + if (equalSignPos != string::npos) { + optionName = arg.substr(n, equalSignPos-n); + optionValue = arg.substr(equalSignPos+1); + } + else { + optionName = arg.substr(n); + } + + OptionList::iterator it = + find_if(m_options.begin(), m_options.end(), same_name(optionName)); + + if (it == m_options.end()) { + stringstream msg; + msg << "Invalid option '--" << optionName << "'"; + throw InvalidProgramOption(msg.str()); + } + + Option* option = *it; + option->setEnabled(true); + + if (option->doesRequireValue()) { + // If the option was specified without '=', we can get the + // value from the next argument. + if (equalSignPos == string::npos) { + if (i+1 >= argc) { + stringstream msg; + msg << "Missing value in '--" << optionName + << "'=VALUE option specification"; + throw ProgramOptionNeedsValue(msg.str()); + } + optionValue = argv[++i]; + } + + // Set the value specified for this argument + option->setValue(optionValue); + } + } + } + // Add values + else { + m_values.push_back(arg); + } + } +} + +void ProgramOptions::reset() +{ + m_values.clear(); + for_each(m_options.begin(), m_options.end(), &ProgramOptions::resetOption); +} + +// static +void ProgramOptions::resetOption(Option* option) +{ + option->setEnabled(false); + option->setValue(""); +} + +} // namespace base + +std::ostream& operator<<(std::ostream& os, const base::ProgramOptions& po) +{ + size_t maxOptionWidth = 0; + for (base::ProgramOptions::OptionList::const_iterator + it=po.options().begin(), end=po.options().end(); it != end; ++it) { + const base::ProgramOptions::Option* option = *it; + size_t optionWidth = std::min(26, 6+option->name().size()+1); + if (maxOptionWidth < optionWidth) + maxOptionWidth = optionWidth; + } + + for (base::ProgramOptions::OptionList::const_iterator + it=po.options().begin(), end=po.options().end(); it != end; ++it) { + const base::ProgramOptions::Option* option = *it; + size_t optionWidth = 6+option->name().size()+1; + + if (option->mnemonic() != 0) + os << setw(3) << '-' << option->mnemonic() << ", "; + else + os << setw(6) << ' '; + os << "--" << option->name(); + + if (!option->description().empty()) { + bool multilines = (option->description().find('\n') != string::npos); + + if (!multilines) { + os << setw(maxOptionWidth - optionWidth + 1) << ' ' << option->description(); + } + else { + istringstream s(option->description()); + std::string line; + if (std::getline(s, line)) { + os << setw(maxOptionWidth - optionWidth + 1) << ' ' << line << '\n'; + while (std::getline(s, line)) { + os << setw(maxOptionWidth+2) << ' ' << line << '\n'; + } + } + } + } + os << "\n"; + } + + return os; +} diff --git a/src/base/program_options.h b/src/base/program_options.h new file mode 100644 index 000000000..b284c18cf --- /dev/null +++ b/src/base/program_options.h @@ -0,0 +1,114 @@ +// ASEPRITE base library +// Copyright (C) 2001-2012 David Capello +// +// This source file is ditributed under a BSD-like license, please +// read LICENSE.txt for more information. + +#ifndef BASE_PROGRAM_OPTIONS_H_INCLUDED +#define BASE_PROGRAM_OPTIONS_H_INCLUDED + +#include +#include +#include +#include + +namespace base { + + class InvalidProgramOption : public std::runtime_error { + public: + InvalidProgramOption(const std::string& msg) + : std::runtime_error(msg) { } + }; + + class InvalidProgramOptionsCombination : public std::runtime_error { + public: + InvalidProgramOptionsCombination(const std::string& msg) + : std::runtime_error(msg) { } + }; + + class ProgramOptionNeedsValue : public std::runtime_error { + public: + ProgramOptionNeedsValue(const std::string& msg) + : std::runtime_error(msg) { } + }; + + class ProgramOptions { + public: + class Option { + public: + Option(const std::string& name) + : m_name(name) + , m_mnemonic(0) + , m_enabled(false) + , m_requiresValue(false) { + } + // Getters + const std::string& name() const { return m_name; } + const std::string& description() const { return m_description; } + const std::string& value() const { return m_value; } + char mnemonic() const { return m_mnemonic; } + bool enabled() const { return m_enabled; } + bool doesRequireValue() const { return m_requiresValue; } + // Setters + Option& description(const std::string& desc) { m_description = desc; return *this; } + Option& mnemonic(char mnemonic) { m_mnemonic = mnemonic; return *this; } + Option& requiresValue() { m_requiresValue = true; return *this; } + private: + void setValue(const std::string& value) { m_value = value; } + void setEnabled(bool enabled) { m_enabled = enabled; } + + std::string m_name; // Name of the option (e.g. "help" for "--help") + std::string m_description; // Description of the option (this can be used when the help is printed). + std::string m_value; // The value specified by the user in the command line. + char m_mnemonic; // One character that can be used in the command line to use this option. + bool m_enabled; // True if the user specified this argument. + bool m_requiresValue; // True if this option needs another argument. + + friend class ProgramOptions; + }; + + typedef std::vector OptionList; + typedef std::vector ValueList; + + ProgramOptions(); + + // After destructing the ProgramOptions, you cannot continue using + // references to "Option" instances obtained through add() function. + ~ProgramOptions(); + + // Adds a option for the program. The options must be specified + // before calling parse(). The returned reference must be used in + // the ProgramOptions lifetime. + Option& add(const std::string& name); + + // Detects which options where specified in the command line. + void parse(int argc, const char* argv[]); + + // Reset all options values/flags. + void reset(); + + // Returns the list of available options. To know the list of + // specified options you can iterate this list asking for + // Option::enabled() flag to know if the option was specified by + // the user in the command line. + const OptionList& options() const { return m_options; } + + // Returns the list of values that are not associated to any + // options. E.g. a list of files specified in the command line to + // be opened. + const ValueList& values() const { return m_values; } + + private: + static void resetOption(Option* option); + + OptionList m_options; + ValueList m_values; + }; + +} // namespace base + +// Prints the program options correctly formatted to be read by +// the user. E.g. This can be used in a --help option. +std::ostream& operator<<(std::ostream& os, const base::ProgramOptions& po); + +#endif diff --git a/src/base/program_options_unittest.cpp b/src/base/program_options_unittest.cpp new file mode 100644 index 000000000..bb1f05c49 --- /dev/null +++ b/src/base/program_options_unittest.cpp @@ -0,0 +1,147 @@ +// ASEPRITE base library +// Copyright (C) 2001-2012 David Capello +// +// This source file is ditributed under a BSD-like license, please +// read LICENSE.txt for more information. + +#include + +#include "base/program_options.h" + +using namespace base; + +TEST(ProgramOptions, OptionMembers) +{ + ProgramOptions po; + ProgramOptions::Option& help = + po.add("help").mnemonic('h').description("Show the help"); + ProgramOptions::Option& output = + po.add("output").mnemonic('O').requiresValue(); + + EXPECT_EQ("help", help.name()); + EXPECT_EQ("Show the help", help.description()); + EXPECT_EQ('h', help.mnemonic()); + EXPECT_FALSE(help.enabled()); + EXPECT_FALSE(help.doesRequireValue()); + + EXPECT_EQ("output", output.name()); + EXPECT_EQ("", output.description()); + EXPECT_EQ('O', output.mnemonic()); + EXPECT_FALSE(output.enabled()); + EXPECT_TRUE(output.doesRequireValue()); +} + +TEST(ProgramOptions, Reset) +{ + ProgramOptions po; + ProgramOptions::Option& help = po.add("help"); + ProgramOptions::Option& file = po.add("file").requiresValue(); + EXPECT_FALSE(help.enabled()); + EXPECT_FALSE(file.enabled()); + EXPECT_EQ("", file.value()); + + const char* argv[] = { "program.exe", "--help", "--file=readme.txt" }; + po.parse(3, argv); + EXPECT_TRUE(help.enabled()); + EXPECT_TRUE(file.enabled()); + EXPECT_EQ("readme.txt", file.value()); + + po.reset(); + EXPECT_FALSE(help.enabled()); + EXPECT_FALSE(file.enabled()); + EXPECT_EQ("", file.value()); +} + +TEST(ProgramOptions, Parse) +{ + ProgramOptions po; + ProgramOptions::Option& help = po.add("help").mnemonic('?'); + ProgramOptions::Option& input = po.add("input").mnemonic('i').requiresValue(); + ProgramOptions::Option& output = po.add("output").mnemonic('o').requiresValue(); + + const char* argv1[] = { "program.exe", "-?" }; + po.parse(2, argv1); + EXPECT_TRUE(help.enabled()); + + const char* argv2[] = { "program.exe", "--help" }; + po.reset(); + po.parse(2, argv2); + EXPECT_TRUE(help.enabled()); + + const char* argv3[] = { "program.exe", "--input", "hello.cpp", "--output", "hello.exe" }; + po.reset(); + po.parse(5, argv3); + EXPECT_FALSE(help.enabled()); + EXPECT_TRUE(input.enabled()); + EXPECT_TRUE(output.enabled()); + EXPECT_EQ("hello.cpp", input.value()); + EXPECT_EQ("hello.exe", output.value()); + + const char* argv4[] = { "program.exe", "--input=hi.c", "--output=out.exe" }; + po.reset(); + po.parse(3, argv4); + EXPECT_FALSE(help.enabled()); + EXPECT_TRUE(input.enabled()); + EXPECT_TRUE(output.enabled()); + EXPECT_EQ("hi.c", input.value()); + EXPECT_EQ("out.exe", output.value()); + + const char* argv5[] = { "program.exe", "-?i", "input.md", "-o", "output.html", "extra-file.txt" }; + po.reset(); + po.parse(6, argv5); + EXPECT_TRUE(help.enabled()); + EXPECT_TRUE(input.enabled()); + EXPECT_TRUE(output.enabled()); + EXPECT_EQ("input.md", input.value()); + EXPECT_EQ("output.html", output.value()); + ASSERT_EQ(1, po.values().size()); + EXPECT_EQ("extra-file.txt", po.values()[0]); + + const char* argv6[] = { "program.exe", "value1", "value2", "-o", "output", "value3", "--input=input", "value4" }; + po.reset(); + po.parse(8, argv6); + ASSERT_EQ(4, po.values().size()); + EXPECT_EQ("value1", po.values()[0]); + EXPECT_EQ("value2", po.values()[1]); + EXPECT_EQ("value3", po.values()[2]); + EXPECT_EQ("value4", po.values()[3]); +} + +TEST(ProgramOptions, ParseErrors) +{ + ProgramOptions po; + ProgramOptions::Option& help = po.add("help").mnemonic('?'); + ProgramOptions::Option& input = po.add("input").mnemonic('i').requiresValue(); + ProgramOptions::Option& output = po.add("output").mnemonic('o').requiresValue(); + + const char* argv1[] = { "program.exe", "--input" }; + EXPECT_THROW(po.parse(2, argv1), ProgramOptionNeedsValue); + + const char* argv2[] = { "program.exe", "-i" }; + EXPECT_THROW(po.parse(2, argv2), ProgramOptionNeedsValue); + + const char* argv3[] = { "program.exe", "--test" }; + EXPECT_THROW(po.parse(2, argv3), InvalidProgramOption); + + const char* argv4[] = { "program.exe", "-?a" }; + po.reset(); + EXPECT_FALSE(help.enabled()); + EXPECT_THROW(po.parse(2, argv4), InvalidProgramOption); + EXPECT_TRUE(help.enabled()); // -? is parsed anyway, -a is the invalid option + + const char* argv5[] = { "program.exe", "-io", "input-and-output.txt" }; + po.reset(); + EXPECT_THROW(po.parse(2, argv5), ProgramOptionNeedsValue); + po.reset(); + EXPECT_THROW(po.parse(3, argv5), InvalidProgramOptionsCombination); + EXPECT_TRUE(input.enabled()); + EXPECT_TRUE(output.enabled()); + EXPECT_EQ("input-and-output.txt", input.value()); + EXPECT_EQ("", output.value()); +} + +int main(int argc, char** argv) +{ + ::testing::InitGoogleTest(&argc, argv); + return RUN_ALL_TESTS(); +}