epubgrep/src/main.cpp

265 lines
7.5 KiB
C++

/* This file is part of epubgrep.
* Copyright © 2021 tastytea <tastytea@tastytea.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero 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 Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "files.hpp"
#include "fs-compat.hpp"
#include "options.hpp"
#include "search.hpp"
#include <boost/locale/generator.hpp>
#include <boost/locale/message.hpp>
#include <boost/program_options/errors.hpp>
#include <boost/program_options/variables_map.hpp>
#include <fmt/format.h>
#include <fmt/ostream.h> // For compatibility with fmt 4.
#include <termcolor/termcolor.hpp>
#include <clocale>
#include <cmath>
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <future>
#include <iostream>
#include <locale>
#include <string>
#include <string_view>
#include <thread>
#include <vector>
int main(int argc, char *argv[])
{
namespace po = boost::program_options;
using namespace epubgrep;
using boost::locale::translate;
using fmt::format;
using std::cerr;
using std::cout;
using std::string;
using std::vector;
// locale_generator("").name.c_str() returns "*" instead of "". That's why
// the global C locale isn't changed. So we have to set it additionally.
std::setlocale(LC_ALL, "");
boost::locale::generator locale_generator;
locale_generator.add_messages_path("translations");
locale_generator.add_messages_path("/usr/share/locale");
locale_generator.add_messages_domain("epubgrep");
std::locale::global(locale_generator(""));
cout.imbue(std::locale());
cerr.imbue(std::locale());
po::variables_map vm;
try
{
vm = options::parse_options(argc, argv);
}
catch (std::exception &e)
{ // Exceptions we can't recover from or ones we don't know.
cerr << '\n' << translate("ERROR: ") << e.what() << '\n';
cerr << translate("Error while parsing options.") << '\n';
return EXIT_FAILURE;
}
if (vm.count("help") + vm.count("version") > 0)
{
return EXIT_SUCCESS;
}
int return_code{EXIT_SUCCESS};
vector<fs::path> input_files;
if (vm.count("input-file") == 0)
{
cout << "NO INPUT FILE\n";
// TODO: Read data from stdin.
return EXIT_FAILURE;
}
for (const auto &filepath : vm["input-file"].as<vector<string>>())
{
if (vm.count("recursive") + vm.count("dereference-recursive") == 0)
{
input_files.emplace_back(filepath);
}
else
{
bool follow_symlinks{false};
if (vm.count("dereference-recursive") > 0)
{
follow_symlinks = true;
}
try
{
auto files_in_dir{
files::list_recursive(filepath, follow_symlinks)};
input_files.insert(input_files.end(), files_in_dir.begin(),
files_in_dir.end());
}
catch (const fs::filesystem_error &e)
{
if (e.code().value() == 20)
{ // Is not a directory.
input_files.emplace_back(filepath);
continue;
}
cerr << '\n'
<< format(translate("ERROR: Could not open {0:s}: {1:s}")
.str()
.data(),
e.path1(), e.what())
<< '\n';
return_code = EXIT_FAILURE;
}
}
}
search::options opts;
if (vm.count("basic-regexp") > 0)
{
opts.regex = search::regex_kind::basic;
}
if (vm.count("extended-regexp") > 0)
{
opts.regex = search::regex_kind::extended;
}
if (vm.count("perl-regexp") > 0)
{
opts.regex = search::regex_kind::perl;
}
if (vm.count("grep") > 0)
{
opts.grep_like = true;
}
if (vm.count("ignore-case") > 0)
{
opts.ignore_case = true;
}
if (vm.count("raw") > 0)
{
opts.raw = true;
}
opts.context = vm["context"].as<std::uint64_t>();
vector<vector<search::match>> matches_all;
vector<std::future<int>> futurepool;
auto search_file{
[&vm, &matches_all, &opts](fs::path filepath)
{
for (const auto &regex : vm["regexp"].as<vector<string>>())
{
try
{
matches_all.emplace_back(
search::search(filepath, regex, opts));
}
catch (const std::exception &e)
{
cerr << '\n' << translate("ERROR: ") << e.what() << '\n';
cerr << format(translate("Error while searching {0:s}.")
.str()
.data(),
filepath)
<< '\n';
return EXIT_FAILURE;
}
}
return EXIT_SUCCESS;
}};
auto futures_cleanup{
[&futurepool, &return_code](const bool wait = false)
{
using namespace std::chrono_literals;
for (auto it{futurepool.begin()}; it != futurepool.end();)
{
if (!wait && it->wait_for(100ms) != std::future_status::ready)
{
++it;
continue;
}
if (int ret{}; (ret = it->get()) != EXIT_SUCCESS)
{
return_code = ret;
}
futurepool.erase(it);
}
return EXIT_SUCCESS;
}};
const auto max_threads{
[]
{
auto n{static_cast<double>(std::thread::hardware_concurrency())};
return static_cast<std::uint32_t>(std::ceil(n / 2 + n / 4));
}()};
for (const auto &filepath : input_files)
{
if (futurepool.size() >= max_threads)
{
futures_cleanup();
}
futurepool.emplace_back(
std::async(std::launch::async, search_file, filepath));
}
futures_cleanup(true);
for (const auto &matches_file : matches_all)
{
for (const auto &match : matches_file)
{
if (vm.count("no-filename") == 0)
{
cout << match.filepath;
}
if (!match.headline.empty())
{
if (vm.count("no-filename") == 0)
{
cout << ", ";
}
cout << match.headline;
}
if (!match.page.empty())
{
cout << ", page " << match.page;
}
cout << ": " << match.context.first;
if (vm.count("nocolor") == 0)
{
cout << termcolor::bright_magenta << match.text
<< termcolor::reset;
}
else
{
cout << match.text;
}
cout << match.context.second << '\n';
}
}
return return_code;
}