/** * Copyright (c) 2022, Timothy Stack * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Timothy Stack nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include "lnav.management_cli.hh" #include "base/itertools.hh" #include "base/result.h" #include "base/string_util.hh" #include "fmt/format.h" #include "itertools.similar.hh" #include "log_format.hh" #include "log_format_ext.hh" #include "mapbox/variant.hpp" #include "regex101.import.hh" #include "session_data.hh" using namespace lnav::roles::literals; namespace lnav { namespace management { struct no_subcmd_t { CLI::App* ns_root_app{nullptr}; }; inline attr_line_t& symbol_reducer(const std::string& elem, attr_line_t& accum) { return accum.append("\n ").append(lnav::roles::symbol(elem)); } inline attr_line_t& subcmd_reducer(const CLI::App* app, attr_line_t& accum) { return accum.append("\n \u2022 ") .append(lnav::roles::keyword(app->get_name())) .append(": ") .append(app->get_description()); } struct subcmd_format_t { using action_t = std::function; CLI::App* sf_format_app{nullptr}; std::string sf_name; CLI::App* sf_regex_app{nullptr}; std::string sf_regex_name; CLI::App* sf_regex101_app{nullptr}; action_t sf_action; subcmd_format_t& set_action(action_t act) { if (!this->sf_action) { this->sf_action = std::move(act); } return *this; } Result, console::user_message> validate_format() const { if (this->sf_name.empty()) { auto um = console::user_message::error( "expecting a format name to operate on"); um.with_note( (log_format::get_root_formats() | lnav::itertools::map(&log_format::get_name) | lnav::itertools::sort_with(intern_string_t::case_lt) | lnav::itertools::map(&intern_string_t::to_string) | lnav::itertools::fold(symbol_reducer, attr_line_t{})) .add_header("the available formats are:")); return Err(um); } auto lformat = log_format::find_root_format(this->sf_name.c_str()); if (lformat == nullptr) { auto um = console::user_message::error( attr_line_t("unknown format: ") .append(lnav::roles::symbol(this->sf_name))); um.with_note( (log_format::get_root_formats() | lnav::itertools::map(&log_format::get_name) | lnav::itertools::similar_to(this->sf_name) | lnav::itertools::map(&intern_string_t::to_string) | lnav::itertools::fold(symbol_reducer, attr_line_t{})) .add_header("did you mean one of the following?")); return Err(um); } return Ok(lformat); } Result validate_external_format() const { auto lformat = TRY(this->validate_format()); auto* ext_lformat = dynamic_cast(lformat.get()); if (ext_lformat == nullptr) { return Err(console::user_message::error( attr_line_t() .append_quoted(lnav::roles::symbol(this->sf_name)) .append(" is an internal format that is not defined in a " "configuration file"))); } return Ok(ext_lformat); } Result>, console::user_message> validate_regex() const { auto* ext_lformat = TRY(this->validate_external_format()); if (this->sf_regex_name.empty()) { auto um = console::user_message::error( "expecting a regex name to operate on"); um.with_note( ext_lformat->elf_pattern_order | lnav::itertools::map(&external_log_format::pattern::p_name) | lnav::itertools::map(&intern_string_t::to_string) | lnav::itertools::fold( symbol_reducer, attr_line_t{"the available regexes are:"})); return Err(um); } for (const auto& pat : ext_lformat->elf_pattern_order) { if (pat->p_name == this->sf_regex_name) { return Ok(std::make_pair(ext_lformat, pat)); } } auto um = console::user_message::error( attr_line_t("unknown regex: ") .append(lnav::roles::symbol(this->sf_regex_name))); um.with_note( (ext_lformat->elf_pattern_order | lnav::itertools::map(&external_log_format::pattern::p_name) | lnav::itertools::map(&intern_string_t::to_string) | lnav::itertools::similar_to(this->sf_regex_name) | lnav::itertools::fold(symbol_reducer, attr_line_t{})) .add_header("did you mean one of the following?")); return Err(um); } static perform_result_t default_action(const subcmd_format_t& sf) { auto validate_res = sf.validate_format(); if (validate_res.isErr()) { return {validate_res.unwrapErr()}; } auto lformat = validate_res.unwrap(); auto* ext_format = dynamic_cast(lformat.get()); attr_line_t ext_details; if (ext_format != nullptr) { ext_details.append("\n ") .append("Regexes"_h3) .append(": ") .join(ext_format->elf_pattern_order | lnav::itertools::map( &external_log_format::pattern::p_name) | lnav::itertools::map(&intern_string_t::to_string), VC_ROLE.value(role_t::VCR_SYMBOL), ", "); } auto um = console::user_message::error( attr_line_t("expecting an operation to perform on the ") .append(lnav::roles::symbol(sf.sf_name)) .append(" format")); um.with_note(attr_line_t() .append(lnav::roles::symbol(sf.sf_name)) .append(": ") .append(lformat->lf_description) .append(ext_details)); um.with_help( sf.sf_format_app->get_subcommands({}) | lnav::itertools::fold( subcmd_reducer, attr_line_t{"the available operations are:"})); return {um}; } static perform_result_t default_regex_action(const subcmd_format_t& sf) { auto validate_res = sf.validate_regex(); if (validate_res.isErr()) { return {validate_res.unwrapErr()}; } auto um = console::user_message::error( attr_line_t("expecting an operation to perform on the ") .append(lnav::roles::symbol(sf.sf_regex_name)) .append(" regular expression")); um.with_help(attr_line_t{"the available subcommands are:"}.append( sf.sf_regex_app->get_subcommands({}) | lnav::itertools::fold(subcmd_reducer, attr_line_t{}))); return {um}; } static perform_result_t get_action(const subcmd_format_t& sf) { auto validate_res = sf.validate_format(); if (validate_res.isErr()) { return {validate_res.unwrapErr()}; } auto format = validate_res.unwrap(); auto um = console::user_message::raw( attr_line_t() .append(lnav::roles::symbol(sf.sf_name)) .append(": ") .append(on_blank(format->lf_description, ""))); return {um}; } static perform_result_t source_action(const subcmd_format_t& sf) { auto validate_res = sf.validate_external_format(); if (validate_res.isErr()) { return {validate_res.unwrapErr()}; } auto* format = validate_res.unwrap(); if (format->elf_format_source_order.empty()) { return { console::user_message::error( "format is builtin, there is no source file"), }; } auto um = console::user_message::raw( format->elf_format_source_order[0].string()); return {um}; } static perform_result_t sources_action(const subcmd_format_t& sf) { auto validate_res = sf.validate_external_format(); if (validate_res.isErr()) { return {validate_res.unwrapErr()}; } auto* format = validate_res.unwrap(); if (format->elf_format_source_order.empty()) { return { console::user_message::error( "format is builtin, there is no source file"), }; } auto um = console::user_message::raw( attr_line_t().join(format->elf_format_source_order, VC_ROLE.value(role_t::VCR_TEXT), "\n")); return {um}; } static perform_result_t regex101_pull_action(const subcmd_format_t& sf) { auto validate_res = sf.validate_regex(); if (validate_res.isErr()) { return {validate_res.unwrapErr()}; } auto format_regex_pair = validate_res.unwrap(); auto get_meta_res = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name); return get_meta_res.match( [&sf]( const lnav::session::regex101::error& err) -> perform_result_t { return { console::user_message::error( attr_line_t("unable to get DB entry for: ") .append(lnav::roles::symbol(sf.sf_name)) .append("/") .append(lnav::roles::symbol(sf.sf_regex_name))) .with_reason(err.e_msg), }; }, [&sf]( const lnav::session::regex101::no_entry&) -> perform_result_t { return { console::user_message::error( attr_line_t("regex ") .append_quoted( lnav::roles::symbol(sf.sf_regex_name)) .append(" of format ") .append_quoted(lnav::roles::symbol(sf.sf_name)) .append(" has not been pushed to regex101.com")) .with_help( attr_line_t("use the ") .append_quoted("push"_keyword) .append(" subcommand to create the regex on " "regex101.com for easy editing")), }; }, [&](const lnav::session::regex101::entry& en) -> perform_result_t { auto retrieve_res = regex101::client::retrieve(en.re_permalink); return retrieve_res.match( [&](const console::user_message& um) -> perform_result_t { return { console::user_message::error( attr_line_t("unable to retrieve entry ") .append_quoted( lnav::roles::symbol(en.re_permalink)) .append(" from regex101.com")) .with_reason(um), }; }, [&](const regex101::client::no_entry&) -> perform_result_t { lnav::session::regex101::delete_entry(sf.sf_name, sf.sf_regex_name); return { console::user_message::error( attr_line_t("entry ") .append_quoted( lnav::roles::symbol(en.re_permalink)) .append( " no longer exists on regex101.com")) .with_help(attr_line_t("use the ") .append_quoted("delete"_keyword) .append(" subcommand to delete " "the association")), }; }, [&](const regex101::client::entry& remote_entry) -> perform_result_t { auto curr_entry = regex101::convert_format_pattern( format_regex_pair.first, format_regex_pair.second); if (curr_entry.e_regex == remote_entry.e_regex) { return { console::user_message::ok( attr_line_t("local regex is in sync " "with entry ") .append_quoted(lnav::roles::symbol( en.re_permalink)) .append(" on regex101.com")) .with_help( attr_line_t("make edits on ") .append_quoted(lnav::roles::file( regex101::client::to_edit_url( en.re_permalink))) .append(" and then run this " "command again to update " "the local values")), }; } auto patch_res = regex101::patch(format_regex_pair.first, sf.sf_regex_name, remote_entry); if (patch_res.isErr()) { return { console::user_message::error( attr_line_t( "unable to patch format regex: ") .append(lnav::roles::symbol(sf.sf_name)) .append("/") .append(lnav::roles::symbol( sf.sf_regex_name))) .with_reason(patch_res.unwrapErr()), }; } auto um = console::user_message::ok( attr_line_t("format patch file written to: ") .append(lnav::roles::file( patch_res.unwrap().string()))); if (!format_regex_pair.first->elf_builtin_format) { um.with_help( attr_line_t("once the regex has been found " "to be working correctly, move the " "contents of the patch file to the " "original file at:\n ") .append(lnav::roles::file( format_regex_pair.first ->elf_format_source_order.front() .string()))); } return {um}; }); }); } static perform_result_t regex101_default_action(const subcmd_format_t& sf) { auto validate_res = sf.validate_regex(); if (validate_res.isErr()) { return {validate_res.unwrapErr()}; } auto um = console::user_message::error( attr_line_t("expecting an operation to perform on the ") .append(lnav::roles::symbol(sf.sf_regex_name)) .append(" regex using regex101.com")); auto get_res = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name); if (get_res.is()) { auto local_entry = get_res.get(); um.with_note( attr_line_t("this regex is currently associated with the " "following regex101.com entry:\n ") .append(lnav::roles::file(regex101::client::to_edit_url( local_entry.re_permalink)))); } um.with_help(attr_line_t{"the available subcommands are:"}.append( sf.sf_regex101_app->get_subcommands({}) | lnav::itertools::fold(subcmd_reducer, attr_line_t{}))); return {um}; } static perform_result_t regex101_push_action(const subcmd_format_t& sf) { auto validate_res = sf.validate_regex(); if (validate_res.isErr()) { return {validate_res.unwrapErr()}; } auto format_regex_pair = validate_res.unwrap(); auto entry = regex101::convert_format_pattern(format_regex_pair.first, format_regex_pair.second); auto get_meta_res = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name); if (get_meta_res.is()) { auto entry_meta = get_meta_res.get(); auto retrieve_res = regex101::client::retrieve(entry_meta.re_permalink); if (retrieve_res.is()) { auto remote_entry = retrieve_res.get(); if (remote_entry == entry) { return { console::user_message::ok( attr_line_t("regex101 entry ") .append(lnav::roles::symbol( entry_meta.re_permalink)) .append(" is already up-to-date")), }; } } else if (retrieve_res.is()) { return { retrieve_res.get(), }; } entry.e_permalink_fragment = entry_meta.re_permalink; } auto upsert_res = regex101::client::upsert(entry); auto upsert_info = upsert_res.unwrap(); if (get_meta_res.is()) { lnav::session::regex101::insert_entry({ format_regex_pair.first->get_name().to_string(), format_regex_pair.second->p_name.to_string(), upsert_info.cr_permalink_fragment, upsert_info.cr_delete_code, }); } return { console::user_message::ok( attr_line_t("pushed regex to -- ") .append(lnav::roles::file(regex101::client::to_edit_url( upsert_info.cr_permalink_fragment)))) .with_help(attr_line_t("use the ") .append_quoted("pull"_keyword) .append(" subcommand to update the format after " "you make changes on regex101.com")), }; } static perform_result_t regex101_delete_action(const subcmd_format_t& sf) { auto get_res = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name); return get_res.match( [&sf]( const lnav::session::regex101::entry& en) -> perform_result_t { { auto validate_res = sf.validate_external_format(); if (validate_res.isOk()) { auto ppath = regex101::patch_path(validate_res.unwrap(), en.re_permalink); if (ghc::filesystem::exists(ppath)) { return { console::user_message::error( attr_line_t("cannot delete regex101 entry " "while patch file exists")) .with_note(attr_line_t(" ").append( lnav::roles::file(ppath.string()))) .with_help(attr_line_t( "move the contents of the patch file " "to the main log format and then " "delete the file to continue")), }; } } } perform_result_t retval; if (en.re_delete_code.empty()) { retval.emplace_back( console::user_message::warning( attr_line_t("not deleting regex101 entry ") .append_quoted( lnav::roles::symbol(en.re_permalink))) .with_reason( "delete code is not known for this entry") .with_note( "formats created by importing a regex101.com " "entry will not have a delete code")); } else { auto delete_res = regex101::client::delete_entry(en.re_delete_code); if (delete_res.isErr()) { return { console::user_message::error( "unable to delete regex101 entry") .with_reason(delete_res.unwrapErr()), }; } } lnav::session::regex101::delete_entry(sf.sf_name, sf.sf_regex_name); retval.emplace_back(console::user_message::ok( attr_line_t("deleted regex101 entry: ") .append(lnav::roles::symbol(en.re_permalink)))); return retval; }, [&sf]( const lnav::session::regex101::no_entry&) -> perform_result_t { return { console::user_message::error( attr_line_t("no regex101 entry for ") .append(lnav::roles::symbol(sf.sf_name)) .append("/") .append(lnav::roles::symbol(sf.sf_regex_name))), }; }, [&sf]( const lnav::session::regex101::error& err) -> perform_result_t { return { console::user_message::error( attr_line_t("unable to get regex101 entry for ") .append(lnav::roles::symbol(sf.sf_name)) .append("/") .append(lnav::roles::symbol(sf.sf_regex_name))) .with_reason(err.e_msg), }; }); } }; struct subcmd_regex101_t { using action_t = std::function; CLI::App* sr_app{nullptr}; action_t sr_action; std::string sr_import_url; std::string sr_import_name; std::string sr_import_regex_name{"std"}; subcmd_regex101_t& set_action(action_t act) { if (!this->sr_action) { this->sr_action = std::move(act); } return *this; } static perform_result_t default_action(const subcmd_regex101_t& sr) { auto um = console::user_message::error( "expecting an operation related to the regex101.com integration"); um.with_help( sr.sr_app->get_subcommands({}) | lnav::itertools::fold( subcmd_reducer, attr_line_t{"the available operations are:"})); return {um}; } static perform_result_t list_action(const subcmd_regex101_t&) { auto get_res = lnav::session::regex101::get_entries(); if (get_res.isErr()) { return { console::user_message::error( "unable to read regex101 entries from DB") .with_reason(get_res.unwrapErr()), }; } auto entries = get_res.unwrap() | lnav::itertools::map([](const auto& elem) { return fmt::format( FMT_STRING(" format {} regex {} regex101\n"), elem.re_format_name, elem.re_regex_name); }) | lnav::itertools::fold( [](const auto& elem, auto& accum) { return accum.append(elem); }, attr_line_t{}); auto um = console::user_message::ok( entries.add_header("the following regex101 entries were found:\n") .with_default("no regex101 entries found")); return {um}; } static perform_result_t import_action(const subcmd_regex101_t& sr) { auto import_res = regex101::import( sr.sr_import_url, sr.sr_import_name, sr.sr_import_regex_name); if (import_res.isOk()) { return { lnav::console::user_message::ok( attr_line_t("converted regex101 entry to format file: ") .append(lnav::roles::file(import_res.unwrap()))) .with_note("the converted format may still have errors") .with_help( attr_line_t( "use the following command to patch the regex as " "more changes are made on regex101.com:\n") .appendf(FMT_STRING(" lnav -m format {} regex {} " "regex101 pull"), sr.sr_import_name, sr.sr_import_regex_name)), }; } return { import_res.unwrapErr(), }; } }; using operations_v = mapbox::util::variant; class operations { public: operations_v o_ops; }; std::shared_ptr describe_cli(CLI::App& app, int argc, char* argv[]) { auto retval = std::make_shared(); retval->o_ops = no_subcmd_t{ &app, }; app.add_flag("-m", "Switch to the management CLI mode."); subcmd_format_t format_args; subcmd_regex101_t regex101_args; { auto* subcmd_format = app.add_subcommand("format", "perform operations on log file formats") ->callback([&]() { format_args.set_action(subcmd_format_t::default_action); retval->o_ops = format_args; }); format_args.sf_format_app = subcmd_format; subcmd_format ->add_option( "format_name", format_args.sf_name, "the name of the format") ->expected(1); { subcmd_format ->add_subcommand("get", "print information about a format") ->callback([&]() { format_args.set_action(subcmd_format_t::get_action); }); } { subcmd_format ->add_subcommand("source", "print the path of the first source file " "containing this format") ->callback([&]() { format_args.set_action(subcmd_format_t::source_action); }); } { subcmd_format ->add_subcommand("sources", "print the paths of all source files " "containing this format") ->callback([&]() { format_args.set_action(subcmd_format_t::sources_action); }); } { auto* subcmd_format_regex = subcmd_format ->add_subcommand( "regex", "operate on the format's regular expressions") ->callback([&]() { format_args.set_action( subcmd_format_t::default_regex_action); }); format_args.sf_regex_app = subcmd_format_regex; subcmd_format_regex->add_option( "regex-name", format_args.sf_regex_name, "the name of the regular expression to operate on"); { auto* subcmd_format_regex_regex101 = subcmd_format_regex ->add_subcommand("regex101", "use regex101.com to edit this " "regular expression") ->callback([&]() { format_args.set_action( subcmd_format_t::regex101_default_action); }); format_args.sf_regex101_app = subcmd_format_regex_regex101; { subcmd_format_regex_regex101 ->add_subcommand("push", "create/update an entry for " "this regex on regex101.com") ->callback([&]() { format_args.set_action( subcmd_format_t::regex101_push_action); }); subcmd_format_regex_regex101 ->add_subcommand( "pull", "create a patch format file for this " "regular expression based on the entry in " "regex101.com") ->callback([&]() { format_args.set_action( subcmd_format_t::regex101_pull_action); }); subcmd_format_regex_regex101 ->add_subcommand( "delete", "delete the entry regex101.com that was " "created by a push operation") ->callback([&]() { format_args.set_action( subcmd_format_t::regex101_delete_action); }); } } } } { auto* subcmd_regex101 = app.add_subcommand("regex101", "create and edit log message regular " "expressions using regex101.com") ->callback([&]() { regex101_args.set_action( subcmd_regex101_t::default_action); retval->o_ops = regex101_args; }); regex101_args.sr_app = subcmd_regex101; { subcmd_regex101 ->add_subcommand("list", "list the log format regular expression " "linked to entries on regex101.com") ->callback([&]() { regex101_args.set_action(subcmd_regex101_t::list_action); }); } { auto* subcmd_regex101_import = subcmd_regex101 ->add_subcommand("import", "create a new format from a regular " "expression on regex101.com") ->callback([&]() { regex101_args.set_action( subcmd_regex101_t::import_action); }); subcmd_regex101_import->add_option( "url", regex101_args.sr_import_url, "The regex101.com url to construct a log format from"); subcmd_regex101_import->add_option("name", regex101_args.sr_import_name, "The name for the log format"); subcmd_regex101_import ->add_option("regex-name", regex101_args.sr_import_regex_name, "The name for the new regex") ->always_capture_default(); } } app.parse(argc, argv); return retval; } perform_result_t perform(std::shared_ptr opts) { return opts->o_ops.match( [](const no_subcmd_t& ns) -> perform_result_t { auto um = console::user_message::error( attr_line_t("expecting an operation to perform")); um.with_help(ns.ns_root_app->get_subcommands({}) | lnav::itertools::fold( subcmd_reducer, attr_line_t{"the available operations are:"})); return {um}; }, [](const subcmd_format_t& sf) { return sf.sf_action(sf); }, [](const subcmd_regex101_t& sr) { return sr.sr_action(sr); }); } } // namespace management } // namespace lnav