diff --git a/.drone.yml b/.drone.yml index 2cefdbb..f442cbc 100644 --- a/.drone.yml +++ b/.drone.yml @@ -13,12 +13,6 @@ trigger: - tag steps: -- name: download - image: plugins/download - settings: - source: https://raw.githubusercontent.com/badaix/popl/v1.2.0/include/popl.hpp - destination: src/cli/popl.hpp - - name: gcc6 image: debian:stretch-slim pull: always @@ -167,12 +161,6 @@ trigger: - tag steps: -- name: download - image: plugins/download - settings: - source: https://raw.githubusercontent.com/badaix/popl/v1.2.0/include/popl.hpp - destination: src/cli/popl.hpp - - name: deb image: debian:stretch-slim pull: always diff --git a/README.adoc b/README.adoc index bad1326..770de23 100644 --- a/README.adoc +++ b/README.adoc @@ -58,7 +58,6 @@ only. https://llvm.org/[clang] 3/7) * https://cmake.org/[cmake] (at least: 3.2) * https://pkgconfig.freedesktop.org/wiki/[pkgconfig] (tested: 0.29) -* https://github.com/badaix/popl[popl] (tested: 1.2) * http://repo.or.cz/w/libxdg-basedir.git[libxdg-basedir] (tested: 1.2) * https://pocoproject.org/[POCO] (tested: 1.9 / 1.7) * http://vsqlite.virtuosic-bytes.com/[vsqlite++] (tested: 0.3) @@ -75,8 +74,6 @@ only. apt-get update apt-get install g++-6 cmake pkg-config libpoco-dev libxdg-basedir-dev \ libvsqlitepp-dev libboost-system-dev libboost-filesystem-dev asciidoc -# Inside the source directory: -wget -O src/cli/popl.hpp https://raw.githubusercontent.com/badaix/popl/v1.2.0/include/popl.hpp export CXX="g++-6" ---- ==== diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index e0e4fd6..540d4fa 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -1,5 +1,7 @@ include(GNUInstallDirs) +find_package(Poco COMPONENTS Util CONFIG) + file(GLOB sources_cli *.cpp) add_executable(${PROJECT_NAME}-cli ${sources_cli}) @@ -11,7 +13,7 @@ target_include_directories(${PROJECT_NAME}-cli PRIVATE "${PROJECT_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(${PROJECT_NAME}-cli - PRIVATE ${PROJECT_NAME}) + PRIVATE ${PROJECT_NAME} Poco::Util) install(TARGETS ${PROJECT_NAME}-cli DESTINATION ${CMAKE_INSTALL_BINDIR}) diff --git a/src/cli/main.cpp b/src/cli/main.cpp index 443214d..d16c76a 100644 --- a/src/cli/main.cpp +++ b/src/cli/main.cpp @@ -18,10 +18,9 @@ #include #include #include -#include #include #include "sqlite.hpp" -#include "parse_options.hpp" +#include "remwharead_cli.hpp" #include "uri.hpp" #include "types.hpp" #include "export/csv.hpp" @@ -31,42 +30,60 @@ #include "search.hpp" using namespace remwharead; -using std::cout; using std::cerr; using std::endl; using std::string; using std::chrono::system_clock; +using std::ofstream; -int main(const int argc, const char *argv[]) +int App::main(const std::vector &args) { std::locale::global(std::locale("")); // Set locale globally. - options opts = parse_options(argc, argv); - if (opts.status_code != 0) + if (_version_requested) { - return opts.status_code; + print_version(); + } + else if (_help_requested) + { + print_help(); + } + else + { + if (_argument_error) + { + return Application::EXIT_USAGE; + } + if (args.size() > 0) + { + _uri = args[0]; + } + if (_uri.empty() && _format == export_format::undefined) + { + cerr << "Error: You have to specify either URI or --export.\n"; + return Application::EXIT_USAGE; + } } Database db; - if (!db) { cerr << "Error: Database connection failed.\n"; - return 2; + return Application::EXIT_IOERR; } - if (!opts.uri.empty()) + if (!_uri.empty()) { - URI uri(opts.uri); + URI uri(_uri); html_extract page = uri.get(); if (!page) { cerr << "Error: Could not fetch page.\n"; cerr << page.error << endl; - return 4; + return Application::EXIT_UNAVAILABLE; } archive_answer archive; - if (opts.archive) + if (_archive) { archive = uri.archive(); if (!archive) @@ -74,35 +91,36 @@ int main(const int argc, const char *argv[]) cerr << "Error archiving URL: " << archive.error << endl; } } - db.store({opts.uri, archive.uri, system_clock::now(), opts.tags, + db.store({_uri, archive.uri, system_clock::now(), _tags, page.title, page.description, page.fulltext}); } - std::ofstream file; - if (!opts.file.empty()) + ofstream file; + if (!_file.empty()) { - file.open(opts.file); + file.open(_file); if (!file.good()) { - cerr << "Error: Could not open file: " << opts.file << endl; - return 3; + cerr << "Error: Could not open file: " << _file << endl; + return Application::EXIT_IOERR; } } - if (opts.format != export_format::undefined) + + if (_format != export_format::undefined) { vector entries; - Search search(db.retrieve(opts.span[0], opts.span[1])); + Search search(db.retrieve(_timespan[0], _timespan[1])); - if (!opts.search_tags.empty()) + if (!_search_tags.empty()) { - entries = search.search_tags(opts.search_tags, opts.regex); + entries = search.search_tags(_search_tags, _regex); } - else if (!opts.search_all.empty()) + else if (!_search_all.empty()) { - entries = search.search_all(opts.search_all, opts.regex); + entries = search.search_all(_search_all, _regex); } - switch (opts.format) + switch (_format) { case export_format::csv: { @@ -163,5 +181,7 @@ int main(const int argc, const char *argv[]) } } - return 0; + return Application::EXIT_OK; } + +POCO_APP_MAIN(App) diff --git a/src/cli/parse_options.cpp b/src/cli/parse_options.cpp index 8910276..4a804fe 100644 --- a/src/cli/parse_options.cpp +++ b/src/cli/parse_options.cpp @@ -15,168 +15,188 @@ */ #include -#include -#include +#include +#include #include "version.hpp" -#include "parse_options.hpp" +#include "remwharead_cli.hpp" using std::cout; using std::cerr; using std::endl; +using Poco::Util::Option; +using Poco::Util::OptionCallback; +using Poco::Util::HelpFormatter; -options::options() +App::App() + : _help_requested(false) + , _version_requested(false) + , _argument_error(false) + , _uri() + , _tags() + , _format(export_format::undefined) + , _timespan({ time_point(), system_clock::now() }) + , _archive(true) + , _regex(false) {} -options::options(const uint8_t &status) - : status_code(status) -{} - -const options parse_options(const int argc, const char *argv[]) +void App::defineOptions(OptionSet& options) { - string tags; - string format; - string span; - options opts; - - try - { - popl::OptionParser op("Available options"); - op.add> - ("t", "tags", "Add tags to URI, delimited by commas.", "", &tags); - auto option_export = op.add> - ("e", "export", "Export to format.", "simple", &format); - op.add> - ("f", "file", "Save output to file.", "", &opts.file); - op.add> - ("T", "time-span", - "Only export entries between YYYY-MM-DD,YYYY-MM-DD.", "", &span); - op.add> - ("s", "search-tags", - "Search in tags. Format: tag1 AND tag2 OR tag3.", - "", &opts.search_tags); - op.add> - ("S", "search-all", - "Search in tags, title, description and full text.", - "", &opts.search_all); - op.add - ("r", "regex", "Use regular expression for search.", &opts.regex); - auto option_noarchive = op.add - ("N", "no-archive", "Do not archive URI."); - auto option_help = op.add - ("h", "help", "Show this help message."); - auto option_version = op.add - ("V", "version", "Print version, copyright and license."); - op.parse(argc, argv); - - if (option_help->is_set()) - { - cout << "Usage: " << argv[0] << " [-t tags] [-N] URI\n" - << " " << argv[0] - << " -e format [-f file] [-T start,end] " - << "[[-s|-S] expression] [-r]\n"; - cout << op; - return options(0); - } - - if (option_version->is_set()) - { - cout << "remwharead " << global::version << endl << - "Copyright (C) 2019 tastytea \n" - "License GPLv3: GNU GPL version 3 " - ".\n" - "This program comes with ABSOLUTELY NO WARRANTY. This is free software,\n" - "and you are welcome to redistribute it under certain conditions.\n"; - return options(0); - } - - if (option_noarchive->is_set()) - { - opts.archive = false; - } - - if (!tags.empty()) - { - size_t pos_end = 0; - size_t pos_start = 0; - while (pos_end != std::string::npos) - { - pos_end = tags.find(',', pos_start); - string buffer = tags.substr(pos_start, pos_end - pos_start); - while (*buffer.begin() == ' ') // Remove leading spaces. - { - buffer.erase(buffer.begin()); - } - while (*buffer.rbegin() == ' ') // Remove trailing spaces. - { - buffer.erase(buffer.end() - 1); - } - opts.tags.push_back(buffer); - pos_start = pos_end + 1; - } - } - - if (option_export->is_set()) - { - if (format == "csv") - { - opts.format = export_format::csv; - } - else if (format == "asciidoc" || format == "adoc") - { - opts.format = export_format::asciidoc; - } - else if (format == "bookmarks") - { - opts.format = export_format::bookmarks; - } - else if (format == "simple") - { - opts.format = export_format::simple; - } - else - { - opts.format = export_format::undefined; - cerr << "Error: Export format must be " - << "csv, asciidoc, bookmarks or simple.\n"; - return options(1); - } - } - - if (!span.empty()) - { - size_t pos = span.find(','); - if (pos != std::string::npos) - { - opts.span = - { - string_to_timepoint(span.substr(0, pos)), - string_to_timepoint(span.substr(pos + 1)) - }; - } - else - { - cerr << "Error: Time span must be in format: " - "YYYY-MM-DD,YYYY-MM-DD.\n"; - return options(1); - } - } - - if (op.non_option_args().size() > 0) - { - opts.uri = op.non_option_args().front(); - } - - if (opts.uri == "" && opts.format == export_format::undefined) - { - cerr << "Error: You have to specify either URI or --export.\n"; - return options(1); - } - } - catch (const std::exception &e) - { - cerr << "Error in " << __func__ << ": " << e.what() << endl; - return options(1); - } - - return opts; + options.addOption( + Option("help", "h", "Show this help message.") + .callback(OptionCallback(this, &App::handle_info))); + options.addOption( + Option("version", "V", "Print version, copyright and license.") + .callback(OptionCallback(this, &App::handle_info))); + options.addOption( + Option("tags", "t", "Add tags to URI, delimited by commas.") + .argument("Tags") + .callback(OptionCallback(this, &App::handle_options))); + options.addOption( + Option("export", "e", "Export to format.") + .argument("Format") + .callback(OptionCallback(this, &App::handle_options))); + options.addOption( + Option("file", "f", "Save output to file.") + .argument("File") + .callback(OptionCallback(this, &App::handle_options))); + options.addOption( + Option("time-span", "T", + "Only export entries between YYYY-MM-DD,YYYY-MM-DD.") + .argument("Times") + .callback(OptionCallback(this, &App::handle_options))); + options.addOption( + Option("search-tags", "s", + "Search in tags. Format: tag1 AND tag2 OR tag3.") + .argument("Expression") + .callback(OptionCallback(this, &App::handle_options))); + options.addOption( + Option("search-all", "S", + "Search in tags, title, description and full text.") + .argument("Expression") + .callback(OptionCallback(this, &App::handle_options))); + options.addOption( + Option("regex", "r", "Use regular expression for search.")); + options.addOption( + Option("no-archive", "N", "Do not archive URI.") + .callback(OptionCallback(this, &App::handle_options))); +} + +void App::handle_info(const std::string &name, const std::string &) +{ + if (name == "help") + { + _help_requested = true; + } + else if (name == "version") + { + _version_requested = true; + } + + stopOptionsProcessing(); +} + +void App::handle_options(const std::string &name, const std::string &value) +{ + if (name == "tags") + { + size_t pos_end = 0; + size_t pos_start = 0; + while (pos_end != std::string::npos) + { + pos_end = value.find(',', pos_start); + string buffer = value.substr(pos_start, pos_end - pos_start); + while (*buffer.begin() == ' ') // Remove leading spaces. + { + buffer.erase(buffer.begin()); + } + while (*buffer.rbegin() == ' ') // Remove trailing spaces. + { + buffer.erase(buffer.end() - 1); + } + _tags.push_back(buffer); + pos_start = pos_end + 1; + } + } + else if (name == "export") + { + if (value == "csv") + { + _format = export_format::csv; + } + else if (value == "asciidoc" || value == "adoc") + { + _format = export_format::asciidoc; + } + else if (value == "bookmarks") + { + _format = export_format::bookmarks; + } + else if (value == "simple") + { + _format = export_format::simple; + } + else + { + cerr << "Error: Unknown format.\n"; + _argument_error = true; + } + } + else if (name == "file") + { + _file = value; + } + else if (name == "time-span") + { + size_t pos = value.find(','); + if (pos != std::string::npos) + { + _timespan = + { + string_to_timepoint(value.substr(0, pos)), + string_to_timepoint(value.substr(pos + 1)) + }; + } + else + { + cerr << "Error: Time span must be in format: " + "YYYY-MM-DD,YYYY-MM-DD.\n"; + _argument_error = true; + } + } + else if (name == "search-tags") + { + _search_tags = value; + } + else if (name == "search-all") + { + _search_all = value; + } + else if (name == "no-archive") + { + _archive = false; + } + else if (name == "regex") + { + _regex = true; + } +} + +void App::print_help() +{ + HelpFormatter helpFormatter(options()); + helpFormatter.setCommand(commandName()); + helpFormatter.setUsage("[-t tags] [-N] URI\n" + "-e format [-f file] [-T start,end]\n" + "[[-s|-S] expression] [-r]"); + helpFormatter.format(cout); +} + +void App::print_version() +{ + cout << "remwharead " << global::version << endl << + "Copyright (C) 2019 tastytea \n" + "License GPLv3: GNU GPL version 3 " + ".\n" + "This program comes with ABSOLUTELY NO WARRANTY. This is free software," + "\nand you are welcome to redistribute it under certain conditions.\n"; } diff --git a/src/cli/parse_options.hpp b/src/cli/remwharead_cli.hpp similarity index 58% rename from src/cli/parse_options.hpp rename to src/cli/remwharead_cli.hpp index d69e6f7..6d472cf 100644 --- a/src/cli/parse_options.hpp +++ b/src/cli/remwharead_cli.hpp @@ -21,7 +21,8 @@ #include #include #include -#include +#include +#include #include "types.hpp" #include "time.hpp" @@ -31,26 +32,34 @@ using std::vector; using std::array; using std::chrono::system_clock; using time_point = system_clock::time_point; -using std::uint8_t; +using Poco::Util::OptionSet; -typedef struct options +class App : public Poco::Util::Application { - vector tags; - export_format format = export_format::undefined; - string file; - array span = {{ time_point(), system_clock::now() }}; - string uri; - string search_tags; - string search_all; - bool regex = false; - bool archive = true; - uint8_t status_code = 0; +public: + App(); - options(); - explicit options(const uint8_t &status); -} options; +protected: + void defineOptions(OptionSet& options); + void handle_info(const std::string &name, const std::string &value); + void handle_options(const std::string &name, const std::string &value); + void print_help(); + void print_version(); + int main(const std::vector &args); -// Parse command-line options. -const options parse_options(const int argc, const char *argv[]); +private: + bool _help_requested; + bool _version_requested; + bool _argument_error; + string _uri; + vector _tags; + export_format _format; + string _file; + array _timespan; + string _search_tags; + string _search_all; + bool _archive; + bool _regex; +}; #endif // REMWHAREAD_PARSE_OPTIONS_HPP diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index cf39c92..9739fd9 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,11 +1,5 @@ include(CTest) -# I'm linking the library into the testlib to get the include directories. -add_library(${PROJECT_NAME}_testlib ../src/cli/parse_options.cpp) -target_include_directories(${PROJECT_NAME}_testlib - PUBLIC "${PROJECT_BINARY_DIR}" "../src/cli") -target_link_libraries(${PROJECT_NAME}_testlib PUBLIC ${PROJECT_NAME}) - file(GLOB sources_tests test_*.cpp) find_package(Catch2 CONFIG) @@ -13,7 +7,7 @@ if(Catch2_FOUND) # Catch 2.x include(Catch) add_executable(all_tests main.cpp ${sources_tests}) target_link_libraries(all_tests - PRIVATE Catch2::Catch2 ${PROJECT_NAME}_testlib) + PRIVATE Catch2::Catch2 ${PROJECT_NAME}) target_include_directories(all_tests PRIVATE "/usr/include/catch2") catch_discover_tests(all_tests EXTRA_ARGS "${EXTRA_TEST_ARGS}") else() # Catch 1.x @@ -23,7 +17,7 @@ else() # Catch 1.x get_filename_component(bin ${src} NAME_WE) add_executable(${bin} main.cpp ${src}) target_link_libraries(${bin} - PRIVATE ${PROJECT_NAME} ${PROJECT_NAME}_testlib) + PRIVATE ${PROJECT_NAME} ${PROJECT_NAME}) add_test(${bin} ${bin} "${EXTRA_TEST_ARGS}") endforeach() else() diff --git a/tests/test_parse_options.cpp b/tests/test_parse_options.cpp deleted file mode 100644 index fef4d4e..0000000 --- a/tests/test_parse_options.cpp +++ /dev/null @@ -1,138 +0,0 @@ -/* This file is part of remwharead. - * Copyright © 2019 tastytea - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU General Public License as published by - * the Free Software Foundation, version 3. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU General Public License for more details. - * - * You should have received a copy of the GNU General Public License - * along with this program. If not, see . - */ - -#include -#include -#include -#include -#include "parse_options.hpp" - -using namespace remwharead; -using std::string; -using std::vector; - -SCENARIO ("The option parser works correctly") -{ - bool exception = false; - options opts; - const string uri = "https://example.com/article.html"; - - WHEN ("The options are --help --file test") - { - try - { - const char *argv[] - = { "remwharead", "--help", "--file", "test" }; - opts = parse_options(4, argv); - } - catch (const std::exception &e) - { - exception = true; - } - - THEN ("No exception is thrown") - AND_THEN ("status code is 0") - AND_THEN ("options.file is empty") - { - REQUIRE_FALSE(exception); - REQUIRE(opts.status_code == 0); - REQUIRE(opts.file == ""); - } - } - - WHEN ("The options are --version --file test") - { - try - { - const char *argv[] - = { "remwharead", "--version", "--file", "test" }; - opts = parse_options(4, argv); - } - catch (const std::exception &e) - { - exception = true; - } - - THEN ("No exception is thrown") - AND_THEN ("status code is 0") - AND_THEN ("options.file is empty") - { - REQUIRE_FALSE(exception); - REQUIRE(opts.status_code == 0); - REQUIRE(opts.file == ""); - } - } - - WHEN ("The options are -t 💩 " + uri) - { - try - { - const char *argv[] - = { "remwharead", "-t", "💩", uri.c_str() }; - opts = parse_options(4, argv); - } - catch (const std::exception &e) - { - exception = true; - } - - THEN ("No exception is thrown") - AND_THEN ("status code is 0") - AND_THEN ("Tag and URI are right") - { - REQUIRE_FALSE(exception); - REQUIRE(opts.status_code == 0); - REQUIRE(opts.tags == vector{ "💩" }); - REQUIRE(opts.uri == uri); - } - } - - WHEN ("A very long string is passed as a tag") - { - const string longstring = // 5 · 60 = 300 - "aw6hui6chieRo9aihai9un1aeke6oushoo2oRo4aeD6eiDeiSheek4ahGiel" - "othaemeeyo4ievieV8kae9xiriejohD0aelah6oophaQueilohyaix3joo7O" - "laiceeFePuetaeBe0aip3eemuaheer0aj8aij8ahchisi4eiperiechoopoo" - "ohmie5doog5ohbahDoodah7daurah6haebeife2tah5Pheeweeb0eishooc4" - "phohKu5Ha3HiCeedeoph1ocaingaHeedeepeesohmee6Equ4meirahk3aihe"; - const string tags = "tag1," + longstring + ",tag3"; - try - { - const char *argv[] = - { - "remwharead", - "-t", - tags.c_str(), - uri.c_str() - }; - opts = parse_options(4, argv); - } - catch (const std::exception &e) - { - exception = true; - } - - THEN ("No exception is thrown") - AND_THEN ("status code is 0") - AND_THEN ("Tag and URI are right") - { - REQUIRE_FALSE(exception); - REQUIRE(opts.status_code == 0); - REQUIRE((opts.tags == vector{ "tag1", longstring, "tag3" })); - REQUIRE(opts.uri == uri); - } - } -}