From 7058c326c6998cb6e706cf804641872fc0a9c8ee Mon Sep 17 00:00:00 2001 From: Armin Friedl Date: Sat, 1 Aug 2020 20:06:54 +0200 Subject: [PATCH] Add argument parser - Provides better handling of invalid invocation - Provides usage output - More flexible for future extension (e.g. compression) --- src/fileformats.hpp | 25 ++++++++++++- src/main.cpp | 17 +++++++-- src/meson.build | 3 +- src/util/argparse.cpp | 29 ++++++++++++++ src/util/argparse.hpp | 79 +++++++++++++++++++++++++++++++++++++++ src/util/log.hpp | 2 +- test/fileformats_test.cpp | 30 +++++++++++++++ 7 files changed, 179 insertions(+), 6 deletions(-) create mode 100644 src/util/argparse.cpp create mode 100644 src/util/argparse.hpp diff --git a/src/fileformats.hpp b/src/fileformats.hpp index b7a2dcd..95c94a6 100644 --- a/src/fileformats.hpp +++ b/src/fileformats.hpp @@ -4,6 +4,7 @@ #pragma once #include +#include namespace logger = spdlog; #include @@ -22,7 +23,7 @@ namespace xwim { */ const std::set fileformats{".7z", ".7zip", ".jar", ".tgz", ".bz2", ".bzip2", ".gz", ".gzip", - ".rar", ".tar", "xz", ".zip"}; + ".rar", ".tar", ".xz", ".zip"}; /** Strip archive extensions from a path * @@ -43,4 +44,26 @@ inline std::filesystem::path stem(const std::filesystem::path& path) { return p_stem; } +/** Get the archive extension of a path. + * + * The archive extension may be a combination of supported fileformats in which + * case all of them are returned. + * + * @returns Archive extension of the archive or path() if no (known) extension + * exists. + */ +inline std::filesystem::path ext(const std::filesystem::path& path) { + std::filesystem::path p_ext{path}; + logger::trace("Extracting extension of {}", p_ext.string()); + + std::filesystem::path p_ext_collector; + while (fileformats.find(p_ext.extension().string()) != fileformats.end()) { + // path extension() const + p_ext_collector = p_ext.extension().concat(p_ext_collector.string()); + p_ext.replace_extension(); + } + + return p_ext_collector; +} + } // namespace xwim diff --git a/src/main.cpp b/src/main.cpp index 5d4cf5b..745e778 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -1,4 +1,5 @@ #include +#include namespace logger = spdlog; #include @@ -7,6 +8,7 @@ namespace logger = spdlog; #include #include "util/log.hpp" +#include "util/argparse.hpp" #include "archive.hpp" #include "spec.hpp" #include "fileformats.hpp" @@ -14,9 +16,18 @@ namespace logger = spdlog; int main(int argc, char** argv) { xwim::log::init(); + xwim::argparse::XwimPath xwim_path; + try { - std::filesystem::path filepath{argv[1]}; - xwim::Archive archive{filepath}; + xwim_path = xwim::argparse::parse(argc, argv); + } catch (xwim::argparse::ArgParseException& ex) { + logger::error("{}\n", ex.what()); + std::cout << xwim::argparse::usage(); + std::exit(1); + } + + try { + xwim::Archive archive{xwim_path.path()}; xwim::ArchiveSpec archive_spec = archive.check(); logger::info("{}", archive_spec); @@ -26,7 +37,7 @@ int main(int argc, char** argv) { if (!archive_spec.has_single_root || !archive_spec.is_root_filename) { extract_spec.make_dir = true; - std::filesystem::path stem = xwim::stem(filepath); + std::filesystem::path stem = xwim::stem(xwim_path.path()); extract_spec.dirname = stem; } diff --git a/src/meson.build b/src/meson.build index 3889453..ab44dda 100644 --- a/src/meson.build +++ b/src/meson.build @@ -1,6 +1,7 @@ xwim_src = ['main.cpp', 'archive.cpp', - 'archive_sys.cpp'] + 'archive_sys.cpp', + 'util/argparse.cpp'] xwim_libs = [dependency('libarchive', required: true), dependency('fmt', required: true), diff --git a/src/util/argparse.cpp b/src/util/argparse.cpp new file mode 100644 index 0000000..0464ca6 --- /dev/null +++ b/src/util/argparse.cpp @@ -0,0 +1,29 @@ +#include "argparse.hpp" + +namespace xwim { +namespace argparse { + XwimPath parse(int argc, char** argv) { + return XwimPath{argc, argv}; + } + + // contructs XwimPath{} first so that destructurs may running + // http://www.vishalchovatiya.com/7-best-practices-for-exception-handling-in-cpp-with-example/ + XwimPath::XwimPath(int argc, char** argv) : XwimPath{} { + if (argc < 2) throw ArgParseException{"No argument provided"}; + if (argc > 2) throw ArgParseException{"Too many arguments provided"}; + + this->_path = std::filesystem::path{argv[1]}; + + // Remove when compression in place + if(!is_archive()) throw ArgParseException{"Not a known archive format"}; + } + + bool XwimPath::is_archive() { + return !xwim::ext(_path).empty(); + } + + std::filesystem::path XwimPath::path() const { + return std::filesystem::path{_path}; + } +} +} diff --git a/src/util/argparse.hpp b/src/util/argparse.hpp new file mode 100644 index 0000000..740761e --- /dev/null +++ b/src/util/argparse.hpp @@ -0,0 +1,79 @@ +#pragma once + +#include +#include +#include + +#include "../fileformats.hpp" + +namespace xwim { +/** + * xwim allows for + * 1. an archive + * 2. a file or folder + * + * In case of (1) the archive will be extracted according to the xwim + * do-what-i-mean rules. + * + * In case of (2) the file or folder will be compressed into a "platform native" + * format, i.e. what appears to be the most widely used format on that platform. + * In case of unix this is tar.gz. In case of windows this is zip. The archive + * gets the same name as the file or folder and a proper extension. + * + * A list of files or folders is unsupported as it would be too ambigious to + * choose a name for the archive. A list of archives is unsupported for + * consistency reasons. Any mixture is unsupported as it would be too ambigious + * what the user wants. This is subject to change in the future. + */ +namespace argparse { + +class XwimPath { + private: + std::filesystem::path _path; + + public: + XwimPath() : _path{} {}; + XwimPath(int argc, char** argv); + + bool is_archive(); + std::filesystem::path path() const; +}; + +class ArgParseException : public std::exception { + private: + std::string _what; + + public: + ArgParseException(std::string what) : _what{what} {}; + + virtual const char* what() const noexcept { return this->_what.c_str(); } +}; + +XwimPath parse(int argc, char** argv); + +inline std::string usage() { + std::stringstream s; + s << "USAGE:" + << "\t xwim \n" + << "\n" + + << "PARAMS:" << std::left << std::setfill('.') << std::setw(10) + << "\t path " + << " Archive\n" + << "\n" + + << "FORMATS:\n" + << "\t .7z, .7zip .jar, .tgz, .bz2, .bzip2\n" + << "\t .gz, .gzip, .rar, .tar, .xz, .zip\n" + << "\n" + + << "EXAMPLES:\n" + << "\t Extract archive archive.tar.gz:\n" + << "\t xwim archive.tar.gz\n" + << std::endl; + + return s.str(); +} + +} // namespace argparse +} // namespace xwim diff --git a/src/util/log.hpp b/src/util/log.hpp index 29d1c50..cbd6585 100644 --- a/src/util/log.hpp +++ b/src/util/log.hpp @@ -34,7 +34,7 @@ spdlog::level::level_enum _init_from_env() { } return lvl; -}; +} /** * Get log level from compile time definition. diff --git a/test/fileformats_test.cpp b/test/fileformats_test.cpp index d0fcebd..acd6e1d 100644 --- a/test/fileformats_test.cpp +++ b/test/fileformats_test.cpp @@ -32,3 +32,33 @@ TEST(FileformatsTest, StemStripsNothingWithoutExtension) { ASSERT_EQ(xwim::stem(archive_path), std::filesystem::path{"filerar"}); } + +TEST(FileExtTest, ExtGetsKnownExtension) { + std::filesystem::path archive_path{"/some/path/to/file.rar"}; + + ASSERT_EQ(xwim::ext(archive_path), std::filesystem::path{".rar"}); +} + +TEST(FileExtTest, CombinedExtensionGetsAll) { + std::filesystem::path archive_path{"/some/path/to/file.tar.gz"}; + + ASSERT_EQ(xwim::ext(archive_path), std::filesystem::path{".tar.gz"}); +} + +TEST(FileExtTest, ExtEmptyForUnknownExtension) { + std::filesystem::path archive_path{"/some/path/to/file.ukn"}; + + ASSERT_TRUE(xwim::ext(archive_path).empty()); +} + +TEST(FileExtTest, CombinedExtensionGetsKnown) { + std::filesystem::path archive_path{"/some/path/to/file.ukn.tar.gz"}; + + ASSERT_EQ(xwim::ext(archive_path), std::filesystem::path{".tar.gz"}); +} + +TEST(FileExtTest, CombinedExtensionLastUnknownEmpty) { + std::filesystem::path archive_path{"/some/path/to/file.tar.gz.ukn"}; + + ASSERT_TRUE(xwim::ext(archive_path).empty()); +}