Add ProgramOptions class.

This commit is contained in:
David Capello 2012-09-03 00:41:17 -03:00
parent 408f54e509
commit 8962c6dbe7
4 changed files with 475 additions and 0 deletions

View File

@ -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

View File

@ -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 <algorithm>
#include <sstream>
#include <iomanip>
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<argc; ++i) {
string arg(argv[i]);
// n = number of dashes ('-') at the beginning of the argument.
size_t n = 0;
for (; arg[n] == '-'; ++n)
;
size_t len = arg.size()-n;
if ((n > 0) && (len > 0)) {
// Use mnemonics
if (n == 1) {
char usedBy = 0;
for (size_t j=1; j<arg.size(); ++j) {
OptionList::iterator it =
find_if(m_options.begin(), m_options.end(), same_mnemonic(arg[j]));
if (it == m_options.end()) {
stringstream msg;
msg << "Invalid option '-" << arg[j] << "'";
throw InvalidProgramOption(msg.str());
}
Option* option = *it;
option->setEnabled(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<int>(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;
}

114
src/base/program_options.h Normal file
View File

@ -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 <string>
#include <vector>
#include <iosfwd>
#include <stdexcept>
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<Option*> OptionList;
typedef std::vector<std::string> 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

View File

@ -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 <gtest/gtest.h>
#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();
}