Add basic option parsing + tests

This commit is contained in:
rr-
2016-02-19 20:47:51 +01:00
committed by Frédéric Mangano
parent 5cdcb5457f
commit 3a320a7b39
4 changed files with 309 additions and 4 deletions

View File

@@ -1,8 +1,60 @@
#include <iostream>
#include "dummy.h"
#include "options.h"
int main(int argc, const char **argv)
static const auto version = "1.1";
static void show_usage(const bool include_help)
{
std::cout << opustags::return_one() << std::endl;
return 0;
static const auto usage =
"Usage: opustags --help\n"
" opustags [OPTIONS] FILE\n"
" opustags OPTIONS FILE -o FILE\n";
static const auto help =
"Options:\n"
" -h, --help print this help\n"
" -o, --output write the modified tags to a file\n"
" -i, --in-place [SUFFIX] use a temporary file then replace the original file\n"
" -y, --overwrite overwrite the output file if it already exists\n"
" -d, --delete FIELD delete all the fields of a specified type\n"
" -a, --add FIELD=VALUE add a field\n"
" -s, --set FIELD=VALUE delete then add a field\n"
" -D, --delete-all delete all the fields!\n"
" -S, --set-all read the fields from stdin\n";
std::cout << "opustags v" << version << "\n";
std::cout << usage;
if (include_help)
{
std::cout << "\n";
std::cout << help;
}
}
int main(int argc, char **argv)
{
if (argc == 1)
{
show_usage(false);
return EXIT_SUCCESS;
}
try
{
const auto options = opustags::parse_args(argc, argv);
if (options.show_help)
{
show_usage(true);
return EXIT_SUCCESS;
}
std::cout << "Working...\n";
}
catch (const std::exception &e)
{
std::cerr << e.what();
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}

94
src/options.cc Normal file
View File

@@ -0,0 +1,94 @@
#include <getopt.h>
#include <regex>
#include "options.h"
using namespace opustags;
ArgumentError::ArgumentError(const std::string &message)
: std::runtime_error(message.c_str())
{
}
Options::Options() :
show_help(false),
overwrite(false),
delete_all(false),
set_all(false)
{
}
Options opustags::parse_args(const int argc, char **argv)
{
static const auto short_def = "ho:i::yd:a:s:DS";
static const option long_def[] = {
{"help", no_argument, 0, 'h'},
{"output", required_argument, 0, 'o'},
{"in-place", optional_argument, 0, 'i'},
{"overwrite", no_argument, 0, 'y'},
{"delete", required_argument, 0, 'd'},
{"add", required_argument, 0, 'a'},
{"set", required_argument, 0, 's'},
{"delete-all", no_argument, 0, 'D'},
{"set-all", no_argument, 0, 'S'},
{NULL, 0, 0, 0}
};
Options options;
char c;
optind = 0;
while ((c = getopt_long(argc, argv, short_def, long_def, nullptr)) != -1)
{
const std::string arg(optarg == nullptr ? "" : optarg);
switch (c)
{
case 'h':
options.show_help = true;
break;
case 'o':
options.path_out = arg;
break;
case 'i':
options.in_place = arg.empty() ? ".otmp" : arg;
break;
case 'y':
options.overwrite = true;
break;
case 'd':
if (arg.find('=') != std::string::npos)
throw ArgumentError("Invalid field: '" + arg + "'");
options.to_delete.push_back(arg);
break;
case 'a':
case 's':
{
std::smatch match;
std::regex regex("^(\\w+)=(.*)$");
if (!std::regex_match(arg, match, regex))
throw ArgumentError("Invalid field: '" + arg + "'");
options.to_add[match[1]] = match[2];
if (c == 's')
options.to_delete.push_back(match[1]);
break;
}
case 'S':
options.set_all = true;
break;
case 'D':
options.delete_all = true;
break;
default:
throw ArgumentError("Invalid flag");
}
}
return options;
}

31
src/options.h Normal file
View File

@@ -0,0 +1,31 @@
#pragma once
#include <stdexcept>
#include <map>
#include <vector>
namespace opustags
{
struct Options final
{
Options();
bool show_help;
bool overwrite;
bool delete_all;
bool set_all;
std::string in_place; // string?
std::string path_out;
std::map<std::string, std::string> to_add;
std::vector<std::string> to_delete;
};
class ArgumentError : public std::runtime_error
{
public:
ArgumentError(const std::string &message);
};
Options parse_args(const int argc, char **argv);
}

128
tests/options_test.cc Normal file
View File

@@ -0,0 +1,128 @@
#include "options.h"
#include <memory>
#include "catch.h"
static std::unique_ptr<char[]> string_to_uptr(const std::string &str)
{
auto ret = std::make_unique<char[]>(str.size() + 1);
for (size_t i = 0; i < str.size(); i++)
ret[i] = str[i];
ret[str.size()] = 0;
return ret;
}
static opustags::Options retrieve_options(std::vector<std::string> args)
{
// need to pass non-const char*, but we got const objects. make copies
std::vector<std::unique_ptr<char[]>> arg_holders;
arg_holders.push_back(string_to_uptr("fake/path/to/program"));
for (size_t i = 0; i < args.size(); i++)
arg_holders.push_back(string_to_uptr(args[i]));
auto plain_args = std::make_unique<char*[]>(arg_holders.size());
for (size_t i = 0; i < arg_holders.size(); i++)
plain_args[i] = arg_holders[i].get();
return opustags::parse_args(arg_holders.size(), plain_args.get());
}
TEST_CASE("Options parsing test")
{
SECTION("--help")
{
REQUIRE(retrieve_options({"--help"}).show_help);
REQUIRE(retrieve_options({"--h"}).show_help);
REQUIRE(!retrieve_options({}).show_help);
}
SECTION("--overwrite")
{
REQUIRE(retrieve_options({"--overwrite"}).overwrite);
REQUIRE(retrieve_options({"-y"}).overwrite);
REQUIRE(!retrieve_options({}).overwrite);
}
SECTION("--set-all")
{
REQUIRE(retrieve_options({"--set-all"}).set_all);
REQUIRE(retrieve_options({"-S"}).set_all);
REQUIRE(!retrieve_options({}).set_all);
}
SECTION("--delete-all")
{
REQUIRE(retrieve_options({"--delete-all"}).delete_all);
REQUIRE(retrieve_options({"-D"}).delete_all);
REQUIRE(!retrieve_options({}).delete_all);
}
SECTION("--in-place")
{
REQUIRE(retrieve_options({"-i"}).in_place == ".otmp");
REQUIRE(retrieve_options({"--in-place"}).in_place == ".otmp");
REQUIRE(retrieve_options({"--in-place=ABC"}).in_place == "ABC");
REQUIRE(retrieve_options({"-iABC"}).in_place == "ABC");
REQUIRE(retrieve_options({"--in-place", "ABC"}).in_place == ".otmp");
REQUIRE(retrieve_options({"-i", "ABC"}).in_place == ".otmp");
REQUIRE(retrieve_options({}).in_place.empty());
}
SECTION("--output")
{
REQUIRE(retrieve_options({"--output", "ABC"}).path_out == "ABC");
REQUIRE(retrieve_options({"-o", "ABC"}).path_out == "ABC");
REQUIRE_THROWS(retrieve_options({"--delete", "invalid="}));
}
SECTION("--delete")
{
REQUIRE(
retrieve_options({"--delete", "ABC"}).to_delete
== std::vector<std::string>{"ABC"});
REQUIRE(
retrieve_options({"-d", "ABC"}).to_delete
== std::vector<std::string>{"ABC"});
REQUIRE(
retrieve_options({"-d", "ABC", "-d", "XYZ"}).to_delete
== (std::vector<std::string>{"ABC", "XYZ"}));
REQUIRE_THROWS(retrieve_options({"--delete", "invalid="}));
}
SECTION("--add")
{
const auto args1 = retrieve_options({"--add", "ABC=XYZ"});
const auto args2 = retrieve_options({"-a", "ABC=XYZ"});
REQUIRE(args1.to_add == args2.to_add);
REQUIRE(args1.to_add
== (std::map<std::string, std::string>{{"ABC", "XYZ"}}));
REQUIRE(args1.to_delete.empty());
REQUIRE(args2.to_delete.empty());
const auto args3 = retrieve_options({"-a", "ABC=XYZ", "-a", "1=2"});
REQUIRE(args3.to_add == (std::map<std::string, std::string>{
{"ABC", "XYZ"},
{"1", "2"}}));
REQUIRE(args3.to_delete.empty());
REQUIRE_THROWS(retrieve_options({"--add", "invalid"}));
}
SECTION("--set")
{
const auto args1 = retrieve_options({"--set", "ABC=XYZ"});
const auto args2 = retrieve_options({"-s", "ABC=XYZ"});
REQUIRE(args1.to_add == args2.to_add);
REQUIRE(args1.to_add
== (std::map<std::string, std::string>{{"ABC", "XYZ"}}));
REQUIRE(args1.to_delete == args2.to_delete);
REQUIRE(args1.to_delete == std::vector<std::string>{"ABC"});
const auto args3 = retrieve_options({"-s", "ABC=XYZ", "-s", "1=2"});
REQUIRE(args3.to_add == (std::map<std::string, std::string>{
{"ABC", "XYZ"},
{"1", "2"}}));
REQUIRE(args3.to_delete == (std::vector<std::string>{"ABC", "1"}));
REQUIRE_THROWS(retrieve_options({"--set", "invalid"}));
}
}