diff --git a/src/main.cc b/src/main.cc index 7de2e9c..42afd4a 100644 --- a/src/main.cc +++ b/src/main.cc @@ -1,8 +1,60 @@ #include -#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; } diff --git a/src/options.cc b/src/options.cc new file mode 100644 index 0000000..a34c906 --- /dev/null +++ b/src/options.cc @@ -0,0 +1,94 @@ +#include +#include +#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; +} diff --git a/src/options.h b/src/options.h new file mode 100644 index 0000000..d63f8ed --- /dev/null +++ b/src/options.h @@ -0,0 +1,31 @@ +#pragma once + +#include +#include +#include + +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 to_add; + std::vector to_delete; + }; + + class ArgumentError : public std::runtime_error + { + public: + ArgumentError(const std::string &message); + }; + + Options parse_args(const int argc, char **argv); +} diff --git a/tests/options_test.cc b/tests/options_test.cc new file mode 100644 index 0000000..a421f66 --- /dev/null +++ b/tests/options_test.cc @@ -0,0 +1,128 @@ +#include "options.h" +#include +#include "catch.h" + +static std::unique_ptr string_to_uptr(const std::string &str) +{ + auto ret = std::make_unique(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 args) +{ + // need to pass non-const char*, but we got const objects. make copies + std::vector> 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(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{"ABC"}); + REQUIRE( + retrieve_options({"-d", "ABC"}).to_delete + == std::vector{"ABC"}); + REQUIRE( + retrieve_options({"-d", "ABC", "-d", "XYZ"}).to_delete + == (std::vector{"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{{"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{ + {"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{{"ABC", "XYZ"}})); + REQUIRE(args1.to_delete == args2.to_delete); + REQUIRE(args1.to_delete == std::vector{"ABC"}); + + const auto args3 = retrieve_options({"-s", "ABC=XYZ", "-s", "1=2"}); + REQUIRE(args3.to_add == (std::map{ + {"ABC", "XYZ"}, + {"1", "2"}})); + REQUIRE(args3.to_delete == (std::vector{"ABC", "1"})); + + REQUIRE_THROWS(retrieve_options({"--set", "invalid"})); + } +}