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
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

View File

@ -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"
----
====

View File

@ -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})

View File

@ -18,10 +18,9 @@
#include <string>
#include <chrono>
#include <fstream>
#include <memory>
#include <locale>
#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<std::string> &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<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:
{
@ -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 <exception>
#include <popl.hpp>
#include <Poco/Util/Option.h>
#include <Poco/Util/HelpFormatter.h>
#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<popl::Value<string>>
("t", "tags", "Add tags to URI, delimited by commas.", "", &tags);
auto option_export = op.add<popl::Value<string>>
("e", "export", "Export to format.", "simple", &format);
op.add<popl::Value<string>>
("f", "file", "Save output to file.", "", &opts.file);
op.add<popl::Value<string>>
("T", "time-span",
"Only export entries between YYYY-MM-DD,YYYY-MM-DD.", "", &span);
op.add<popl::Value<string>>
("s", "search-tags",
"Search in tags. Format: tag1 AND tag2 OR tag3.",
"", &opts.search_tags);
op.add<popl::Value<string>>
("S", "search-all",
"Search in tags, title, description and full text.",
"", &opts.search_all);
op.add<popl::Switch>
("r", "regex", "Use regular expression for search.", &opts.regex);
auto option_noarchive = op.add<popl::Switch>
("N", "no-archive", "Do not archive URI.");
auto option_help = op.add<popl::Switch>
("h", "help", "Show this help message.");
auto option_version = op.add<popl::Switch>
("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 <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,\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<App>(this, &App::handle_info)));
options.addOption(
Option("version", "V", "Print version, copyright and license.")
.callback(OptionCallback<App>(this, &App::handle_info)));
options.addOption(
Option("tags", "t", "Add tags to URI, delimited by commas.")
.argument("Tags")
.callback(OptionCallback<App>(this, &App::handle_options)));
options.addOption(
Option("export", "e", "Export to format.")
.argument("Format")
.callback(OptionCallback<App>(this, &App::handle_options)));
options.addOption(
Option("file", "f", "Save output to file.")
.argument("File")
.callback(OptionCallback<App>(this, &App::handle_options)));
options.addOption(
Option("time-span", "T",
"Only export entries between YYYY-MM-DD,YYYY-MM-DD.")
.argument("Times")
.callback(OptionCallback<App>(this, &App::handle_options)));
options.addOption(
Option("search-tags", "s",
"Search in tags. Format: tag1 AND tag2 OR tag3.")
.argument("Expression")
.callback(OptionCallback<App>(this, &App::handle_options)));
options.addOption(
Option("search-all", "S",
"Search in tags, title, description and full text.")
.argument("Expression")
.callback(OptionCallback<App>(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<App>(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 <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 <array>
#include <chrono>
#include <cstdint>
#include <Poco/Util/Application.h>
#include <Poco/Util/OptionSet.h>
#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<string> tags;
export_format format = export_format::undefined;
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;
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<std::string> &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<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

View File

@ -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()

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);
}
}
}