diff --git a/NEWS b/NEWS index fb51dede..fd09659f 100644 --- a/NEWS +++ b/NEWS @@ -3,6 +3,10 @@ lnav v0.8.5: Features: * The ":write-*" commands will now accept "/dev/clipboard" as a file name that writes to the system clipboard. + * Added a ":redirect-to " command to redirect command output to the + given file. This command is mostly useful in scripts where one might + want to redirect all output from commands like ":echo" and ":write-to -" + to a single file. Interface Changes: * The auto-complete behavior in the prompt has been modified to fall back diff --git a/docs/source/commands.rst b/docs/source/commands.rst index a676e043..f67bd51c 100644 --- a/docs/source/commands.rst +++ b/docs/source/commands.rst @@ -143,6 +143,10 @@ Output shell command and open the output in lnav. * pipe-line-to - Pipe the top line in the current view to a shell command and open the output in lnav. +* redirect-to [path] - If a path is given, all output from commands, like + ":echo" and when writing to stdout (e.g. :write-to -), will be sent to the + given file. If no path is specified, the current redirect will be cleared + and output will be captured as it was before the redirect was done. .. _misc-cmd: diff --git a/src/command_executor.cc b/src/command_executor.cc index 8032bdbe..abfd5872 100644 --- a/src/command_executor.cc +++ b/src/command_executor.cc @@ -385,6 +385,7 @@ static string execute_file_contents(exec_context &ec, const string &path, bool m pair dir_and_base = split_path(path); ec.ec_path_stack.push_back(dir_and_base.first); + ec.ec_output_stack.emplace_back(nonstd::nullopt); while ((line_size = getline(&line, &line_max_size, file)) != -1) { line_number += 1; @@ -431,6 +432,7 @@ static string execute_file_contents(exec_context &ec, const string &path, bool m } else { fclose(file); } + ec.ec_output_stack.pop_back(); ec.ec_path_stack.pop_back(); return retval; @@ -731,7 +733,26 @@ int sql_callback(exec_context &ec, sqlite3_stmt *stmt) future pipe_callback(exec_context &ec, const string &cmdline, auto_fd &fd) { - if (lnav_data.ld_output_stack.empty()) { + auto out = ec.get_output(); + + if (out) { + FILE *file = *out; + + return std::async(std::launch::async, [&fd, file]() { + char buffer[1024]; + ssize_t rc; + + if (file == stdout) { + lnav_data.ld_stdout_used = true; + } + + while ((rc = read(fd, buffer, sizeof(buffer))) > 0) { + fwrite(buffer, rc, 1, file); + } + + return string(); + }); + } else { auto pp = make_shared(fd, false); static int exec_count = 0; char desc[128]; @@ -755,22 +776,6 @@ future pipe_callback(exec_context &ec, const string &cmdline, auto_fd &f task(); return task.get_future(); - } else { - return std::async(std::launch::async, [&]() { - FILE *file = lnav_data.ld_output_stack.top(); - char buffer[1024]; - ssize_t rc; - - if (file == stdout) { - lnav_data.ld_stdout_used = true; - } - - while ((rc = read(fd, buffer, sizeof(buffer))) > 0) { - fwrite(buffer, rc, 1, file); - } - - return string(); - }); } } diff --git a/src/command_executor.hh b/src/command_executor.hh index cf7a8fa5..96b0e4b9 100644 --- a/src/command_executor.hh +++ b/src/command_executor.hh @@ -35,6 +35,7 @@ #include #include +#include "optional.hpp" #include "auto_fd.hh" #include "attr_line.hh" #include "textview_curses.hh" @@ -47,9 +48,9 @@ typedef std::future (*pipe_callback_t)( exec_context &ec, const std::string &cmdline, auto_fd &fd); struct exec_context { - exec_context(std::vector *line_values = NULL, - sql_callback_t sql_callback = NULL, - pipe_callback_t pipe_callback = NULL) + exec_context(std::vector *line_values = nullptr, + sql_callback_t sql_callback = nullptr, + pipe_callback_t pipe_callback = nullptr) : ec_top_line(vis_line_t(0)), ec_dry_run(false), ec_line_values(line_values), @@ -58,6 +59,7 @@ struct exec_context { this->ec_local_vars.push(std::map()); this->ec_path_stack.emplace_back("."); this->ec_source.emplace("unknown", 0); + this->ec_output_stack.emplace_back(nonstd::nullopt); } std::string get_error_prefix() { @@ -70,6 +72,18 @@ struct exec_context { return "error:" + source.first + ":" + std::to_string(source.second) + ":"; } + nonstd::optional get_output() { + for (auto iter = this->ec_output_stack.rbegin(); + iter != this->ec_output_stack.rend(); + ++iter) { + if (*iter) { + return *iter; + } + } + + return nonstd::nullopt; + } + vis_line_t ec_top_line; bool ec_dry_run; @@ -79,6 +93,7 @@ struct exec_context { std::map ec_global_vars; std::vector ec_path_stack; std::stack> ec_source; + std::vector> ec_output_stack; attr_line_t ec_accumulator; diff --git a/src/lnav.cc b/src/lnav.cc index b546d79f..359f95a2 100644 --- a/src/lnav.cc +++ b/src/lnav.cc @@ -3302,10 +3302,8 @@ int main(int argc, char *argv[]) lnav_data.ld_vtab_manager->register_vtab(new log_format_vtab_impl( *log_format::find_root_format("generic_log"))); - for (std::vector::iterator iter = log_format::get_root_formats().begin(); - iter != log_format::get_root_formats().end(); - ++iter) { - log_vtab_impl *lvi = (*iter)->get_vtab_impl(); + for (auto &iter : log_format::get_root_formats()) { + log_vtab_impl *lvi = iter->get_vtab_impl(); if (lvi != NULL) { lnav_data.ld_vtab_manager->register_vtab(lvi); @@ -3481,10 +3479,8 @@ int main(int argc, char *argv[]) if (lnav_data.ld_flags & LNF_CHECK_CONFIG) { rescan_files(true); - for (auto file_iter = lnav_data.ld_files.begin(); - file_iter != lnav_data.ld_files.end(); - ++file_iter) { - auto lf = (*file_iter); + for (auto &ld_file : lnav_data.ld_files) { + auto lf = ld_file; lf->rebuild_index(); @@ -3496,7 +3492,7 @@ int main(int argc, char *argv[]) retval = EXIT_FAILURE; continue; } - for (logfile::iterator line_iter = lf->begin(); + for (auto line_iter = lf->begin(); line_iter != lf->end(); ++line_iter) { if (!line_iter->is_continued()) { @@ -3587,14 +3583,14 @@ int main(int argc, char *argv[]) log_info("lnav_data:"); log_info(" flags=%x", lnav_data.ld_flags); log_info(" commands:"); - for (std::list::iterator cmd_iter = + for (auto cmd_iter = lnav_data.ld_commands.begin(); cmd_iter != lnav_data.ld_commands.end(); ++cmd_iter) { log_info(" %s", cmd_iter->c_str()); } log_info(" files:"); - for (map::iterator file_iter = + for (auto file_iter = lnav_data.ld_file_names.begin(); file_iter != lnav_data.ld_file_names.end(); ++file_iter) { @@ -3608,7 +3604,7 @@ int main(int argc, char *argv[]) bool found_error = false; init_session(); - lnav_data.ld_output_stack.push(stdout); + lnav_data.ld_exec_context.ec_output_stack.back() = stdout; alerter::singleton().enabled(false); log_tc = &lnav_data.ld_views[LNV_LOG]; diff --git a/src/lnav.hh b/src/lnav.hh index dfa05f8c..fd684d56 100644 --- a/src/lnav.hh +++ b/src/lnav.hh @@ -305,8 +305,6 @@ struct _lnav_data { relative_time ld_last_relative_time; - std::stack ld_output_stack; - std::map > ld_scripts; exec_context ld_exec_context; diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index 3d54ab96..9e876052 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -568,7 +568,9 @@ static string com_save_to(exec_context &ec, string cmdline, vector &args toclose = outfile; } else if (split_args[0] == "-" || split_args[0] == "/dev/stdout") { - if (lnav_data.ld_output_stack.empty()) { + auto ec_out = ec.get_output(); + + if (!ec_out) { outfile = stdout; nodelay(lnav_data.ld_window, 0); endwin(); @@ -583,7 +585,7 @@ static string com_save_to(exec_context &ec, string cmdline, vector &args "----------------\n\n"); } else { - outfile = lnav_data.ld_output_stack.top(); + outfile = *ec_out; } if (outfile == stdout) { lnav_data.ld_stdout_used = true; @@ -673,7 +675,7 @@ static string com_save_to(exec_context &ec, string cmdline, vector &args if ((handle = yajl_gen_alloc(NULL)) == NULL) { if (outfile != stdout) { - fclose(outfile); + closer(outfile); } return "error: unable to allocate memory"; } @@ -945,6 +947,52 @@ static string com_pipe_to(exec_context &ec, string cmdline, vector &args return retval; } +static string com_redirect_to(exec_context &ec, string cmdline, vector &args) +{ + if (args.empty()) { + args.emplace_back("filename"); + return ""; + } + + if (args.size() == 1) { + if (ec.ec_dry_run) { + return "info: redirect will be cleared"; + } + + ec.ec_output_stack.back() = nonstd::nullopt; + return "info: cleared redirect"; + } + + string fn = trim(remaining_args(cmdline, args)); + vector split_args; + shlex lexer(fn); + scoped_resolver scopes = { + &ec.ec_local_vars.top(), + &ec.ec_global_vars, + }; + + if (!lexer.split(split_args, scopes)) { + return "error: unable to parse arguments"; + } + if (split_args.size() > 1) { + return "error: more than one file name was matched"; + } + + if (ec.ec_dry_run) { + return "info: output will be redirected to -- " + split_args[0]; + } + + FILE *file = fopen(split_args[0].c_str(), "w"); + + if (file == nullptr) { + return "error: unable to open file -- " + split_args[0]; + } + + ec.ec_output_stack.back() = file; + + return "info: redirecting output to file -- " + split_args[0]; +} + static string com_highlight(exec_context &ec, string cmdline, vector &args) { string retval = "error: expecting regular expression to highlight"; @@ -2933,14 +2981,15 @@ static string com_echo(exec_context &ec, string cmdline, vector &args) retval = ""; } + auto ec_out = ec.get_output(); if (ec.ec_dry_run) { lnav_data.ld_preview_status_source.get_description() .set_value("The text to output:"); lnav_data.ld_preview_source.replace_with(attr_line_t(retval)); retval = ""; } - else if (!lnav_data.ld_output_stack.empty()) { - FILE *outfile = lnav_data.ld_output_stack.top(); + else if (ec_out) { + FILE *outfile = *ec_out; if (outfile == stdout) { lnav_data.ld_stdout_used = true; @@ -2951,6 +3000,8 @@ static string com_echo(exec_context &ec, string cmdline, vector &args) putc('\n', outfile); } fflush(outfile); + + retval = ""; } } @@ -3838,6 +3889,19 @@ readline_context::command_t STD_COMMANDS[] = { .with_tags({"io"}) .with_example({"sed -e 's/foo/bar/g'"}) }, + { + "redirect-to", + com_redirect_to, + + help_text(":redirect-to") + .with_summary("Redirect the output of commands 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_tags({"io", "scripting"}) + .with_example({"/tmp/script-output.txt"}) + }, { "enable-filter", com_enable_filter, diff --git a/src/readline_callbacks.cc b/src/readline_callbacks.cc index 97a9ef6a..85c74b8c 100644 --- a/src/readline_callbacks.cc +++ b/src/readline_callbacks.cc @@ -484,14 +484,14 @@ void rl_callback(void *dummy, readline_curses *rc) time_t current_time = time(NULL); string path_and_args = rc->get_value(); - lnav_data.ld_output_stack.push(tmpout); + ec.ec_output_stack.back() = tmpout.in(); string result = execute_file(ec, path_and_args); string::size_type lf_index = result.find('\n'); if (lf_index != string::npos) { result = result.substr(0, lf_index); } rc->set_value(result); - lnav_data.ld_output_stack.pop(); + ec.ec_output_stack.back() = nonstd::nullopt; struct stat st; @@ -506,7 +506,7 @@ void rl_callback(void *dummy, readline_curses *rc) lnav_data.ld_file_names[desc] .with_fd(fd_copy) .with_detect_format(false); - lnav_data.ld_files_to_front.push_back(make_pair(desc, 0)); + lnav_data.ld_files_to_front.emplace_back(desc, 0); if (lnav_data.ld_rl_view != NULL) { lnav_data.ld_rl_view->set_alt_value( diff --git a/test/Makefile.am b/test/Makefile.am index fdf81bf3..40262768 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -348,6 +348,8 @@ dist_noinst_DATA = \ formats/jsontest3/format.json \ formats/nestedjson/format.json \ formats/scripts/multiline-echo.lnav \ + formats/scripts/redirecting.lnav \ + formats/scripts/nested-redirecting.lnav \ formats/sqldir/init.sql \ formats/timestamp/format.json \ log-samples/sample-27353a72ba4025448f261dcfa6ea16e474187795.txt \ diff --git a/test/formats/scripts/nested-redirecting.lnav b/test/formats/scripts/nested-redirecting.lnav new file mode 100644 index 00000000..785c6052 --- /dev/null +++ b/test/formats/scripts/nested-redirecting.lnav @@ -0,0 +1,5 @@ +:echo HOWDY! +:redirect-to hw2.txt +:echo HELLO, WORLD! +:redirect-to +:echo GOODBYE, WORLD! diff --git a/test/formats/scripts/redirecting.lnav b/test/formats/scripts/redirecting.lnav new file mode 100644 index 00000000..84b42c2e --- /dev/null +++ b/test/formats/scripts/redirecting.lnav @@ -0,0 +1,6 @@ +:echo Howdy! +:redirect-to hw.txt +:echo Hello, World! +|nested-redirecting +:redirect-to +:echo Goodbye, World! diff --git a/test/test_scripts.sh b/test/test_scripts.sh index 0ee818ab..cbd43e4b 100644 --- a/test/test_scripts.sh +++ b/test/test_scripts.sh @@ -16,3 +16,36 @@ check_output "multiline-echo is not working?" <