Moved from popl to Poco for option parsing.
continuous-integration/drone/push Build is failing Details

This commit is contained in:
tastytea 2019-08-06 17:14:32 +02:00
parent 786912133b
commit aedbebc6f6
Signed by: tastytea
GPG Key ID: CFC39497F1B26E07
8 changed files with 254 additions and 362 deletions

View File

@ -13,12 +13,6 @@ trigger:
- tag - tag
steps: 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 - name: gcc6
image: debian:stretch-slim image: debian:stretch-slim
pull: always pull: always
@ -167,12 +161,6 @@ trigger:
- tag - tag
steps: 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 - name: deb
image: debian:stretch-slim image: debian:stretch-slim
pull: always pull: always

View File

@ -58,7 +58,6 @@ only.
https://llvm.org/[clang] 3/7) https://llvm.org/[clang] 3/7)
* https://cmake.org/[cmake] (at least: 3.2) * https://cmake.org/[cmake] (at least: 3.2)
* https://pkgconfig.freedesktop.org/wiki/[pkgconfig] (tested: 0.29) * 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) * http://repo.or.cz/w/libxdg-basedir.git[libxdg-basedir] (tested: 1.2)
* https://pocoproject.org/[POCO] (tested: 1.9 / 1.7) * https://pocoproject.org/[POCO] (tested: 1.9 / 1.7)
* http://vsqlite.virtuosic-bytes.com/[vsqlite++] (tested: 0.3) * http://vsqlite.virtuosic-bytes.com/[vsqlite++] (tested: 0.3)
@ -75,8 +74,6 @@ only.
apt-get update apt-get update
apt-get install g++-6 cmake pkg-config libpoco-dev libxdg-basedir-dev \ apt-get install g++-6 cmake pkg-config libpoco-dev libxdg-basedir-dev \
libvsqlitepp-dev libboost-system-dev libboost-filesystem-dev asciidoc 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" export CXX="g++-6"
---- ----
==== ====

View File

@ -1,5 +1,7 @@
include(GNUInstallDirs) include(GNUInstallDirs)
find_package(Poco COMPONENTS Util CONFIG)
file(GLOB sources_cli *.cpp) file(GLOB sources_cli *.cpp)
add_executable(${PROJECT_NAME}-cli ${sources_cli}) 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}") PRIVATE "${PROJECT_BINARY_DIR}" "${CMAKE_CURRENT_SOURCE_DIR}")
target_link_libraries(${PROJECT_NAME}-cli target_link_libraries(${PROJECT_NAME}-cli
PRIVATE ${PROJECT_NAME}) PRIVATE ${PROJECT_NAME} Poco::Util)
install(TARGETS ${PROJECT_NAME}-cli install(TARGETS ${PROJECT_NAME}-cli
DESTINATION ${CMAKE_INSTALL_BINDIR}) DESTINATION ${CMAKE_INSTALL_BINDIR})

View File

@ -18,10 +18,9 @@
#include <string> #include <string>
#include <chrono> #include <chrono>
#include <fstream> #include <fstream>
#include <memory>
#include <locale> #include <locale>
#include "sqlite.hpp" #include "sqlite.hpp"
#include "parse_options.hpp" #include "remwharead_cli.hpp"
#include "uri.hpp" #include "uri.hpp"
#include "types.hpp" #include "types.hpp"
#include "export/csv.hpp" #include "export/csv.hpp"
@ -31,42 +30,60 @@
#include "search.hpp" #include "search.hpp"
using namespace remwharead; using namespace remwharead;
using std::cout;
using std::cerr; using std::cerr;
using std::endl; using std::endl;
using std::string; using std::string;
using std::chrono::system_clock; using std::chrono::system_clock;
using std::ofstream;
int main(const int argc, const char *argv[]) int App::main(const std::vector<std::string> &args)
{ {
std::locale::global(std::locale("")); // Set locale globally. std::locale::global(std::locale("")); // Set locale globally.
options opts = parse_options(argc, argv); if (_version_requested)
if (opts.status_code != 0)
{ {
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; Database db;
if (!db) if (!db)
{ {
cerr << "Error: Database connection failed.\n"; 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(); html_extract page = uri.get();
if (!page) if (!page)
{ {
cerr << "Error: Could not fetch page.\n"; cerr << "Error: Could not fetch page.\n";
cerr << page.error << endl; cerr << page.error << endl;
return 4; return Application::EXIT_UNAVAILABLE;
} }
archive_answer archive; archive_answer archive;
if (opts.archive) if (_archive)
{ {
archive = uri.archive(); archive = uri.archive();
if (!archive) if (!archive)
@ -74,35 +91,36 @@ int main(const int argc, const char *argv[])
cerr << "Error archiving URL: " << archive.error << endl; 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}); page.title, page.description, page.fulltext});
} }
std::ofstream file; ofstream file;
if (!opts.file.empty()) if (!_file.empty())
{ {
file.open(opts.file); file.open(_file);
if (!file.good()) if (!file.good())
{ {
cerr << "Error: Could not open file: " << opts.file << endl; cerr << "Error: Could not open file: " << _file << endl;
return 3; return Application::EXIT_IOERR;
} }
} }
if (opts.format != export_format::undefined)
if (_format != export_format::undefined)
{ {
vector<Database::entry> entries; vector<Database::entry> 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: 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)

View File

@ -15,168 +15,188 @@
*/ */
#include <iostream> #include <iostream>
#include <exception> #include <Poco/Util/Option.h>
#include <popl.hpp> #include <Poco/Util/HelpFormatter.h>
#include "version.hpp" #include "version.hpp"
#include "parse_options.hpp" #include "remwharead_cli.hpp"
using std::cout; using std::cout;
using std::cerr; using std::cerr;
using std::endl; 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) void App::defineOptions(OptionSet& options)
: status_code(status)
{}
const options parse_options(const int argc, const char *argv[])
{ {
string tags; options.addOption(
string format; Option("help", "h", "Show this help message.")
string span; .callback(OptionCallback<App>(this, &App::handle_info)));
options opts; options.addOption(
Option("version", "V", "Print version, copyright and license.")
try .callback(OptionCallback<App>(this, &App::handle_info)));
{ options.addOption(
popl::OptionParser op("Available options"); Option("tags", "t", "Add tags to URI, delimited by commas.")
op.add<popl::Value<string>> .argument("Tags")
("t", "tags", "Add tags to URI, delimited by commas.", "", &tags); .callback(OptionCallback<App>(this, &App::handle_options)));
auto option_export = op.add<popl::Value<string>> options.addOption(
("e", "export", "Export to format.", "simple", &format); Option("export", "e", "Export to format.")
op.add<popl::Value<string>> .argument("Format")
("f", "file", "Save output to file.", "", &opts.file); .callback(OptionCallback<App>(this, &App::handle_options)));
op.add<popl::Value<string>> options.addOption(
("T", "time-span", Option("file", "f", "Save output to file.")
"Only export entries between YYYY-MM-DD,YYYY-MM-DD.", "", &span); .argument("File")
op.add<popl::Value<string>> .callback(OptionCallback<App>(this, &App::handle_options)));
("s", "search-tags", options.addOption(
"Search in tags. Format: tag1 AND tag2 OR tag3.", Option("time-span", "T",
"", &opts.search_tags); "Only export entries between YYYY-MM-DD,YYYY-MM-DD.")
op.add<popl::Value<string>> .argument("Times")
("S", "search-all", .callback(OptionCallback<App>(this, &App::handle_options)));
"Search in tags, title, description and full text.", options.addOption(
"", &opts.search_all); Option("search-tags", "s",
op.add<popl::Switch> "Search in tags. Format: tag1 AND tag2 OR tag3.")
("r", "regex", "Use regular expression for search.", &opts.regex); .argument("Expression")
auto option_noarchive = op.add<popl::Switch> .callback(OptionCallback<App>(this, &App::handle_options)));
("N", "no-archive", "Do not archive URI."); options.addOption(
auto option_help = op.add<popl::Switch> Option("search-all", "S",
("h", "help", "Show this help message."); "Search in tags, title, description and full text.")
auto option_version = op.add<popl::Switch> .argument("Expression")
("V", "version", "Print version, copyright and license."); .callback(OptionCallback<App>(this, &App::handle_options)));
op.parse(argc, argv); options.addOption(
Option("regex", "r", "Use regular expression for search."));
if (option_help->is_set()) options.addOption(
{ Option("no-archive", "N", "Do not archive URI.")
cout << "Usage: " << argv[0] << " [-t tags] [-N] URI\n" .callback(OptionCallback<App>(this, &App::handle_options)));
<< " " << argv[0] }
<< " -e format [-f file] [-T start,end] "
<< "[[-s|-S] expression] [-r]\n"; void App::handle_info(const std::string &name, const std::string &)
cout << op; {
return options(0); if (name == "help")
} {
_help_requested = true;
if (option_version->is_set()) }
{ else if (name == "version")
cout << "remwharead " << global::version << endl << {
"Copyright (C) 2019 tastytea <tastytea@tastytea.de>\n" _version_requested = true;
"License GPLv3: GNU GPL version 3 " }
"<https://www.gnu.org/licenses/gpl-3.0.html>.\n"
"This program comes with ABSOLUTELY NO WARRANTY. This is free software,\n" stopOptionsProcessing();
"and you are welcome to redistribute it under certain conditions.\n"; }
return options(0);
} void App::handle_options(const std::string &name, const std::string &value)
{
if (option_noarchive->is_set()) if (name == "tags")
{ {
opts.archive = false; size_t pos_end = 0;
} size_t pos_start = 0;
while (pos_end != std::string::npos)
if (!tags.empty()) {
{ pos_end = value.find(',', pos_start);
size_t pos_end = 0; string buffer = value.substr(pos_start, pos_end - pos_start);
size_t pos_start = 0; while (*buffer.begin() == ' ') // Remove leading spaces.
while (pos_end != std::string::npos) {
{ buffer.erase(buffer.begin());
pos_end = tags.find(',', pos_start); }
string buffer = tags.substr(pos_start, pos_end - pos_start); while (*buffer.rbegin() == ' ') // Remove trailing spaces.
while (*buffer.begin() == ' ') // Remove leading spaces. {
{ buffer.erase(buffer.end() - 1);
buffer.erase(buffer.begin()); }
} _tags.push_back(buffer);
while (*buffer.rbegin() == ' ') // Remove trailing spaces. pos_start = pos_end + 1;
{ }
buffer.erase(buffer.end() - 1); }
} else if (name == "export")
opts.tags.push_back(buffer); {
pos_start = pos_end + 1; if (value == "csv")
} {
} _format = export_format::csv;
}
if (option_export->is_set()) else if (value == "asciidoc" || value == "adoc")
{ {
if (format == "csv") _format = export_format::asciidoc;
{ }
opts.format = export_format::csv; else if (value == "bookmarks")
} {
else if (format == "asciidoc" || format == "adoc") _format = export_format::bookmarks;
{ }
opts.format = export_format::asciidoc; else if (value == "simple")
} {
else if (format == "bookmarks") _format = export_format::simple;
{ }
opts.format = export_format::bookmarks; else
} {
else if (format == "simple") cerr << "Error: Unknown format.\n";
{ _argument_error = true;
opts.format = export_format::simple; }
} }
else else if (name == "file")
{ {
opts.format = export_format::undefined; _file = value;
cerr << "Error: Export format must be " }
<< "csv, asciidoc, bookmarks or simple.\n"; else if (name == "time-span")
return options(1); {
} size_t pos = value.find(',');
} if (pos != std::string::npos)
{
if (!span.empty()) _timespan =
{ {
size_t pos = span.find(','); string_to_timepoint(value.substr(0, pos)),
if (pos != std::string::npos) string_to_timepoint(value.substr(pos + 1))
{ };
opts.span = }
{ else
string_to_timepoint(span.substr(0, pos)), {
string_to_timepoint(span.substr(pos + 1)) cerr << "Error: Time span must be in format: "
}; "YYYY-MM-DD,YYYY-MM-DD.\n";
} _argument_error = true;
else }
{ }
cerr << "Error: Time span must be in format: " else if (name == "search-tags")
"YYYY-MM-DD,YYYY-MM-DD.\n"; {
return options(1); _search_tags = value;
} }
} else if (name == "search-all")
{
if (op.non_option_args().size() > 0) _search_all = value;
{ }
opts.uri = op.non_option_args().front(); else if (name == "no-archive")
} {
_archive = false;
if (opts.uri == "" && opts.format == export_format::undefined) }
{ else if (name == "regex")
cerr << "Error: You have to specify either URI or --export.\n"; {
return options(1); _regex = true;
} }
} }
catch (const std::exception &e)
{ void App::print_help()
cerr << "Error in " << __func__ << ": " << e.what() << endl; {
return options(1); HelpFormatter helpFormatter(options());
} helpFormatter.setCommand(commandName());
helpFormatter.setUsage("[-t tags] [-N] URI\n"
return opts; "-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 <tastytea@tastytea.de>\n"
"License GPLv3: GNU GPL version 3 "
"<https://www.gnu.org/licenses/gpl-3.0.html>.\n"
"This program comes with ABSOLUTELY NO WARRANTY. This is free software,"
"\nand you are welcome to redistribute it under certain conditions.\n";
} }

View File

@ -21,7 +21,8 @@
#include <vector> #include <vector>
#include <array> #include <array>
#include <chrono> #include <chrono>
#include <cstdint> #include <Poco/Util/Application.h>
#include <Poco/Util/OptionSet.h>
#include "types.hpp" #include "types.hpp"
#include "time.hpp" #include "time.hpp"
@ -31,26 +32,34 @@ using std::vector;
using std::array; using std::array;
using std::chrono::system_clock; using std::chrono::system_clock;
using time_point = system_clock::time_point; 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<string> tags; public:
export_format format = export_format::undefined; App();
string file;
array<time_point, 2> 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;
options(); protected:
explicit options(const uint8_t &status); void defineOptions(OptionSet& options);
} 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<std::string> &args);
// Parse command-line options. private:
const options parse_options(const int argc, const char *argv[]); bool _help_requested;
bool _version_requested;
bool _argument_error;
string _uri;
vector<string> _tags;
export_format _format;
string _file;
array<time_point, 2> _timespan;
string _search_tags;
string _search_all;
bool _archive;
bool _regex;
};
#endif // REMWHAREAD_PARSE_OPTIONS_HPP #endif // REMWHAREAD_PARSE_OPTIONS_HPP

View File

@ -1,11 +1,5 @@
include(CTest) 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) file(GLOB sources_tests test_*.cpp)
find_package(Catch2 CONFIG) find_package(Catch2 CONFIG)
@ -13,7 +7,7 @@ if(Catch2_FOUND) # Catch 2.x
include(Catch) include(Catch)
add_executable(all_tests main.cpp ${sources_tests}) add_executable(all_tests main.cpp ${sources_tests})
target_link_libraries(all_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") target_include_directories(all_tests PRIVATE "/usr/include/catch2")
catch_discover_tests(all_tests EXTRA_ARGS "${EXTRA_TEST_ARGS}") catch_discover_tests(all_tests EXTRA_ARGS "${EXTRA_TEST_ARGS}")
else() # Catch 1.x else() # Catch 1.x
@ -23,7 +17,7 @@ else() # Catch 1.x
get_filename_component(bin ${src} NAME_WE) get_filename_component(bin ${src} NAME_WE)
add_executable(${bin} main.cpp ${src}) add_executable(${bin} main.cpp ${src})
target_link_libraries(${bin} target_link_libraries(${bin}
PRIVATE ${PROJECT_NAME} ${PROJECT_NAME}_testlib) PRIVATE ${PROJECT_NAME} ${PROJECT_NAME})
add_test(${bin} ${bin} "${EXTRA_TEST_ARGS}") add_test(${bin} ${bin} "${EXTRA_TEST_ARGS}")
endforeach() endforeach()
else() else()

View File

@ -1,138 +0,0 @@
/* This file is part of remwharead.
* Copyright © 2019 tastytea <tastytea@tastytea.de>
*
* 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 <http://www.gnu.org/licenses/>.
*/
#include <exception>
#include <string>
#include <vector>
#include <catch.hpp>
#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<string>{ "💩" });
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<string>{ "tag1", longstring, "tag3" }));
REQUIRE(opts.uri == uri);
}
}
}