/** * Copyright (c) 2025, Timothy Stack * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Timothy Stack nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include #include "cmd.parser.hh" #include "base/attr_line.hh" #include "base/itertools.enumerate.hh" #include "base/lnav_log.hh" #include "data_scanner.hh" #include "shlex.hh" #include "sql_help.hh" #include "sql_util.hh" namespace lnav::command { static bool is_separator(data_token_t tok) { switch (tok) { case DT_COLON: case DT_EQUALS: case DT_COMMA: case DT_SEMI: case DT_EMDASH: case DT_LCURLY: case DT_RCURLY: case DT_LSQUARE: case DT_RSQUARE: case DT_LPAREN: case DT_RPAREN: case DT_LANGLE: case DT_RANGLE: case DT_LINE: case DT_WHITE: case DT_DOT: case DT_ESCAPED_CHAR: return true; default: return false; } } std::optional parsed::arg_at(int x) const { log_debug("BEGIN arg_at"); for (const auto& se : this->p_free_args) { if (se.se_origin.sf_begin <= x && x <= se.se_origin.sf_end) { log_debug(" free arg [%d:%d) '%s'", se.se_origin.sf_begin, se.se_origin.sf_end, se.se_value.c_str()); return arg_at_result{this->p_help, false, se}; } } for (const auto& arg : this->p_args) { log_debug( " arg %s[%zu]", arg.first.c_str(), arg.second.a_values.size()); for (const auto& [index, se] : lnav::itertools::enumerate(arg.second.a_values)) { log_debug(" val [%d:%d) '%.*s' -> '%s'", se.se_origin.sf_begin, se.se_origin.sf_end, se.se_origin.length(), se.se_origin.data(), se.se_value.c_str()); if (se.se_origin.sf_begin <= x && x <= se.se_origin.sf_end) { switch (arg.second.a_help->ht_format) { case help_parameter_format_t::HPF_SQL: case help_parameter_format_t::HPF_SQL_EXPR: { auto al = attr_line_t(se.se_value); auto al_x = x - se.se_origin.sf_begin; annotate_sql_statement(al, lnav::sql::dialect::sqlite); for (const auto& attr : al.al_attrs) { if (al_x < attr.sa_range.lr_start || attr.sa_range.lr_end < al_x) { continue; } auto sf = al.to_string_fragment(attr); if (attr.sa_type == &SQL_GARBAGE_ATTR && attr.sa_range.length() == 1) { switch (al.al_string[attr.sa_range.lr_start]) { case ':': case '$': case '@': return arg_at_result{ arg.second.a_help, false, { sf, sf.to_string(), }, }; } } if (attr.sa_type != &SQL_IDENTIFIER_ATTR && attr.sa_type != &SQL_STRING_ATTR && attr.sa_type != &SQL_KEYWORD_ATTR) { continue; } return arg_at_result{ arg.second.a_help, false, { sf, sf.to_string(), }, }; } return arg_at_result{arg.second.a_help, false, {}}; } case help_parameter_format_t::HPF_ALL_FILTERS: case help_parameter_format_t::HPF_ENABLED_FILTERS: case help_parameter_format_t::HPF_DISABLED_FILTERS: case help_parameter_format_t::HPF_HIGHLIGHTS: { return arg_at_result{arg.second.a_help, true, se}; } case help_parameter_format_t::HPF_CONFIG_VALUE: case help_parameter_format_t::HPF_MULTILINE_TEXT: case help_parameter_format_t::HPF_TEXT: case help_parameter_format_t::HPF_LOCATION: case help_parameter_format_t::HPF_REGEX: case help_parameter_format_t::HPF_TIME_FILTER_POINT: { std::optional cap_to_start; data_scanner ds(se.se_origin, false); while (true) { auto tok_res = ds.tokenize2(); if (!tok_res) { break; } auto tok = tok_res.value(); log_debug("cap b:%d x:%d e:%d %s", tok.tr_capture.c_begin, x, tok.tr_capture.c_end, data_scanner::token2name(tok.tr_token)); if (cap_to_start && (tok.tr_token == DT_GARBAGE || tok.tr_token == DT_DOT || tok.tr_token == DT_ESCAPED_CHAR)) { log_debug("expanding cap"); tok.tr_capture.c_begin = cap_to_start->c_begin; } if (tok.tr_capture.c_begin <= x && x <= tok.tr_capture.c_end && !is_separator(tok.tr_token)) { log_debug( " in token %s", data_scanner::token2name(tok.tr_token)); return arg_at_result{ arg.second.a_help, false, shlex::split_element_t{ tok.to_string_fragment(), tok.to_string(), }}; } if (!cap_to_start && tok.tr_token != DT_WHITE) { cap_to_start = tok.tr_capture; } else { switch (tok.tr_token) { case DT_WHITE: cap_to_start = std::nullopt; break; case DT_GARBAGE: case DT_DOT: case DT_ESCAPED_CHAR: break; default: cap_to_start = tok.tr_capture; break; } } } log_debug("end of input"); return arg_at_result{ arg.second.a_help, false, shlex::split_element_t{}}; } default: return arg_at_result{arg.second.a_help, index == 0, se}; } } } } for (const auto& param : this->p_help->ht_parameters) { if (startswith(param.ht_name, "-")) { continue; } const auto p_iter = this->p_args.find(param.ht_name); if ((p_iter->second.a_values.empty() || param.is_trailing_arg()) || param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE) { log_debug(" or-more"); return arg_at_result{ p_iter->second.a_help, p_iter->second.a_values.empty() && !param.is_trailing_arg(), shlex::split_element_t{}}; } } return std::nullopt; } enum class mode_t { prompt, call, }; static Result parse_for(mode_t mode, exec_context& ec, string_fragment args, const help_text& ht) { parsed retval; shlex lexer(args); auto split_res = lexer.split(ec.create_resolver()); if (split_res.isErr()) { auto te = split_res.unwrapErr(); if (mode == mode_t::call) { return Err( lnav::console::user_message::error("unable to parse arguments") .with_reason(te.se_error.te_msg)); } } auto split_args = split_res.isOk() ? split_res.unwrap() : split_res.unwrapErr().se_elements; auto split_index = size_t{0}; retval.p_help = &ht; for (const auto& param : ht.ht_parameters) { auto& arg = retval.p_args[param.ht_name]; arg.a_help = ¶m; switch (param.ht_nargs) { case help_nargs_t::HN_REQUIRED: case help_nargs_t::HN_ONE_OR_MORE: { if (split_index >= split_args.size()) { if (mode == mode_t::call) { return Err(lnav::console::user_message::error( "missing required argument")); } continue; } break; } case help_nargs_t::HN_OPTIONAL: case help_nargs_t::HN_ZERO_OR_MORE: { if (split_index >= split_args.size()) { continue; } break; } } do { const auto& se = split_args[split_index]; if (se.se_value == "-" || startswith(se.se_value, "--")) { retval.p_free_args.emplace_back(se); } else if (startswith(param.ht_name, "-")) { break; } else { switch (param.ht_format) { case help_parameter_format_t::HPF_TEXT: case help_parameter_format_t::HPF_MULTILINE_TEXT: case help_parameter_format_t::HPF_REGEX: case help_parameter_format_t::HPF_LOCATION: case help_parameter_format_t::HPF_SQL: case help_parameter_format_t::HPF_SQL_EXPR: case help_parameter_format_t::HPF_TIME_FILTER_POINT: case help_parameter_format_t::HPF_ALL_FILTERS: case help_parameter_format_t::HPF_CONFIG_VALUE: case help_parameter_format_t::HPF_ENABLED_FILTERS: case help_parameter_format_t::HPF_DISABLED_FILTERS: case help_parameter_format_t::HPF_HIGHLIGHTS: { auto sf = string_fragment{ se.se_origin.sf_string, se.se_origin.sf_begin, args.sf_end - args.sf_begin, }; arg.a_values.emplace_back( shlex::split_element_t{sf, sf.to_string()}); split_index = split_args.size() - 1; break; } case help_parameter_format_t::HPF_INTEGER: case help_parameter_format_t::HPF_NUMBER: case help_parameter_format_t::HPF_CONFIG_PATH: case help_parameter_format_t::HPF_TAG: case help_parameter_format_t::HPF_ADJUSTED_TIME: case help_parameter_format_t::HPF_LINE_TAG: case help_parameter_format_t::HPF_LOGLINE_TABLE: case help_parameter_format_t::HPF_SEARCH_TABLE: case help_parameter_format_t::HPF_STRING: case help_parameter_format_t::HPF_FILENAME: case help_parameter_format_t::HPF_LOCAL_FILENAME: case help_parameter_format_t::HPF_DIRECTORY: case help_parameter_format_t::HPF_LOADED_FILE: case help_parameter_format_t::HPF_FORMAT_FIELD: case help_parameter_format_t::HPF_NUMERIC_FIELD: case help_parameter_format_t::HPF_HIGHLIGHTED_FIELD: case help_parameter_format_t::HPF_TIMEZONE: case help_parameter_format_t::HPF_FILE_WITH_ZONE: case help_parameter_format_t::HPF_VISIBLE_FILES: case help_parameter_format_t::HPF_HIDDEN_FILES: case help_parameter_format_t::HPF_BREAKPOINT: case help_parameter_format_t::HPF_KNOWN_BREAKPOINT: case help_parameter_format_t::HPF_KNOWN_APP: { if (!param.ht_enum_values.empty()) { auto enum_iter = std::find(param.ht_enum_values.begin(), param.ht_enum_values.end(), se.se_value); if (enum_iter == param.ht_enum_values.end()) { if (mode == mode_t::call) { return Err( lnav::console::user_message::error( "bad enum")); } } } arg.a_values.emplace_back(se); break; } case help_parameter_format_t::HPF_NONE: { if (se.se_value != param.ht_name) { log_debug("skip flag '%s' '%s'", se.se_value.c_str(), param.ht_name); continue; } arg.a_values.emplace_back(se); break; } } } split_index += 1; } while (split_index < split_args.size() && (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE)); } for (auto free_iter = retval.p_free_args.begin(); free_iter != retval.p_free_args.end();) { auto free_sf = string_fragment::from_str(free_iter->se_value); auto [flag_name, flag_value] = free_sf.split_when(string_fragment::tag1{'='}); auto consumed = false; for (const auto& param : ht.ht_parameters) { if (param.ht_name == flag_name) { retval.p_args[param.ht_name].a_values.emplace_back( shlex::split_element_t{ free_iter->se_origin.substr(flag_name.length() + 1), flag_value.to_string(), }); consumed = true; break; } } if (consumed) { free_iter = retval.p_free_args.erase(free_iter); } else { ++free_iter; } } return Ok(retval); } parsed parse_for_prompt(exec_context& ec, string_fragment args, const help_text& ht) { return parse_for(mode_t::prompt, ec, args, ht).unwrap(); } Result parse_for_call(exec_context& ec, string_fragment args, const help_text& ht) { return parse_for(mode_t::call, ec, args, ht); } } // namespace lnav::command