diff options
Diffstat (limited to '')
-rw-r--r-- | src/lnav_commands.cc | 2258 |
1 files changed, 1731 insertions, 527 deletions
diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index d766d99..18f440b 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -44,6 +44,7 @@ #include "base/attr_line.builder.hh" #include "base/auto_mem.hh" #include "base/fs_util.hh" +#include "base/humanize.hh" #include "base/humanize.network.hh" #include "base/injector.hh" #include "base/isc.hh" @@ -51,20 +52,27 @@ #include "base/paths.hh" #include "base/string_util.hh" #include "bound_tags.hh" +#include "breadcrumb_curses.hh" #include "command_executor.hh" #include "config.h" #include "curl_looper.hh" +#include "date/tz.h" #include "db_sub_source.hh" #include "field_overlay_source.hh" #include "fmt/printf.h" +#include "hasher.hh" +#include "itertools.similar.hh" #include "lnav.indexing.hh" #include "lnav_commands.hh" #include "lnav_config.hh" #include "lnav_util.hh" +#include "log.annotate.hh" #include "log_data_helper.hh" #include "log_data_table.hh" +#include "log_format_loader.hh" #include "log_search_table.hh" #include "log_search_table_fwd.hh" +#include "ptimec.hh" #include "readline_callbacks.hh" #include "readline_curses.hh" #include "readline_highlighters.hh" @@ -80,13 +88,27 @@ #include "sysclip.hh" #include "tailer/tailer.looper.hh" #include "text_anonymizer.hh" +#include "url_handler.cfg.hh" #include "url_loader.hh" #include "yajl/api/yajl_parse.h" #include "yajlpp/json_op.hh" #include "yajlpp/yajlpp.hh" +#if !CURL_AT_LEAST_VERSION(7, 80, 0) +extern "C" +{ +const char* curl_url_strerror(CURLUcode error); +} +#endif + using namespace lnav::roles::literals; +inline attr_line_t& +symbol_reducer(const std::string& elem, attr_line_t& accum) +{ + return accum.append("\n ").append(lnav::roles::symbol(elem)); +} + static std::string remaining_args(const std::string& cmdline, const std::vector<std::string>& args, @@ -261,7 +283,10 @@ com_unix_time(exec_context& ec, char* rest; u_time = time(nullptr); - log_time = *localtime(&u_time); + if (localtime_r(&u_time, &log_time) == nullptr) { + return ec.make_error( + "invalid epoch time: {} -- {}", u_time, strerror(errno)); + } log_time.tm_isdst = -1; @@ -282,7 +307,10 @@ com_unix_time(exec_context& ec, u_time = mktime(&log_time); parsed = true; } else if (sscanf(args[1].c_str(), "%ld", &u_time)) { - log_time = *localtime(&u_time); + if (localtime_r(&u_time, &log_time) == nullptr) { + return ec.make_error( + "invalid epoch time: {} -- {}", args[1], strerror(errno)); + } parsed = true; } @@ -293,7 +321,7 @@ com_unix_time(exec_context& ec, strftime(ftime, sizeof(ftime), "%a %b %d %H:%M:%S %Y %z %Z", - localtime(&u_time)); + localtime_r(&u_time, &log_time)); len = strlen(ftime); snprintf(ftime + len, sizeof(ftime) - len, " -- %ld", u_time); retval = std::string(ftime); @@ -308,6 +336,321 @@ com_unix_time(exec_context& ec, } static Result<std::string, lnav::console::user_message> +com_set_file_timezone(exec_context& ec, + std::string cmdline, + std::vector<std::string>& args) +{ + static const intern_string_t SRC = intern_string::lookup("args"); + std::string retval; + + if (args.empty()) { + args.emplace_back("timezone"); + return Ok(retval); + } + + if (args.size() == 1) { + return ec.make_error("expecting a timezone name"); + } + + auto* tc = *lnav_data.ld_view_stack.top(); + auto* lss = dynamic_cast<logfile_sub_source*>(tc->get_sub_source()); + + if (lss != nullptr) { + if (lss->text_line_count() == 0) { + return ec.make_error("no log messages to examine"); + } + + auto line_pair = lss->find_line_with_file(lss->at(tc->get_selection())); + if (!line_pair) { + return ec.make_error(FMT_STRING("cannot find line: {}"), + (int) tc->get_selection()); + } + + shlex lexer(cmdline); + auto split_res = lexer.split(ec.create_resolver()); + if (split_res.isErr()) { + auto split_err = split_res.unwrapErr(); + auto um = lnav::console::user_message::error( + "unable to parse arguments") + .with_reason(split_err.te_msg) + .with_snippet(lnav::console::snippet::from( + SRC, lexer.to_attr_line(split_err))); + + return Err(um); + } + + auto split_args + = split_res.unwrap() | lnav::itertools::map([](const auto& elem) { + return elem.se_value; + }); + try { + const auto* tz = date::locate_zone(split_args[1]); + auto pattern = split_args.size() == 2 + ? line_pair->first->get_filename() + : ghc::filesystem::path(split_args[2]); + + if (!ec.ec_dry_run) { + static auto& safe_options_hier + = injector::get<lnav::safe_file_options_hier&>(); + + safe::WriteAccess<lnav::safe_file_options_hier> options_hier( + safe_options_hier); + + options_hier->foh_generation += 1; + auto& coll = options_hier->foh_path_to_collection["/"]; + + log_info("setting timezone for %s to %s", + pattern.c_str(), + args[1].c_str()); + coll.foc_pattern_to_options[pattern] = lnav::file_options{ + {intern_string_t{}, source_location{}, tz}, + }; + + auto opt_path = lnav::paths::dotlnav() / "file-options.json"; + auto coll_str = coll.to_json(); + lnav::filesystem::write_file(opt_path, coll_str); + } + } catch (const std::runtime_error& e) { + attr_line_t note; + + try { + note = (date::get_tzdb().zones + | lnav::itertools::map(&date::time_zone::name) + | lnav::itertools::similar_to(split_args[1]) + | lnav::itertools::fold(symbol_reducer, attr_line_t{})) + .add_header("did you mean one of the following?"); + } catch (const std::runtime_error& e) { + log_error("unable to get timezones: %s", e.what()); + } + auto um = lnav::console::user_message::error( + attr_line_t() + .append_quoted(split_args[1]) + .append(" is not a valid timezone")) + .with_reason(e.what()) + .with_note(note); + return Err(um); + } + } else { + return ec.make_error( + ":set-file-timezone is only supported for the LOG view"); + } + + return Ok(retval); +} + +static readline_context::prompt_result_t +com_set_file_timezone_prompt(exec_context& ec, const std::string& cmdline) +{ + auto* tc = *lnav_data.ld_view_stack.top(); + auto* lss = dynamic_cast<logfile_sub_source*>(tc->get_sub_source()); + + if (lss == nullptr || lss->text_line_count() == 0) { + return {}; + } + + shlex lexer(cmdline); + auto split_res = lexer.split(ec.create_resolver()); + if (split_res.isErr()) { + return {}; + } + + auto line_pair = lss->find_line_with_file(lss->at(tc->get_selection())); + if (!line_pair) { + return {}; + } + + auto elems = split_res.unwrap(); + auto pattern_arg = line_pair->first->get_filename(); + if (elems.size() == 1) { + try { + static auto& safe_options_hier + = injector::get<lnav::safe_file_options_hier&>(); + + safe::ReadAccess<lnav::safe_file_options_hier> options_hier( + safe_options_hier); + auto file_zone = date::get_tzdb().current_zone()->name(); + auto match_res = options_hier->match(pattern_arg); + if (match_res) { + file_zone = match_res->second.fo_default_zone.pp_value->name(); + pattern_arg = match_res->first; + + auto new_prompt = fmt::format(FMT_STRING("{} {} {}"), + trim(cmdline), + file_zone, + pattern_arg); + + return {new_prompt}; + } + + return {"", file_zone + " "}; + } catch (const std::runtime_error& e) { + log_error("cannot get timezones: %s", e.what()); + } + } + auto arg_path = ghc::filesystem::path(pattern_arg); + auto arg_parent = arg_path.parent_path().string() + "/"; + if (elems.size() == 2 && endswith(cmdline, " ")) { + return {"", arg_parent}; + } + if (elems.size() == 3 && elems.back().se_value == arg_parent) { + return {"", arg_path.filename().string()}; + } + + return {}; +} + +static readline_context::prompt_result_t +com_clear_file_timezone_prompt(exec_context& ec, const std::string& cmdline) +{ + std::string retval; + + auto* tc = *lnav_data.ld_view_stack.top(); + auto* lss = dynamic_cast<logfile_sub_source*>(tc->get_sub_source()); + + if (lss != nullptr && lss->text_line_count() > 0) { + auto line_pair = lss->find_line_with_file(lss->at(tc->get_selection())); + if (line_pair) { + try { + static auto& safe_options_hier + = injector::get<lnav::safe_file_options_hier&>(); + + safe::ReadAccess<lnav::safe_file_options_hier> options_hier( + safe_options_hier); + auto file_zone = date::get_tzdb().current_zone()->name(); + auto pattern_arg = line_pair->first->get_filename(); + auto match_res + = options_hier->match(line_pair->first->get_filename()); + if (match_res) { + file_zone + = match_res->second.fo_default_zone.pp_value->name(); + pattern_arg = match_res->first; + } + + retval = fmt::format( + FMT_STRING("{} {}"), trim(cmdline), pattern_arg); + } catch (const std::runtime_error& e) { + log_error("cannot get timezones: %s", e.what()); + } + } + } + + return {retval}; +} + +static Result<std::string, lnav::console::user_message> +com_clear_file_timezone(exec_context& ec, + std::string cmdline, + std::vector<std::string>& args) +{ + std::string retval; + + if (args.empty()) { + args.emplace_back("file-with-zone"); + return Ok(retval); + } + + if (args.size() != 2) { + return ec.make_error("expecting a single file path or pattern"); + } + + auto* tc = *lnav_data.ld_view_stack.top(); + auto* lss = dynamic_cast<logfile_sub_source*>(tc->get_sub_source()); + + if (lss != nullptr) { + if (!ec.ec_dry_run) { + static auto& safe_options_hier + = injector::get<lnav::safe_file_options_hier&>(); + + safe::WriteAccess<lnav::safe_file_options_hier> options_hier( + safe_options_hier); + + options_hier->foh_generation += 1; + auto& coll = options_hier->foh_path_to_collection["/"]; + const auto iter = coll.foc_pattern_to_options.find(args[1]); + + if (iter == coll.foc_pattern_to_options.end()) { + return ec.make_error(FMT_STRING("no timezone set for: {}"), + args[1]); + } + + log_info("clearing timezone for %s", args[1].c_str()); + iter->second.fo_default_zone.pp_value = nullptr; + if (iter->second.empty()) { + coll.foc_pattern_to_options.erase(iter); + } + + auto opt_path = lnav::paths::dotlnav() / "file-options.json"; + auto coll_str = coll.to_json(); + lnav::filesystem::write_file(opt_path, coll_str); + } + } else { + return ec.make_error( + ":clear-file-timezone is only supported for the LOG view"); + } + + return Ok(retval); +} + +static Result<std::string, lnav::console::user_message> +com_convert_time_to(exec_context& ec, + std::string cmdline, + std::vector<std::string>& args) +{ + std::string retval; + + if (args.empty()) { + args.emplace_back("timezone"); + return Ok(retval); + } + + if (args.size() == 1) { + return ec.make_error("expecting a timezone name"); + } + + const auto* tc = *lnav_data.ld_view_stack.top(); + auto* lss = dynamic_cast<logfile_sub_source*>(tc->get_sub_source()); + + if (lss != nullptr) { + if (lss->text_line_count() == 0) { + return ec.make_error("no log messages to examine"); + } + + const auto* ll = lss->find_line(lss->at(tc->get_selection())); + try { + auto* dst_tz = date::locate_zone(args[1]); + auto utime = date::local_time<std::chrono::seconds>{ + std::chrono::seconds{ll->get_time()}}; + auto cz_time = lnav::to_sys_time(utime); + auto dz_time = date::make_zoned(dst_tz, cz_time); + auto etime = std::chrono::duration_cast<std::chrono::seconds>( + dz_time.get_local_time().time_since_epoch()); + char ftime[128]; + sql_strftime( + ftime, sizeof(ftime), etime.count(), ll->get_millis(), 'T'); + retval = ftime; + + off_t off = 0; + exttm tm; + tm.et_flags |= ETF_ZONE_SET; + tm.et_gmtoff = dz_time.get_info().offset.count(); + ftime_Z(ftime, off, sizeof(ftime), tm); + ftime[off] = '\0'; + retval.append(" "); + retval.append(ftime); + } catch (const std::runtime_error& e) { + return ec.make_error(FMT_STRING("Unable to get timezone: {} -- {}"), + args[1], + e.what()); + } + } else { + return ec.make_error( + ":convert-time-to is only supported for the LOG view"); + } + + return Ok(retval); +} + +static Result<std::string, lnav::console::user_message> com_current_time(exec_context& ec, std::string cmdline, std::vector<std::string>& args) @@ -356,6 +699,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args) std::string all_args = remaining_args(cmdline, args); auto* tc = *lnav_data.ld_view_stack.top(); nonstd::optional<vis_line_t> dst_vl; + auto is_location = false; if (startswith(all_args, "#")) { auto* ta = dynamic_cast<text_anchors*>(tc->get_sub_source()); @@ -368,6 +712,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args) if (!dst_vl) { return ec.make_error("unable to find anchor: {}", all_args); } + is_location = true; } auto* ttt = dynamic_cast<text_time_translator*>(tc->get_sub_source()); @@ -383,7 +728,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args) auto top_time_opt = ttt->time_for_row(tc->get_selection()); if (top_time_opt) { - auto top_time_tv = top_time_opt.value(); + auto top_time_tv = top_time_opt.value().ri_time; struct tm top_tm; localtime_r(&top_time_tv.tv_sec, &top_tm); @@ -404,7 +749,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args) if (!tv_opt) { return ec.make_error("cannot get time for the top row"); } - tv = tv_opt.value(); + tv = tv_opt.value().ri_time; vis_line_t vl = tc->get_selection(), new_vl; bool done = false; @@ -485,7 +830,10 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args) alb.append("^"); if (unmatched_size > 1) { - alb.append(unmatched_size - 2, '-').append("^"); + if (unmatched_size > 2) { + alb.append(unmatched_size - 2, '-'); + } + alb.append("^"); } alb.append(" unrecognized input"); } @@ -509,8 +857,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args) { tm.et_nsec = 0; } - tv.tv_sec = tm2sec(&tm.et_tm); - tv.tv_usec = tm.et_nsec / 1000; + tv = tm.to_timeval(); dst_vl = ttt->row_for_time(tv); } else if (sscanf(args[1].c_str(), "%f%n", &value, &consumed) == 1) { if (args[1][consumed] == '%') { @@ -534,7 +881,7 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args) return Err(um); } - dst_vl | [&ec, tc, &retval](auto new_top) { + dst_vl | [&ec, tc, &retval, is_location](auto new_top) { if (ec.ec_dry_run) { retval = "info: will move to line " + std::to_string((int) new_top); @@ -542,6 +889,9 @@ com_goto(exec_context& ec, std::string cmdline, std::vector<std::string>& args) tc->get_sub_source()->get_location_history() | [new_top](auto lh) { lh->loc_history_append(new_top); }; tc->set_selection(new_top); + if (tc->is_selectable() && is_location) { + tc->set_top(new_top - 2_vl, false); + } retval = ""; } @@ -576,7 +926,8 @@ com_relative_goto(exec_context& ec, retval = "info: shifting top by " + std::to_string(line_offset) + " lines"; } else { - tc->shift_top(vis_line_t(line_offset), true); + tc->set_selection(tc->get_selection() + + vis_line_t(line_offset)); retval = ""; } @@ -591,6 +942,40 @@ com_relative_goto(exec_context& ec, } static Result<std::string, lnav::console::user_message> +com_annotate(exec_context& ec, + std::string cmdline, + std::vector<std::string>& args) +{ + std::string retval; + + if (args.empty()) { + } else if (!ec.ec_dry_run) { + auto* tc = *lnav_data.ld_view_stack.top(); + auto* lss = dynamic_cast<logfile_sub_source*>(tc->get_sub_source()); + + if (lss != nullptr) { + auto sel = tc->get_selection(); + auto applicable_annos = lnav::log::annotate::applicable(sel); + + if (applicable_annos.empty()) { + return ec.make_error( + "no annotations available for this log message"); + } + + auto apply_res = lnav::log::annotate::apply(sel, applicable_annos); + if (apply_res.isErr()) { + return Err(apply_res.unwrapErr()); + } + } else { + return ec.make_error( + ":annotate is only supported for the LOG view"); + } + } + + return Ok(retval); +} + +static Result<std::string, lnav::console::user_message> com_mark(exec_context& ec, std::string cmdline, std::vector<std::string>& args) { std::string retval; @@ -660,7 +1045,7 @@ com_mark_expr(exec_context& ec, if (set_res.isErr()) { return Err(set_res.unwrapErr()); } - lnav_data.ld_preview_status_source.get_description().set_value( + lnav_data.ld_preview_status_source[0].get_description().set_value( "Matches are highlighted in the text view"); } else { auto set_res = lss.set_sql_marker(expr, stmt.release()); @@ -674,18 +1059,20 @@ com_mark_expr(exec_context& ec, return Ok(retval); } -static std::string +static readline_context::prompt_result_t com_mark_expr_prompt(exec_context& ec, const std::string& cmdline) { textview_curses* tc = *lnav_data.ld_view_stack.top(); if (tc != &lnav_data.ld_views[LNV_LOG]) { - return ""; + return {""}; } - return fmt::format(FMT_STRING("{} {}"), - trim(cmdline), - trim(lnav_data.ld_log_source.get_sql_marker_text())); + return { + fmt::format(FMT_STRING("{} {}"), + trim(cmdline), + trim(lnav_data.ld_log_source.get_sql_marker_text())), + }; } static Result<std::string, lnav::console::user_message> @@ -829,13 +1216,80 @@ com_goto_location(exec_context& ec, ? lh->loc_history_back(tc->get_selection()) : lh->loc_history_forward(tc->get_selection()); } - | [tc](auto new_top) { tc->set_selection(new_top); }; + | [tc](auto new_top) { + tc->set_selection(new_top); + if (tc->is_selectable()) { + tc->set_top(new_top - 2_vl, false); + } + }; }; } return Ok(retval); } +static Result<std::string, lnav::console::user_message> +com_next_section(exec_context& ec, + std::string cmdline, + std::vector<std::string>& args) +{ + std::string retval; + + if (args.empty()) { + } else if (!ec.ec_dry_run) { + auto* tc = *lnav_data.ld_view_stack.top(); + auto* ta = dynamic_cast<text_anchors*>(tc->get_sub_source()); + + if (ta == nullptr) { + return ec.make_error("view does not support sections"); + } + + auto adj_opt = ta->adjacent_anchor(tc->get_selection(), + text_anchors::direction::next); + if (!adj_opt) { + return ec.make_error("no next section found"); + } + + tc->set_selection(adj_opt.value()); + if (tc->is_selectable()) { + tc->set_top(adj_opt.value() - 2_vl, false); + } + } + + return Ok(retval); +} + +static Result<std::string, lnav::console::user_message> +com_prev_section(exec_context& ec, + std::string cmdline, + std::vector<std::string>& args) +{ + std::string retval; + + if (args.empty()) { + } else if (!ec.ec_dry_run) { + auto* tc = *lnav_data.ld_view_stack.top(); + auto* ta = dynamic_cast<text_anchors*>(tc->get_sub_source()); + + if (ta == nullptr) { + return ec.make_error("view does not support sections"); + } + + auto adj_opt = ta->adjacent_anchor(tc->get_selection(), + text_anchors::direction::prev); + if (!adj_opt) { + return ec.make_error("no previous section found"); + } + + tc->set_selection(adj_opt.value()); + if (tc->is_selectable()) { + tc->set_top(adj_opt.value() - 2_vl, false); + } + } + + return Ok(retval); +} + static bool csv_needs_quoting(const std::string& str) { @@ -909,15 +1363,14 @@ json_write_row(yajl_gen handle, case SQLITE_TEXT: switch (hm.hm_sub_type) { case 74: { - auto_mem<yajl_handle_t> parse_handle(yajl_free); unsigned char* err; json_ptr jp(""); json_op jo(jp); jo.jo_ptr_callbacks = json_op::gen_callbacks; jo.jo_ptr_data = handle; - parse_handle.reset( - yajl_alloc(&json_op::ptr_callbacks, nullptr, &jo)); + auto parse_handle = yajlpp::alloc_handle( + &json_op::ptr_callbacks, &jo); const unsigned char* json_in = (const unsigned char*) dls.dls_rows[row][col]; @@ -962,14 +1415,14 @@ json_write_row(yajl_gen handle, default: obj_map.gen(anonymize ? ta.next(string_fragment::from_c_str( - dls.dls_rows[row][col])) + dls.dls_rows[row][col])) : dls.dls_rows[row][col]); break; } break; default: obj_map.gen(anonymize ? ta.next(string_fragment::from_c_str( - dls.dls_rows[row][col])) + dls.dls_rows[row][col])) : dls.dls_rows[row][col]); break; } @@ -981,6 +1434,8 @@ com_save_to(exec_context& ec, std::string cmdline, std::vector<std::string>& args) { + static const intern_string_t SRC = intern_string::lookup("path"); + FILE *outfile = nullptr, *toclose = nullptr; const char* mode = ""; std::string fn, retval; @@ -995,13 +1450,22 @@ com_save_to(exec_context& ec, fn = trim(remaining_args(cmdline, args)); - std::vector<std::string> split_args; shlex lexer(fn); - if (!lexer.split(split_args, ec.create_resolver())) { - return ec.make_error("unable to parse arguments"); + auto split_args_res = lexer.split(ec.create_resolver()); + if (split_args_res.isErr()) { + auto split_err = split_args_res.unwrapErr(); + auto um + = lnav::console::user_message::error("unable to parse file name") + .with_reason(split_err.te_msg) + .with_snippet(lnav::console::snippet::from( + SRC, lexer.to_attr_line(split_err))); + + return Err(um); } + auto split_args = split_args_res.unwrap() + | lnav::itertools::map([](const auto& elem) { return elem.se_value; }); auto anon_iter = std::find(split_args.begin(), split_args.end(), "--anonymize"); if (anon_iter != split_args.end()) { @@ -1031,9 +1495,9 @@ com_save_to(exec_context& ec, } if (args[0] == "append-to") { - mode = "a"; + mode = "ae"; } else { - mode = "w"; + mode = "we"; } auto& dls = lnav_data.ld_db_row_source; @@ -1259,31 +1723,33 @@ com_save_to(exec_context& ec, } else if (args[0] == "write-screen-to") { bool wrapped = tc->get_word_wrap(); vis_line_t orig_top = tc->get_top(); + auto inner_height = tc->get_inner_height(); tc->set_word_wrap(to_term); vis_line_t top = tc->get_top(); vis_line_t bottom = tc->get_bottom(); - if (lnav_data.ld_flags & LNF_HEADLESS && tc->get_inner_height() > 0_vl) - { - bottom = tc->get_inner_height() - 1_vl; + if (lnav_data.ld_flags & LNF_HEADLESS && inner_height > 0_vl) { + bottom = inner_height - 1_vl; } + auto screen_height = inner_height == 0 ? 0 : bottom - top + 1; auto y = 0_vl; auto wrapped_count = 0_vl; - std::vector<attr_line_t> rows(bottom - top + 1); + std::vector<attr_line_t> rows(screen_height); auto dim = tc->get_dimensions(); attr_line_t ov_al; auto* los = tc->get_overlay_source(); + while ( + los != nullptr + && los->list_static_overlay(*tc, y, tc->get_inner_height(), ov_al)) + { + write_line_to(outfile, ov_al); + ov_al.clear(); + ++y; + } tc->listview_value_for_rows(*tc, top, rows); for (auto& al : rows) { - while (los != nullptr - && los->list_value_for_overlay( - *tc, y, tc->get_inner_height(), top, ov_al)) - { - write_line_to(outfile, ov_al); - ++y; - } wrapped_count += vis_line_t((al.length() - 1) / (dim.second - 2)); if (anonymize) { al.al_attrs.clear(); @@ -1291,17 +1757,18 @@ com_save_to(exec_context& ec, } write_line_to(outfile, al); + ++y; + if (los != nullptr) { + std::vector<attr_line_t> row_overlay_content; + los->list_value_for_overlay(*tc, top, row_overlay_content); + for (const auto& ov_row : row_overlay_content) { + write_line_to(outfile, ov_row); + line_count += 1; + ++y; + } + } line_count += 1; ++top; - ++y; - } - while (los != nullptr - && los->list_value_for_overlay( - *tc, y, tc->get_inner_height(), top, ov_al) - && !ov_al.empty()) - { - write_line_to(outfile, ov_al); - ++y; } tc->set_word_wrap(wrapped); @@ -1411,10 +1878,25 @@ com_save_to(exec_context& ec, tc->set_word_wrap(wrapped); } else { auto* los = tc->get_overlay_source(); + auto* fos = dynamic_cast<field_overlay_source*>(los); std::vector<attr_line_t> rows(1); attr_line_t ov_al; size_t count = 0; + if (fos != nullptr) { + fos->fos_contexts.push( + field_overlay_source::context{"", false, false, false}); + } + + auto y = 0_vl; + while ( + los != nullptr + && los->list_static_overlay(*tc, y, tc->get_inner_height(), ov_al)) + { + write_line_to(outfile, ov_al); + ov_al.clear(); + ++y; + } for (auto iter = all_user_marks.begin(); iter != all_user_marks.end(); iter++, count++) { @@ -1428,17 +1910,22 @@ com_save_to(exec_context& ec, } write_line_to(outfile, rows[0]); - auto y = 1_vl; - while (los != nullptr - && los->list_value_for_overlay( - *tc, y, tc->get_inner_height(), *iter, ov_al)) - { - write_line_to(outfile, ov_al); - ++y; + y = 0_vl; + if (los != nullptr) { + std::vector<attr_line_t> row_overlay_content; + los->list_value_for_overlay(*tc, (*iter), row_overlay_content); + for (const auto& ov_row : row_overlay_content) { + write_line_to(outfile, ov_row); + line_count += 1; + ++y; + } } - line_count += 1; } + + if (fos != nullptr) { + fos->fos_contexts.pop(); + } } fflush(outfile); @@ -1457,10 +1944,13 @@ com_save_to(exec_context& ec, attr_line_t al(std::string(buffer, rc)); - lnav_data.ld_preview_source.replace_with(al) + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0] + .replace_with(al) .set_text_format(detect_text_format(al.get_string())) .truncate_to(10); - lnav_data.ld_preview_status_source.get_description().set_value( + lnav_data.ld_preview_status_source[0].get_description().set_value( "First lines of file: %s", split_args[0].c_str()); } else { retval = fmt::format(FMT_STRING("info: Wrote {:L} rows to {}"), @@ -1502,18 +1992,63 @@ com_pipe_to(exec_context& ec, auto* tc = *lnav_data.ld_view_stack.top(); auto bv = combined_user_marks(tc->get_bookmarks()); bool pipe_line_to = (args[0] == "pipe-line-to"); + auto path_v = ec.ec_path_stack; + std::map<std::string, std::string> extra_env; + + if (pipe_line_to && tc == &lnav_data.ld_views[LNV_LOG]) { + log_data_helper ldh(lnav_data.ld_log_source); + char tmp_str[64]; + + ldh.parse_line(ec.ec_top_line, true); + auto format = ldh.ldh_file->get_format(); + auto source_path = format->get_source_path(); + path_v.insert(path_v.end(), source_path.begin(), source_path.end()); + + extra_env["log_line"] = fmt::to_string((int) ec.ec_top_line); + sql_strftime(tmp_str, sizeof(tmp_str), ldh.ldh_line->get_timeval()); + extra_env["log_time"] = tmp_str; + extra_env["log_path"] = ldh.ldh_file->get_filename(); + extra_env["log_level"] = ldh.ldh_line->get_level_name(); + if (ldh.ldh_line_values.lvv_opid_value) { + extra_env["log_opid"] = ldh.ldh_line_values.lvv_opid_value.value(); + } + auto read_res = ldh.ldh_file->read_raw_message(ldh.ldh_line); + if (read_res.isOk()) { + auto raw_text = to_string(read_res.unwrap()); + extra_env["log_raw_text"] = raw_text; + } + for (auto& ldh_line_value : ldh.ldh_line_values.lvv_values) { + extra_env[ldh_line_value.lv_meta.lvm_name.to_string()] + = ldh_line_value.to_string(); + } + auto iter = ldh.ldh_parser->dp_pairs.begin(); + for (size_t lpc = 0; lpc < ldh.ldh_parser->dp_pairs.size(); + lpc++, ++iter) + { + std::string colname = ldh.ldh_parser->get_element_string( + iter->e_sub_elements->front()); + colname = ldh.ldh_namer->add_column(colname).to_string(); + std::string val = ldh.ldh_parser->get_element_string( + iter->e_sub_elements->back()); + extra_env[colname] = val; + } + } std::string cmd = trim(remaining_args(cmdline, args)); - auto_pipe in_pipe(STDIN_FILENO); - auto_pipe out_pipe(STDOUT_FILENO); + auto for_child_res = auto_pipe::for_child_fds(STDIN_FILENO, STDOUT_FILENO); + + if (for_child_res.isErr()) { + return ec.make_error(FMT_STRING("unable to open pipe to child: {}"), + for_child_res.unwrapErr()); + } - in_pipe.open(); - out_pipe.open(); + auto child_fds = for_child_res.unwrap(); pid_t child_pid = fork(); - in_pipe.after_fork(child_pid); - out_pipe.after_fork(child_pid); + for (auto& child_fd : child_fds) { + child_fd.after_fork(child_pid); + } switch (child_pid) { case -1: @@ -1521,55 +2056,22 @@ com_pipe_to(exec_context& ec, strerror(errno)); case 0: { - const char* args[] = { + const char* exec_args[] = { "sh", "-c", cmd.c_str(), nullptr, }; - auto path_v = ec.ec_path_stack; std::string path; dup2(STDOUT_FILENO, STDERR_FILENO); path_v.emplace_back(lnav::paths::dotlnav() / "formats/default"); - if (pipe_line_to && tc == &lnav_data.ld_views[LNV_LOG]) { - logfile_sub_source& lss = lnav_data.ld_log_source; - log_data_helper ldh(lss); - char tmp_str[64]; - - ldh.parse_line(ec.ec_top_line, true); - auto format = ldh.ldh_file->get_format(); - auto source_path = format->get_source_path(); - path_v.insert( - path_v.end(), source_path.begin(), source_path.end()); - - snprintf(tmp_str, sizeof(tmp_str), "%d", (int) ec.ec_top_line); - setenv("log_line", tmp_str, 1); - sql_strftime( - tmp_str, sizeof(tmp_str), ldh.ldh_line->get_timeval()); - setenv("log_time", tmp_str, 1); - setenv("log_path", ldh.ldh_file->get_filename().c_str(), 1); - for (auto& ldh_line_value : ldh.ldh_line_values.lvv_values) { - setenv(ldh_line_value.lv_meta.lvm_name.get(), - ldh_line_value.to_string().c_str(), - 1); - } - auto iter = ldh.ldh_parser->dp_pairs.begin(); - for (size_t lpc = 0; lpc < ldh.ldh_parser->dp_pairs.size(); - lpc++, ++iter) - { - std::string colname = ldh.ldh_parser->get_element_string( - iter->e_sub_elements->front()); - colname = ldh.ldh_namer->add_column(colname).to_string(); - std::string val = ldh.ldh_parser->get_element_string( - iter->e_sub_elements->back()); - setenv(colname.c_str(), val.c_str(), 1); - } - } - setenv("PATH", lnav::filesystem::build_path(path_v).c_str(), 1); - execvp(args[0], (char* const*) args); + for (const auto& pair : extra_env) { + setenv(pair.first.c_str(), pair.second.c_str(), 1); + } + execvp(exec_args[0], (char* const*) exec_args); _exit(1); break; } @@ -1578,15 +2080,13 @@ com_pipe_to(exec_context& ec, bookmark_vector<vis_line_t>::iterator iter; std::string line; - in_pipe.read_end().close_on_exec(); - in_pipe.write_end().close_on_exec(); - lnav_data.ld_children.push_back(child_pid); std::future<std::string> reader; - if (out_pipe.read_end() != -1) { - reader = ec.ec_pipe_callback(ec, cmdline, out_pipe.read_end()); + if (child_fds[1].read_end() != -1) { + reader + = ec.ec_pipe_callback(ec, cmdline, child_fds[1].read_end()); } if (pipe_line_to) { @@ -1599,37 +2099,41 @@ com_pipe_to(exec_context& ec, shared_buffer_ref sbr; lf->read_full_message(lf->message_start(lf->begin() + cl), sbr); - if (write(in_pipe.write_end(), sbr.get_data(), sbr.length()) + if (write(child_fds[0].write_end(), + sbr.get_data(), + sbr.length()) == -1) { return ec.make_error("Unable to write to pipe -- {}", strerror(errno)); } - log_perror(write(in_pipe.write_end(), "\n", 1)); + log_perror(write(child_fds[0].write_end(), "\n", 1)); } else { tc->grep_value_for_line(tc->get_top(), line); - if (write(in_pipe.write_end(), line.c_str(), line.size()) + if (write( + child_fds[0].write_end(), line.c_str(), line.size()) == -1) { return ec.make_error("Unable to write to pipe -- {}", strerror(errno)); } - log_perror(write(in_pipe.write_end(), "\n", 1)); + log_perror(write(child_fds[0].write_end(), "\n", 1)); } } else { for (iter = bv.begin(); iter != bv.end(); iter++) { tc->grep_value_for_line(*iter, line); - if (write(in_pipe.write_end(), line.c_str(), line.size()) + if (write( + child_fds[0].write_end(), line.c_str(), line.size()) == -1) { return ec.make_error("Unable to write to pipe -- {}", strerror(errno)); } - log_perror(write(in_pipe.write_end(), "\n", 1)); + log_perror(write(child_fds[0].write_end(), "\n", 1)); } } - in_pipe.write_end().reset(); + child_fds[0].write_end().reset(); if (reader.valid()) { retval = reader.get(); @@ -1647,6 +2151,8 @@ com_redirect_to(exec_context& ec, std::string cmdline, std::vector<std::string>& args) { + static const intern_string_t SRC = intern_string::lookup("path"); + if (args.empty()) { args.emplace_back("filename"); return Ok(std::string()); @@ -1662,16 +2168,21 @@ com_redirect_to(exec_context& ec, } std::string fn = trim(remaining_args(cmdline, args)); - std::vector<std::string> split_args; shlex lexer(fn); - scoped_resolver scopes = { - &ec.ec_local_vars.top(), - &ec.ec_global_vars, - }; - if (!lexer.split(split_args, scopes)) { - return ec.make_error("unable to parse arguments"); + auto split_args_res = lexer.split(ec.create_resolver()); + if (split_args_res.isErr()) { + auto split_err = split_args_res.unwrapErr(); + auto um + = lnav::console::user_message::error("unable to parse file name") + .with_reason(split_err.te_msg) + .with_snippet(lnav::console::snippet::from( + SRC, lexer.to_attr_line(split_err))); + + return Err(um); } + auto split_args = split_args_res.unwrap() + | lnav::itertools::map([](const auto& elem) { return elem.se_value; }); if (split_args.size() > 1) { return ec.make_error("more than one file name was matched"); } @@ -1750,7 +2261,7 @@ com_highlight(exec_context& ec, if (ec.ec_dry_run) { hm[{highlight_source_t::PREVIEW, "preview"}] = hl; - lnav_data.ld_preview_status_source.get_description().set_value( + lnav_data.ld_preview_status_source[0].get_description().set_value( "Matches are highlighted in the view"); retval = ""; @@ -1870,9 +2381,12 @@ com_filter(exec_context& ec, } if (ec.ec_dry_run) { if (args[0] == "filter-in" && !fs.empty()) { - lnav_data.ld_preview_status_source.get_description().set_value( - "Match preview for :filter-in only works if there are no " - "other filters"); + lnav_data.ld_preview_status_source[0] + .get_description() + .set_value( + "Match preview for :filter-in only works if there are " + "no " + "other filters"); retval = ""; } else { auto& hm = tc->get_highlights(); @@ -1885,9 +2399,11 @@ com_filter(exec_context& ec, hm[{highlight_source_t::PREVIEW, "preview"}] = hl; tc->reload_data(); - lnav_data.ld_preview_status_source.get_description().set_value( - "Matches are highlighted in %s in the text view", - role == role_t::VCR_DIFF_DELETE ? "red" : "green"); + lnav_data.ld_preview_status_source[0] + .get_description() + .set_value( + "Matches are highlighted in %s in the text view", + role == role_t::VCR_DIFF_DELETE ? "red" : "green"); retval = ""; } @@ -1918,6 +2434,24 @@ com_filter(exec_context& ec, return Ok(retval); } +static readline_context::prompt_result_t +com_filter_prompt(exec_context& ec, const std::string& cmdline) +{ + const auto* tc = lnav_data.ld_view_stack.top().value(); + std::vector<std::string> args; + + split_ws(cmdline, args); + if (args.size() > 1) { + return {}; + } + + if (tc->tc_selected_text) { + return {"", tc->tc_selected_text->sti_value}; + } + + return {"", tc->get_current_search()}; +} + static Result<std::string, lnav::console::user_message> com_delete_filter(exec_context& ec, std::string cmdline, @@ -2074,7 +2608,7 @@ com_filter_expr(exec_context& ec, if (set_res.isErr()) { return Err(set_res.unwrapErr()); } - lnav_data.ld_preview_status_source.get_description().set_value( + lnav_data.ld_preview_status_source[0].get_description().set_value( "Matches are highlighted in the text view"); } else { lnav_data.ld_log_source.set_preview_sql_filter(nullptr); @@ -2094,18 +2628,20 @@ com_filter_expr(exec_context& ec, return Ok(retval); } -static std::string +static readline_context::prompt_result_t com_filter_expr_prompt(exec_context& ec, const std::string& cmdline) { - textview_curses* tc = *lnav_data.ld_view_stack.top(); + auto* tc = *lnav_data.ld_view_stack.top(); if (tc != &lnav_data.ld_views[LNV_LOG]) { - return ""; + return {""}; } - return fmt::format(FMT_STRING("{} {}"), - trim(cmdline), - trim(lnav_data.ld_log_source.get_sql_filter_text())); + return { + fmt::format(FMT_STRING("{} {}"), + trim(cmdline), + trim(lnav_data.ld_log_source.get_sql_filter_text())), + }; } static Result<std::string, lnav::console::user_message> @@ -2187,9 +2723,12 @@ com_create_logline_table(exec_context& ec, if (ec.ec_dry_run) { attr_line_t al(ldt->get_table_statement()); - lnav_data.ld_preview_status_source.get_description().set_value( - "The following table will be created:"); - lnav_data.ld_preview_source.replace_with(al).set_text_format( + lnav_data.ld_preview_status_source[0] + .get_description() + .set_value("The following table will be created:"); + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0].replace_with(al).set_text_format( text_format_t::TF_SQL); return Ok(std::string()); @@ -2304,10 +2843,12 @@ com_create_search_table(exec_context& ec, attr_line_t al(lst->get_table_statement()); - lnav_data.ld_preview_status_source.get_description().set_value( + lnav_data.ld_preview_status_source[0].get_description().set_value( "The following table will be created:"); - lnav_data.ld_preview_source.replace_with(al).set_text_format( + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0].replace_with(al).set_text_format( text_format_t::TF_SQL); return Ok(std::string()); @@ -2444,6 +2985,7 @@ com_session(exec_context& ec, static Result<std::string, lnav::console::user_message> com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) { + static const intern_string_t SRC = intern_string::lookup("path"); std::string retval; if (args.empty()) { @@ -2465,19 +3007,30 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) pat = trim(remaining_args(cmdline, args)); - std::vector<std::string> split_args; shlex lexer(pat); - scoped_resolver scopes = { - &ec.ec_local_vars.top(), - &ec.ec_global_vars, - }; - - if (!lexer.split(split_args, scopes)) { - return ec.make_error("unable to parse arguments"); + auto split_args_res = lexer.split(ec.create_resolver()); + if (split_args_res.isErr()) { + auto split_err = split_args_res.unwrapErr(); + auto um + = lnav::console::user_message::error("unable to parse file names") + .with_reason(split_err.te_msg) + .with_snippet(lnav::console::snippet::from( + SRC, lexer.to_attr_line(split_err))); + + return Err(um); } + auto split_args = split_args_res.unwrap() + | lnav::itertools::map([](const auto& elem) { return elem.se_value; }); + std::vector<std::pair<std::string, file_location_t>> files_to_front; std::vector<std::string> closed_files; + logfile_open_options loo; + + auto prov = ec.get_provenance<exec_context::file_open>(); + if (prov) { + loo.with_filename(prov->fo_name); + } for (auto fn : split_args) { file_location_t file_loc; @@ -2498,6 +3051,7 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) file_loc = fn.substr(hash_index); fn = fn.substr(0, hash_index); } + loo.with_init_location(file_loc); } auto file_iter = lnav_data.ld_active_files.fc_files.begin(); @@ -2520,6 +3074,7 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) if (file_iter == lnav_data.ld_active_files.fc_files.end()) { auto_mem<char> abspath; struct stat st; + size_t url_index; if (is_url(fn.c_str())) { #ifndef HAVE_LIBCURL @@ -2528,8 +3083,9 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) if (!ec.ec_dry_run) { auto ul = std::make_shared<url_loader>(fn); - lnav_data.ld_active_files.fc_file_names[fn].with_fd( - ul->copy_fd()); + lnav_data.ld_active_files.fc_file_names[ul->get_path()] + .with_filename(fn) + .with_init_location(file_loc); isc::to<curl_looper&, services::curl_streamer_t>().send( [ul](auto& clooper) { clooper.add_request(ul); }); lnav_data.ld_files_to_front.emplace_back(fn, file_loc); @@ -2538,12 +3094,69 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) retval = ""; } #endif - } else if (is_glob(fn.c_str())) { - fc.fc_file_names.emplace(fn, logfile_open_options()); + } else if ((url_index = fn.find("://")) != std::string::npos) { + const auto& cfg + = injector::get<const lnav::url_handler::config&>(); + const auto HOST_REGEX + = lnav::pcre2pp::code::from_const("://(?:\\?|$)"); + + auto find_res = HOST_REGEX.find_in(fn).ignore_error(); + if (find_res) { + fn.insert(url_index + 3, "localhost"); + } + + auto_mem<CURLU> cu(curl_url_cleanup); + cu = curl_url(); + auto set_rc = curl_url_set( + cu, CURLUPART_URL, fn.c_str(), CURLU_NON_SUPPORT_SCHEME); + if (set_rc != CURLUE_OK) { + return Err(lnav::console::user_message::error( + attr_line_t("invalid URL: ") + .append(lnav::roles::file(fn))) + .with_reason(curl_url_strerror(set_rc))); + } + + auto_mem<char> scheme_part(curl_free); + auto get_rc + = curl_url_get(cu, CURLUPART_SCHEME, scheme_part.out(), 0); + if (get_rc != CURLUE_OK) { + return Err(lnav::console::user_message::error( + attr_line_t("cannot get scheme from URL: ") + .append(lnav::roles::file(fn))) + .with_reason(curl_url_strerror(set_rc))); + } + + auto proto_iter = cfg.c_schemes.find(scheme_part.in()); + if (proto_iter == cfg.c_schemes.end()) { + return Err( + lnav::console::user_message::error( + attr_line_t("no defined handler for URL scheme: ") + .append(lnav::roles::file(scheme_part.in()))) + .with_reason(curl_url_strerror(set_rc))); + } + + auto path_and_args + = fmt::format(FMT_STRING("{} {}"), + proto_iter->second.p_handler.pp_value, + fn); + + exec_context::provenance_guard pg(&ec, + exec_context::file_open{fn}); + + auto exec_res = execute_file(ec, path_and_args); + if (exec_res.isErr()) { + return exec_res; + } + + retval = "info: watching -- " + fn; + } else if (lnav::filesystem::is_glob(fn.c_str())) { + fc.fc_file_names.emplace(fn, loo); + files_to_front.emplace_back( + loo.loo_filename.empty() ? fn : loo.loo_filename, file_loc); retval = "info: watching -- " + fn; } else if (stat(fn.c_str(), &st) == -1) { if (fn.find(':') != std::string::npos) { - fc.fc_file_names.emplace(fn, logfile_open_options()); + fc.fc_file_names.emplace(fn, loo); retval = "info: watching -- " + fn; } else { auto um = lnav::console::user_message::error( @@ -2571,25 +3184,23 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) } else if (ec.ec_dry_run) { retval = ""; } else { - auto fifo_piper = std::make_shared<piper_proc>( - std::move(fifo_fd), - false, - lnav::filesystem::open_temp_file( - ghc::filesystem::temp_directory_path() - / "lnav.fifo.XXXXXX") - .map([](auto pair) { - ghc::filesystem::remove(pair.first); - - return pair; - }) - .expect("Cannot create temporary file for FIFO") - .second); - auto fifo_out_fd = fifo_piper->get_fd(); auto desc = fmt::format(FMT_STRING("FIFO [{}]"), lnav_data.ld_fifo_counter++); - lnav_data.ld_active_files.fc_file_names[desc].with_fd( - std::move(fifo_out_fd)); - lnav_data.ld_pipers.push_back(fifo_piper); + if (prov) { + desc = prov->fo_name; + } + auto create_piper_res = lnav::piper::create_looper( + desc, std::move(fifo_fd), auto_fd{}); + if (create_piper_res.isErr()) { + auto um = lnav::console::user_message::error( + attr_line_t("cannot create piper: ") + .append(lnav::roles::file(fn))) + .with_reason(create_piper_res.unwrapErr()) + .with_snippets(ec.ec_source); + return Err(um); + } + lnav_data.ld_active_files.fc_file_names[desc].with_piper( + create_piper_res.unwrap()); } } else if ((abspath = realpath(fn.c_str(), nullptr)) == nullptr) { auto um = lnav::console::user_message::error( @@ -2607,8 +3218,7 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) if (dir_wild[dir_wild.size() - 1] == '/') { dir_wild.resize(dir_wild.size() - 1); } - fc.fc_file_names.emplace(dir_wild + "/*", - logfile_open_options()); + fc.fc_file_names.emplace(dir_wild + "/*", loo); retval = "info: watching -- " + dir_wild; } else if (!S_ISREG(st.st_mode)) { auto um = lnav::console::user_message::error( @@ -2632,7 +3242,7 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) return Err(um); } else { fn = abspath.in(); - fc.fc_file_names.emplace(fn, logfile_open_options()); + fc.fc_file_names.emplace(fn, loo); retval = "info: opened -- " + fn; files_to_front.emplace_back(fn, file_loc); @@ -2646,31 +3256,37 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) } if (ec.ec_dry_run) { - lnav_data.ld_preview_source.clear(); + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0].clear(); if (!fc.fc_file_names.empty()) { auto iter = fc.fc_file_names.begin(); - std::string fn = iter->first; - auto_fd preview_fd; + std::string fn_str = iter->first; - if (fn.find(':') != std::string::npos) { + if (fn_str.find(':') != std::string::npos) { auto id = lnav_data.ld_preview_generation; - lnav_data.ld_preview_status_source.get_description() + lnav_data.ld_preview_status_source[0] + .get_description() .set_cylon(true) - .set_value("Loading %s...", fn.c_str()); - lnav_data.ld_preview_source.clear(); + .set_value("Loading %s...", fn_str.c_str()); + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0].clear(); isc::to<tailer::looper&, services::remote_tailer_t>().send( - [id, fn](auto& tlooper) { - auto rp_opt = humanize::network::path::from_str(fn); + [id, fn_str](auto& tlooper) { + auto rp_opt = humanize::network::path::from_str(fn_str); if (rp_opt) { tlooper.load_preview(id, *rp_opt); } }); - lnav_data.ld_preview_view.set_needs_update(); - } else if (is_glob(fn.c_str())) { + lnav_data.ld_preview_view[0].set_needs_update(); + } else if (lnav::filesystem::is_glob(fn_str)) { static_root_mem<glob_t, globfree> gl; - if (glob(fn.c_str(), GLOB_NOCHECK, nullptr, gl.inout()) == 0) { + if (glob(fn_str.c_str(), GLOB_NOCHECK, nullptr, gl.inout()) + == 0) + { attr_line_t al; for (size_t lpc = 0; lpc < gl->gl_pathc && lpc < 10; lpc++) @@ -2683,45 +3299,134 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) std::to_string(gl->gl_pathc - 10))) .append(" files not shown ..."); } - lnav_data.ld_preview_status_source.get_description() + lnav_data.ld_preview_status_source[0] + .get_description() .set_value("The following files will be loaded:"); - lnav_data.ld_preview_source.replace_with(al); + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0].replace_with(al); } else { - return ec.make_error("failed to evaluate glob -- {}", fn); + return ec.make_error("failed to evaluate glob -- {}", + fn_str); } - } else if ((preview_fd = open(fn.c_str(), O_RDONLY)) == -1) { - return ec.make_error( - "unable to open file3: {} -- {}", fn, strerror(errno)); } else { - line_buffer lb; + auto fn = ghc::filesystem::path(fn_str); + auto detect_res = detect_file_format(fn); attr_line_t al; - file_range range; - std::string lines; - - lb.set_fd(preview_fd); - for (int lpc = 0; lpc < 10; lpc++) { - auto load_result = lb.load_next_line(range); - - if (load_result.isErr()) { + attr_line_builder alb(al); + + switch (detect_res) { + case file_format_t::ARCHIVE: { + auto describe_res = archive_manager::describe(fn); + + if (describe_res.isOk()) { + auto arc_res = describe_res.unwrap(); + + if (arc_res.is<archive_manager::archive_info>()) { + auto ai + = arc_res + .get<archive_manager::archive_info>(); + auto lines_remaining = size_t{9}; + + al.append("Archive: ") + .append( + lnav::roles::symbol(ai.ai_format_name)) + .append("\n"); + for (const auto& entry : ai.ai_entries) { + if (lines_remaining == 0) { + break; + } + lines_remaining -= 1; + + char timebuf[64]; + sql_strftime(timebuf, + sizeof(timebuf), + entry.e_mtime, + 0, + 'T'); + al.append(" ") + .append(entry.e_mode) + .append(" ") + .appendf( + FMT_STRING("{:>8}"), + humanize::file_size( + entry.e_size.value(), + humanize::alignment::columnar)) + .append(" ") + .append(timebuf) + .append(" ") + .append(lnav::roles::file(entry.e_name)) + .append("\n"); + } + } + } else { + al.append(describe_res.unwrapErr()); + } break; } + case file_format_t::UNKNOWN: { + auto open_res + = lnav::filesystem::open_file(fn, O_RDONLY); + + if (open_res.isErr()) { + return ec.make_error("unable to open -- {}", fn); + } + auto preview_fd = open_res.unwrap(); + line_buffer lb; + file_range range; + + lb.set_fd(preview_fd); + for (int lpc = 0; lpc < 10; lpc++) { + auto load_result = lb.load_next_line(range); + + if (load_result.isErr()) { + break; + } - auto li = load_result.unwrap(); + auto li = load_result.unwrap(); - range = li.li_file_range; - auto read_result = lb.read_range(range); - if (read_result.isErr()) { + range = li.li_file_range; + if (!li.li_utf8_scan_result.is_valid()) { + range.fr_size = 16; + } + auto read_result = lb.read_range(range); + if (read_result.isErr()) { + break; + } + + auto sbr = read_result.unwrap(); + auto sf = sbr.to_string_fragment(); + if (li.li_utf8_scan_result.is_valid()) { + alb.append(sf); + } else { + { + auto ag = alb.with_attr( + VC_ROLE.value(role_t::VCR_FILE_OFFSET)); + alb.appendf(FMT_STRING("{: >16x} "), + range.fr_offset); + } + alb.append_as_hexdump(sf); + alb.append("\n"); + } + } + break; + } + case file_format_t::SQLITE_DB: { + alb.append(fmt::to_string(detect_res)); + break; + } + case file_format_t::REMOTE: { break; } - - auto sbr = read_result.unwrap(); - lines.append(sbr.get_data(), sbr.length()); } - lnav_data.ld_preview_source.replace_with(al.with_string(lines)) - .set_text_format(detect_text_format(al.get_string())); - lnav_data.ld_preview_status_source.get_description().set_value( - "For file: %s", fn.c_str()); + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0].replace_with(al).set_text_format( + detect_text_format(al.get_string())); + lnav_data.ld_preview_status_source[0] + .get_description() + .set_value("For file: %s", fn.c_str()); } } } else { @@ -2741,50 +3446,103 @@ com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args) static Result<std::string, lnav::console::user_message> com_close(exec_context& ec, std::string cmdline, std::vector<std::string>& args) { + static const intern_string_t SRC = intern_string::lookup("path"); std::string retval; if (args.empty()) { - } else { - textview_curses* tc = *lnav_data.ld_view_stack.top(); - nonstd::optional<ghc::filesystem::path> actual_path; - std::string fn; + args.emplace_back("loaded-files"); + return Ok(retval); + } - if (tc == &lnav_data.ld_views[LNV_TEXT]) { - textfile_sub_source& tss = lnav_data.ld_text_source; + auto* tc = *lnav_data.ld_view_stack.top(); + std::vector<nonstd::optional<ghc::filesystem::path>> actual_path_v; + std::vector<std::string> fn_v; - if (tss.empty()) { - return ec.make_error("no text files are opened"); - } else { - fn = tss.current_file()->get_filename(); - lnav_data.ld_active_files.request_close(tss.current_file()); + if (args.size() > 1) { + auto lexer = shlex(cmdline); - if (tss.size() == 1) { - lnav_data.ld_view_stack.pop_back(); - } + auto split_args_res = lexer.split(ec.create_resolver()); + if (split_args_res.isErr()) { + auto split_err = split_args_res.unwrapErr(); + auto um = lnav::console::user_message::error( + "unable to parse file name") + .with_reason(split_err.te_msg) + .with_snippet(lnav::console::snippet::from( + SRC, lexer.to_attr_line(split_err))); + + return Err(um); + } + + auto args = split_args_res.unwrap() + | lnav::itertools::map( + [](const auto& elem) { return elem.se_value; }); + args.erase(args.begin()); + + for (const auto& lf : lnav_data.ld_active_files.fc_files) { + if (lf.get() == nullptr) { + continue; } - } else if (tc == &lnav_data.ld_views[LNV_LOG]) { - if (tc->get_inner_height() == 0) { - return ec.make_error("no log files loaded"); - } else { - logfile_sub_source& lss = lnav_data.ld_log_source; - vis_line_t vl = tc->get_selection(); - content_line_t cl = lss.at(vl); - std::shared_ptr<logfile> lf = lss.find(cl); - actual_path = lf->get_actual_path(); - fn = lf->get_filename(); - if (!ec.ec_dry_run) { - lnav_data.ld_active_files.request_close(lf); - } + auto find_iter + = find_if(args.begin(), args.end(), [&lf](const auto& arg) { + return fnmatch(arg.c_str(), lf->get_filename().c_str(), 0) + == 0; + }); + + if (find_iter == args.end()) { + continue; + } + + actual_path_v.push_back(lf->get_actual_path()); + fn_v.emplace_back(lf->get_filename()); + if (!ec.ec_dry_run) { + lnav_data.ld_active_files.request_close(lf); + } + } + } else if (tc == &lnav_data.ld_views[LNV_TEXT]) { + auto& tss = lnav_data.ld_text_source; + + if (tss.empty()) { + return ec.make_error("no text files are opened"); + } else if (!ec.ec_dry_run) { + auto lf = tss.current_file(); + actual_path_v.emplace_back(lf->get_actual_path()); + fn_v.emplace_back(lf->get_filename()); + lnav_data.ld_active_files.request_close(lf); + + if (tss.size() == 1) { + lnav_data.ld_view_stack.pop_back(); } } else { - return ec.make_error( - "close must be run in the log or text file views"); + retval = fmt::format(FMT_STRING("closing -- {}"), + tss.current_file()->get_filename()); } - if (!fn.empty()) { - if (ec.ec_dry_run) { - retval = ""; - } else { + } else if (tc == &lnav_data.ld_views[LNV_LOG]) { + if (tc->get_inner_height() == 0) { + return ec.make_error("no log files loaded"); + } else { + auto& lss = lnav_data.ld_log_source; + auto vl = tc->get_selection(); + auto cl = lss.at(vl); + auto lf = lss.find(cl); + + actual_path_v.push_back(lf->get_actual_path()); + fn_v.emplace_back(lf->get_filename()); + if (!ec.ec_dry_run) { + lnav_data.ld_active_files.request_close(lf); + } + } + } else { + return ec.make_error("close must be run in the log or text file views"); + } + if (!fn_v.empty()) { + if (ec.ec_dry_run) { + retval = ""; + } else { + for (size_t lpc = 0; lpc < actual_path_v.size(); lpc++) { + const auto& fn = fn_v[lpc]; + const auto& actual_path = actual_path_v[lpc]; + if (is_url(fn.c_str())) { isc::to<curl_looper&, services::curl_streamer_t>().send( [fn](auto& clooper) { clooper.close_request(fn); }); @@ -2794,8 +3552,9 @@ com_close(exec_context& ec, std::string cmdline, std::vector<std::string>& args) actual_path.value().string()); } lnav_data.ld_active_files.fc_closed_files.insert(fn); - retval = "info: closed -- " + fn; } + retval = fmt::format(FMT_STRING("info: closed -- {}"), + fmt::join(fn_v, ", ")); } } @@ -2807,6 +3566,7 @@ com_file_visibility(exec_context& ec, std::string cmdline, std::vector<std::string>& args) { + static const intern_string_t SRC = intern_string::lookup("path"); bool only_this_file = false; bool make_visible; std::string retval; @@ -2821,11 +3581,11 @@ com_file_visibility(exec_context& ec, } if (args.size() == 1 || only_this_file) { - textview_curses* tc = *lnav_data.ld_view_stack.top(); + auto* tc = *lnav_data.ld_view_stack.top(); std::shared_ptr<logfile> lf; if (tc == &lnav_data.ld_views[LNV_TEXT]) { - textfile_sub_source& tss = lnav_data.ld_text_source; + const auto& tss = lnav_data.ld_text_source; if (tss.empty()) { return ec.make_error("no text files are opened"); @@ -2848,8 +3608,10 @@ com_file_visibility(exec_context& ec, if (only_this_file) { for (const auto& ld : lnav_data.ld_log_source) { ld->set_visibility(false); + ld->get_file_ptr()->set_indexing(false); } } + lf->set_indexing(make_visible); lnav_data.ld_log_source.find_data(lf) | [make_visible](auto ld) { ld->set_visibility(make_visible); }; tc->get_sub_source()->text_filters_changed(); @@ -2858,10 +3620,25 @@ com_file_visibility(exec_context& ec, make_visible ? "showing" : "hiding", lf->get_filename()); } else { + auto* top_tc = *lnav_data.ld_view_stack.top(); int text_file_count = 0, log_file_count = 0; auto lexer = shlex(cmdline); - lexer.split(args, ec.create_resolver()); + auto split_args_res = lexer.split(ec.create_resolver()); + if (split_args_res.isErr()) { + auto split_err = split_args_res.unwrapErr(); + auto um = lnav::console::user_message::error( + "unable to parse file name") + .with_reason(split_err.te_msg) + .with_snippet(lnav::console::snippet::from( + SRC, lexer.to_attr_line(split_err))); + + return Err(um); + } + + auto args = split_args_res.unwrap() + | lnav::itertools::map( + [](const auto& elem) { return elem.se_value; }); args.erase(args.begin()); for (const auto& lf : lnav_data.ld_active_files.fc_files) { @@ -2887,6 +3664,7 @@ com_file_visibility(exec_context& ec, if (!ec.ec_dry_run) { ld_opt | [make_visible](auto ld) { + ld->get_file_ptr()->set_indexing(make_visible); ld->set_visibility(make_visible); }; } @@ -2900,6 +3678,11 @@ com_file_visibility(exec_context& ec, lnav_data.ld_views[LNV_LOG] .get_sub_source() ->text_filters_changed(); + if (top_tc == &lnav_data.ld_views[LNV_GANTT]) { + lnav_data.ld_views[LNV_GANTT] + .get_sub_source() + ->text_filters_changed(); + } } if (!ec.ec_dry_run && text_file_count > 0) { lnav_data.ld_views[LNV_TEXT] @@ -3007,13 +3790,13 @@ com_comment(exec_context& ec, return Ok(retval); } -static std::string +static readline_context::prompt_result_t com_comment_prompt(exec_context& ec, const std::string& cmdline) { - textview_curses* tc = *lnav_data.ld_view_stack.top(); + auto* tc = *lnav_data.ld_view_stack.top(); if (tc != &lnav_data.ld_views[LNV_LOG]) { - return ""; + return {""}; } auto& lss = lnav_data.ld_log_source; @@ -3024,10 +3807,10 @@ com_comment_prompt(exec_context& ec, const std::string& cmdline) auto buf = auto_buffer::alloc(trimmed_comment.size() + 16); quote_content(buf, trimmed_comment, 0); - return trim(cmdline) + " " + buf.to_string(); + return {trim(cmdline) + " " + buf.to_string()}; } - return ""; + return {""}; } static Result<std::string, lnav::console::user_message> @@ -3046,7 +3829,8 @@ com_clear_comment(exec_context& ec, if (tc != &lnav_data.ld_views[LNV_LOG]) { return ec.make_error( - "The :clear-comment command only works in the log view"); + "The :clear-comment command only works in the log " + "view"); } auto& lss = lnav_data.ld_log_source; @@ -3055,10 +3839,12 @@ com_clear_comment(exec_context& ec, bookmark_metadata& line_meta = *(line_meta_opt.value()); line_meta.bm_comment.clear(); - if (line_meta.empty()) { - lss.erase_bookmark_metadata(tc->get_selection()); + if (line_meta.empty(bookmark_metadata::categories::notes)) { tc->set_user_mark( &textview_curses::BM_META, tc->get_selection(), false); + if (line_meta.empty(bookmark_metadata::categories::any)) { + lss.erase_bookmark_metadata(tc->get_selection()); + } } lss.set_line_meta_changed(); @@ -3138,7 +3924,7 @@ com_untag(exec_context& ec, std::string cmdline, std::vector<std::string>& args) auto line_meta_opt = lss.find_bookmark_metadata(tc->get_selection()); if (line_meta_opt) { - bookmark_metadata& line_meta = *(line_meta_opt.value()); + auto& line_meta = *(line_meta_opt.value()); for (size_t lpc = 1; lpc < args.size(); lpc++) { std::string tag = args[lpc]; @@ -3148,7 +3934,7 @@ com_untag(exec_context& ec, std::string cmdline, std::vector<std::string>& args) } line_meta.remove_tag(tag); } - if (line_meta.empty()) { + if (line_meta.empty(bookmark_metadata::categories::notes)) { tc->set_user_mark( &textview_curses::BM_META, tc->get_selection(), false); } @@ -3184,7 +3970,8 @@ com_delete_tags(exec_context& ec, if (tc != &lnav_data.ld_views[LNV_LOG]) { return ec.make_error( - "The :delete-tag command only works in the log view"); + "The :delete-tag command only works in the log " + "view"); } auto& known_tags = bookmark_metadata::KNOWN_TAGS; @@ -3220,12 +4007,15 @@ com_delete_tags(exec_context& ec, line_meta->remove_tag(tag); } - if (line_meta->empty()) { - lss.erase_bookmark_metadata(*iter); - size_t off = distance(vbm.begin(), iter); + if (line_meta->empty(bookmark_metadata::categories::notes)) { + size_t off = std::distance(vbm.begin(), iter); + auto vl = *iter; + tc->set_user_mark(&textview_curses::BM_META, vl, false); + if (line_meta->empty(bookmark_metadata::categories::any)) { + lss.erase_bookmark_metadata(vl); + } - tc->set_user_mark(&textview_curses::BM_META, *iter, false); - iter = next(vbm.begin(), off); + iter = std::next(vbm.begin(), off); } else { ++iter; } @@ -3258,7 +4048,7 @@ com_partition_name(exec_context& ec, args[1] = trim(remaining_args(cmdline, args)); tc.set_user_mark( - &textview_curses::BM_META, tc.get_selection(), true); + &textview_curses::BM_PARTITION, tc.get_selection(), true); auto& line_meta = lss.get_bookmark_metadata(tc.get_selection()); @@ -3284,7 +4074,7 @@ com_clear_partition(exec_context& ec, } else if (args.size() == 1) { textview_curses& tc = lnav_data.ld_views[LNV_LOG]; logfile_sub_source& lss = lnav_data.ld_log_source; - auto& bv = tc.get_bookmarks()[&textview_curses::BM_META]; + auto& bv = tc.get_bookmarks()[&textview_curses::BM_PARTITION]; nonstd::optional<vis_line_t> part_start; if (binary_search(bv.begin(), bv.end(), tc.get_selection())) { @@ -3300,10 +4090,12 @@ com_clear_partition(exec_context& ec, auto& line_meta = lss.get_bookmark_metadata(part_start.value()); line_meta.bm_name.clear(); - if (line_meta.empty()) { - lss.erase_bookmark_metadata(part_start.value()); + if (line_meta.empty(bookmark_metadata::categories::partition)) { tc.set_user_mark( - &textview_curses::BM_META, part_start.value(), false); + &textview_curses::BM_PARTITION, part_start.value(), false); + if (line_meta.empty(bookmark_metadata::categories::any)) { + lss.erase_bookmark_metadata(part_start.value()); + } } retval = "info: cleared partition name"; @@ -3453,7 +4245,8 @@ com_summarize(exec_context& ec, query += ","; } query_frag = sqlite3_mprintf( - " \"count_%s\" desc, \"c_%s\" collate naturalnocase asc", + " \"count_%s\" desc, \"c_%s\" collate " + "naturalnocase asc", iter->c_str(), iter->c_str()); query += query_frag; @@ -3638,7 +4431,7 @@ com_zoom_to(exec_context& ec, auto old_time_opt = lnav_data.ld_hist_source2.time_for_row( lnav_data.ld_views[LNV_HISTOGRAM].get_top()); if (old_time_opt) { - old_time = old_time_opt.value(); + old_time = old_time_opt.value().ri_time; rebuild_hist(); lnav_data.ld_hist_source2.row_for_time(old_time) | [](auto new_top) { @@ -3659,7 +4452,7 @@ com_zoom_to(exec_context& ec, spectro_view.reload_data(); if (old_time_opt) { lnav_data.ld_spectro_source->row_for_time( - old_time_opt.value()) + old_time_opt.value().ri_time) | [](auto new_top) { lnav_data.ld_views[LNV_SPECTRO].set_selection( new_top); @@ -3709,6 +4502,7 @@ com_load_session(exec_context& ec, if (args.empty()) { } else if (!ec.ec_dry_run) { load_session(); + lnav::session::restore_view_states(); lnav_data.ld_views[LNV_LOG].reload_data(); } @@ -3755,10 +4549,11 @@ com_export_session_to(exec_context& ec, tcsetattr(1, TCSANOW, &curr_termios); setvbuf(stdout, nullptr, _IONBF, 0); to_term = true; - fprintf( - outfile, - "\n---------------- Press any key to exit lo-fi display " - "----------------\n\n"); + fprintf(outfile, + "\n---------------- Press any key to exit " + "lo-fi " + "display " + "----------------\n\n"); } else { outfile = auto_mem<FILE>::leak(ec_out.value()); } @@ -3881,7 +4676,9 @@ com_toggle_field(exec_context& ec, if (hide) { if (lnav_data.ld_rl_view != nullptr) { lnav_data.ld_rl_view->set_alt_value( - HELP_MSG_1(x, "to quickly show hidden fields")); + HELP_MSG_1(x, + "to quickly show hidden " + "fields")); } } tc->set_needs_update(); @@ -3915,35 +4712,38 @@ com_hide_line(exec_context& ec, if (args.empty()) { args.emplace_back("move-time"); } else if (args.size() == 1) { - textview_curses* tc = *lnav_data.ld_view_stack.top(); - logfile_sub_source& lss = lnav_data.ld_log_source; + auto* tc = *lnav_data.ld_view_stack.top(); + auto& lss = lnav_data.ld_log_source; if (tc == &lnav_data.ld_views[LNV_LOG]) { - struct timeval min_time, max_time; - bool have_min_time = lss.get_min_log_time(min_time); - bool have_max_time = lss.get_max_log_time(max_time); + auto min_time_opt = lss.get_min_log_time(); + auto max_time_opt = lss.get_max_log_time(); char min_time_str[32], max_time_str[32]; - if (have_min_time) { - sql_strftime(min_time_str, sizeof(min_time_str), min_time); + if (min_time_opt) { + sql_strftime( + min_time_str, sizeof(min_time_str), min_time_opt.value()); } - if (have_max_time) { - sql_strftime(max_time_str, sizeof(max_time_str), max_time); + if (max_time_opt) { + sql_strftime( + max_time_str, sizeof(max_time_str), max_time_opt.value()); } - if (have_min_time && have_max_time) { + if (min_time_opt && max_time_opt) { retval - = fmt::format("info: hiding lines before {} and after {}", + = fmt::format(FMT_STRING("info: hiding lines before {} and " + "after {}"), min_time_str, max_time_str); - } else if (have_min_time) { - retval - = fmt::format("info: hiding lines before {}", min_time_str); - } else if (have_max_time) { - retval - = fmt::format("info: hiding lines after {}", max_time_str); + } else if (min_time_opt) { + retval = fmt::format(FMT_STRING("info: hiding lines before {}"), + min_time_str); + } else if (max_time_opt) { + retval = fmt::format(FMT_STRING("info: hiding lines after {}"), + max_time_str); } else { retval - = "info: no lines hidden by time, pass an absolute or " + = "info: no lines hidden by time, pass an " + "absolute or " "relative time"; } } else { @@ -3952,55 +4752,55 @@ com_hide_line(exec_context& ec, } } else if (args.size() >= 2) { std::string all_args = remaining_args(cmdline, args); - textview_curses* tc = *lnav_data.ld_view_stack.top(); - logfile_sub_source& lss = lnav_data.ld_log_source; + auto* tc = *lnav_data.ld_view_stack.top(); + auto* ttt = dynamic_cast<text_time_translator*>(tc->get_sub_source()); + auto& lss = lnav_data.ld_log_source; date_time_scanner dts; - struct timeval tv; - bool tv_set = false; + struct timeval tv_abs; + nonstd::optional<timeval> tv_opt; auto parse_res = relative_time::from_str(all_args); if (parse_res.isOk()) { - if (tc == &lnav_data.ld_views[LNV_LOG]) { + if (ttt != nullptr) { if (tc->get_inner_height() > 0) { - content_line_t cl; struct exttm tm; - vis_line_t vl; - logline* ll; - - vl = tc->get_selection(); - cl = lnav_data.ld_log_source.at(vl); - ll = lnav_data.ld_log_source.find_line(cl); - ll->to_exttm(tm); - tv = parse_res.unwrap().adjust(tm).to_timeval(); - tv_set = true; + auto vl = tc->get_selection(); + auto log_vl_ri = ttt->time_for_row(vl); + if (log_vl_ri) { + tm = exttm::from_tv(log_vl_ri.value().ri_time); + tv_opt = parse_res.unwrap().adjust(tm).to_timeval(); + } } } else { return ec.make_error( - "relative time values only work in the log view"); - } - } else if (dts.convert_to_timeval(all_args, tv)) { - if (tc == &lnav_data.ld_views[LNV_LOG]) { - tv_set = true; - } else { - return ec.make_error("time values only work in the log view"); + "relative time values only work in a " + "time-based view"); } + } else if (dts.convert_to_timeval(all_args, tv_abs)) { + tv_opt = tv_abs; } - if (tv_set && !ec.ec_dry_run) { + if (tv_opt && !ec.ec_dry_run) { char time_text[256]; std::string relation; - sql_strftime(time_text, sizeof(time_text), tv); + sql_strftime(time_text, sizeof(time_text), tv_opt.value()); if (args[0] == "hide-lines-before") { - lss.set_min_log_time(tv); + lss.set_min_log_time(tv_opt.value()); relation = "before"; } else { - lss.set_max_log_time(tv); + lss.set_max_log_time(tv_opt.value()); relation = "after"; } - retval = "info: hiding lines " + relation + " " + time_text; + if (ttt != nullptr && tc != &lnav_data.ld_views[LNV_LOG]) { + tc->get_sub_source()->text_filters_changed(); + tc->reload_data(); + } + + retval = fmt::format( + FMT_STRING("info: hiding lines {} {}"), relation, time_text); } } @@ -4085,6 +4885,219 @@ com_rebuild(exec_context& ec, } static Result<std::string, lnav::console::user_message> +com_cd(exec_context& ec, std::string cmdline, std::vector<std::string>& args) +{ + static const intern_string_t SRC = intern_string::lookup("path"); + + if (args.empty()) { + args.emplace_back("dirname"); + return Ok(std::string()); + } + + if (lnav_data.ld_flags & LNF_SECURE_MODE) { + return ec.make_error("{} -- unavailable in secure mode", args[0]); + } + + std::vector<std::string> word_exp; + std::string pat; + + pat = trim(remaining_args(cmdline, args)); + + shlex lexer(pat); + auto split_args_res = lexer.split(ec.create_resolver()); + if (split_args_res.isErr()) { + auto split_err = split_args_res.unwrapErr(); + auto um + = lnav::console::user_message::error("unable to parse file name") + .with_reason(split_err.te_msg) + .with_snippet(lnav::console::snippet::from( + SRC, lexer.to_attr_line(split_err))); + + return Err(um); + } + + auto split_args = split_args_res.unwrap() + | lnav::itertools::map([](const auto& elem) { return elem.se_value; }); + + if (split_args.size() != 1) { + return ec.make_error("expecting a single argument"); + } + + struct stat st; + + if (stat(split_args[0].c_str(), &st) != 0) { + return Err(ec.make_error_msg("cannot access -- {}", split_args[0]) + .with_errno_reason()); + } + + if (!S_ISDIR(st.st_mode)) { + return ec.make_error("{} is not a directory", split_args[0]); + } + + if (!ec.ec_dry_run) { + chdir(split_args[0].c_str()); + } + + return Ok(std::string()); +} + +static Result<std::string, lnav::console::user_message> +com_sh(exec_context& ec, std::string cmdline, std::vector<std::string>& args) +{ + if (args.empty()) { + args.emplace_back("filename"); + return Ok(std::string()); + } + + if (lnav_data.ld_flags & LNF_SECURE_MODE) { + return ec.make_error("{} -- unavailable in secure mode", args[0]); + } + + static size_t EXEC_COUNT = 0; + + if (!ec.ec_dry_run) { + nonstd::optional<std::string> name_flag; + + shlex lexer(cmdline); + auto cmd_start = args[0].size(); + auto split_res = lexer.split(ec.create_resolver()); + if (split_res.isOk()) { + auto flags = split_res.unwrap(); + if (flags.size() >= 2) { + static const char* NAME_FLAG = "--name="; + + if (startswith(flags[1].se_value, NAME_FLAG)) { + name_flag = flags[1].se_value.substr(strlen(NAME_FLAG)); + cmd_start = flags[1].se_origin.sf_end; + } + } + } + + auto carg = trim(cmdline.substr(cmd_start)); + + log_info("executing: %s", carg.c_str()); + + auto child_fds_res + = auto_pipe::for_child_fds(STDOUT_FILENO, STDERR_FILENO); + if (child_fds_res.isErr()) { + auto um = lnav::console::user_message::error( + "unable to create child pipes") + .with_reason(child_fds_res.unwrapErr()); + ec.add_error_context(um); + return Err(um); + } + auto child_res = lnav::pid::from_fork(); + if (child_res.isErr()) { + auto um + = lnav::console::user_message::error("unable to fork() child") + .with_reason(child_res.unwrapErr()); + ec.add_error_context(um); + return Err(um); + } + + auto child_fds = child_fds_res.unwrap(); + auto child = child_res.unwrap(); + for (auto& child_fd : child_fds) { + child_fd.after_fork(child.in()); + } + if (child.in_child()) { + auto dev_null = open("/dev/null", O_RDONLY | O_CLOEXEC); + + dup2(dev_null, STDIN_FILENO); + const char* exec_args[] = { + getenv_opt("SHELL").value_or("bash"), + "-c", + carg.c_str(), + nullptr, + }; + + for (const auto& pair : ec.ec_local_vars.top()) { + pair.second.match( + [&pair](const std::string& val) { + setenv(pair.first.c_str(), val.c_str(), 1); + }, + [&pair](const string_fragment& sf) { + setenv(pair.first.c_str(), sf.to_string().c_str(), 1); + }, + [](null_value_t) {}, + [&pair](int64_t val) { + setenv( + pair.first.c_str(), fmt::to_string(val).c_str(), 1); + }, + [&pair](double val) { + setenv( + pair.first.c_str(), fmt::to_string(val).c_str(), 1); + }); + } + + execvp(exec_args[0], (char**) exec_args); + _exit(EXIT_FAILURE); + } + + std::string display_name; + auto open_prov = ec.get_provenance<exec_context::file_open>(); + if (open_prov) { + if (name_flag) { + display_name = fmt::format( + FMT_STRING("{}/{}"), open_prov->fo_name, name_flag.value()); + } else { + display_name = open_prov->fo_name; + } + } else if (name_flag) { + display_name = name_flag.value(); + } else { + display_name + = fmt::format(FMT_STRING("[{}] {}"), EXEC_COUNT++, carg); + } + + auto name_base = display_name; + size_t name_counter = 0; + + while (true) { + auto fn_iter + = lnav_data.ld_active_files.fc_file_names.find(display_name); + if (fn_iter == lnav_data.ld_active_files.fc_file_names.end()) { + break; + } + name_counter += 1; + display_name + = fmt::format(FMT_STRING("{} [{}]"), name_base, name_counter); + } + + auto create_piper_res + = lnav::piper::create_looper(display_name, + std::move(child_fds[0].read_end()), + std::move(child_fds[1].read_end())); + + if (create_piper_res.isErr()) { + auto um + = lnav::console::user_message::error("unable to create piper") + .with_reason(create_piper_res.unwrapErr()); + ec.add_error_context(um); + return Err(um); + } + + lnav_data.ld_active_files.fc_file_names[display_name].with_piper( + create_piper_res.unwrap()); + lnav_data.ld_child_pollers.emplace_back(child_poller{ + display_name, + std::move(child), + [](auto& fc, auto& child) {}, + }); + lnav_data.ld_files_to_front.emplace_back(display_name, + file_location_t{}); + + if (lnav_data.ld_rl_view != nullptr) { + lnav_data.ld_rl_view->set_alt_value( + HELP_MSG_CTRL(C, "to send SIGINT to child process")); + } + return Ok(fmt::format(FMT_STRING("info: executing -- {}"), carg)); + } + + return Ok(std::string()); +} + +static Result<std::string, lnav::console::user_message> com_shexec(exec_context& ec, std::string cmdline, std::vector<std::string>& args) @@ -4159,9 +5172,11 @@ com_echo(exec_context& ec, std::string cmdline, std::vector<std::string>& args) auto ec_out = ec.get_output(); if (ec.ec_dry_run) { - lnav_data.ld_preview_status_source.get_description().set_value( + lnav_data.ld_preview_status_source[0].get_description().set_value( "The text to output:"); - lnav_data.ld_preview_source.replace_with(attr_line_t(retval)); + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0].replace_with(attr_line_t(retval)); retval = ""; } else if (ec_out) { FILE* outfile = *ec_out; @@ -4243,35 +5258,23 @@ com_eval(exec_context& ec, std::string cmdline, std::vector<std::string>& args) if (ec.ec_dry_run) { attr_line_t al(expanded_cmd); - lnav_data.ld_preview_status_source.get_description().set_value( + lnav_data.ld_preview_status_source[0].get_description().set_value( "The command to be executed:"); - lnav_data.ld_preview_source.replace_with(al); + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0].replace_with(al); return Ok(std::string()); } auto src_guard = ec.enter_source(EVAL_SRC, 1, expanded_cmd); - std::string alt_msg; - switch (expanded_cmd[0]) { - case ':': - return execute_command(ec, expanded_cmd.substr(1)); - case ';': - return execute_sql(ec, expanded_cmd.substr(1), alt_msg); - case '|': - return execute_file(ec, expanded_cmd.substr(1)); - case '/': { - auto search_cmd = expanded_cmd.substr(1); - lnav_data.ld_view_stack.top() | - [&search_cmd](auto tc) { tc->execute_search(search_cmd); }; - break; - } - default: - return ec.make_error( - "expecting argument to start with ':', ';', '/', " - "or '|' to signify a command, SQL query, or script to " - "execute"); + auto content = string_fragment::from_str(expanded_cmd); + multiline_executor me(ec, ":eval"); + for (auto line : content.split_lines()) { + TRY(me.push_back(line)); } + retval = TRY(me.final()); } else { return ec.make_error("expecting a command or query to evaluate"); } @@ -4292,14 +5295,16 @@ com_config(exec_context& ec, static const auto INPUT_SRC = intern_string::lookup("input"); yajlpp_parse_context ypc(INPUT_SRC, &lnav_config_handlers); - std::vector<lnav::console::user_message> errors; + std::vector<lnav::console::user_message> errors, errors_ignored; std::string option = args[1]; lnav_config = rollback_lnav_config; ypc.set_path(option) .with_obj(lnav_config) .with_error_reporter([&errors](const auto& ypc, auto msg) { - errors.push_back(msg); + if (msg.um_level == lnav::console::user_message::level::error) { + errors.push_back(msg); + } }); ypc.ypc_active_paths.insert(option); ypc.update_callbacks(); @@ -4331,10 +5336,14 @@ com_config(exec_context& ec, if (ec.ec_dry_run) { attr_line_t al(old_value); - lnav_data.ld_preview_source.replace_with(al) + lnav_data.ld_preview_view[0].set_sub_source( + &lnav_data.ld_preview_source[0]); + lnav_data.ld_preview_source[0] + .replace_with(al) .set_text_format(detect_text_format(old_value)) .truncate_to(10); - lnav_data.ld_preview_status_source.get_description() + lnav_data.ld_preview_status_source[0] + .get_description() .set_value("Value of option: %s", option.c_str()); char help_text[1024]; @@ -4401,8 +5410,18 @@ com_config(exec_context& ec, return ec.make_error("unhandled type"); } + while (!errors.empty()) { + if (errors.back().um_level + == lnav::console::user_message::level::error) + { + break; + } else { + errors.pop_back(); + } + } + if (!errors.empty()) { - return Err(errors[0]); + return Err(errors.back()); } if (changed) { @@ -4412,10 +5431,20 @@ com_config(exec_context& ec, = ec.ec_source.back().s_location; reload_config(errors); + while (!errors.empty()) { + if (errors.back().um_level + == lnav::console::user_message::level::error) + { + break; + } else { + errors.pop_back(); + } + } + if (!errors.empty()) { lnav_config = rollback_lnav_config; - reload_config(errors); - return Err(errors[0]); + reload_config(errors_ignored); + return Err(errors.back()); } if (!ec.ec_dry_run) { retval = "info: changed config option -- " + option; @@ -4531,6 +5560,7 @@ com_spectrogram(exec_context& ec, } if (found) { + lnav_data.ld_views[LNV_SPECTRO].reload_data(); ss.text_selection_changed(lnav_data.ld_views[LNV_SPECTRO]); ensure_view(&lnav_data.ld_views[LNV_SPECTRO]); @@ -4559,6 +5589,12 @@ com_quit(exec_context& ec, std::string cmdline, std::vector<std::string>& args) } static void +breadcrumb_prompt(std::vector<std::string>& args) +{ + set_view_mode(ln_mode_t::BREADCRUMBS); +} + +static void command_prompt(std::vector<std::string>& args) { auto* tc = *lnav_data.ld_view_stack.top(); @@ -4658,6 +5694,9 @@ command_prompt(std::vector<std::string>& args) rollback_lnav_config = lnav_config; lnav_data.ld_doc_status_source.set_title("Command Help"); + lnav_data.ld_doc_status_source.set_description( + " See " ANSI_BOLD("https://docs.lnav.org/en/latest/" + "commands.html") " for more details"); add_view_text_possibilities(lnav_data.ld_rl_view, ln_mode_t::COMMAND, "filter", @@ -4672,6 +5711,7 @@ command_prompt(std::vector<std::string>& args) add_tag_possibilities(); add_file_possibilities(); add_recent_netlocs_possibilities(); + add_tz_possibilities(ln_mode_t::COMMAND); auto* ta = dynamic_cast<text_anchors*>(tc->get_sub_source()); if (ta != nullptr) { @@ -4687,6 +5727,8 @@ command_prompt(std::vector<std::string>& args) lnav_data.ld_rl_view->focus(ln_mode_t::COMMAND, cget(args, 2).value_or(":"), cget(args, 3).value_or("")); + + rl_set_help(); } static void @@ -4727,6 +5769,7 @@ search_prompt(std::vector<std::string>& args) cget(args, 2).value_or("/"), cget(args, 3).value_or("")); lnav_data.ld_doc_status_source.set_title("Syntax Help"); + lnav_data.ld_doc_status_source.set_description(""); rl_set_help(); lnav_data.ld_bottom_source.set_prompt( "Search for: " @@ -4760,7 +5803,7 @@ search_files_prompt(std::vector<std::string>& args) lnav_data.ld_mode = ln_mode_t::SEARCH_FILES; for (const auto& lf : lnav_data.ld_active_files.fc_files) { - auto path = lnav::pcre2pp::quote(lf->get_unique_path()); + auto path = lnav::pcre2pp::quote(lf->get_unique_path().string()); lnav_data.ld_rl_view->add_possibility( ln_mode_t::SEARCH_FILES, "*", path); } @@ -4794,8 +5837,8 @@ search_spectro_details_prompt(std::vector<std::string>& args) static void sql_prompt(std::vector<std::string>& args) { - textview_curses* tc = *lnav_data.ld_view_stack.top(); - textview_curses& log_view = lnav_data.ld_views[LNV_LOG]; + auto* tc = *lnav_data.ld_view_stack.top(); + auto& log_view = lnav_data.ld_views[LNV_LOG]; lnav_data.ld_exec_context.ec_top_line = tc->get_selection(); @@ -4806,6 +5849,9 @@ sql_prompt(std::vector<std::string>& args) cget(args, 3).value_or("")); lnav_data.ld_doc_status_source.set_title("Query Help"); + lnav_data.ld_doc_status_source.set_description( + "See " ANSI_BOLD("https://docs.lnav.org/en/latest/" + "sqlext.html") " for more details"); rl_set_help(); lnav_data.ld_bottom_source.update_loading(0, 0); lnav_data.ld_status[LNS_BOTTOM].do_update(); @@ -4816,6 +5862,8 @@ sql_prompt(std::vector<std::string>& args) tc->reload_data(); lnav_data.ld_bottom_source.set_prompt( "Enter an SQL query: (Press " ANSI_BOLD("CTRL+]") " to abort)"); + + add_sqlite_possibilities(); } static void @@ -4841,6 +5889,7 @@ com_prompt(exec_context& ec, { static std::map<std::string, std::function<void(std::vector<std::string>&)>> PROMPT_TYPES = { + {"breadcrumb", breadcrumb_prompt}, {"command", command_prompt}, {"script", script_prompt}, {"search", search_prompt}, @@ -4853,25 +5902,40 @@ com_prompt(exec_context& ec, if (args.empty()) { } else if (!ec.ec_dry_run) { - args.clear(); + static const intern_string_t SRC = intern_string::lookup("flags"); auto lexer = shlex(cmdline); - lexer.split(args, ec.create_resolver()); + auto split_args_res = lexer.split(ec.create_resolver()); + if (split_args_res.isErr()) { + auto split_err = split_args_res.unwrapErr(); + auto um + = lnav::console::user_message::error("unable to parse prompt") + .with_reason(split_err.te_msg) + .with_snippet(lnav::console::snippet::from( + SRC, lexer.to_attr_line(split_err))); + + return Err(um); + } - auto alt_flag = std::find(args.begin(), args.end(), "--alt"); + auto split_args = split_args_res.unwrap() + | lnav::itertools::map( + [](const auto& elem) { return elem.se_value; }); + + auto alt_flag + = std::find(split_args.begin(), split_args.end(), "--alt"); auto is_alt = false; - if (alt_flag != args.end()) { - args.erase(alt_flag); + if (alt_flag != split_args.end()) { + split_args.erase(alt_flag); is_alt = true; } - auto prompter = PROMPT_TYPES.find(args[1]); + auto prompter = PROMPT_TYPES.find(split_args[1]); if (prompter == PROMPT_TYPES.end()) { - return ec.make_error("Unknown prompt type: {}", args[1]); + return ec.make_error("Unknown prompt type: {}", split_args[1]); } - prompter->second(args); + prompter->second(split_args); lnav_data.ld_rl_view->set_alt_focus(is_alt); } return Ok(std::string()); @@ -4883,16 +5947,22 @@ readline_context::command_t STD_COMMANDS[] = { help_text(":prompt") .with_summary("Open the given prompt") - .with_parameter( - {"type", - "The type of prompt -- command, script, search, sql, user"}) - .with_parameter( - help_text( - "--alt", - "Perform the alternate action for this prompt by default") - .optional()) - .with_parameter( - help_text("prompt", "The prompt to display").optional()) + .with_parameter({"type", + "The type of prompt -- command, script, " + "search, sql, user"}) + .with_parameter(help_text("--alt", + "Perform the alternate action " + "for this prompt by default") + .optional()) + .with_parameter(help_text("prompt", "The prompt to display") + .with_enum_values({ + "breadcrumb", + "command", + "script", + "search", + "sql", + }) + .optional()) .with_parameter( help_text("initial-value", "The initial value to fill in for the prompt") @@ -4928,6 +5998,44 @@ readline_context::command_t STD_COMMANDS[] = { .with_parameter(help_text("seconds", "The epoch timestamp to convert") .with_format(help_parameter_format_t::HPF_INTEGER)) .with_example({"To convert the epoch time 1490191111", "1490191111"})}, + { + "convert-time-to", + com_convert_time_to, + help_text(":convert-time-to") + .with_summary("Convert the focused timestamp to the " + "given timezone") + .with_parameter(help_text("zone", "The timezone name")), + }, + { + "set-file-timezone", + com_set_file_timezone, + help_text(":set-file-timezone") + .with_summary("Set the timezone to use for log messages that do " + "not include a timezone. The timezone is applied " + "to " + "the focused file or the given glob pattern.") + .with_parameter({"zone", "The timezone name"}) + .with_parameter(help_text{"pattern", + "The glob pattern to match against " + "files that should use this timezone"} + .optional()) + .with_tags({"file-options"}), + com_set_file_timezone_prompt, + }, + { + "clear-file-timezone", + com_clear_file_timezone, + help_text(":clear-file-timezone") + .with_summary("Clear the timezone setting for the " + "focused file or " + "the given glob pattern.") + .with_parameter(help_text{"pattern", + "The glob pattern to match against files " + "that should " + "no longer use this timezone"}) + .with_tags({"file-options"}), + com_clear_file_timezone_prompt, + }, {"current-time", com_current_time, @@ -4946,7 +6054,8 @@ readline_context::command_t STD_COMMANDS[] = { .with_examples( {{"To go to line 22", "22"}, {"To go to the line 75% of the way into the view", "75%"}, - {"To go to the first message on the first day of 2017", + {"To go to the first message on the first day of " + "2017", "2017-01-01"}, {"To go to the Screenshots section", "#screenshots"}}) .with_tags({"navigation"})}, @@ -4961,12 +6070,23 @@ readline_context::command_t STD_COMMANDS[] = { {"To move 10 percent back in the view", "-10%"}, }) .with_tags({"navigation"})}, + + { + "annotate", + com_annotate, + + help_text(":annotate") + .with_summary("Analyze the focused log message and " + "attach annotations") + .with_tags({"metadata"}), + }, + {"mark", com_mark, help_text(":mark") - .with_summary( - "Toggle the bookmark state for the top line in the current view") + .with_summary("Toggle the bookmark state for the top line in the " + "current view") .with_tags({"bookmarks"})}, { "mark-expr", @@ -4974,16 +6094,18 @@ readline_context::command_t STD_COMMANDS[] = { help_text(":mark-expr") .with_summary("Set the bookmark expression") - .with_parameter(help_text( - "expr", - "The SQL expression to evaluate for each log message. " - "The message values can be accessed using column names " - "prefixed with a colon")) + .with_parameter(help_text("expr", + "The SQL expression to evaluate for each " + "log message. " + "The message values can be accessed " + "using column names " + "prefixed with a colon")) .with_opposites({"clear-mark-expr"}) .with_tags({"bookmarks"}) - .with_example( - {"To mark lines from 'dhclient' that mention 'eth0'", - ":log_procname = 'dhclient' AND :log_body LIKE '%eth0%'"}), + .with_example({"To mark lines from 'dhclient' that " + "mention 'eth0'", + ":log_procname = 'dhclient' AND " + ":log_body LIKE '%eth0%'"}), com_mark_expr_prompt, }, @@ -4998,8 +6120,8 @@ readline_context::command_t STD_COMMANDS[] = { com_goto_mark, help_text(":next-mark") - .with_summary( - "Move to the next bookmark of the given type in the current view") + .with_summary("Move to the next bookmark of the given type in the " + "current view") .with_parameter(help_text("type", "The type of bookmark -- error, warning, " "search, user, file, meta") @@ -5010,7 +6132,8 @@ readline_context::command_t STD_COMMANDS[] = { com_goto_mark, help_text(":prev-mark") - .with_summary("Move to the previous bookmark of the given type in the " + .with_summary("Move to the previous bookmark of the given " + "type in the " "current view") .with_parameter(help_text("type", "The type of bookmark -- error, warning, " @@ -5028,8 +6151,27 @@ readline_context::command_t STD_COMMANDS[] = { com_goto_location, help_text(":prev-location") - .with_summary("Move to the previous position in the location history") + .with_summary("Move to the previous position in the " + "location history") .with_tags({"navigation"})}, + + { + "next-section", + com_next_section, + + help_text(":next-section") + .with_summary("Move to the next section in the document") + .with_tags({"navigation"}), + }, + { + "prev-section", + com_prev_section, + + help_text(":prev-section") + .with_summary("Move to the previous section in the document") + .with_tags({"navigation"}), + }, + {"help", com_help, @@ -5038,21 +6180,23 @@ readline_context::command_t STD_COMMANDS[] = { com_toggle_field, help_text(":hide-fields") - .with_summary( - "Hide log message fields by replacing them with an ellipsis") + .with_summary("Hide log message fields by replacing them " + "with an ellipsis") .with_parameter( help_text("field-name", - "The name of the field to hide in the format for the " + "The name of the field to hide in the format for " + "the " "top log line. " - "A qualified name can be used where the field name is " + "A qualified name can be used where the field " + "name is " "prefixed " "by the format name and a dot to hide any field.") .one_or_more()) .with_example( {"To hide the log_procname fields in all formats", "log_procname"}) - .with_example( - {"To hide only the log_procname field in the syslog format", - "syslog_log.log_procname"}) + .with_example({"To hide only the log_procname field in " + "the syslog format", + "syslog_log.log_procname"}) .with_tags({"display"})}, {"show-fields", com_toggle_field, @@ -5092,8 +6236,8 @@ readline_context::command_t STD_COMMANDS[] = { com_show_lines, help_text(":show-lines-before-and-after") - .with_summary( - "Show lines that were hidden by the 'hide-lines' commands") + .with_summary("Show lines that were hidden by the " + "'hide-lines' commands") .with_opposites({"hide-lines-before", "hide-lines-after"}) .with_tags({"filtering"})}, {"hide-unmarked-lines", @@ -5113,7 +6257,8 @@ readline_context::command_t STD_COMMANDS[] = { com_highlight, help_text(":highlight") - .with_summary("Add coloring to log messages fragments that match the " + .with_summary("Add coloring to log messages fragments " + "that match the " "given regular expression") .with_parameter( help_text("pattern", "The regular expression to match")) @@ -5125,37 +6270,45 @@ readline_context::command_t STD_COMMANDS[] = { help_text(":clear-highlight") .with_summary("Remove a previously set highlight regular expression") - .with_parameter(help_text( - "pattern", - "The regular expression previously used with :highlight")) + .with_parameter(help_text("pattern", + "The regular expression previously used " + "with :highlight")) .with_tags({"display"}) .with_opposites({"highlight"}) .with_example( {"To clear the highlight with the pattern 'foobar'", "foobar"})}, - {"filter-in", - com_filter, + { + "filter-in", + com_filter, - help_text(":filter-in") - .with_summary("Only show lines that match the given regular " - "expression in the current view") - .with_parameter( - help_text("pattern", "The regular expression to match")) - .with_tags({"filtering"}) - .with_example({"To filter out log messages that do not have the " - "string 'dhclient'", - "dhclient"})}, - {"filter-out", - com_filter, - - help_text(":filter-out") - .with_summary("Remove lines that match the given regular expression " - "in the current view") - .with_parameter( - help_text("pattern", "The regular expression to match")) - .with_tags({"filtering"}) - .with_example({"To filter out log messages that contain the string " - "'last message repeated'", - "last message repeated"})}, + help_text(":filter-in") + .with_summary("Only show lines that match the given regular " + "expression in the current view") + .with_parameter( + help_text("pattern", "The regular expression to match")) + .with_tags({"filtering"}) + .with_example({"To filter out log messages that do not have the " + "string 'dhclient'", + "dhclient"}), + com_filter_prompt, + }, + { + "filter-out", + com_filter, + + help_text(":filter-out") + .with_summary("Remove lines that match the given " + "regular expression " + "in the current view") + .with_parameter( + help_text("pattern", "The regular expression to match")) + .with_tags({"filtering"}) + .with_example({"To filter out log messages that " + "contain the string " + "'last message repeated'", + "last message repeated"}), + com_filter_prompt, + }, {"delete-filter", com_delete_filter, @@ -5166,29 +6319,34 @@ readline_context::command_t STD_COMMANDS[] = { help_text("pattern", "The regular expression to match")) .with_opposites({"filter-in", "filter-out"}) .with_tags({"filtering"}) - .with_example( - {"To delete the filter with the pattern 'last message repeated'", - "last message repeated"})}, + .with_example({"To delete the filter with the pattern 'last " + "message repeated'", + "last message repeated"})}, { "filter-expr", com_filter_expr, help_text(":filter-expr") .with_summary("Set the filter expression") - .with_parameter(help_text( - "expr", - "The SQL expression to evaluate for each log message. " - "The message values can be accessed using column names " - "prefixed with a colon")) + .with_parameter(help_text("expr", + "The SQL expression to evaluate for each " + "log message. " + "The message values can be accessed " + "using column names " + "prefixed with a colon")) .with_opposites({"clear-filter-expr"}) .with_tags({"filtering"}) .with_example({"To set a filter expression that matched syslog " "messages from 'syslogd'", ":log_procname = 'syslogd'"}) - .with_example( - {"To set a filter expression that matches log messages where " - "'id' is followed by a number and contains the string 'foo'", - ":log_body REGEXP 'id\\d+' AND :log_body REGEXP 'foo'"}), + .with_example({"To set a filter expression that " + "matches log messages " + "where " + "'id' is followed by a number and " + "contains the string " + "'foo'", + ":log_body REGEXP 'id\\d+' AND " + ":log_body REGEXP 'foo'"}), com_filter_expr_prompt, }, @@ -5203,26 +6361,27 @@ readline_context::command_t STD_COMMANDS[] = { com_save_to, help_text(":append-to") - .with_summary( - "Append marked lines in the current view to the given file") + .with_summary("Append marked lines in the current view to " + "the given file") .with_parameter(help_text("path", "The path to the file to append to")) .with_tags({"io"}) - .with_example( - {"To append marked lines to the file /tmp/interesting-lines.txt", - "/tmp/interesting-lines.txt"})}, + .with_example({"To append marked lines to the file " + "/tmp/interesting-lines.txt", + "/tmp/interesting-lines.txt"})}, {"write-to", com_save_to, help_text(":write-to") - .with_summary("Overwrite the given file with any marked lines in the " + .with_summary("Overwrite the given file with any marked " + "lines in the " "current view") .with_parameter( help_text("--anonymize", "Anonymize the lines").optional()) .with_parameter(help_text("path", "The path to the file to write")) .with_tags({"io", "scripting"}) - .with_example( - {"To write marked lines to the file /tmp/interesting-lines.txt", - "/tmp/interesting-lines.txt"})}, + .with_example({"To write marked lines to the file " + "/tmp/interesting-lines.txt", + "/tmp/interesting-lines.txt"})}, {"write-csv-to", com_save_to, @@ -5249,20 +6408,21 @@ readline_context::command_t STD_COMMANDS[] = { com_save_to, help_text(":write-jsonlines-to") - .with_summary( - "Write SQL results to the given file in JSON Lines format") + .with_summary("Write SQL results to the given file in " + "JSON Lines format") .with_parameter( help_text("--anonymize", "Anonymize the JSON values").optional()) .with_parameter(help_text("path", "The path to the file to write")) .with_tags({"io", "scripting", "sql"}) - .with_example({"To write SQL results as JSON Lines to /tmp/table.json", + .with_example({"To write SQL results as JSON Lines to " + "/tmp/table.json", "/tmp/table.json"})}, {"write-table-to", com_save_to, help_text(":write-table-to") - .with_summary( - "Write SQL results to the given file in a tabular format") + .with_summary("Write SQL results to the given file in a " + "tabular format") .with_parameter( help_text("--anonymize", "Anonymize the table contents") .optional()) @@ -5274,10 +6434,10 @@ readline_context::command_t STD_COMMANDS[] = { com_save_to, help_text(":write-raw-to") - .with_summary( - "In the log view, write the original log file content " - "of the marked messages to the file. In the DB view, " - "the contents of the cells are written to the output file.") + .with_summary("In the log view, write the original log file content " + "of the marked messages to the file. In the DB view, " + "the contents of the cells are written to the output " + "file.") .with_parameter(help_text("--view={log,db}", "The view to use as the source of data") .optional()) @@ -5285,9 +6445,9 @@ readline_context::command_t STD_COMMANDS[] = { help_text("--anonymize", "Anonymize the lines").optional()) .with_parameter(help_text("path", "The path to the file to write")) .with_tags({"io", "scripting", "sql"}) - .with_example( - {"To write the marked lines in the log view to /tmp/table.txt", - "/tmp/table.txt"})}, + .with_example({"To write the marked lines in the log view " + "to /tmp/table.txt", + "/tmp/table.txt"})}, {"write-view-to", com_save_to, @@ -5326,7 +6486,10 @@ readline_context::command_t STD_COMMANDS[] = { com_pipe_to, help_text(":pipe-line-to") - .with_summary("Pipe the top line to the given shell command") + .with_summary("Pipe the focused line to the given shell " + "command. Any fields " + "defined by the format will be set as " + "environment variables.") .with_parameter( help_text("shell-cmd", "The shell command-line to execute")) .with_tags({"io"}) @@ -5338,12 +6501,11 @@ readline_context::command_t STD_COMMANDS[] = { help_text(":redirect-to") .with_summary("Redirect the output of commands that write to " "stdout to the given file") - .with_parameter( - help_text( - "path", - "The path to the file to write." - " If not specified, the current redirect will be cleared") - .optional()) + .with_parameter(help_text("path", + "The path to the file to write." + " If not specified, the current redirect " + "will be cleared") + .optional()) .with_tags({"io", "scripting"}) .with_example({"To write the output of lnav commands to the file " "/tmp/script-output.txt", @@ -5357,7 +6519,8 @@ readline_context::command_t STD_COMMANDS[] = { "pattern", "The regular expression used in the filter command")) .with_tags({"filtering"}) .with_opposites({"disable-filter"}) - .with_example({"To enable the disabled filter with the pattern 'last " + .with_example({"To enable the disabled filter with the " + "pattern 'last " "message repeated'", "last message repeated"})}, {"disable-filter", @@ -5369,9 +6532,9 @@ readline_context::command_t STD_COMMANDS[] = { "pattern", "The regular expression used in the filter command")) .with_tags({"filtering"}) .with_opposites({"filter-out", "filter-in"}) - .with_example( - {"To disable the filter with the pattern 'last message repeated'", - "last message repeated"})}, + .with_example({"To disable the filter with the pattern 'last " + "message repeated'", + "last message repeated"})}, {"enable-word-wrap", com_enable_word_wrap, @@ -5389,13 +6552,14 @@ readline_context::command_t STD_COMMANDS[] = { com_create_logline_table, help_text(":create-logline-table") - .with_summary("Create an SQL table using the top line of the log view " + .with_summary("Create an SQL table using the top line of " + "the log view " "as a template") .with_parameter(help_text("table-name", "The name for the new table")) .with_tags({"vtables", "sql"}) - .with_example( - {"To create a logline-style table named 'task_durations'", - "task_durations"})}, + .with_example({"To create a logline-style table named " + "'task_durations'", + "task_durations"})}, {"delete-logline-table", com_delete_logline_table, @@ -5405,9 +6569,9 @@ readline_context::command_t STD_COMMANDS[] = { help_text("table-name", "The name of the table to delete")) .with_opposites({"delete-logline-table"}) .with_tags({"vtables", "sql"}) - .with_example( - {"To delete the logline-style table named 'task_durations'", - "task_durations"})}, + .with_example({"To delete the logline-style table named " + "'task_durations'", + "task_durations"})}, {"create-search-table", com_create_search_table, @@ -5416,16 +6580,18 @@ readline_context::command_t STD_COMMANDS[] = { .with_parameter( help_text("table-name", "The name of the table to create")) .with_parameter( - help_text( - "pattern", - "The regular expression used to capture the table columns. " - "If not given, the current search pattern is used.") + help_text("pattern", + "The regular expression used to capture the table " + "columns. " + "If not given, the current search pattern is " + "used.") .optional()) .with_tags({"vtables", "sql"}) - .with_example( - {"To create a table named 'task_durations' that matches log " - "messages with the pattern 'duration=(?<duration>\\d+)'", - R"(task_durations duration=(?<duration>\d+))"})}, + .with_example({"To create a table named 'task_durations' that " + "matches log " + "messages with the pattern " + "'duration=(?<duration>\\d+)'", + R"(task_durations duration=(?<duration>\d+))"})}, {"delete-search-table", com_delete_search_table, @@ -5441,10 +6607,10 @@ readline_context::command_t STD_COMMANDS[] = { com_open, help_text(":open") - .with_summary( - "Open the given file(s) in lnav. Opening files on machines " - "accessible via SSH can be done using the syntax: " - "[user@]host:/path/to/logs") + .with_summary("Open the given file(s) in lnav. Opening files on " + "machines " + "accessible via SSH can be done using the syntax: " + "[user@]host:/path/to/logs") .with_parameter( help_text{"path", "The path to the file to open"}.one_or_more()) .with_example({"To open the file '/path/to/file'", "/path/to/file"}) @@ -5457,8 +6623,9 @@ readline_context::command_t STD_COMMANDS[] = { .with_summary("Hide the given file(s) and skip indexing until it " "is shown again. If no path is given, the current " "file in the view is hidden") - .with_parameter(help_text{ - "path", "A path or glob pattern that specifies the files to hide"} + .with_parameter(help_text{"path", + "A path or glob pattern that " + "specifies the files to hide"} .zero_or_more()) .with_opposites({"show-file"})}, {"show-file", @@ -5466,9 +6633,9 @@ readline_context::command_t STD_COMMANDS[] = { help_text(":show-file") .with_summary("Show the given file(s) and resume indexing.") - .with_parameter(help_text{ - "path", - "The path or glob pattern that specifies the files to show"} + .with_parameter(help_text{"path", + "The path or glob pattern that " + "specifies the files to show"} .zero_or_more()) .with_opposites({"hide-file"})}, {"show-only-this-file", @@ -5481,18 +6648,24 @@ readline_context::command_t STD_COMMANDS[] = { com_close, help_text(":close") - .with_summary("Close the top file in the view") + .with_summary("Close the given file(s) or the top file in the view") + .with_parameter(help_text{"path", + "A path or glob pattern that " + "specifies the files to close"} + .zero_or_more()) .with_opposites({"open"})}, { "comment", com_comment, help_text(":comment") - .with_summary( - "Attach a comment to the top log line. The comment will be " - "displayed right below the log message it is associated with. " - "The comment can be formatted using markdown and you can add " - "new-lines with '\\n'.") + .with_summary("Attach a comment to the top log line. The " + "comment will be " + "displayed right below the log message it is " + "associated with. " + "The comment can be formatted using markdown and " + "you can add " + "new-lines with '\\n'.") .with_parameter(help_text("text", "The comment text")) .with_example({"To add the comment 'This is where it all went " "wrong' to the top line", @@ -5527,7 +6700,8 @@ readline_context::command_t STD_COMMANDS[] = { help_text(":untag") .with_summary("Detach tags from the top log line") .with_parameter(help_text("tag", "The tags to detach").one_or_more()) - .with_example({"To remove the tags '#BUG123' and '#needs-review' from " + .with_example({"To remove the tags '#BUG123' and " + "'#needs-review' from " "the top line", "#BUG123 #needs-review"}) .with_opposites({"tag"}) @@ -5538,7 +6712,8 @@ readline_context::command_t STD_COMMANDS[] = { help_text(":delete-tags") .with_summary("Remove the given tags from all log lines") .with_parameter(help_text("tag", "The tags to delete").one_or_more()) - .with_example({"To remove the tags '#BUG123' and '#needs-review' from " + .with_example({"To remove the tags '#BUG123' and " + "'#needs-review' from " "all log lines", "#BUG123 #needs-review"}) .with_opposites({"tag"}) @@ -5563,23 +6738,24 @@ readline_context::command_t STD_COMMANDS[] = { com_session, help_text(":session") - .with_summary( - "Add the given command to the session file (~/.lnav/session)") + .with_summary("Add the given command to the session file " + "(~/.lnav/session)") .with_parameter(help_text("lnav-command", "The lnav command to save.")) - .with_example( - {"To add the command ':highlight foobar' to the session file", - ":highlight foobar"})}, + .with_example({"To add the command ':highlight foobar' to " + "the session file", + ":highlight foobar"})}, {"summarize", com_summarize, help_text(":summarize") - .with_summary("Execute a SQL query that computes the characteristics " + .with_summary("Execute a SQL query that computes the " + "characteristics " "of the values in the given column") .with_parameter( help_text("column-name", "The name of the column to analyze.")) - .with_example( - {"To get a summary of the sc_bytes column in the access_log table", - "sc_bytes"})}, + .with_example({"To get a summary of the sc_bytes column in the " + "access_log table", + "sc_bytes"})}, {"switch-to-view", com_switch_to_view, @@ -5592,12 +6768,13 @@ readline_context::command_t STD_COMMANDS[] = { com_switch_to_view, help_text(":toggle-view") - .with_summary( - "Switch to the given view or, if it is already displayed, " - "switch to the previous view") + .with_summary("Switch to the given view or, if it is " + "already displayed, " + "switch to the previous view") .with_parameter(help_text( "view-name", "The name of the view to toggle the display of.")) - .with_example({"To switch to the 'schema' view if it is not displayed " + .with_example({"To switch to the 'schema' view if it is " + "not displayed " "or switch back to the previous view", "schema"})}, {"toggle-filtering", @@ -5658,15 +6835,17 @@ readline_context::command_t STD_COMMANDS[] = { com_echo, help_text(":echo") - .with_summary( - "Echo the given message to the screen or, if :redirect-to has " - "been called, to output file specified in the redirect. " - "Variable substitution is performed on the message. Use a " - "backslash to escape any special characters, like '$'") - .with_parameter( - help_text("-n", - "Do not print a line-feed at the end of the output") - .optional()) + .with_summary("Echo the given message to the screen or, if " + ":redirect-to has " + "been called, to output file specified in the " + "redirect. " + "Variable substitution is performed on the message. " + "Use a " + "backslash to escape any special characters, like '$'") + .with_parameter(help_text("-n", + "Do not print a line-feed at " + "the end of the output") + .optional()) .with_parameter(help_text("msg", "The message to display")) .with_tags({"io", "scripting"}) .with_example({"To output 'Hello, World!'", "Hello, World!"})}, @@ -5691,6 +6870,31 @@ readline_context::command_t STD_COMMANDS[] = { .with_tags({"scripting"}) .with_examples({{"To substitute the table name from a variable", ";SELECT * FROM ${table}"}})}, + + { + "sh", + com_sh, + + help_text(":sh") + .with_summary("Execute the given command-line and display the " + "captured output") + .with_parameter(help_text( + "--name=<name>", "The name to give to the captured output")) + .with_parameter( + help_text("cmdline", "The command-line to execute.")) + .with_tags({"scripting"}), + }, + + { + "cd", + com_cd, + + help_text(":cd") + .with_summary("Change the current directory") + .with_parameter(help_text("dir", "The new current directory")) + .with_tags({"scripting"}), + }, + {"config", com_config, @@ -5701,9 +6905,9 @@ readline_context::command_t STD_COMMANDS[] = { "The value to write. If not given, the " "current value is returned") .optional()) - .with_example( - {"To read the configuration of the '/ui/clock-format' option", - "/ui/clock-format"}) + .with_example({"To read the configuration of the " + "'/ui/clock-format' option", + "/ui/clock-format"}) .with_example({"To set the '/ui/dim-text' option to 'false'", "/ui/dim-text false"}) .with_tags({"configuration"})}, @@ -5725,9 +6929,9 @@ readline_context::command_t STD_COMMANDS[] = { "using a spectrogram") .with_parameter(help_text( "field-name", "The name of the numeric field to visualize.")) - .with_example( - {"To visualize the sc_bytes field in the access_log format", - "sc_bytes"})}, + .with_example({"To visualize the sc_bytes field in the " + "access_log format", + "sc_bytes"})}, {"quit", com_quit, |