• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

tstack / lnav / 19840228543-2726

01 Dec 2025 10:52PM UTC coverage: 68.835% (-0.002%) from 68.837%
19840228543-2726

push

github

tstack
[cmd.parser] ignore flags for now

1 of 2 new or added lines in 1 file covered. (50.0%)

1 existing line in 1 file now uncovered.

51302 of 74529 relevant lines covered (68.83%)

435423.02 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

54.89
/src/cmd.parser.cc
1
/**
2
 * Copyright (c) 2025, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <algorithm>
31
#include <optional>
32
#include <utility>
33

34
#include "cmd.parser.hh"
35

36
#include "base/attr_line.hh"
37
#include "base/itertools.enumerate.hh"
38
#include "base/lnav_log.hh"
39
#include "data_scanner.hh"
40
#include "shlex.hh"
41
#include "sql_help.hh"
42
#include "sql_util.hh"
43

44
namespace lnav::command {
45

46
static bool
47
is_separator(data_token_t tok)
5✔
48
{
49
    switch (tok) {
5✔
50
        case DT_COLON:
1✔
51
        case DT_EQUALS:
52
        case DT_COMMA:
53
        case DT_SEMI:
54
        case DT_EMDASH:
55
        case DT_LCURLY:
56
        case DT_RCURLY:
57
        case DT_LSQUARE:
58
        case DT_RSQUARE:
59
        case DT_LPAREN:
60
        case DT_RPAREN:
61
        case DT_LANGLE:
62
        case DT_RANGLE:
63
        case DT_LINE:
64
        case DT_WHITE:
65
        case DT_DOT:
66
        case DT_ESCAPED_CHAR:
67
            return true;
1✔
68
        default:
4✔
69
            return false;
4✔
70
    }
71
}
72

73
std::optional<parsed::arg_at_result>
74
parsed::arg_at(int x) const
5✔
75
{
76
    log_debug("BEGIN arg_at");
5✔
77
    for (const auto& se : this->p_free_args) {
5✔
78
        if (se.se_origin.sf_begin <= x && x <= se.se_origin.sf_end) {
×
79
            log_debug("  free arg [%d:%d) '%s'",
×
80
                      se.se_origin.sf_begin,
81
                      se.se_origin.sf_end,
82
                      se.se_value.c_str());
83
            return arg_at_result{this->p_help, false, se};
×
84
        }
85
    }
86
    for (const auto& arg : this->p_args) {
5✔
87
        log_debug(
5✔
88
            "  arg %s[%zu]", arg.first.c_str(), arg.second.a_values.size());
89
        for (const auto& [index, se] :
10✔
90
             lnav::itertools::enumerate(arg.second.a_values))
10✔
91
        {
92
            log_debug("    val [%d:%d) '%.*s' -> '%s'",
5✔
93
                      se.se_origin.sf_begin,
94
                      se.se_origin.sf_end,
95
                      se.se_origin.length(),
96
                      se.se_origin.data(),
97
                      se.se_value.c_str());
98
            if (se.se_origin.sf_begin <= x && x <= se.se_origin.sf_end) {
5✔
99
                switch (arg.second.a_help->ht_format) {
5✔
100
                    case help_parameter_format_t::HPF_SQL:
×
101
                    case help_parameter_format_t::HPF_SQL_EXPR: {
102
                        auto al = attr_line_t(se.se_value);
×
103
                        auto al_x = x - se.se_origin.sf_begin;
×
104

105
                        annotate_sql_statement(al, lnav::sql::dialect::sqlite);
×
106
                        for (const auto& attr : al.al_attrs) {
×
107
                            if (al_x < attr.sa_range.lr_start
×
108
                                || attr.sa_range.lr_end < al_x)
×
109
                            {
110
                                continue;
×
111
                            }
112

113
                            auto sf = al.to_string_fragment(attr);
×
114
                            if (attr.sa_type == &SQL_GARBAGE_ATTR
×
115
                                && attr.sa_range.length() == 1)
×
116
                            {
117
                                switch (al.al_string[attr.sa_range.lr_start]) {
×
118
                                    case ':':
×
119
                                    case '$':
120
                                    case '@':
121
                                        return arg_at_result{
×
122
                                            arg.second.a_help,
×
123
                                            false,
124
                                            {
125
                                                sf,
126
                                                sf.to_string(),
127
                                            },
128
                                        };
129
                                }
130
                            }
131

132
                            if (attr.sa_type != &SQL_IDENTIFIER_ATTR
×
133
                                && attr.sa_type != &SQL_STRING_ATTR
×
134
                                && attr.sa_type != &SQL_KEYWORD_ATTR)
×
135
                            {
136
                                continue;
×
137
                            }
138
                            return arg_at_result{
×
139
                                arg.second.a_help,
×
140
                                false,
141
                                {
142
                                    sf,
143
                                    sf.to_string(),
144
                                },
145
                            };
146
                        }
147
                        return arg_at_result{arg.second.a_help, false, {}};
×
148
                    }
149
                    case help_parameter_format_t::HPF_ALL_FILTERS:
×
150
                    case help_parameter_format_t::HPF_ENABLED_FILTERS:
151
                    case help_parameter_format_t::HPF_DISABLED_FILTERS:
152
                    case help_parameter_format_t::HPF_HIGHLIGHTS: {
153
                        return arg_at_result{arg.second.a_help, true, se};
×
154
                    }
155
                    case help_parameter_format_t::HPF_CONFIG_VALUE:
5✔
156
                    case help_parameter_format_t::HPF_MULTILINE_TEXT:
157
                    case help_parameter_format_t::HPF_TEXT:
158
                    case help_parameter_format_t::HPF_LOCATION:
159
                    case help_parameter_format_t::HPF_REGEX:
160
                    case help_parameter_format_t::HPF_TIME_FILTER_POINT: {
161
                        std::optional<data_scanner::capture_t> cap_to_start;
5✔
162
                        data_scanner ds(se.se_origin, false);
5✔
163

164
                        while (true) {
165
                            auto tok_res = ds.tokenize2();
8✔
166

167
                            if (!tok_res) {
8✔
168
                                break;
1✔
169
                            }
170
                            auto tok = tok_res.value();
7✔
171

172
                            log_debug("cap b:%d  x:%d  e:%d %s",
7✔
173
                                      tok.tr_capture.c_begin,
174
                                      x,
175
                                      tok.tr_capture.c_end,
176
                                      data_scanner::token2name(tok.tr_token));
177
                            if (cap_to_start
7✔
178
                                && (tok.tr_token == DT_GARBAGE
8✔
179
                                    || tok.tr_token == DT_DOT
1✔
180
                                    || tok.tr_token == DT_ESCAPED_CHAR))
8✔
181
                            {
182
                                log_debug("expanding cap");
1✔
183
                                tok.tr_capture.c_begin = cap_to_start->c_begin;
1✔
184
                            }
185
                            if (tok.tr_capture.c_begin <= x
14✔
186
                                && x <= tok.tr_capture.c_end
7✔
187
                                && !is_separator(tok.tr_token))
14✔
188
                            {
189
                                log_debug(
4✔
190
                                    "  in token %s",
191
                                    data_scanner::token2name(tok.tr_token));
192
                                return arg_at_result{
8✔
193
                                    arg.second.a_help,
4✔
194
                                    false,
195
                                    shlex::split_element_t{
196
                                        tok.to_string_fragment(),
4✔
197
                                        tok.to_string(),
198
                                    }};
4✔
199
                            }
200
                            if (!cap_to_start && tok.tr_token != DT_WHITE) {
3✔
201
                                cap_to_start = tok.tr_capture;
2✔
202
                            } else {
203
                                switch (tok.tr_token) {
1✔
204
                                    case DT_WHITE:
1✔
205
                                        cap_to_start = std::nullopt;
1✔
206
                                        break;
1✔
207
                                    case DT_GARBAGE:
×
208
                                    case DT_DOT:
209
                                    case DT_ESCAPED_CHAR:
210
                                        break;
×
211
                                    default:
×
212
                                        cap_to_start = tok.tr_capture;
×
213
                                        break;
×
214
                                }
215
                            }
216
                        }
3✔
217
                        log_debug("end of input");
1✔
218
                        return arg_at_result{
1✔
219
                            arg.second.a_help, false, shlex::split_element_t{}};
1✔
220
                    }
5✔
221
                    default:
×
222
                        return arg_at_result{arg.second.a_help, index == 0, se};
×
223
                }
224
            }
225
        }
226
    }
227

228
    for (const auto& param : this->p_help->ht_parameters) {
×
229
        if (startswith(param.ht_name, "-")) {
×
230
            continue;
×
231
        }
232
        const auto p_iter = this->p_args.find(param.ht_name);
×
233
        if ((p_iter->second.a_values.empty() || param.is_trailing_arg())
×
234
            || param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE
×
235
            || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE)
×
236
        {
237
            log_debug("  or-more");
×
238
            return arg_at_result{
×
239
                p_iter->second.a_help,
×
240
                p_iter->second.a_values.empty() && !param.is_trailing_arg(),
×
241
                shlex::split_element_t{}};
×
242
        }
243
    }
244

245
    return std::nullopt;
×
246
}
247

248
enum class mode_t {
249
    prompt,
250
    call,
251
};
252

253
static Result<parsed, lnav::console::user_message>
254
parse_for(mode_t mode,
25✔
255
          exec_context& ec,
256
          string_fragment args,
257
          const help_text& ht)
258
{
259
    parsed retval;
25✔
260
    shlex lexer(args);
25✔
261
    auto split_res = lexer.split(ec.create_resolver());
25✔
262

263
    if (split_res.isErr()) {
25✔
264
        auto te = split_res.unwrapErr();
1✔
265
        if (mode == mode_t::call) {
1✔
266
            return Err(
×
267
                lnav::console::user_message::error("unable to parse arguments")
×
268
                    .with_reason(te.se_error.te_msg));
×
269
        }
270
    }
1✔
271
    auto split_args = split_res.isOk() ? split_res.unwrap()
25✔
272
                                       : split_res.unwrapErr().se_elements;
25✔
273
    auto split_index = size_t{0};
25✔
274

275
    retval.p_help = &ht;
25✔
276
    for (const auto& param : ht.ht_parameters) {
70✔
277
        auto& arg = retval.p_args[param.ht_name];
45✔
278
        arg.a_help = &param;
45✔
279
        switch (param.ht_nargs) {
45✔
280
            case help_nargs_t::HN_REQUIRED:
25✔
281
            case help_nargs_t::HN_ONE_OR_MORE: {
282
                if (split_index >= split_args.size()) {
25✔
283
                    if (mode == mode_t::call) {
×
284
                        return Err(lnav::console::user_message::error(
×
285
                            "missing required argument"));
×
286
                    }
287
                    continue;
×
288
                }
289
                break;
25✔
290
            }
291
            case help_nargs_t::HN_OPTIONAL:
20✔
292
            case help_nargs_t::HN_ZERO_OR_MORE: {
293
                if (split_index >= split_args.size()) {
20✔
294
                    continue;
8✔
295
                }
296
                break;
12✔
297
            }
298
        }
299

300
        do {
301
            const auto& se = split_args[split_index];
37✔
302
            if (se.se_value == "-" || startswith(se.se_value, "--")) {
37✔
303
                retval.p_free_args.emplace_back(se);
×
304
            } else if (startswith(param.ht_name, "-")) {
37✔
NEW
305
                break;
×
306
            } else {
307
                switch (param.ht_format) {
37✔
308
                    case help_parameter_format_t::HPF_TEXT:
17✔
309
                    case help_parameter_format_t::HPF_MULTILINE_TEXT:
310
                    case help_parameter_format_t::HPF_REGEX:
311
                    case help_parameter_format_t::HPF_LOCATION:
312
                    case help_parameter_format_t::HPF_SQL:
313
                    case help_parameter_format_t::HPF_SQL_EXPR:
314
                    case help_parameter_format_t::HPF_TIME_FILTER_POINT:
315
                    case help_parameter_format_t::HPF_ALL_FILTERS:
316
                    case help_parameter_format_t::HPF_CONFIG_VALUE:
317
                    case help_parameter_format_t::HPF_ENABLED_FILTERS:
318
                    case help_parameter_format_t::HPF_DISABLED_FILTERS:
319
                    case help_parameter_format_t::HPF_HIGHLIGHTS: {
320
                        auto sf = string_fragment{
321
                            se.se_origin.sf_string,
17✔
322
                            se.se_origin.sf_begin,
17✔
323
                            args.sf_end - args.sf_begin,
17✔
324
                        };
17✔
325
                        arg.a_values.emplace_back(
17✔
326
                            shlex::split_element_t{sf, sf.to_string()});
34✔
327
                        split_index = split_args.size() - 1;
17✔
328
                        break;
17✔
329
                    }
330
                    case help_parameter_format_t::HPF_INTEGER:
20✔
331
                    case help_parameter_format_t::HPF_NUMBER:
332
                    case help_parameter_format_t::HPF_CONFIG_PATH:
333
                    case help_parameter_format_t::HPF_TAG:
334
                    case help_parameter_format_t::HPF_ADJUSTED_TIME:
335
                    case help_parameter_format_t::HPF_LINE_TAG:
336
                    case help_parameter_format_t::HPF_LOGLINE_TABLE:
337
                    case help_parameter_format_t::HPF_SEARCH_TABLE:
338
                    case help_parameter_format_t::HPF_STRING:
339
                    case help_parameter_format_t::HPF_FILENAME:
340
                    case help_parameter_format_t::HPF_LOCAL_FILENAME:
341
                    case help_parameter_format_t::HPF_DIRECTORY:
342
                    case help_parameter_format_t::HPF_LOADED_FILE:
343
                    case help_parameter_format_t::HPF_FORMAT_FIELD:
344
                    case help_parameter_format_t::HPF_NUMERIC_FIELD:
345
                    case help_parameter_format_t::HPF_TIMEZONE:
346
                    case help_parameter_format_t::HPF_FILE_WITH_ZONE:
347
                    case help_parameter_format_t::HPF_VISIBLE_FILES:
348
                    case help_parameter_format_t::HPF_HIDDEN_FILES:
349
                    case help_parameter_format_t::HPF_BREAKPOINT:
350
                    case help_parameter_format_t::HPF_KNOWN_BREAKPOINT:
351
                    case help_parameter_format_t::HPF_KNOWN_APP: {
352
                        if (!param.ht_enum_values.empty()) {
20✔
353
                            auto enum_iter
354
                                = std::find(param.ht_enum_values.begin(),
×
355
                                            param.ht_enum_values.end(),
356
                                            se.se_value);
×
357
                            if (enum_iter == param.ht_enum_values.end()) {
×
358
                                if (mode == mode_t::call) {
×
359
                                    return Err(
×
360
                                        lnav::console::user_message::error(
×
361
                                            "bad enum"));
×
362
                                }
363
                            }
364
                        }
365

366
                        arg.a_values.emplace_back(se);
20✔
367
                        break;
20✔
368
                    }
369
                    case help_parameter_format_t::HPF_NONE: {
×
370
                        if (se.se_value != param.ht_name) {
×
371
                            log_debug("skip flag '%s' '%s'",
×
372
                                      se.se_value.c_str(),
373
                                      param.ht_name);
374
                            continue;
×
375
                        }
376
                        arg.a_values.emplace_back(se);
×
377
                        break;
×
378
                    }
379
                }
380
            }
381
            split_index += 1;
37✔
382
        } while (split_index < split_args.size()
37✔
383
                 && (param.ht_nargs == help_nargs_t::HN_ZERO_OR_MORE
49✔
384
                     || param.ht_nargs == help_nargs_t::HN_ONE_OR_MORE));
12✔
385
    }
386

387
    for (auto free_iter = retval.p_free_args.begin();
25✔
388
         free_iter != retval.p_free_args.end();)
25✔
389
    {
390
        auto free_sf = string_fragment::from_str(free_iter->se_value);
×
391
        auto [flag_name, flag_value]
×
392
            = free_sf.split_when(string_fragment::tag1{'='});
×
393
        auto consumed = false;
×
394
        for (const auto& param : ht.ht_parameters) {
×
395
            if (param.ht_name == flag_name) {
×
396
                retval.p_args[param.ht_name].a_values.emplace_back(
×
397
                    shlex::split_element_t{
×
398
                        free_iter->se_origin.substr(flag_name.length() + 1),
×
399
                        flag_value.to_string(),
400
                    });
401
                consumed = true;
×
402
                break;
×
403
            }
404
        }
405
        if (consumed) {
×
406
            free_iter = retval.p_free_args.erase(free_iter);
×
407
        } else {
408
            ++free_iter;
×
409
        }
410
    }
411

412
    return Ok(retval);
25✔
413
}
25✔
414

415
parsed
416
parse_for_prompt(exec_context& ec, string_fragment args, const help_text& ht)
5✔
417
{
418
    return parse_for(mode_t::prompt, ec, args, ht).unwrap();
5✔
419
}
420

421
Result<parsed, lnav::console::user_message>
422
parse_for_call(exec_context& ec, string_fragment args, const help_text& ht)
20✔
423
{
424
    return parse_for(mode_t::call, ec, args, ht);
20✔
425
}
426

427
}  // namespace lnav::command
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc