mirror of
https://github.com/fmang/opustags.git
synced 2025-10-16 10:49:59 +02:00
Add basic option parsing + tests
This commit is contained in:
60
src/main.cc
60
src/main.cc
@@ -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
94
src/options.cc
Normal 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
31
src/options.h
Normal 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
128
tests/options_test.cc
Normal 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"}));
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user