From 8776f6a703798c0030f479a3a8baa143915207aa Mon Sep 17 00:00:00 2001 From: Timothy Stack Date: Sun, 26 Mar 2017 06:02:53 -0700 Subject: [PATCH] [sql] do some minimal parsing/annotation of SQL statements Defect Number: Reviewed By: Testing Done: --- NEWS | 2 + src/CMakeLists.txt | 6 + src/Makefile.am | 4 + src/attr_line.hh | 423 ++++++++++++++++++++++++++++++ src/bottom_status_source.hh | 6 +- src/default-log-formats.json | 45 ++-- src/doc_status_source.hh | 70 +++++ src/fs-extension-functions.cc | 30 +-- src/help_text_formatter.cc | 147 +++++++++++ src/help_text_formatter.hh | 156 +++++++++++ src/listview_curses.hh | 9 +- src/lnav.cc | 68 ++++- src/lnav.hh | 6 + src/lnav_commands.cc | 289 +++++++++++++++++++- src/log_data_table.hh | 4 +- src/log_format.hh | 5 + src/log_format_loader.cc | 5 + src/plain_text_source.hh | 37 ++- src/readline_callbacks.cc | 61 +++-- src/readline_curses.cc | 21 +- src/readline_curses.hh | 3 + src/readline_highlighters.cc | 3 +- src/sql_util.cc | 110 ++++++++ src/sql_util.hh | 13 + src/statusview_curses.cc | 9 +- src/statusview_curses.hh | 14 +- src/string-extension-functions.cc | 28 +- src/top_status_source.hh | 3 +- src/view_curses.cc | 100 +++++++ src/view_curses.hh | 261 +----------------- src/vt52_curses.cc | 2 +- test/CMakeLists.txt | 11 + test/Makefile.am | 18 ++ test/drive_listview.cc | 12 +- test/drive_sql_anno.cc | 76 ++++++ test/test_help_text_formatter.cc | 87 ++++++ test/test_sql.sh | 12 + test/test_sql_str_func.sh | 29 ++ test/test_top_status.cc | 2 +- 39 files changed, 1810 insertions(+), 377 deletions(-) create mode 100644 src/attr_line.hh create mode 100644 src/doc_status_source.hh create mode 100644 src/help_text_formatter.cc create mode 100644 src/help_text_formatter.hh create mode 100644 test/drive_sql_anno.cc create mode 100644 test/test_help_text_formatter.cc diff --git a/NEWS b/NEWS index f1c76248..663e8e5a 100644 --- a/NEWS +++ b/NEWS @@ -37,6 +37,8 @@ lnav v0.8.2: string. Interface Changes: + * Command documentation is now displayed in a section at the bottom of + the screen when a command is being entered. * The color used for text colored via ":highlight" is now based on the the regex instead of randomly picked so that colors are consistent across invocations. diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bb67ff1d..adc0d47b 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -13,6 +13,7 @@ set(diag_STAT_SRCS file_vtab.cc fs-extension-functions.cc grep_proc.cc + help_text_formatter.cc hist_source.cc hotkeys.cc intern_string.cc @@ -52,11 +53,13 @@ set(diag_STAT_SRCS sysclip.cc pcrepp.cc piper_proc.cc + ptimec.cc sql_util.cc state-extension-functions.cc strnatcmp.c text_format.cc textview_curses.cc + time-extension-functions.cc timer.cc view_curses.cc views_vtab.cc @@ -83,6 +86,7 @@ set(diag_STAT_SRCS spookyhash/SpookyV2.cpp all_logs_vtab.hh + attr_line.hh auto_fd.hh auto_mem.hh auto_pid.hh @@ -93,6 +97,7 @@ set(diag_STAT_SRCS concise_index.hh column_namer.hh curl_looper.hh + doc_status_source.hh elem_to_json.hh field_overlay_source.hh file_vtab.hh @@ -100,6 +105,7 @@ set(diag_STAT_SRCS format-text-files.hh grep_highlighter.hh help.hh + help_text_formatter.hh hotkeys.hh init-sql.hh intern_string.hh diff --git a/src/Makefile.am b/src/Makefile.am index 44d04303..14111960 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -120,6 +120,7 @@ dist_noinst_DATA = \ noinst_HEADERS = \ all_logs_vtab.hh \ ansi_scrubber.hh \ + attr_line.hh \ auto_fd.hh \ auto_mem.hh \ auto_pid.hh \ @@ -136,6 +137,7 @@ noinst_HEADERS = \ data_parser.hh \ default-log-formats-json.hh \ db_sub_source.hh \ + doc_status_source.hh \ elem_to_json.hh \ environ_vtab.hh \ field_overlay_source.hh \ @@ -146,6 +148,7 @@ noinst_HEADERS = \ grep_proc.hh \ help.hh \ help.txt \ + help_text_formatter.hh \ hist_source.hh \ hotkeys.hh \ init.sql \ @@ -252,6 +255,7 @@ libdiag_a_SOURCES = \ file_vtab.cc \ fs-extension-functions.cc \ grep_proc.cc \ + help_text_formatter.cc \ hist_source.cc \ hotkeys.cc \ intern_string.cc \ diff --git a/src/attr_line.hh b/src/attr_line.hh new file mode 100644 index 00000000..1d854d5f --- /dev/null +++ b/src/attr_line.hh @@ -0,0 +1,423 @@ +/** + * Copyright (c) 2017, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + * + * @file attr_line.hh + */ + +#ifndef __attr_line_hh +#define __attr_line_hh + +#include +#include + +/** + * Encapsulates a range in a string. + */ +struct line_range { + int lr_start; + int lr_end; + + explicit line_range(int start = -1, int end = -1) : lr_start(start), lr_end(end) { }; + + bool is_valid() const { + return this->lr_start != -1; + } + + int length() const + { + return this->lr_end == -1 ? INT_MAX : this->lr_end - this->lr_start; + }; + + bool contains(int pos) const { + return this->lr_start <= pos && pos < this->lr_end; + }; + + bool contains(const struct line_range &other) const { + return this->contains(other.lr_start) && other.lr_end <= this->lr_end; + }; + + bool intersects(const struct line_range &other) const { + return this->contains(other.lr_start) || this->contains(other.lr_end); + }; + + line_range intersection(const struct line_range &other) const { + return line_range{std::max(this->lr_start, other.lr_start), + std::min(this->lr_end, other.lr_end)}; + }; + + line_range &shift(int32_t start, int32_t amount) { + if (this->lr_start >= start) { + this->lr_start = std::max(0, this->lr_start + amount); + } + if (this->lr_end != -1 && start < this->lr_end) { + this->lr_end += amount; + if (this->lr_end < this->lr_start) { + this->lr_end = this->lr_start; + } + } + + return *this; + }; + + void ltrim(const char *str) { + while (this->lr_start < this->lr_end && isspace(str[this->lr_start])) { + this->lr_start += 1; + } + }; + + bool operator<(const struct line_range &rhs) const + { + if (this->lr_start < rhs.lr_start) { return true; } + else if (this->lr_start > rhs.lr_start) { return false; } + + if (this->lr_end == rhs.lr_end) { return false; } + + if (this->lr_end < rhs.lr_end) { return true; } + return false; + }; + + bool operator==(const struct line_range &rhs) const { + return (this->lr_start == rhs.lr_start && this->lr_end == rhs.lr_end); + }; + + const char *substr(const std::string &str) const { + if (this->lr_start == -1) { + return str.c_str(); + } + return &(str.c_str()[this->lr_start]); + } + + size_t sublen(const std::string &str) const { + if (this->lr_start == -1) { + return str.length(); + } + if (this->lr_end == -1) { + return str.length() - this->lr_start; + } + return this->length(); + } +}; + +/** + * Container for attribute values for a substring. + */ +typedef union { + void *sav_ptr; + int64_t sav_int; +} string_attr_value_t; + +class string_attr_type { +public: + string_attr_type(const char *name = nullptr) + : sat_name(name) { + }; + + const char *sat_name; +}; +typedef string_attr_type *string_attr_type_t; + +struct string_attr { + string_attr(const struct line_range &lr, string_attr_type_t type, void *val) + : sa_range(lr), sa_type(type) { + this->sa_value.sav_ptr = val; + }; + + string_attr(const struct line_range &lr, string_attr_type_t type, int64_t val = 0) + : sa_range(lr), sa_type(type) { + this->sa_value.sav_int = val; + }; + + string_attr(const struct line_range &lr, string_attr_type_t type, string_attr_value_t val) + : sa_range(lr), sa_type(type), sa_value(val) { + }; + + string_attr() : sa_type(NULL) { }; + + bool operator<(const struct string_attr &rhs) const + { + return this->sa_range < rhs.sa_range; + }; + + struct line_range sa_range; + string_attr_type_t sa_type; + string_attr_value_t sa_value; +}; + +/** A map of line ranges to attributes for that range. */ +typedef std::vector string_attrs_t; + +inline string_attrs_t::const_iterator +find_string_attr(const string_attrs_t &sa, string_attr_type_t type, int start = 0) +{ + string_attrs_t::const_iterator iter; + + for (iter = sa.begin(); iter != sa.end(); ++iter) { + if (iter->sa_type == type && iter->sa_range.lr_start >= start) { + break; + } + } + + return iter; +} + +inline string_attrs_t::iterator +find_string_attr(string_attrs_t &sa, const struct line_range &lr) +{ + string_attrs_t::iterator iter; + + for (iter = sa.begin(); iter != sa.end(); ++iter) { + if (lr.contains(iter->sa_range)) { + break; + } + } + + return iter; +} + +inline string_attrs_t::const_iterator +find_string_attr(const string_attrs_t &sa, size_t near) +{ + string_attrs_t::const_iterator iter, nearest = sa.end(); + ssize_t last_diff = INT_MAX; + + for (iter = sa.begin(); iter != sa.end(); ++iter) { + auto &lr = iter->sa_range; + + if (!lr.is_valid() || !lr.contains(near)) { + continue; + } + + ssize_t diff = near - lr.lr_start; + if (diff < last_diff) { + last_diff = diff; + nearest = iter; + } + } + + return nearest; +} + +inline struct line_range +find_string_attr_range(const string_attrs_t &sa, string_attr_type_t type) +{ + string_attrs_t::const_iterator iter = find_string_attr(sa, type); + + if (iter != sa.end()) { + return iter->sa_range; + } + + return line_range(); +} + +inline void remove_string_attr(string_attrs_t &sa, const struct line_range &lr) +{ + string_attrs_t::iterator iter; + + while ((iter = find_string_attr(sa, lr)) != sa.end()) { + sa.erase(iter); + } +} + +inline void remove_string_attr(string_attrs_t &sa, string_attr_type_t type) +{ + string_attrs_t::iterator iter; + + for (auto iter = sa.begin(); iter != sa.end();) { + if (iter->sa_type == type) { + iter = sa.erase(iter); + } else { + ++iter; + } + } +} + +inline void shift_string_attrs(string_attrs_t &sa, int32_t start, int32_t amount) +{ + for (string_attrs_t::iterator iter = sa.begin(); iter != sa.end(); ++iter) { + iter->sa_range.shift(start, amount); + } +} + +struct text_wrap_settings { + text_wrap_settings() : tws_indent(2), tws_width(80) {}; + + text_wrap_settings &with_indent(int indent) { + this->tws_indent = indent; + return *this; + }; + + text_wrap_settings &with_width(int width) { + this->tws_width = width; + return *this; + }; + + int tws_indent; + int tws_width; +}; + +/** + * A line that has attributes. + */ +class attr_line_t { +public: + attr_line_t() { + this->al_attrs.reserve(RESERVE_SIZE); + }; + + attr_line_t(const std::string &str) : al_string(str) { + this->al_attrs.reserve(RESERVE_SIZE); + }; + + attr_line_t(const char *str) : al_string(str) { + this->al_attrs.reserve(RESERVE_SIZE); + }; + + static inline attr_line_t from_ansi_str(const char *str) { + attr_line_t retval; + + return retval.with_ansi_string(str); + }; + + /** @return The string itself. */ + std::string &get_string() { return this->al_string; }; + + /** @return The attributes for the string. */ + string_attrs_t &get_attrs() { return this->al_attrs; }; + + attr_line_t &with_string(const std::string &str) { + this->al_string = str; + return *this; + } + + attr_line_t &with_ansi_string(const char *str, ...); + + attr_line_t &with_attr(const string_attr &sa) { + this->al_attrs.push_back(sa); + return *this; + }; + + template + attr_line_t &append(const char *str, + string_attr_type_t type = nullptr, + T val = T()) { + size_t start_len = this->al_string.length(); + + this->al_string.append(str); + if (type != nullptr) { + line_range lr{(int) start_len, (int) this->al_string.length()}; + + this->al_attrs.emplace_back(lr, type, val); + } + return *this; + }; + + attr_line_t &append(const attr_line_t &al, text_wrap_settings *tws = nullptr); + + attr_line_t &append(size_t len, char c) { + this->al_string.append(len, c); + return *this; + }; + + attr_line_t &insert(size_t index, size_t len, char c) { + this->al_string.insert(index, len, c); + + shift_string_attrs(this->al_attrs, index, len); + + return *this; + } + + attr_line_t &insert(size_t index, const char *str) { + this->al_string.insert(index, str); + + shift_string_attrs(this->al_attrs, index, strlen(str)); + + return *this; + } + + attr_line_t &right_justify(unsigned long width) { + long padding = width - this->length(); + if (padding > 0) { + this->al_string.insert(0, padding, ' '); + for (std::vector::iterator iter = this->al_attrs.begin(); + iter != this->al_attrs.end(); + ++iter) { + iter->sa_range.lr_start += padding; + iter->sa_range.lr_end += padding; + } + } + + return *this; + } + + ssize_t length() const { + size_t retval = this->al_string.length(); + + for (std::vector::const_iterator iter = this->al_attrs.begin(); + iter != this->al_attrs.end(); + ++iter) { + retval = std::max(retval, (size_t) iter->sa_range.lr_start); + if (iter->sa_range.lr_end != -1) { + retval = std::max(retval, (size_t) iter->sa_range.lr_end); + } + } + + return retval; + }; + + std::string get_substring(const line_range &lr) const { + if (!lr.is_valid()) { + return ""; + } + return this->al_string.substr(lr.lr_start, lr.length()); + }; + + bool empty() const { + return this->length() == 0; + }; + + /** Clear the string and the attributes for the string. */ + attr_line_t &clear() + { + this->al_string.clear(); + this->al_attrs.clear(); + + return *this; + }; + + attr_line_t subline(size_t start, size_t len = std::string::npos) const; + + void split_lines(std::vector &lines) const; + +private: + const static size_t RESERVE_SIZE = 128; + + std::string al_string; + string_attrs_t al_attrs; +}; + +#endif diff --git a/src/bottom_status_source.hh b/src/bottom_status_source.hh index e36db845..bc732a39 100644 --- a/src/bottom_status_source.hh +++ b/src/bottom_status_source.hh @@ -78,8 +78,10 @@ public: this->bss_fields[BSF_HELP].set_width(14); this->bss_fields[BSF_HELP].set_value("?:View Help"); this->bss_fields[BSF_HELP].right_justify(true); + this->bss_prompt.set_left_pad(1); this->bss_prompt.set_min_width(35); this->bss_prompt.set_share(1); + this->bss_error.set_left_pad(1); this->bss_error.set_min_width(35); this->bss_error.set_share(1); }; @@ -134,10 +136,10 @@ public: status_field &sf = this->bss_fields[BSF_LINE_NUMBER]; if (lc->get_inner_height() == 0) { - sf.set_value("L0"); + sf.set_value(" L0"); } else { - sf.set_value("L%'d", (int)lc->get_top()); + sf.set_value(" L%'d", (int)lc->get_top()); } }; diff --git a/src/default-log-formats.json b/src/default-log-formats.json index debd10ae..c1a8725a 100644 --- a/src/default-log-formats.json +++ b/src/default-log-formats.json @@ -28,42 +28,52 @@ "c_ip" : { "kind" : "string", "collate" : "ipaddress", - "identifier" : true + "identifier" : true, + "description" : "The client IP address" }, "cs_username" : { "kind" : "string", - "identifier" : true + "identifier" : true, + "description" : "The username passed from the client to the server" }, "cs_method" : { "kind" : "string", - "identifier" : true + "identifier" : true, + "description" : "The request method" }, "cs_uri_stem" : { "kind" : "string", - "identifier" : true + "identifier" : true, + "description" : "The path part of the request URI" }, "cs_uri_query" : { - "kind" : "string" + "kind" : "string", + "description" : "The query parameters in the request URI" }, "cs_version" : { "kind" : "string", - "identifier" : true + "identifier" : true, + "description" : "The client's HTTP version" }, "sc_status" : { "kind" : "integer", "foreign-key" : true, - "rewriter" : ";SELECT :sc_status || ' (' || (SELECT message FROM http_status_codes WHERE status = :sc_status) || ') '" + "rewriter" : ";SELECT :sc_status || ' (' || (SELECT message FROM http_status_codes WHERE status = :sc_status) || ') '", + "description" : "The status code returned by the server" }, "sc_bytes" : { - "kind" : "integer" + "kind" : "integer", + "description" : "The number of bytes returned by the server" }, "cs_referer" : { "kind" : "string", - "identifier" : true + "identifier" : true, + "description" : "The client's referrer" }, "cs_user_agent" : { "kind" : "string", - "identifier" : true + "identifier" : true, + "description" : "The client's HTTP agent" } }, "sample" : [ @@ -96,11 +106,13 @@ "pid" : { "kind" : "integer", "identifier" : true, - "foreign-key" : true + "foreign-key" : true, + "description" : "The ID of the process that generated the message" }, "module" : { "kind" : "string", - "identifier" : true + "identifier" : true, + "description" : "The name of the module that generated the message" } }, "sample" : [ @@ -972,16 +984,19 @@ "log_hostname" : { "kind" : "string", "collate" : "ipaddress", - "identifier" : true + "identifier" : true, + "description" : "The name of the host that generated the message" }, "log_procname" : { "kind" : "string", - "identifier" : true + "identifier" : true, + "description" : "The name of the process that generated the message" }, "log_pid" : { "kind" : "string", "identifier" : true, - "action-list" : ["dump_pid"] + "action-list" : ["dump_pid"], + "description" : "The ID of the process that generated the message" } }, "action" : { diff --git a/src/doc_status_source.hh b/src/doc_status_source.hh new file mode 100644 index 00000000..41c79eb4 --- /dev/null +++ b/src/doc_status_source.hh @@ -0,0 +1,70 @@ +/** + * Copyright (c) 2017, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef _doc_status_source_hh +#define _doc_status_source_hh + +#include + +#include "lnav_config.hh" +#include "logfile_sub_source.hh" +#include "statusview_curses.hh" + +class doc_status_source + : public status_data_source { +public: + typedef enum { + TSF_TITLE, + TSF_STITCH_TITLE, + + TSF__MAX + } field_t; + + doc_status_source() + { + this->tss_fields[TSF_TITLE].set_width(14); + this->tss_fields[TSF_TITLE].set_role(view_colors::VCR_VIEW_STATUS); + this->tss_fields[TSF_TITLE].set_value(" Command Help "); + this->tss_fields[TSF_STITCH_TITLE].set_width(2); + this->tss_fields[TSF_STITCH_TITLE].set_stitch_value( + view_colors::ansi_color_pair_index(COLOR_BLUE, COLOR_WHITE)); + }; + + size_t statusview_fields(void) { return TSF__MAX; }; + + status_field &statusview_value_for_field(int field) + { + return this->tss_fields[field]; + }; + +private: + status_field tss_fields[TSF__MAX]; +}; + +#endif diff --git a/src/fs-extension-functions.cc b/src/fs-extension-functions.cc index 6a3dc95f..f953950d 100644 --- a/src/fs-extension-functions.cc +++ b/src/fs-extension-functions.cc @@ -76,19 +76,12 @@ sql_basename(const char *path_in) } } -static void sql_dirname(sqlite3_context *context, - int argc, sqlite3_value **argv) +static +util::variant +sql_dirname(const char *path_in) { - const char *path_in; ssize_t text_end; - if (sqlite3_value_type(argv[0]) == SQLITE_NULL) { - sqlite3_result_null(context); - return; - } - - path_in = (const char *)sqlite3_value_text(argv[0]); - text_end = strlen(path_in) - 1; while (text_end >= 0 && (path_in[text_end] == '/' || path_in[text_end] == '\\')) { @@ -97,18 +90,13 @@ static void sql_dirname(sqlite3_context *context, while (text_end >= 0) { if (path_in[text_end] == '/' || path_in[text_end] == '\\') { - sqlite3_result_text(context, - path_in, (int) (text_end == 0 ? 1 : text_end), - SQLITE_TRANSIENT); - return; + return string_fragment(path_in, 0, text_end == 0 ? 1 : text_end); } text_end -= 1; } - sqlite3_result_text(context, - path_in[0] == '/' ? "/" : ".", 1, - SQLITE_STATIC); + return path_in[0] == '/' ? "/" : "."; } static void sql_joinpath(sqlite3_context *context, @@ -160,7 +148,13 @@ int fs_extension_functions(const struct FuncDef **basic_funcs, {"path", "The path"}, }), - { "dirname", 1, 0, SQLITE_UTF8, 0, sql_dirname }, + sqlite_func_adapter::builder( + "dirname", + "Extract the directory portion of a pathname", + { + {"path", "The path"}, + }), + { "joinpath", -1, 0, SQLITE_UTF8, 0, sql_joinpath }, /* diff --git a/src/help_text_formatter.cc b/src/help_text_formatter.cc new file mode 100644 index 00000000..15518808 --- /dev/null +++ b/src/help_text_formatter.cc @@ -0,0 +1,147 @@ +/** + * Copyright (c) 2017, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#include "config.h" + +#include +#include + +#include "ansi_scrubber.hh" +#include "help_text_formatter.hh" +#include "readline_highlighters.hh" + +using namespace std; + + +void format_help_text_for_term(const help_text &ht, int width, attr_line_t &out) +{ + static size_t body_indent = 2; + + text_wrap_settings tws; + + tws.with_width(width); + + switch (ht.ht_context) { + case HC_COMMAND: { + out.append("Synopsis", &view_curses::VC_STYLE, A_UNDERLINE) + .append("\n") + .append(body_indent, ' ') + .append(":") + .append(ht.ht_name, &view_curses::VC_STYLE, A_BOLD); + for (auto ¶m : ht.ht_parameters) { + out.append(" "); + out.append(param.ht_name, &view_curses::VC_STYLE, A_UNDERLINE); + if (param.ht_nargs == HN_ONE_OR_MORE) { + out.append("1", &view_curses::VC_STYLE, A_UNDERLINE); + out.append(" ["); + out.append("...", &view_curses::VC_STYLE, A_UNDERLINE); + out.append(" "); + out.append(param.ht_name, &view_curses::VC_STYLE, A_UNDERLINE); + out.append("N", &view_curses::VC_STYLE, A_UNDERLINE); + out.append("]"); + } + } + out.append(" - ") + .append(attr_line_t::from_ansi_str(ht.ht_summary), + &tws.with_indent(body_indent + 4)) + .append("\n"); + break; + } + case HC_SQL_FUNCTION: { + bool needs_comma = false; + + out.append(ht.ht_name, &view_curses::VC_STYLE, A_BOLD); + out.append("("); + for (auto ¶m : ht.ht_parameters) { + if (needs_comma) { + out.append(", "); + } + out.append(param.ht_name, &view_curses::VC_STYLE, A_UNDERLINE); + needs_comma = true; + } + out.append(") -- ") + .append(attr_line_t::from_ansi_str(ht.ht_summary), &tws) + .append("\n"); + break; + } + } + + if (!ht.ht_parameters.empty()) { + size_t max_param_name_width = 0; + + for (auto ¶m : ht.ht_parameters) { + max_param_name_width = std::max(strlen(param.ht_name), max_param_name_width); + } + + out.append(ht.ht_parameters.size() == 1 ? "Parameter" : "Parameters", + &view_curses::VC_STYLE, + A_UNDERLINE) + .append("\n"); + + for (auto ¶m : ht.ht_parameters) { + out.append(body_indent, ' ') + .append(param.ht_name, + &view_curses::VC_STYLE, + view_colors::ansi_color_pair(COLOR_CYAN, COLOR_BLACK) | A_BOLD) + .append(max_param_name_width - strlen(param.ht_name), ' ') + .append(" ") + .append(attr_line_t::from_ansi_str(param.ht_summary), + &(tws.with_indent(2 + max_param_name_width + 3))) + .append("\n"); + } + } + + if (!ht.ht_example.empty()) { + map vars; + + vars["name"] = ht.ht_name; + add_ansi_vars(vars); + + out.append(ht.ht_example.size() == 1 ? "Example" : "Examples", + &view_curses::VC_STYLE, + A_UNDERLINE) + .append("\n"); + for (auto &ex : ht.ht_example) { + attr_line_t ex_line(ex.he_cmd); + + switch (ht.ht_context) { + case HC_COMMAND: + ex_line.insert(0, 1, ' '); + ex_line.insert(0, 1, ':'); + ex_line.insert(1, ht.ht_name); + readline_command_highlighter(ex_line, 0); + break; + } + + out.append(body_indent, ' ') + .append(ex_line, &tws.with_indent(body_indent + 2)); + out.append("\n"); + } + } +} diff --git a/src/help_text_formatter.hh b/src/help_text_formatter.hh new file mode 100644 index 00000000..cbad96cd --- /dev/null +++ b/src/help_text_formatter.hh @@ -0,0 +1,156 @@ +/** + * Copyright (c) 2017, Timothy Stack + * + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * * Neither the name of Timothy Stack nor the names of its contributors + * may be used to endorse or promote products derived from this software + * without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY + * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED + * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY + * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES + * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON + * ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +#ifndef LNAV_HELP_TEXT_FORMATTER_HH +#define LNAV_HELP_TEXT_FORMATTER_HH + +#include +#include + +#include "attr_line.hh" + +enum help_context_t { + HC_NONE, + HC_PARAMETER, + HC_COMMAND, + HC_SQL_FUNCTION, +}; + +enum help_nargs_t { + HN_REQUIRED, + HN_OPTIONAL, + HN_ZERO_OR_MORE, + HN_ONE_OR_MORE, +}; + +enum help_parameter_format_t { + HPF_STRING, + HPF_REGEX, + HPF_INTEGER, + HPF_NUMBER, + HPF_DATETIME, + HPF_ENUM, +}; + +struct help_example { + const char *he_cmd; + const char *he_result; +}; + +struct help_text { + help_context_t ht_context; + const char *ht_name; + const char *ht_summary; + const char *ht_description; + std::vector ht_parameters; + std::vector ht_example; + help_nargs_t ht_nargs; + help_parameter_format_t ht_format; + std::vector ht_enum_values; + + help_text() : ht_context(HC_NONE) { + + }; + + help_text(const char *name, const char *summary = nullptr) + : ht_context(HC_NONE), + ht_name(name), + ht_summary(summary), + ht_description(nullptr), + ht_nargs(HN_REQUIRED), + ht_format(HPF_STRING) { + if (name[0] == ':') { + this->ht_context = HC_COMMAND; + this->ht_name = &name[1]; + } + } + + help_text &command() { + this->ht_context = HC_COMMAND; + return *this; + }; + + help_text &with_summary(const char *summary) { + this->ht_summary = summary; + return *this; + }; + + help_text &with_parameters(const std::initializer_list ¶ms) { + this->ht_parameters = params; + for (auto ¶m : this->ht_parameters) { + param.ht_context = HC_PARAMETER; + } + return *this; + } + + help_text &with_parameter(const help_text &ht) { + this->ht_parameters.emplace_back(ht); + this->ht_parameters.back().ht_context = HC_PARAMETER; + return *this; + }; + + help_text &with_examples(const std::initializer_list &examples) { + this->ht_example = examples; + return *this; + } + + help_text &with_example(const help_example &example) { + this->ht_example.emplace_back(example); + return *this; + } + + help_text &optional() { + this->ht_nargs = HN_OPTIONAL; + return *this; + }; + + help_text &zero_or_more() { + this->ht_nargs = HN_ZERO_OR_MORE; + return *this; + }; + + help_text &one_or_more() { + this->ht_nargs = HN_ONE_OR_MORE; + return *this; + }; + + help_text &with_format(help_parameter_format_t format) { + this->ht_format = format; + return *this; + } + + help_text &with_enum_values(const std::initializer_list &enum_values) { + this->ht_enum_values = enum_values; + return *this; + }; +}; + +void format_help_text_for_term(const help_text &ht, int width, attr_line_t &out); + +#endif //LNAV_HELP_TEXT_FORMATTER_HH diff --git a/src/listview_curses.hh b/src/listview_curses.hh index 9197505d..36016928 100644 --- a/src/listview_curses.hh +++ b/src/listview_curses.hh @@ -197,7 +197,12 @@ public: void set_show_scrollbar(bool ss) { this->lv_show_scrollbar = ss; }; bool get_show_scrollbar() const { return this->lv_show_scrollbar; }; - void set_show_bottom_border(bool val) { this->lv_show_bottom_border = val; }; + void set_show_bottom_border(bool val) { + if (this->lv_show_bottom_border != val) { + this->lv_show_bottom_border = val; + this->set_needs_update(); + } + }; bool get_show_bottom_border() const { return this->lv_show_bottom_border; }; listview_curses &set_word_wrap(bool ww) { @@ -459,7 +464,7 @@ public: } else { getmaxyx(this->lv_window, height, width_out); - if (this->lv_height < 1) { + if (this->lv_height < 0) { height_out = vis_line_t(height) + this->lv_height - vis_line_t(this->lv_y); diff --git a/src/lnav.cc b/src/lnav.cc index 0cc615d9..5b3d1f3a 100644 --- a/src/lnav.cc +++ b/src/lnav.cc @@ -407,8 +407,9 @@ private: void do_update(void) { lnav_data.ld_top_source.update_time(); - lnav_data.ld_status[LNS_TOP].do_update(); - lnav_data.ld_status[LNS_BOTTOM].do_update(); + for (auto &sc : lnav_data.ld_status) { + sc.do_update(); + } refresh(); }; @@ -1916,17 +1917,19 @@ static void looper(void) textview_curses::action(update_hits)); } + lnav_data.ld_doc_view.set_window(lnav_data.ld_window); + lnav_data.ld_status[LNS_TOP].set_top(0); lnav_data.ld_status[LNS_BOTTOM].set_top(-(rlc.get_height() + 1)); - for (lpc = 0; lpc < LNS__MAX; lpc++) { - lnav_data.ld_status[lpc].set_window(lnav_data.ld_window); + for (auto &sc : lnav_data.ld_status) { + sc.set_window(lnav_data.ld_window); } lnav_data.ld_status[LNS_TOP].set_data_source( &lnav_data.ld_top_source); lnav_data.ld_status[LNS_BOTTOM].set_data_source( &lnav_data.ld_bottom_source); - - lnav_data.ld_match_view.set_show_bottom_border(true); + lnav_data.ld_status[LNS_DOC].set_data_source( + &lnav_data.ld_doc_status_source); vsb.push_back(sb.get_functor()); @@ -1939,8 +1942,11 @@ static void looper(void) sb.push_back(&lnav_data.ld_bottom_source.marks_wire); sb.push_back(&lnav_data.ld_term_extra.filename_wire); - lnav_data.ld_status[0].window_change(); - lnav_data.ld_status[1].window_change(); + lnav_data.ld_match_view.set_show_bottom_border(true); + + for (auto &sc : lnav_data.ld_status) { + sc.window_change(); + } execute_file(ec, dotlnav_path("session")); @@ -1963,12 +1969,45 @@ static void looper(void) rebuild_indexes(true); } - lnav_data.ld_status[LNS_TOP].do_update(); + { + unsigned long width, height; + + getmaxyx(lnav_data.ld_window, height, width); + bool doc_open = lnav_data.ld_doc_view.get_height() > 0; + int bottom_height = + (doc_open ? 1 : 0) + + lnav_data.ld_doc_view.get_height() + + lnav_data.ld_match_view.get_height() + + 1 + + lnav_data.ld_rl_view->get_height(); + + for (int lpc = 0; lpc < LNV__MAX; lpc++) { + textview_curses &tc = lnav_data.ld_views[lpc]; + + tc.set_height(vis_line_t(-bottom_height)); + } + lnav_data.ld_status[LNS_BOTTOM].set_top( + -(lnav_data.ld_match_view.get_height() + 2)); + lnav_data.ld_status[LNS_DOC].set_top(height - bottom_height); + lnav_data.ld_status[LNS_DOC].set_enabled(doc_open); + + lnav_data.ld_doc_view.set_y(height - bottom_height + 1); + lnav_data.ld_match_view.set_y( + height + - bottom_height + + (doc_open ? 1 : 0) + + 1 + + lnav_data.ld_doc_view.get_height()); + } + if (!lnav_data.ld_view_stack.empty()) { lnav_data.ld_view_stack.back()->do_update(); } + lnav_data.ld_doc_view.do_update(); lnav_data.ld_match_view.do_update(); - lnav_data.ld_status[LNS_BOTTOM].do_update(); + for (auto &sc : lnav_data.ld_status) { + sc.do_update(); + } rlc.do_update(); refresh(); @@ -2187,11 +2226,14 @@ static void looper(void) resizeterm(size.ws_row, size.ws_col); } rlc.window_change(); - lnav_data.ld_status[0].window_change(); - lnav_data.ld_status[1].window_change(); + for (auto &sc : lnav_data.ld_status) { + sc.window_change(); + } if (!lnav_data.ld_view_stack.empty()) { lnav_data.ld_view_stack.back()->set_needs_update(); } + lnav_data.ld_doc_view.set_needs_update(); + lnav_data.ld_match_view.set_needs_update(); } if (lnav_data.ld_child_terminated) { @@ -2934,6 +2976,8 @@ int main(int argc, char *argv[]) .add_input_delegate(lnav_data.ld_spectro_source) .set_tail_space(vis_line_t(2)); + lnav_data.ld_doc_view.set_sub_source(&lnav_data.ld_doc_source) + .set_left(0); lnav_data.ld_match_view.set_left(0); for (lpc = 0; lpc < LNV__MAX; lpc++) { diff --git a/src/lnav.hh b/src/lnav.hh index b8359617..857eb37e 100644 --- a/src/lnav.hh +++ b/src/lnav.hh @@ -49,6 +49,7 @@ #include "listview_curses.hh" #include "top_status_source.hh" #include "bottom_status_source.hh" +#include "doc_status_source.hh" #include "grep_highlighter.hh" #include "db_sub_source.hh" #include "textfile_sub_source.hh" @@ -64,6 +65,7 @@ #include "log_format_loader.hh" #include "spectro_source.hh" #include "command_executor.hh" +#include "plain_text_source.hh" /** The command modes that are available while viewing a file. */ typedef enum { @@ -133,6 +135,7 @@ extern const char *lnav_zoom_strings[]; typedef enum { LNS_TOP, LNS_BOTTOM, + LNS_DOC, LNS__MAX } lnav_status_t; @@ -241,6 +244,7 @@ struct _lnav_data { statusview_curses ld_status[LNS__MAX]; top_status_source ld_top_source; bottom_status_source ld_bottom_source; + doc_status_source ld_doc_status_source; listview_curses::action::broadcaster ld_scroll_broadcaster; listview_curses::action::broadcaster ld_view_stack_broadcaster; @@ -249,6 +253,8 @@ struct _lnav_data { time_t ld_bottom_time; int ld_bottom_time_millis; + plain_text_source ld_doc_source; + textview_curses ld_doc_view; textview_curses ld_match_view; std::vector ld_view_stack; diff --git a/src/lnav_commands.cc b/src/lnav_commands.cc index 9af99b94..3f62968b 100644 --- a/src/lnav_commands.cc +++ b/src/lnav_commands.cc @@ -2856,7 +2856,13 @@ readline_context::command_t STD_COMMANDS[] = { "adjust-log-time", "", "Change the timestamps of the top file to be relative to the given date", - com_adjust_log_time + com_adjust_log_time, + + help_text(":adjust-log-time") + .with_summary("Change the timestamps of the top file to be relative to the given date") + .with_parameter(help_text("timestamp", "The new timestamp for the top line in the view") + .with_format(HPF_DATETIME)) + .with_example({"2017-01-02T05:33:00", ""}) }, { @@ -2864,18 +2870,37 @@ readline_context::command_t STD_COMMANDS[] = { "", "Convert epoch time to a human-readable form", com_unix_time, + + help_text(":unix-time") + .with_summary("Convert epoch time to a human-readable form") + .with_parameter(help_text("seconds", "The epoch timestamp to convert") + .with_format(HPF_INTEGER)) + .with_example({"1490191111", "Wed Mar 22 06:58:31 2017 -0700 PDT -- 1490191111"}) }, { "current-time", NULL, "Print the current time in human-readable form and seconds since the epoch", com_current_time, + + help_text(":current-time") + .with_summary("Print the current time in human-readable form and seconds since the epoch") }, { "goto", "", "Go to the given line number, N percent into the file, or the given timestamp in the log view", com_goto, + + help_text(":goto") + .with_summary("Go to the given location in the top view") + .with_parameter(help_text("line#|N%|date", "A line number, percent into the file, or a timestamp")) + .with_examples( + { + {"22"}, + {"75%"}, + {"2017-01-01"} + }) }, { "relative-goto", @@ -2888,180 +2913,336 @@ readline_context::command_t STD_COMMANDS[] = { NULL, "Toggle the bookmark state for the top line in the current view", com_mark, + + help_text(":mark") + .with_summary("Toggle the bookmark state for the top line in the current view") }, { "next-mark", "error|warning|search|user|file|partition", "Move to the next bookmark of the given type in the current view", com_goto_mark, + + help_text(":next-mark") + .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, partition")) + .with_example({"error"}) }, { "prev-mark", "error|warning|search|user|file|partition", "Move to the previous bookmark of the given type in the current view", com_goto_mark, + + help_text(":prev-mark") + .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, search, user, file, partition")) + .with_example({"error"}) }, { "help", NULL, "Open the help text view", com_help, + + help_text(":help") + .with_summary("Open the help text view") }, { "hide-fields", " [ ... ]", "Hide log message fields by replacing them with an ellipsis", com_toggle_field, + + help_text(":hide-fields") + .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") + .one_or_more()) + .with_example({"log_procname"}) }, { "show-fields", " [ ... ]", "Show log message fields that were previously hidden", com_toggle_field, + + help_text(":show-fields") + .with_summary("Show log message fields that were previously hidden") + .with_parameter(help_text("field-name", "The name of the field to show") + .one_or_more()) + .with_example({"log_procname"}) }, { "hide-lines-before", "", "Hide lines that come before the given line number or date", com_hide_line, + + help_text(":hide-lines-before") + .with_summary("Hide lines that come before the given date") + .with_parameter(help_text("date", "An absolute or relative date")) + .with_examples( + { + {"here"}, + {"6am"}, + }) }, { "hide-lines-after", "", "Hide lines that come after the given line number or date", com_hide_line, + + help_text(":hide-lines-after") + .with_summary("Hide lines that come after the given date") + .with_parameter(help_text("date", "An absolute or relative date")) + .with_examples( + { + {"here"}, + {"6am"}, + }) }, { "show-lines-before-and-after", NULL, "Show lines that were hidden by the 'hide-lines' commands", com_show_lines, + + help_text(":show-lines-before-and-after") + .with_summary("Show lines that were hidden by the 'hide-lines' commands") }, { "highlight", - "", + "", "Add coloring to log messages fragments that match the given regular expression", com_highlight, + + help_text(":highlight") + .with_summary("Add coloring to log messages fragments that match the given regular expression") + .with_parameter(help_text("pattern", "The regular expression to match")) + .with_example({R"(\d{3,})"}) }, { "clear-highlight", - "", + "", "Remove a previously set highlight regular expression", com_clear_highlight, + + 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_example({"foobar"}) }, { "filter-in", "", "Only show lines that match the given regular expression in the current view", 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_example({"dhclient"}) }, { "filter-out", "", "Remove lines that match the given regular expression in the current view", 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_example({"last message repeated"}) }, { "delete-filter", "", "Delete the given filter", com_delete_filter, + + help_text(":filter-out") + .with_summary("Delete the filter created with " + ANSI_BOLD(":filter-in") " or " ANSI_BOLD(":filter-out")) + .with_parameter(help_text("pattern", "The regular expression to match")) + .with_example({"last message repeated"}) }, { "append-to", "", "Append marked lines in the current view to the given file", com_save_to, + + help_text(":append-to") + .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_example({"/tmp/interesting-lines.txt"}) }, { "write-to", "", "Overwrite the given file with any marked lines in the current view", com_save_to, + + help_text(":write-to") + .with_summary("Overwrite the given file with any marked lines in the current view") + .with_parameter(help_text("path", "The path to the file to write")) + .with_example({"/tmp/interesting-lines.txt"}) }, { "write-csv-to", "", "Write SQL results to the given file in CSV format", com_save_to, + + help_text(":write-csv-to") + .with_summary("Write SQL results to the given file in CSV format") + .with_parameter(help_text("path", "The path to the file to write")) + .with_example({"/tmp/table.csv"}) }, { "write-json-to", "", "Write SQL results to the given file in JSON format", com_save_to, + + help_text(":write-json-to") + .with_summary("Write SQL results to the given file in JSON format") + .with_parameter(help_text("path", "The path to the file to write")) + .with_example({"/tmp/table.json"}) }, { "write-cols-to", "", "Write SQL results to the given file in a columnar format", com_save_to, + + help_text(":write-cols-to") + .with_summary("Write SQL results to the given file in a columnar format") + .with_parameter(help_text("path", "The path to the file to write")) + .with_example({"/tmp/table.txt"}) }, { "write-raw-to", "", "Write SQL results to the given file without any formatting", com_save_to, + + help_text(":write-raw-to") + .with_summary("Write SQL results to the given file without any formatting") + .with_parameter(help_text("path", "The path to the file to write")) + .with_example({"/tmp/table.txt"}) }, { "pipe-to", "", "Pipe the marked lines to the given shell command", com_pipe_to, + + help_text(":pipe-to") + .with_summary("Pipe the marked lines to the given shell command") + .with_parameter(help_text("shell-cmd", "The shell command-line to execute")) + .with_example({"sed -e s/foo/bar/g"}) }, { "pipe-line-to", "", "Pipe the top line to the given shell command", com_pipe_to, + + help_text(":pipe-line-to") + .with_summary("Pipe the top line to the given shell command") + .with_parameter(help_text("shell-cmd", "The shell command-line to execute")) + .with_example({"sed -e 's/foo/bar/g'"}) }, { "enable-filter", "", "Enable a previously created and disabled filter", com_enable_filter, + + help_text(":enable-filter") + .with_summary("Enable a previously created and disabled filter") + .with_parameter(help_text("pattern", "The regular expression used in the filter command")) + .with_example({"last message repeated"}) }, { "disable-filter", "", "Disable a filter created with filter-in/filter-out", com_disable_filter, + + help_text(":disable-filter") + .with_summary("Disable a filter created with filter-in/filter-out") + .with_parameter(help_text("pattern", "The regular expression used in the filter command")) + .with_example({"last message repeated"}) }, { "enable-word-wrap", NULL, "Enable word-wrapping for the current view", com_enable_word_wrap, + + help_text(":enable-word-wrap") + .with_summary("Enable word-wrapping for the current view") }, { "disable-word-wrap", NULL, "Disable word-wrapping for the current view", com_disable_word_wrap, + + help_text(":disable-word-wrap") + .with_summary("Disable word-wrapping for the current view") }, { "create-logline-table", "", "Create an SQL table using the top line of the log view as a template", com_create_logline_table, + + help_text(":create-logline-table") + .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_example({"task_durations"}) }, { "delete-logline-table", "", "Delete a table created with create-logline-table", com_delete_logline_table, + + help_text(":delete-logline-table") + .with_summary("Delete a table created with create-logline-table") + .with_parameter(help_text("table-name", "The name of the table to delete")) + .with_example({"task_durations"}) }, { "create-search-table", " []", "Create an SQL table based on a regex search", com_create_search_table, + + help_text(":create-search-table") + .with_summary("Create an SQL table based on a regex search") + .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.") + .optional()) + .with_example({R"(task_durations duration=(?\d+))"}) }, { "delete-search-table", "", "Delete a table created with create-search-table", com_delete_search_table, + + help_text(":delete-search-table") + .with_summary("Create an SQL table based on a regex search") + .with_parameter(help_text("table-name", "The name of the table to create")) + .with_example({"task_durations"}) }, { "open", @@ -3072,24 +3253,48 @@ readline_context::command_t STD_COMMANDS[] = { "Open the given file(s) in lnav", #endif com_open, + + help_text(":open") + .with_summary( +#ifdef HAVE_LIBCURL + "Open the given file(s) or URLs in lnav" +#else + "Open the given file(s) in lnav" +#endif + ) + .with_parameter( + help_text{"path", "The path to the file to open"} + .one_or_more()) + .with_example({"~/.lnav/example", ""}) }, { "close", NULL, "Close the top file", com_close, + + help_text(":close") + .with_summary("Close the top file in the view") }, { "partition-name", "", "Mark the top line in the log view as the start of a new partition with the given name", com_partition_name, + + help_text(":partition-name") + .with_summary("Mark the top line in the log view as the start of a new partition with the given name") + .with_parameter(help_text("name", "The name for the new partition")) + .with_example({"reboot"}) }, { "clear-partition", NULL, "Clear the partition the top line is a part of", com_clear_partition, + + help_text(":clear-partition") + .with_summary("Clear the partition the top line is a part of") }, { "pt-min-time", @@ -3108,96 +3313,174 @@ readline_context::command_t STD_COMMANDS[] = { "", "Add the given command to the session file (~/.lnav/session)", com_session, + + help_text(":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({":highlight foobar"}) }, { "summarize", "", "Execute a SQL query that computes the characteristics of the values in the given column", com_summarize, + + help_text(":summarize") + .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({"sc_bytes"}) }, { "switch-to-view", "", "Switch to the given view", com_switch_to_view, + + help_text(":switch-to-view") + .with_summary("Switch to the given view") + .with_parameter(help_text("view-name", "The name of the view to switch to.")) + .with_example({"schema"}) }, { "reset-session", NULL, "Reset the session state, clearing all filters, highlights, and bookmarks", com_reset_session, + + help_text(":reset-session") + .with_summary("Reset the session state, clearing all filters, highlights, and bookmarks") }, { "load-session", NULL, "Load the latest session state", com_load_session, + + help_text(":load-session") + .with_summary("Load the latest session state") }, { "save-session", NULL, "Save the current state as a session", com_save_session, + + help_text(":save-session") + .with_summary("Save the current state as a session") }, { "set-min-log-level", "", "Set the minimum log level to display in the log view", com_set_min_log_level, + + help_text(":set-min-log-level") + .with_summary("Set the minimum log level to display in the log view") + .with_parameter(help_text("log-level", "The new minimum log level")) + .with_example({"error"}) }, { "redraw", NULL, "Do a full redraw of the screen", com_redraw, + + help_text(":redraw") + .with_summary("Do a full redraw of the screen") }, { "zoom-to", "", "Zoom the histogram view to the given level", com_zoom_to, + + help_text(":zoom-to") + .with_summary("Zoom the histogram view to the given level") + .with_parameter(help_text("zoom-level", "The zoom level")) + .with_example({"1-week"}) }, { "echo", "[-n] ", "Echo the given message", com_echo, + + help_text(":echo") + .with_summary("Echo the given message") + .with_parameter(help_text("msg", "The message to display")) + .with_example({"Hello, World!"}) }, { "alt-msg", "", "Display a message in the alternate command position", com_alt_msg, + + help_text(":alt-msg") + .with_summary("Display a message in the alternate command position") + .with_parameter(help_text("msg", "The message to display")) + .with_example({"Press t to switch to the text view"}) }, { "eval", "", "Evaluate the given command/query after doing environment variable substitution", com_eval, + + help_text(":eval") + .with_summary( + "Evaluate the given command/query after doing environment variable substitution") + .with_parameter(help_text("command", + "The command or query to perform substitution on.")) + .with_examples( + { + {":echo $HOME"}, + {";SELECT * FROM ${table}"} + }) }, { "config", "