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

tstack / lnav / 17589970077-2502

09 Sep 2025 05:00PM UTC coverage: 65.196% (-5.0%) from 70.225%
17589970077-2502

push

github

tstack
[format] add fields for source file/line

Knowing the source file/line context in a log
message can help find log messages when using
log2src.

56 of 70 new or added lines in 2 files covered. (80.0%)

13954 existing lines in 210 files now uncovered.

45516 of 69814 relevant lines covered (65.2%)

404154.37 hits per line

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

31.29
/src/lnav.management_cli.cc
1
/**
2
 * Copyright (c) 2022, 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 "lnav.management_cli.hh"
31

32
#include <glob.h>
33

34
#include "base/fs_util.hh"
35
#include "base/humanize.hh"
36
#include "base/humanize.time.hh"
37
#include "base/itertools.hh"
38
#include "base/paths.hh"
39
#include "base/result.h"
40
#include "base/string_util.hh"
41
#include "crashd.client.hh"
42
#include "file_options.hh"
43
#include "fmt/chrono.h"
44
#include "fmt/format.h"
45
#include "itertools.similar.hh"
46
#include "lnav.hh"
47
#include "lnav_config.hh"
48
#include "log_format.hh"
49
#include "log_format_ext.hh"
50
#include "mapbox/variant.hpp"
51
#include "piper.header.hh"
52
#include "regex101.import.hh"
53
#include "session_data.hh"
54
#include "yajlpp/yajlpp_def.hh"
55

56
using namespace lnav::roles::literals;
57

58
namespace lnav::management {
59

60
struct no_subcmd_t {
61
    CLI::App* ns_root_app{nullptr};
62
};
63

64
static auto DEFAULT_WRAPPING
65
    = text_wrap_settings{}.with_padding_indent(4).with_width(60);
66

67
inline attr_line_t&
UNCOV
68
symbol_reducer(const std::string& elem, attr_line_t& accum)
×
69
{
UNCOV
70
    if (!accum.empty()) {
×
71
        accum.append(", ");
×
72
    }
UNCOV
73
    return accum.append(lnav::roles::symbol(elem));
×
74
}
75

76
inline attr_line_t&
UNCOV
77
subcmd_reducer(const CLI::App* app, attr_line_t& accum)
×
78
{
UNCOV
79
    return accum.append("\n ")
×
80
        .append("\u2022"_list_glyph)
×
81
        .append(" ")
×
82
        .append(lnav::roles::keyword(app->get_name()))
×
83
        .append(": ")
×
84
        .append(app->get_description());
×
85
}
86

87
struct subcmd_config_t {
88
    using action_t = std::function<perform_result_t(const subcmd_config_t&)>;
89

90
    CLI::App* sc_config_app{nullptr};
91
    action_t sc_action;
92
    std::string sc_path;
93

94
    static perform_result_t default_action(const subcmd_config_t& sc)
95
    {
96
        auto um
UNCOV
97
            = console::user_message::error(
×
98
                  "expecting an operation related to the regex101.com "
99
                  "integration")
UNCOV
100
                  .with_help(sc.sc_config_app->get_subcommands({})
×
101
                             | lnav::itertools::fold(
×
102
                                 subcmd_reducer,
UNCOV
103
                                 attr_line_t{"the available operations are:"}))
×
104
                  .move();
×
105

UNCOV
106
        return {std::move(um)};
×
107
    }
108

109
    static perform_result_t get_action(const subcmd_config_t&)
1✔
110
    {
111
        auto config_str = dump_config();
1✔
112
        auto um = console::user_message::raw(config_str);
1✔
113

114
        return {std::move(um)};
4✔
115
    }
2✔
116

117
    static perform_result_t blame_action(const subcmd_config_t&)
1✔
118
    {
119
        auto blame = attr_line_t();
1✔
120

121
        for (const auto& pair : lnav_config_locations) {
1,509✔
122
            blame.appendf(FMT_STRING("{} -> {}:{}\n"),
4,524✔
123
                          pair.first,
1,508✔
124
                          pair.second.sl_source,
1,508✔
125
                          pair.second.sl_line_number);
1,508✔
126
        }
127

128
        auto um = console::user_message::raw(blame.rtrim());
1✔
129

130
        return {std::move(um)};
4✔
131
    }
2✔
132

133
    static perform_result_t file_options_action(const subcmd_config_t& sc)
134
    {
135
        auto& safe_options_hier
UNCOV
136
            = injector::get<lnav::safe_file_options_hier&>();
×
137

UNCOV
138
        if (sc.sc_path.empty()) {
×
139
            auto um = lnav::console::user_message::error(
UNCOV
140
                "Expecting a file path to check for options");
×
141

UNCOV
142
            return {std::move(um)};
×
143
        }
144

145
        safe::ReadAccess<lnav::safe_file_options_hier> options_hier(
UNCOV
146
            safe_options_hier);
×
147

UNCOV
148
        auto realpath_res = lnav::filesystem::realpath(sc.sc_path);
×
149
        if (realpath_res.isErr()) {
×
UNCOV
150
            auto um = lnav::console::user_message::error(
×
151
                          attr_line_t("Unable to get full path for file: ")
×
152
                              .append(lnav::roles::file(sc.sc_path)))
×
153
                          .with_reason(realpath_res.unwrapErr())
×
154
                          .move();
×
155

156
            return {std::move(um)};
×
157
        }
UNCOV
158
        auto full_path = realpath_res.unwrap();
×
159
        auto file_opts = options_hier->match(full_path);
×
UNCOV
160
        if (file_opts) {
×
161
            auto content = attr_line_t()
×
162
                               .append(file_opts->second.to_json_string()
×
163
                                           .to_string_fragment())
×
164
                               .move();
×
165
            auto um = lnav::console::user_message::raw(content);
×
166
            perform_result_t retval;
×
167

168
            retval.emplace_back(um);
×
169

UNCOV
170
            return retval;
×
171
        }
172

173
        auto um
UNCOV
174
            = lnav::console::user_message::info(
×
UNCOV
175
                  attr_line_t("no options found for file: ")
×
UNCOV
176
                      .append(lnav::roles::file(full_path.string())))
×
177
                  .with_help(
×
178
                      attr_line_t("Use the ")
×
179
                          .append(":set-file-timezone"_symbol)
×
180
                          .append(
×
181
                              " command to set the zone for messages in files "
182
                              "that do not include a zone in the timestamp"))
183
                  .move();
×
184

UNCOV
185
        return {std::move(um)};
×
186
    }
187

188
    subcmd_config_t& set_action(action_t act)
4✔
189
    {
190
        if (!this->sc_action) {
4✔
191
            this->sc_action = std::move(act);
2✔
192
        }
193
        return *this;
4✔
194
    }
195
};
196

197
struct subcmd_format_t {
198
    using action_t = std::function<perform_result_t(const subcmd_format_t&)>;
199

200
    CLI::App* sf_format_app{nullptr};
201
    std::string sf_name;
202
    std::string sf_path;
203
    CLI::App* sf_regex_app{nullptr};
204
    std::string sf_regex_name;
205
    CLI::App* sf_regex101_app{nullptr};
206
    action_t sf_action;
207

208
    subcmd_format_t& set_action(action_t act)
12✔
209
    {
210
        if (!this->sf_action) {
12✔
211
            this->sf_action = std::move(act);
6✔
212
        }
213
        return *this;
12✔
214
    }
215

216
    Result<std::shared_ptr<log_format>, console::user_message> validate_format()
6✔
217
        const
218
    {
219
        if (this->sf_name.empty()) {
6✔
220
            auto um
221
                = console::user_message::error(
×
222
                      "expecting a format name to operate on")
223
                      .with_note(
×
UNCOV
224
                          (log_format::get_root_formats()
×
225
                           | lnav::itertools::map(&log_format::get_name)
×
226
                           | lnav::itertools::sort_with(
×
227
                               intern_string_t::case_lt)
228
                           | lnav::itertools::map(&intern_string_t::to_string)
×
UNCOV
229
                           | lnav::itertools::fold(symbol_reducer,
×
230
                                                   attr_line_t{}))
×
231
                              .add_header("the available formats are: ")
×
232
                              .wrap_with(&DEFAULT_WRAPPING))
×
233
                      .move();
×
234

235
            return Err(um);
×
236
        }
237

238
        auto lformat = log_format::find_root_format(this->sf_name.c_str());
6✔
239
        if (lformat == nullptr) {
6✔
240
            auto um
241
                = console::user_message::error(
×
UNCOV
242
                      attr_line_t("unknown format: ")
×
243
                          .append(lnav::roles::symbol(this->sf_name)))
×
244
                      .with_note(
×
245
                          (log_format::get_root_formats()
×
246
                           | lnav::itertools::map(&log_format::get_name)
×
247
                           | lnav::itertools::similar_to(this->sf_name)
×
248
                           | lnav::itertools::map(&intern_string_t::to_string)
×
249
                           | lnav::itertools::fold(symbol_reducer,
×
250
                                                   attr_line_t{}))
×
251
                              .add_header(
×
252
                                  "did you mean one of the following?\n")
253
                              .wrap_with(&DEFAULT_WRAPPING))
×
UNCOV
254
                      .move();
×
255

256
            return Err(um);
×
257
        }
258

259
        return Ok(lformat);
6✔
260
    }
6✔
261

262
    Result<external_log_format*, console::user_message>
263
    validate_external_format() const
6✔
264
    {
265
        auto lformat = TRY(this->validate_format());
6✔
266
        auto* ext_lformat = dynamic_cast<external_log_format*>(lformat.get());
6✔
267

268
        if (ext_lformat == nullptr) {
6✔
UNCOV
269
            return Err(console::user_message::error(
×
270
                attr_line_t()
×
271
                    .append_quoted(lnav::roles::symbol(this->sf_name))
×
272
                    .append(" is an internal format that is not defined in a "
×
273
                            "configuration file")));
×
274
        }
275

276
        return Ok(ext_lformat);
6✔
277
    }
6✔
278

279
    Result<std::pair<external_log_format*,
280
                     std::shared_ptr<external_log_format::pattern>>,
281
           console::user_message>
UNCOV
282
    validate_regex() const
×
283
    {
284
        auto* ext_lformat = TRY(this->validate_external_format());
×
285

286
        if (this->sf_regex_name.empty()) {
×
287
            auto um
288
                = console::user_message::error(
×
289
                      "expecting a regex name to operate on")
290
                      .with_note(
×
UNCOV
291
                          ext_lformat->elf_pattern_order
×
292
                          | lnav::itertools::map(
×
293
                              &external_log_format::pattern::p_name)
294
                          | lnav::itertools::map(&intern_string_t::to_string)
×
UNCOV
295
                          | lnav::itertools::fold(
×
296
                              symbol_reducer,
297
                              attr_line_t{"the available regexes are: "}))
×
UNCOV
298
                      .move();
×
299

300
            return Err(um);
×
301
        }
302

UNCOV
303
        for (const auto& pat : ext_lformat->elf_pattern_order) {
×
UNCOV
304
            if (pat->p_name == this->sf_regex_name) {
×
305
                return Ok(std::make_pair(ext_lformat, pat));
×
306
            }
307
        }
308

309
        auto um
UNCOV
310
            = console::user_message::error(
×
UNCOV
311
                  attr_line_t("unknown regex: ")
×
312
                      .append(lnav::roles::symbol(this->sf_regex_name)))
×
313
                  .with_note(
×
314
                      (ext_lformat->elf_pattern_order
×
315
                       | lnav::itertools::map(
×
316
                           &external_log_format::pattern::p_name)
317
                       | lnav::itertools::map(&intern_string_t::to_string)
×
UNCOV
318
                       | lnav::itertools::similar_to(this->sf_regex_name)
×
319
                       | lnav::itertools::fold(symbol_reducer, attr_line_t{}))
×
320
                          .add_header("did you mean one of the following?\n"))
×
321
                  .move();
×
322

323
        return Err(um);
×
324
    }
325

326
    static perform_result_t default_action(const subcmd_format_t& sf)
327
    {
UNCOV
328
        auto validate_res = sf.validate_format();
×
UNCOV
329
        if (validate_res.isErr()) {
×
330
            return {validate_res.unwrapErr()};
×
331
        }
332

UNCOV
333
        auto lformat = validate_res.unwrap();
×
UNCOV
334
        auto* ext_format = dynamic_cast<external_log_format*>(lformat.get());
×
335

336
        attr_line_t ext_details;
×
UNCOV
337
        if (ext_format != nullptr) {
×
338
            ext_details.append("\n   ")
×
339
                .append("Regexes"_h3)
×
340
                .append(": ")
×
341
                .join(ext_format->elf_pattern_order
×
342
                          | lnav::itertools::map(
×
343
                              &external_log_format::pattern::p_name)
344
                          | lnav::itertools::map(&intern_string_t::to_string),
×
UNCOV
345
                      VC_ROLE.value(role_t::VCR_SYMBOL),
×
346
                      ", ");
347
        }
348

349
        auto um
UNCOV
350
            = console::user_message::error(
×
UNCOV
351
                  attr_line_t("expecting an operation to perform on the ")
×
352
                      .append(lnav::roles::symbol(sf.sf_name))
×
353
                      .append(" format"))
×
354
                  .with_note(attr_line_t()
×
355
                                 .append(lnav::roles::symbol(sf.sf_name))
×
356
                                 .append(": ")
×
357
                                 .append(lformat->lf_description)
×
358
                                 .append(ext_details))
×
359
                  .with_help(sf.sf_format_app->get_subcommands({})
×
360
                             | lnav::itertools::fold(
×
361
                                 subcmd_reducer,
362
                                 attr_line_t{"the available operations are:"}))
×
UNCOV
363
                  .move();
×
364

365
        return {std::move(um)};
×
366
    }
367

368
    static perform_result_t default_regex_action(const subcmd_format_t& sf)
369
    {
UNCOV
370
        auto validate_res = sf.validate_regex();
×
371

372
        if (validate_res.isErr()) {
×
UNCOV
373
            return {validate_res.unwrapErr()};
×
374
        }
375

UNCOV
376
        auto um = console::user_message::error(
×
UNCOV
377
                      attr_line_t("expecting an operation to perform on the ")
×
378
                          .append(lnav::roles::symbol(sf.sf_regex_name))
×
379
                          .append(" regular expression"))
×
380
                      .with_help(
×
381
                          attr_line_t{"the available subcommands are:"}.append(
×
382
                              sf.sf_regex_app->get_subcommands({})
×
383
                              | lnav::itertools::fold(subcmd_reducer,
×
384
                                                      attr_line_t{})))
×
385
                      .move();
×
386

387
        return {std::move(um)};
×
388
    }
389

390
    static perform_result_t get_action(const subcmd_format_t& sf)
391
    {
UNCOV
392
        auto validate_res = sf.validate_format();
×
393

394
        if (validate_res.isErr()) {
×
UNCOV
395
            return {validate_res.unwrapErr()};
×
396
        }
397

UNCOV
398
        auto format = validate_res.unwrap();
×
399

400
        auto um = console::user_message::raw(
UNCOV
401
            attr_line_t()
×
UNCOV
402
                .append(lnav::roles::symbol(sf.sf_name))
×
403
                .append(": ")
×
404
                .append(on_blank(format->lf_description, "<no description>")));
×
405

406
        return {std::move(um)};
×
407
    }
408

409
    static perform_result_t test_action(const subcmd_format_t& sf)
6✔
410
    {
411
        auto validate_res = sf.validate_external_format();
6✔
412
        if (validate_res.isErr()) {
6✔
413
            return {validate_res.unwrapErr()};
×
414
        }
415
        auto* format = validate_res.unwrap();
6✔
416

417
        if (sf.sf_path.empty()) {
6✔
418
            auto um = lnav::console::user_message::error(
419
                "Expecting a file path to test");
×
420

421
            return {std::move(um)};
×
422
        }
423
        auto stat_res = lnav::filesystem::stat_file(sf.sf_path);
6✔
424
        if (stat_res.isErr()) {
6✔
UNCOV
425
            auto um = lnav::console::user_message::error(
×
426
                          attr_line_t("Unable to stat file: ")
1✔
427
                              .append(lnav::roles::file(sf.sf_path)))
2✔
428
                          .with_reason(stat_res.unwrapErr());
1✔
429

430
            return {std::move(um)};
3✔
431
        }
1✔
432
        auto st = stat_res.unwrap();
5✔
433
        if (!S_ISREG(st.st_mode)) {
5✔
UNCOV
434
            auto um = lnav::console::user_message::error(
×
435
                          attr_line_t("Unable to test with path: ")
1✔
436
                              .append(lnav::roles::file(sf.sf_path)))
2✔
437
                          .with_reason("not a regular file");
1✔
438

439
            return {std::move(um)};
3✔
440
        }
1✔
441

442
        auto open_res = lnav::filesystem::open_file(sf.sf_path, O_RDONLY);
4✔
443
        if (open_res.isErr()) {
4✔
444
            auto um = lnav::console::user_message::error(
×
UNCOV
445
                          attr_line_t("Unable to open file")
×
UNCOV
446
                              .append(lnav::roles::file(sf.sf_path)))
×
UNCOV
447
                          .with_reason(open_res.unwrapErr());
×
448

UNCOV
449
            return {std::move(um)};
×
450
        }
451
        auto test_file_fd = open_res.unwrap();
4✔
452

453
        line_buffer lb;
4✔
454
        lb.set_fd(test_file_fd);
4✔
455

456
        auto load_res = lb.load_next_line();
4✔
457
        if (load_res.isErr()) {
4✔
UNCOV
458
            auto um = lnav::console::user_message::error(
×
UNCOV
459
                          attr_line_t("Unable load line from file")
×
UNCOV
460
                              .append(lnav::roles::file(sf.sf_path)))
×
461
                          .with_reason(load_res.unwrapErr());
×
462

463
            return {std::move(um)};
×
464
        }
465
        auto li = load_res.unwrap();
4✔
466
        auto read_res = lb.read_range(li.li_file_range);
4✔
467
        if (read_res.isErr()) {
4✔
468
            auto um = lnav::console::user_message::error(
×
UNCOV
469
                          attr_line_t("Unable read line from file")
×
UNCOV
470
                              .append(lnav::roles::file(sf.sf_path)))
×
471
                          .with_reason(read_res.unwrapErr());
×
472

UNCOV
473
            return {std::move(um)};
×
474
        }
475
        auto sbr = read_res.unwrap();
4✔
476

477
        auto sample = log_format::sample_t{{
4✔
478
            intern_string::lookup("/"),
479
            source_location{
480
                intern_string::lookup(sf.sf_path),
4✔
481
                1,
482
            },
483
            sbr.to_string_fragment().rtrim("\n").to_string(),
4✔
484
        }};
8✔
485
        perform_result_t retval;
4✔
486
        auto test_res = format->test_line(sample, retval);
4✔
487
        test_res.match(
4✔
488
            [&retval, &sample](const log_format::scan_no_match& nope) {
×
489
                if (retval.empty()) {
2✔
490
                    auto um = lnav::console::user_message::error(
491
                        attr_line_t("test file did not match any of this "
2✔
492
                                    "format's patterns"));
1✔
493
                    if (nope.snm_reason != nullptr) {
1✔
494
                        um.with_reason(nope.snm_reason)
2✔
495
                            .with_snippet(lnav::console::snippet::from(
2✔
496
                                sample.s_line.pp_location,
1✔
497
                                sample.s_line.pp_value));
1✔
498
                    }
499
                    retval.emplace_back(um);
1✔
500
                }
1✔
501
            },
2✔
UNCOV
502
            [&retval, &sample](const log_format::scan_match& yep) mutable {
×
503
                auto al = attr_line_t("test file matched format");
2✔
504
                if (!sample.s_matched_regexes.empty()) {
2✔
505
                    al.append(" pattern ")
1✔
506
                        .append(lnav::roles::symbol(
1✔
507
                            *sample.s_matched_regexes.begin()));
2✔
508
                }
509

510
                auto um = lnav::console::user_message::ok(al).with_snippet(
4✔
511
                    lnav::console::snippet::from(sample.s_line.pp_location,
4✔
512
                                                 sample.s_line.pp_value));
4✔
513
                if (sample.s_matched_regexes.empty()) {
2✔
514
                    auto note_al = attr_line_t("quality value of ")
1✔
515
                                       .append(lnav::roles::number(
1✔
516
                                           fmt::to_string(yep.sm_quality)))
2✔
517
                                       .append(" and ")
1✔
518
                                       .append(lnav::roles::number(
2✔
519
                                           fmt::to_string(yep.sm_strikes)))
2✔
520
                                       .append(" strikes");
1✔
521
                    um.with_note(note_al);
1✔
522
                }
1✔
523
                retval.emplace_back(um);
2✔
524
            },
2✔
525
            [&retval](const log_format::scan_incomplete& inc) {
4✔
UNCOV
526
                auto um = lnav::console::user_message::error(attr_line_t(
×
UNCOV
527
                    "test file did not have enough data to match against"));
×
UNCOV
528
                retval.emplace_back(um);
×
529
            });
×
530

531
        return retval;
4✔
532
    }
8✔
533

534
    static perform_result_t source_action(const subcmd_format_t& sf)
535
    {
536
        auto validate_res = sf.validate_external_format();
×
537

UNCOV
538
        if (validate_res.isErr()) {
×
539
            return {validate_res.unwrapErr()};
×
540
        }
541

542
        auto* format = validate_res.unwrap();
×
543

544
        if (format->elf_format_source_order.empty()) {
×
545
            return {
546
                console::user_message::error(
547
                    "format is builtin, there is no source file"),
548
            };
549
        }
550

551
        auto um = console::user_message::raw(
UNCOV
552
            format->elf_format_source_order[0].string());
×
553

554
        return {std::move(um)};
×
555
    }
556

557
    static perform_result_t sources_action(const subcmd_format_t& sf)
558
    {
UNCOV
559
        auto validate_res = sf.validate_external_format();
×
560

561
        if (validate_res.isErr()) {
×
UNCOV
562
            return {validate_res.unwrapErr()};
×
563
        }
564

565
        auto* format = validate_res.unwrap();
×
566

567
        if (format->elf_format_source_order.empty()) {
×
568
            return {
569
                console::user_message::error(
570
                    "format is builtin, there is no source file"),
571
            };
572
        }
573

574
        auto um = console::user_message::raw(
575
            attr_line_t().join(format->elf_format_source_order,
×
576
                               VC_ROLE.value(role_t::VCR_TEXT),
×
577
                               "\n"));
×
578

UNCOV
579
        return {std::move(um)};
×
580
    }
581

582
    static perform_result_t regex101_pull_action(const subcmd_format_t& sf)
583
    {
584
        auto validate_res = sf.validate_regex();
×
UNCOV
585
        if (validate_res.isErr()) {
×
UNCOV
586
            return {validate_res.unwrapErr()};
×
587
        }
588

589
        auto format_regex_pair = validate_res.unwrap();
×
590
        auto get_meta_res
UNCOV
591
            = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
×
592

593
        return get_meta_res.match(
594
            [&sf](
×
595
                const lnav::session::regex101::error& err) -> perform_result_t {
596
                return {
597
                    console::user_message::error(
×
UNCOV
598
                        attr_line_t("unable to get DB entry for: ")
×
UNCOV
599
                            .append(lnav::roles::symbol(sf.sf_name))
×
UNCOV
600
                            .append("/")
×
601
                            .append(lnav::roles::symbol(sf.sf_regex_name)))
×
602
                        .with_reason(err.e_msg),
×
603
                };
UNCOV
604
            },
×
UNCOV
605
            [&sf](
×
606
                const lnav::session::regex101::no_entry&) -> perform_result_t {
607
                return {
608
                    console::user_message::error(
×
609
                        attr_line_t("regex ")
×
610
                            .append_quoted(
×
UNCOV
611
                                lnav::roles::symbol(sf.sf_regex_name))
×
612
                            .append(" of format ")
×
UNCOV
613
                            .append_quoted(lnav::roles::symbol(sf.sf_name))
×
UNCOV
614
                            .append(" has not been pushed to regex101.com"))
×
UNCOV
615
                        .with_help(
×
616
                            attr_line_t("use the ")
×
617
                                .append_quoted("push"_keyword)
×
618
                                .append(" subcommand to create the regex on "
×
619
                                        "regex101.com for easy editing")),
620
                };
UNCOV
621
            },
×
UNCOV
622
            [&](const lnav::session::regex101::entry& en) -> perform_result_t {
×
UNCOV
623
                auto retrieve_res = regex101::client::retrieve(en.re_permalink);
×
624

625
                return retrieve_res.match(
626
                    [&](const console::user_message& um) -> perform_result_t {
×
627
                        return {
UNCOV
628
                            console::user_message::error(
×
UNCOV
629
                                attr_line_t("unable to retrieve entry ")
×
630
                                    .append_quoted(
×
631
                                        lnav::roles::symbol(en.re_permalink))
×
632
                                    .append(" from regex101.com"))
×
UNCOV
633
                                .with_reason(um),
×
634
                        };
UNCOV
635
                    },
×
636
                    [&](const regex101::client::no_entry&) -> perform_result_t {
×
UNCOV
637
                        lnav::session::regex101::delete_entry(sf.sf_name,
×
638
                                                              sf.sf_regex_name);
×
639
                        return {
640
                            console::user_message::error(
×
UNCOV
641
                                attr_line_t("entry ")
×
642
                                    .append_quoted(
×
643
                                        lnav::roles::symbol(en.re_permalink))
×
UNCOV
644
                                    .append(
×
645
                                        " no longer exists on regex101.com"))
UNCOV
646
                                .with_help(attr_line_t("use the ")
×
UNCOV
647
                                               .append_quoted("delete"_keyword)
×
648
                                               .append(" subcommand to delete "
×
649
                                                       "the association")),
650
                        };
651
                    },
×
UNCOV
652
                    [&](const regex101::client::entry& remote_entry)
×
653
                        -> perform_result_t {
654
                        auto curr_entry = regex101::convert_format_pattern(
UNCOV
655
                            format_regex_pair.first, format_regex_pair.second);
×
656

UNCOV
657
                        if (curr_entry.e_regex == remote_entry.e_regex) {
×
658
                            return {
UNCOV
659
                                console::user_message::ok(
×
660
                                    attr_line_t("local regex is in sync "
×
661
                                                "with entry ")
UNCOV
662
                                        .append_quoted(lnav::roles::symbol(
×
663
                                            en.re_permalink))
×
664
                                        .append(" on regex101.com"))
×
UNCOV
665
                                    .with_help(
×
666
                                        attr_line_t("make edits on ")
×
667
                                            .append_quoted(lnav::roles::file(
×
668
                                                regex101::client::to_edit_url(
×
669
                                                    en.re_permalink)))
×
UNCOV
670
                                            .append(" and then run this "
×
671
                                                    "command again to update "
672
                                                    "the local values")),
673
                            };
674
                        }
675

676
                        auto patch_res
677
                            = regex101::patch(format_regex_pair.first,
×
678
                                              sf.sf_regex_name,
×
UNCOV
679
                                              remote_entry);
×
680

681
                        if (patch_res.isErr()) {
×
682
                            return {
UNCOV
683
                                console::user_message::error(
×
UNCOV
684
                                    attr_line_t(
×
685
                                        "unable to patch format regex: ")
UNCOV
686
                                        .append(lnav::roles::symbol(sf.sf_name))
×
UNCOV
687
                                        .append("/")
×
UNCOV
688
                                        .append(lnav::roles::symbol(
×
UNCOV
689
                                            sf.sf_regex_name)))
×
690
                                    .with_reason(patch_res.unwrapErr()),
×
691
                            };
692
                        }
693

694
                        auto um = console::user_message::ok(
UNCOV
695
                            attr_line_t("format patch file written to: ")
×
696
                                .append(lnav::roles::file(
×
UNCOV
697
                                    patch_res.unwrap().string())));
×
698
                        if (!format_regex_pair.first->elf_builtin_format) {
×
699
                            um.with_help(
×
700
                                attr_line_t("once the regex has been found "
×
701
                                            "to be working correctly, move the "
702
                                            "contents of the patch file to the "
703
                                            "original file at:\n   ")
704
                                    .append(lnav::roles::file(
×
705
                                        format_regex_pair.first
×
UNCOV
706
                                            ->elf_format_source_order.front()
×
707
                                            .string())));
×
708
                        }
709

UNCOV
710
                        return {std::move(um)};
×
UNCOV
711
                    });
×
UNCOV
712
            });
×
713
    }
714

715
    static perform_result_t regex101_default_action(const subcmd_format_t& sf)
716
    {
UNCOV
717
        auto validate_res = sf.validate_regex();
×
718

719
        if (validate_res.isErr()) {
×
720
            return {validate_res.unwrapErr()};
×
721
        }
722

723
        auto um = console::user_message::error(
724
            attr_line_t("expecting an operation to perform on the ")
×
725
                .append(lnav::roles::symbol(sf.sf_regex_name))
×
UNCOV
726
                .append(" regex using regex101.com"));
×
727

728
        auto get_res
UNCOV
729
            = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
×
UNCOV
730
        if (get_res.is<lnav::session::regex101::entry>()) {
×
UNCOV
731
            auto local_entry = get_res.get<lnav::session::regex101::entry>();
×
732
            um.with_note(
×
UNCOV
733
                attr_line_t("this regex is currently associated with the "
×
734
                            "following regex101.com entry:\n   ")
UNCOV
735
                    .append(lnav::roles::file(regex101::client::to_edit_url(
×
736
                        local_entry.re_permalink))));
737
        }
738

UNCOV
739
        um.with_help(attr_line_t{"the available subcommands are:"}.append(
×
UNCOV
740
            sf.sf_regex101_app->get_subcommands({})
×
UNCOV
741
            | lnav::itertools::fold(subcmd_reducer, attr_line_t{})));
×
742

743
        return {std::move(um)};
×
744
    }
745

746
    static perform_result_t regex101_push_action(const subcmd_format_t& sf)
747
    {
748
        auto validate_res = sf.validate_regex();
×
UNCOV
749
        if (validate_res.isErr()) {
×
750
            return {validate_res.unwrapErr()};
×
751
        }
752

753
        auto format_regex_pair = validate_res.unwrap();
×
UNCOV
754
        auto entry = regex101::convert_format_pattern(format_regex_pair.first,
×
UNCOV
755
                                                      format_regex_pair.second);
×
756
        auto get_meta_res
757
            = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
×
758

759
        if (get_meta_res.is<lnav::session::regex101::entry>()) {
×
760
            auto entry_meta
UNCOV
761
                = get_meta_res.get<lnav::session::regex101::entry>();
×
762
            auto retrieve_res
763
                = regex101::client::retrieve(entry_meta.re_permalink);
×
764

765
            if (retrieve_res.is<regex101::client::entry>()) {
×
766
                auto remote_entry = retrieve_res.get<regex101::client::entry>();
×
767

768
                if (remote_entry == entry) {
×
769
                    return {
770
                        console::user_message::ok(
UNCOV
771
                            attr_line_t("regex101 entry ")
×
772
                                .append(lnav::roles::symbol(
×
773
                                    entry_meta.re_permalink))
UNCOV
774
                                .append(" is already up-to-date")),
×
775
                    };
776
                }
UNCOV
777
            } else if (retrieve_res.is<console::user_message>()) {
×
778
                return {
UNCOV
779
                    retrieve_res.get<console::user_message>(),
×
780
                };
781
            }
782

UNCOV
783
            entry.e_permalink_fragment = entry_meta.re_permalink;
×
784
        }
785

UNCOV
786
        auto upsert_res = regex101::client::upsert(entry);
×
UNCOV
787
        auto upsert_info = upsert_res.unwrap();
×
788

UNCOV
789
        if (get_meta_res.is<lnav::session::regex101::no_entry>()) {
×
UNCOV
790
            lnav::session::regex101::insert_entry({
×
UNCOV
791
                format_regex_pair.first->get_name().to_string(),
×
UNCOV
792
                format_regex_pair.second->p_name.to_string(),
×
793
                upsert_info.cr_permalink_fragment,
794
                upsert_info.cr_delete_code,
795
            });
796
        }
797

798
        return {
799
            console::user_message::ok(
×
UNCOV
800
                attr_line_t("pushed regex to -- ")
×
801
                    .append(lnav::roles::file(regex101::client::to_edit_url(
×
802
                        upsert_info.cr_permalink_fragment))))
UNCOV
803
                .with_help(attr_line_t("use the ")
×
UNCOV
804
                               .append_quoted("pull"_keyword)
×
UNCOV
805
                               .append(" subcommand to update the format after "
×
806
                                       "you make changes on regex101.com")),
807
        };
808
    }
809

810
    static perform_result_t regex101_delete_action(const subcmd_format_t& sf)
811
    {
812
        auto get_res
UNCOV
813
            = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
×
814

815
        return get_res.match(
UNCOV
816
            [&sf](
×
817
                const lnav::session::regex101::entry& en) -> perform_result_t {
818
                {
UNCOV
819
                    auto validate_res = sf.validate_external_format();
×
820

UNCOV
821
                    if (validate_res.isOk()) {
×
UNCOV
822
                        auto ppath = regex101::patch_path(validate_res.unwrap(),
×
823
                                                          en.re_permalink);
×
824

825
                        if (std::filesystem::exists(ppath)) {
×
826
                            return {
UNCOV
827
                                console::user_message::error(
×
UNCOV
828
                                    attr_line_t("cannot delete regex101 entry "
×
829
                                                "while patch file exists"))
UNCOV
830
                                    .with_note(attr_line_t("  ").append(
×
UNCOV
831
                                        lnav::roles::file(ppath.string())))
×
UNCOV
832
                                    .with_help(attr_line_t(
×
833
                                        "move the contents of the patch file "
834
                                        "to the main log format and then "
835
                                        "delete the file to continue")),
836
                            };
837
                        }
838
                    }
839
                }
840

841
                perform_result_t retval;
×
842
                if (en.re_delete_code.empty()) {
×
UNCOV
843
                    retval.emplace_back(
×
844
                        console::user_message::warning(
×
UNCOV
845
                            attr_line_t("not deleting regex101 entry ")
×
UNCOV
846
                                .append_quoted(
×
UNCOV
847
                                    lnav::roles::symbol(en.re_permalink)))
×
UNCOV
848
                            .with_reason(
×
849
                                "delete code is not known for this entry")
850
                            .with_note(
851
                                "formats created by importing a regex101.com "
852
                                "entry will not have a delete code"));
853
                } else {
854
                    auto delete_res
UNCOV
855
                        = regex101::client::delete_entry(en.re_delete_code);
×
856

UNCOV
857
                    if (delete_res.isErr()) {
×
858
                        return {
UNCOV
859
                            console::user_message::error(
×
860
                                "unable to delete regex101 entry")
UNCOV
861
                                .with_reason(delete_res.unwrapErr()),
×
862
                        };
863
                    }
864
                }
865

UNCOV
866
                lnav::session::regex101::delete_entry(sf.sf_name,
×
UNCOV
867
                                                      sf.sf_regex_name);
×
868

UNCOV
869
                retval.emplace_back(console::user_message::ok(
×
UNCOV
870
                    attr_line_t("deleted regex101 entry: ")
×
871
                        .append(lnav::roles::symbol(en.re_permalink))));
×
872

UNCOV
873
                return retval;
×
874
            },
×
UNCOV
875
            [&sf](
×
876
                const lnav::session::regex101::no_entry&) -> perform_result_t {
877
                return {
878
                    console::user_message::error(
879
                        attr_line_t("no regex101 entry for ")
×
UNCOV
880
                            .append(lnav::roles::symbol(sf.sf_name))
×
881
                            .append("/")
×
UNCOV
882
                            .append(lnav::roles::symbol(sf.sf_regex_name))),
×
883
                };
UNCOV
884
            },
×
885
            [&sf](
×
886
                const lnav::session::regex101::error& err) -> perform_result_t {
887
                return {
UNCOV
888
                    console::user_message::error(
×
UNCOV
889
                        attr_line_t("unable to get regex101 entry for ")
×
UNCOV
890
                            .append(lnav::roles::symbol(sf.sf_name))
×
UNCOV
891
                            .append("/")
×
UNCOV
892
                            .append(lnav::roles::symbol(sf.sf_regex_name)))
×
UNCOV
893
                        .with_reason(err.e_msg),
×
894
                };
UNCOV
895
            });
×
896
    }
897
};
898

899
struct subcmd_piper_t {
900
    using action_t = std::function<perform_result_t(const subcmd_piper_t&)>;
901

902
    CLI::App* sp_app{nullptr};
903
    action_t sp_action;
904

905
    subcmd_piper_t& set_action(action_t act)
4✔
906
    {
907
        if (!this->sp_action) {
4✔
908
            this->sp_action = std::move(act);
2✔
909
        }
910
        return *this;
4✔
911
    }
912

913
    static perform_result_t default_action(const subcmd_piper_t& sp)
914
    {
915
        auto um
UNCOV
916
            = console::user_message::error(
×
917
                  "expecting an operation related to piper storage")
UNCOV
918
                  .with_help(sp.sp_app->get_subcommands({})
×
UNCOV
919
                             | lnav::itertools::fold(
×
920
                                 subcmd_reducer,
921
                                 attr_line_t{"the available operations are:"}))
×
922
                  .move();
×
923

924
        return {std::move(um)};
×
925
    }
926

927
    static perform_result_t list_action(const subcmd_piper_t&)
2✔
928
    {
929
        static const intern_string_t SRC = intern_string::lookup("piper");
6✔
930
        static const auto DOT_HEADER = std::filesystem::path(".header");
2✔
931

932
        struct item {
933
            lnav::piper::header i_header;
934
            std::string i_url;
935
            file_size_t i_total_size{0};
936
        };
937

938
        file_size_t grand_total{0};
2✔
939
        std::vector<item> items;
2✔
940
        std::error_code ec;
2✔
941

942
        for (const auto& instance_dir : std::filesystem::directory_iterator(
2✔
943
                 lnav::piper::storage_path(), ec))
4✔
944
        {
945
            if (!instance_dir.is_directory()) {
2✔
UNCOV
946
                log_warning("piper directory entry is not a directory: %s",
×
947
                            instance_dir.path().c_str());
948
                continue;
×
949
            }
950

951
            std::optional<lnav::piper::header> hdr_opt;
2✔
952
            auto url = fmt::format(FMT_STRING("piper://{}"),
6✔
953
                                   instance_dir.path().filename().string());
4✔
954
            file_size_t total_size{0};
2✔
955
            auto hdr_path = instance_dir / DOT_HEADER;
2✔
956
            if (std::filesystem::exists(hdr_path)) {
2✔
UNCOV
957
                auto hdr_read_res = lnav::filesystem::read_file(hdr_path);
×
958
                if (hdr_read_res.isOk()) {
×
959
                    auto hdr = string_fragment::from_str(hdr_read_res.unwrap());
×
960
                    auto parse_res
UNCOV
961
                        = lnav::piper::header_handlers.parser_for(SRC).of(hdr);
×
962
                    if (parse_res.isOk()) {
×
UNCOV
963
                        hdr_opt = parse_res.unwrap();
×
964
                    } else {
UNCOV
965
                        log_error("failed to parse header: %s -- %s",
×
966
                                  hdr_path.c_str(),
967
                                  parse_res.unwrapErr()[0]
968
                                      .to_attr_line()
969
                                      .get_string()
970
                                      .c_str());
971
                    }
UNCOV
972
                } else {
×
UNCOV
973
                    log_error("failed to read header file: %s -- %s",
×
974
                              hdr_path.c_str(),
975
                              hdr_read_res.unwrapErr().c_str());
976
                }
977
            }
978

979
            for (const auto& entry :
2✔
980
                 std::filesystem::directory_iterator(instance_dir.path()))
4✔
981
            {
982
                if (entry.path().filename() == DOT_HEADER) {
2✔
UNCOV
983
                    continue;
×
984
                }
985

986
                total_size += entry.file_size();
2✔
987
                char buffer[lnav::piper::HEADER_SIZE];
988

989
                auto entry_open_res
990
                    = lnav::filesystem::open_file(entry.path(), O_RDONLY);
2✔
991
                if (entry_open_res.isErr()) {
2✔
992
                    log_warning("unable to open piper file: %s -- %s",
×
993
                                entry.path().c_str(),
994
                                entry_open_res.unwrapErr().c_str());
UNCOV
995
                    continue;
×
996
                }
997

998
                auto entry_fd = entry_open_res.unwrap();
2✔
999
                if (read(entry_fd, buffer, sizeof(buffer)) != sizeof(buffer)) {
2✔
UNCOV
1000
                    log_warning("piper file is too small: %s",
×
1001
                                entry.path().c_str());
UNCOV
1002
                    continue;
×
1003
                }
1004
                auto hdr_bits_opt = lnav::piper::read_header(entry_fd, buffer);
2✔
1005
                if (!hdr_bits_opt) {
2✔
1006
                    log_warning("could not read piper header: %s",
×
1007
                                entry.path().c_str());
1008
                    continue;
×
1009
                }
1010

1011
                auto hdr_buf = std::move(hdr_bits_opt.value());
2✔
1012

1013
                total_size -= hdr_buf.size();
2✔
1014
                auto hdr_sf
1015
                    = string_fragment::from_bytes(hdr_buf.in(), hdr_buf.size());
2✔
1016
                auto hdr_parse_res
1017
                    = lnav::piper::header_handlers.parser_for(SRC).of(hdr_sf);
2✔
1018
                if (hdr_parse_res.isErr()) {
2✔
UNCOV
1019
                    log_error("failed to parse piper header: %s",
×
1020
                              hdr_parse_res.unwrapErr()[0]
1021
                                  .to_attr_line()
1022
                                  .get_string()
1023
                                  .c_str());
UNCOV
1024
                    continue;
×
1025
                }
1026

1027
                auto hdr = hdr_parse_res.unwrap();
2✔
1028

1029
                if (!hdr_opt || hdr < hdr_opt.value()) {
2✔
1030
                    hdr_opt = hdr;
2✔
1031
                }
1032
            }
4✔
1033

1034
            if (hdr_opt) {
2✔
1035
                items.emplace_back(item{hdr_opt.value(), url, total_size});
2✔
1036
            }
1037

1038
            grand_total += total_size;
2✔
1039
        }
4✔
1040

1041
        if (ec && ec.value() != ENOENT) {
2✔
UNCOV
1042
            auto um = lnav::console::user_message::error(
×
UNCOV
1043
                          attr_line_t("unable to access piper directory: ")
×
UNCOV
1044
                              .append(lnav::roles::file(
×
UNCOV
1045
                                  lnav::piper::storage_path().string())))
×
UNCOV
1046
                          .with_reason(ec.message())
×
UNCOV
1047
                          .move();
×
UNCOV
1048
            return {std::move(um)};
×
1049
        }
1050

1051
        if (items.empty()) {
2✔
1052
            if (verbosity != verbosity_t::quiet) {
×
1053
                auto um
1054
                    = lnav::console::user_message::info(
×
UNCOV
1055
                          attr_line_t("no piper captures were found in:\n\t")
×
1056
                              .append(lnav::roles::file(
×
UNCOV
1057
                                  lnav::piper::storage_path().string())))
×
UNCOV
1058
                          .with_help(
×
UNCOV
1059
                              attr_line_t("You can create a capture by "
×
1060
                                          "piping data into ")
UNCOV
1061
                                  .append(lnav::roles::file("lnav"))
×
UNCOV
1062
                                  .append(" or using the ")
×
UNCOV
1063
                                  .append_quoted(lnav::roles::symbol(":sh"))
×
UNCOV
1064
                                  .append(" command"))
×
UNCOV
1065
                          .move();
×
UNCOV
1066
                return {std::move(um)};
×
1067
            }
1068

UNCOV
1069
            return {};
×
1070
        }
1071

1072
        auto txt
1073
            = items
1074
            | lnav::itertools::sort_with([](const item& lhs, const item& rhs) {
4✔
1075
                  if (lhs.i_header < rhs.i_header) {
×
1076
                      return true;
×
1077
                  }
1078

UNCOV
1079
                  if (rhs.i_header < lhs.i_header) {
×
UNCOV
1080
                      return false;
×
1081
                  }
1082

UNCOV
1083
                  return lhs.i_url < rhs.i_url;
×
1084
              })
1085
            | lnav::itertools::map([](const item& it) {
6✔
1086
                  auto ago = humanize::time::point::from_tv(it.i_header.h_ctime)
2✔
1087
                                 .as_time_ago();
2✔
1088
                  auto retval = attr_line_t()
2✔
1089
                                    .append(lnav::roles::list_glyph(
2✔
1090
                                        fmt::format(FMT_STRING("{:>18}"), ago)))
10✔
1091
                                    .append("  ")
2✔
1092
                                    .append(lnav::roles::file(it.i_url))
4✔
1093
                                    .append(" ")
2✔
1094
                                    .append(lnav::roles::number(fmt::format(
4✔
1095
                                        FMT_STRING("{:>8}"),
6✔
1096
                                        humanize::file_size(
2✔
1097
                                            it.i_total_size,
2✔
1098
                                            humanize::alignment::columnar))))
1099
                                    .append(" ")
2✔
1100
                                    .append_quoted(lnav::roles::comment(
4✔
1101
                                        it.i_header.h_name))
2✔
1102
                                    .append("\n");
2✔
1103
                  if (verbosity == verbosity_t::verbose) {
2✔
1104
                      auto env_al
UNCOV
1105
                          = it.i_header.h_env
×
UNCOV
1106
                          | lnav::itertools::map([](const auto& pair) {
×
UNCOV
1107
                                return attr_line_t()
×
1108
                                    .append(lnav::roles::identifier(pair.first))
×
1109
                                    .append("=")
×
UNCOV
1110
                                    .append(pair.second)
×
1111
                                    .append("\n");
×
1112
                            })
1113
                          | lnav::itertools::fold(
×
1114
                                [](const auto& elem, auto& accum) {
×
1115
                                    if (!accum.empty()) {
×
UNCOV
1116
                                        accum.append(28, ' ');
×
1117
                                    }
1118
                                    return accum.append(elem);
×
1119
                                },
UNCOV
1120
                                attr_line_t());
×
1121

UNCOV
1122
                      retval.append(23, ' ')
×
UNCOV
1123
                          .append("cwd: ")
×
1124
                          .append(lnav::roles::file(it.i_header.h_cwd))
×
1125
                          .append("\n")
×
UNCOV
1126
                          .append(23, ' ')
×
1127
                          .append("env: ")
×
UNCOV
1128
                          .append(env_al);
×
1129
                  }
1130
                  return retval;
4✔
1131
              })
2✔
1132
            | lnav::itertools::fold(
4✔
1133
                  [](const auto& elem, auto& accum) {
2✔
1134
                      return accum.append(elem);
2✔
1135
                  },
1136
                  attr_line_t{});
6✔
1137
        txt.rtrim();
2✔
1138

1139
        perform_result_t retval;
2✔
1140
        if (verbosity != verbosity_t::quiet) {
2✔
1141
            auto extra_um
1142
                = lnav::console::user_message::info(
×
1143
                      attr_line_t(
1✔
1144
                          "the following piper captures were found in:\n\t")
1145
                          .append(lnav::roles::file(
2✔
1146
                              lnav::piper::storage_path().string())))
2✔
1147
                      .with_note(
2✔
1148
                          attr_line_t("The captures currently consume ")
2✔
1149
                              .append(lnav::roles::number(humanize::file_size(
2✔
1150
                                  grand_total, humanize::alignment::none)))
1151
                              .append(" of disk space.  File sizes include "
1✔
1152
                                      "associated metadata."))
1153
                      .with_help(
2✔
1154
                          "You can reopen a capture by passing the piper URL "
1155
                          "to lnav")
1156
                      .move();
1✔
1157
            retval.emplace_back(extra_um);
1✔
1158
        }
1✔
1159
        retval.emplace_back(lnav::console::user_message::raw(txt));
2✔
1160

1161
        return retval;
2✔
1162
    }
2✔
1163

1164
    static perform_result_t clean_action(const subcmd_piper_t&)
1165
    {
1166
        std::error_code ec;
×
1167

1168
        std::filesystem::remove_all(lnav::piper::storage_path(), ec);
×
1169
        if (ec) {
×
1170
            return {
1171
                lnav::console::user_message::error(
×
1172
                    "unable to remove piper storage directory")
UNCOV
1173
                    .with_reason(ec.message()),
×
1174
            };
1175
        }
1176

1177
        return {};
×
1178
    }
1179
};
1180

1181
struct subcmd_regex101_t {
1182
    using action_t = std::function<perform_result_t(const subcmd_regex101_t&)>;
1183

1184
    CLI::App* sr_app{nullptr};
1185
    action_t sr_action;
1186
    std::string sr_import_url;
1187
    std::string sr_import_name;
1188
    std::string sr_import_regex_name{"std"};
1189

UNCOV
1190
    subcmd_regex101_t& set_action(action_t act)
×
1191
    {
UNCOV
1192
        if (!this->sr_action) {
×
UNCOV
1193
            this->sr_action = std::move(act);
×
1194
        }
1195
        return *this;
×
1196
    }
1197

1198
    static perform_result_t default_action(const subcmd_regex101_t& sr)
1199
    {
1200
        auto um
UNCOV
1201
            = console::user_message::error(
×
1202
                  "expecting an operation related to the regex101.com "
1203
                  "integration")
1204
                  .with_help(sr.sr_app->get_subcommands({})
×
UNCOV
1205
                             | lnav::itertools::fold(
×
1206
                                 subcmd_reducer,
1207
                                 attr_line_t{"the available operations are:"}))
×
1208
                  .move();
×
1209

UNCOV
1210
        return {std::move(um)};
×
1211
    }
1212

1213
    static perform_result_t list_action(const subcmd_regex101_t&)
1214
    {
UNCOV
1215
        auto get_res = lnav::session::regex101::get_entries();
×
1216

UNCOV
1217
        if (get_res.isErr()) {
×
1218
            return {
1219
                console::user_message::error(
×
1220
                    "unable to read regex101 entries from DB")
1221
                    .with_reason(get_res.unwrapErr()),
×
1222
            };
1223
        }
1224

1225
        auto entries = get_res.unwrap()
×
1226
            | lnav::itertools::map([](const auto& elem) {
×
1227
                           return fmt::format(
1228
                               FMT_STRING("   format {} regex {} regex101\n"),
×
1229
                               elem.re_format_name,
×
1230
                               elem.re_regex_name);
×
1231
                       })
UNCOV
1232
            | lnav::itertools::fold(
×
1233
                           [](const auto& elem, auto& accum) {
×
UNCOV
1234
                               return accum.append(elem);
×
1235
                           },
UNCOV
1236
                           attr_line_t{});
×
1237

1238
        auto um = console::user_message::ok(
1239
            entries.add_header("the following regex101 entries were found:\n")
×
UNCOV
1240
                .with_default("no regex101 entries found"));
×
1241

1242
        return {std::move(um)};
×
1243
    }
1244

1245
    static perform_result_t import_action(const subcmd_regex101_t& sr)
1246
    {
1247
        auto import_res = regex101::import(
UNCOV
1248
            sr.sr_import_url, sr.sr_import_name, sr.sr_import_regex_name);
×
1249

UNCOV
1250
        if (import_res.isOk()) {
×
1251
            return {
UNCOV
1252
                lnav::console::user_message::ok(
×
UNCOV
1253
                    attr_line_t("converted regex101 entry to format file: ")
×
UNCOV
1254
                        .append(lnav::roles::file(import_res.unwrap())))
×
UNCOV
1255
                    .with_note("the converted format may still have errors")
×
UNCOV
1256
                    .with_help(
×
UNCOV
1257
                        attr_line_t(
×
1258
                            "use the following command to patch the regex as "
1259
                            "more changes are made on regex101.com:\n")
UNCOV
1260
                            .appendf(FMT_STRING("   lnav -m format {} regex {} "
×
1261
                                                "regex101 pull"),
UNCOV
1262
                                     sr.sr_import_name,
×
UNCOV
1263
                                     sr.sr_import_regex_name)),
×
1264
            };
1265
        }
1266

1267
        return {
1268
            import_res.unwrapErr(),
1269
        };
1270
    }
1271
};
1272

1273
struct subcmd_crash_t {
1274
    using action_t = std::function<perform_result_t(const subcmd_crash_t&)>;
1275

1276
    CLI::App* sc_app{nullptr};
1277
    action_t sc_action;
1278

UNCOV
1279
    subcmd_crash_t& set_action(action_t act)
×
1280
    {
UNCOV
1281
        if (!this->sc_action) {
×
UNCOV
1282
            this->sc_action = std::move(act);
×
1283
        }
UNCOV
1284
        return *this;
×
1285
    }
1286

1287
    static perform_result_t default_action(const subcmd_crash_t& sc)
1288
    {
1289
        auto um
UNCOV
1290
            = console::user_message::error(
×
1291
                  "expecting an operation related to crash logs")
UNCOV
1292
                  .with_help(sc.sc_app->get_subcommands({})
×
UNCOV
1293
                             | lnav::itertools::fold(
×
1294
                                 subcmd_reducer,
UNCOV
1295
                                 attr_line_t{"the available operations are:"}))
×
UNCOV
1296
                  .move();
×
1297

UNCOV
1298
        return {std::move(um)};
×
1299
    }
1300

1301
    static perform_result_t upload_action(const subcmd_crash_t&)
1302
    {
1303
        static constexpr char SPINNER_CHARS[] = "-\\|/";
UNCOV
1304
        constexpr size_t SPINNER_SIZE = sizeof(SPINNER_CHARS) - 1;
×
1305

1306
        static_root_mem<glob_t, globfree> gl;
1307
        const auto path = lnav::paths::dotlnav() / "crash" / "crash-*.log";
×
UNCOV
1308
        perform_result_t retval;
×
1309

UNCOV
1310
        auto glob_rc = glob(path.c_str(), 0, nullptr, gl.inout());
×
UNCOV
1311
        if (glob_rc == GLOB_NOMATCH) {
×
UNCOV
1312
            auto um = console::user_message::info("no crash logs to upload");
×
UNCOV
1313
            return {std::move(um)};
×
1314
        }
1315
        if (glob_rc != 0) {
×
1316
            auto um = console::user_message::error("unable to find crash logs");
×
1317
            return {std::move(um)};
×
1318
        }
1319

UNCOV
1320
        for (size_t lpc = 0; lpc < gl->gl_pathc; lpc++) {
×
UNCOV
1321
            auto crash_file = std::filesystem::path(gl->gl_pathv[lpc]);
×
UNCOV
1322
            int spinner_index = 0;
×
1323

UNCOV
1324
            log_info("uploading crash log: %s", crash_file.c_str());
×
UNCOV
1325
            printf("~");
×
UNCOV
1326
            fflush(stdout);
×
1327
            auto upload_res = crashd::client::upload(
1328
                crash_file,
1329
                [&spinner_index](double dltotal,
×
1330
                                 double dlnow,
1331
                                 double ultotal,
1332
                                 double ulnow) {
UNCOV
1333
                    printf("\b%c", SPINNER_CHARS[spinner_index % SPINNER_SIZE]);
×
UNCOV
1334
                    spinner_index += 1;
×
UNCOV
1335
                    fflush(stdout);
×
UNCOV
1336
                    return crashd::client::progress_result_t::ok;
×
UNCOV
1337
                });
×
1338
            if (spinner_index > 0) {
×
1339
                printf("\b");
×
1340
            }
UNCOV
1341
            printf(".");
×
UNCOV
1342
            fflush(stdout);
×
UNCOV
1343
            if (upload_res.isErr()) {
×
UNCOV
1344
                retval.push_back(upload_res.unwrapErr());
×
1345
            } else {
UNCOV
1346
                std::error_code ec;
×
1347

1348
                std::filesystem::remove(crash_file, ec);
×
1349
            }
1350
        }
1351

UNCOV
1352
        printf("\n");
×
1353
        auto um = console::user_message::ok(
UNCOV
1354
            attr_line_t("uploaded ")
×
UNCOV
1355
                .append(lnav::roles::number(fmt::to_string(gl->gl_pathc)))
×
UNCOV
1356
                .append(" crash logs, thank you!"));
×
UNCOV
1357
        retval.push_back(um);
×
1358

1359
        return retval;
×
1360
    }
1361
};
1362

1363
using operations_v = mapbox::util::variant<no_subcmd_t,
1364
                                           subcmd_config_t,
1365
                                           subcmd_format_t,
1366
                                           subcmd_piper_t,
1367
                                           subcmd_regex101_t,
1368
                                           subcmd_crash_t>;
1369

1370
class operations {
1371
public:
1372
    operations_v o_ops;
1373
};
1374

1375
std::shared_ptr<operations>
1376
describe_cli(CLI::App& app, int argc, char* argv[])
10✔
1377
{
1378
    auto retval = std::make_shared<operations>();
10✔
1379

1380
    retval->o_ops = no_subcmd_t{
20✔
1381
        &app,
1382
    };
10✔
1383

1384
    app.add_flag("-m", "Switch to the management CLI mode.");
10✔
1385

1386
    subcmd_config_t config_args;
10✔
1387
    subcmd_format_t format_args;
10✔
1388
    subcmd_piper_t piper_args;
10✔
1389
    subcmd_regex101_t regex101_args;
10✔
1390
    subcmd_crash_t crash_args;
10✔
1391

1392
    {
1393
        auto* subcmd_config
1394
            = app.add_subcommand("config",
1395
                                 "perform operations on the lnav configuration")
1396
                  ->callback([&]() {
30✔
1397
                      config_args.set_action(subcmd_config_t::default_action);
2✔
1398
                      retval->o_ops = config_args;
2✔
1399
                  });
2✔
1400
        config_args.sc_config_app = subcmd_config;
10✔
1401

1402
        subcmd_config->add_subcommand("get", "print the current configuration")
1403
            ->callback(
50✔
1404
                [&]() { config_args.set_action(subcmd_config_t::get_action); });
21✔
1405

1406
        subcmd_config
1407
            ->add_subcommand("blame",
1408
                             "print the configuration options and their source")
1409
            ->callback([&]() {
40✔
1410
                config_args.set_action(subcmd_config_t::blame_action);
1✔
1411
            });
1✔
1412

1413
        auto* sub_file_options = subcmd_config->add_subcommand(
40✔
1414
            "file-options", "print the options applied to specific files");
1415

1416
        sub_file_options->add_option(
40✔
1417
            "path", config_args.sc_path, "the path to the file");
1418
        sub_file_options->callback([&]() {
10✔
UNCOV
1419
            config_args.set_action(subcmd_config_t::file_options_action);
×
UNCOV
1420
        });
×
1421
    }
1422

1423
    {
1424
        auto* subcmd_format
1425
            = app.add_subcommand("format",
1426
                                 "perform operations on log file formats")
1427
                  ->callback([&]() {
30✔
1428
                      format_args.set_action(subcmd_format_t::default_action);
6✔
1429
                      retval->o_ops = format_args;
6✔
1430
                  });
6✔
1431
        format_args.sf_format_app = subcmd_format;
10✔
1432
        subcmd_format
1433
            ->add_option(
1434
                "format_name", format_args.sf_name, "the name of the format")
1435
            ->expected(1);
40✔
1436

1437
        {
1438
            subcmd_format
1439
                ->add_subcommand("get", "print information about a format")
1440
                ->callback([&]() {
40✔
1441
                    format_args.set_action(subcmd_format_t::get_action);
×
UNCOV
1442
                });
×
1443
        }
1444

1445
        {
1446
            auto* subcmd_format_test
1447
                = subcmd_format
1448
                      ->add_subcommand("test",
1449
                                       "test this format against a file")
1450
                      ->callback([&]() {
40✔
1451
                          format_args.set_action(subcmd_format_t::test_action);
6✔
1452
                      });
6✔
1453
            subcmd_format_test->add_option(
40✔
1454
                "path", format_args.sf_path, "the path to the file to test");
1455
        }
1456

1457
        {
1458
            subcmd_format
1459
                ->add_subcommand("source",
1460
                                 "print the path of the first source file "
1461
                                 "containing this format")
1462
                ->callback([&]() {
40✔
UNCOV
1463
                    format_args.set_action(subcmd_format_t::source_action);
×
UNCOV
1464
                });
×
1465
        }
1466

1467
        {
1468
            subcmd_format
1469
                ->add_subcommand("sources",
1470
                                 "print the paths of all source files "
1471
                                 "containing this format")
1472
                ->callback([&]() {
40✔
UNCOV
1473
                    format_args.set_action(subcmd_format_t::sources_action);
×
UNCOV
1474
                });
×
1475
        }
1476

1477
        {
1478
            auto* subcmd_format_regex
1479
                = subcmd_format
1480
                      ->add_subcommand(
1481
                          "regex",
1482
                          "operate on the format's regular expressions")
1483
                      ->callback([&]() {
40✔
1484
                          format_args.set_action(
×
1485
                              subcmd_format_t::default_regex_action);
UNCOV
1486
                      });
×
1487
            format_args.sf_regex_app = subcmd_format_regex;
10✔
1488
            subcmd_format_regex->add_option(
40✔
1489
                "regex-name",
1490
                format_args.sf_regex_name,
1491
                "the name of the regular expression to operate on");
1492

1493
            {
1494
                auto* subcmd_format_regex_regex101
1495
                    = subcmd_format_regex
1496
                          ->add_subcommand("regex101",
1497
                                           "use regex101.com to edit this "
1498
                                           "regular expression")
1499
                          ->callback([&]() {
40✔
UNCOV
1500
                              format_args.set_action(
×
1501
                                  subcmd_format_t::regex101_default_action);
UNCOV
1502
                          });
×
1503
                format_args.sf_regex101_app = subcmd_format_regex_regex101;
10✔
1504

1505
                {
1506
                    subcmd_format_regex_regex101
1507
                        ->add_subcommand("push",
1508
                                         "create/update an entry for "
1509
                                         "this regex on regex101.com")
1510
                        ->callback([&]() {
40✔
1511
                            format_args.set_action(
×
1512
                                subcmd_format_t::regex101_push_action);
UNCOV
1513
                        });
×
1514
                    subcmd_format_regex_regex101
1515
                        ->add_subcommand(
1516
                            "pull",
1517
                            "create a patch format file for this "
1518
                            "regular expression based on the entry in "
1519
                            "regex101.com")
1520
                        ->callback([&]() {
40✔
UNCOV
1521
                            format_args.set_action(
×
1522
                                subcmd_format_t::regex101_pull_action);
UNCOV
1523
                        });
×
1524
                    subcmd_format_regex_regex101
1525
                        ->add_subcommand(
1526
                            "delete",
1527
                            "delete the entry regex101.com that was "
1528
                            "created by a push operation")
1529
                        ->callback([&]() {
50✔
UNCOV
1530
                            format_args.set_action(
×
1531
                                subcmd_format_t::regex101_delete_action);
UNCOV
1532
                        });
×
1533
                }
1534
            }
1535
        }
1536
    }
1537

1538
    {
1539
        auto* subcmd_piper
1540
            = app.add_subcommand("piper", "perform operations on piper storage")
1541
                  ->callback([&]() {
30✔
1542
                      piper_args.set_action(subcmd_piper_t::default_action);
2✔
1543
                      retval->o_ops = piper_args;
2✔
1544
                  });
2✔
1545
        piper_args.sp_app = subcmd_piper;
10✔
1546

1547
        subcmd_piper
1548
            ->add_subcommand("list", "print the available piper captures")
1549
            ->callback(
50✔
1550
                [&]() { piper_args.set_action(subcmd_piper_t::list_action); });
22✔
1551

1552
        subcmd_piper->add_subcommand("clean", "remove all piper captures")
1553
            ->callback(
60✔
1554
                [&]() { piper_args.set_action(subcmd_piper_t::clean_action); });
20✔
1555
    }
1556

1557
    {
1558
        auto* subcmd_regex101
1559
            = app.add_subcommand("regex101",
1560
                                 "create and edit log message regular "
1561
                                 "expressions using regex101.com")
1562
                  ->callback([&]() {
30✔
UNCOV
1563
                      regex101_args.set_action(
×
1564
                          subcmd_regex101_t::default_action);
UNCOV
1565
                      retval->o_ops = regex101_args;
×
UNCOV
1566
                  });
×
1567
        regex101_args.sr_app = subcmd_regex101;
10✔
1568

1569
        {
1570
            subcmd_regex101
1571
                ->add_subcommand("list",
1572
                                 "list the log format regular expression "
1573
                                 "linked to entries on regex101.com")
1574
                ->callback([&]() {
40✔
UNCOV
1575
                    regex101_args.set_action(subcmd_regex101_t::list_action);
×
UNCOV
1576
                });
×
1577
        }
1578
        {
1579
            auto* subcmd_regex101_import
1580
                = subcmd_regex101
1581
                      ->add_subcommand("import",
1582
                                       "create a new format from a regular "
1583
                                       "expression on regex101.com")
1584
                      ->callback([&]() {
40✔
UNCOV
1585
                          regex101_args.set_action(
×
1586
                              subcmd_regex101_t::import_action);
UNCOV
1587
                      });
×
1588

1589
            subcmd_regex101_import->add_option(
40✔
1590
                "url",
1591
                regex101_args.sr_import_url,
1592
                "The regex101.com url to construct a log format from");
1593
            subcmd_regex101_import->add_option("name",
40✔
1594
                                               regex101_args.sr_import_name,
1595
                                               "The name for the log format");
1596
            subcmd_regex101_import
1597
                ->add_option("regex-name",
60✔
1598
                             regex101_args.sr_import_regex_name,
1599
                             "The name for the new regex")
1600
                ->always_capture_default();
10✔
1601
        }
1602
    }
1603

1604
    {
1605
        auto* subcmd_crash
1606
            = app.add_subcommand("crash", "manage crash logs")->callback([&]() {
30✔
UNCOV
1607
                  crash_args.set_action(subcmd_crash_t::default_action);
×
UNCOV
1608
                  retval->o_ops = crash_args;
×
UNCOV
1609
              });
×
1610
        crash_args.sc_app = subcmd_crash;
10✔
1611

1612
        {
1613
            subcmd_crash->add_subcommand("upload", "upload crash logs")
1614
                ->callback([&]() {
40✔
UNCOV
1615
                    crash_args.set_action(subcmd_crash_t::upload_action);
×
UNCOV
1616
                });
×
1617
        }
1618
    }
1619

1620
    app.parse(argc, argv);
10✔
1621

1622
    return retval;
20✔
1623
}
10✔
1624

1625
perform_result_t
1626
perform(std::shared_ptr<operations> opts)
10✔
1627
{
1628
    return opts->o_ops.match(
10✔
UNCOV
1629
        [](const no_subcmd_t& ns) -> perform_result_t {
×
UNCOV
1630
            auto um = console::user_message::error(
×
UNCOV
1631
                          attr_line_t("expecting an operation to perform"))
×
UNCOV
1632
                          .with_help(
×
UNCOV
1633
                              ns.ns_root_app->get_subcommands({})
×
UNCOV
1634
                              | lnav::itertools::fold(
×
1635
                                  subcmd_reducer,
UNCOV
1636
                                  attr_line_t{"the available operations are:"}))
×
UNCOV
1637
                          .move();
×
1638

UNCOV
1639
            return {std::move(um)};
×
UNCOV
1640
        },
×
1641
        [](const subcmd_config_t& sc) { return sc.sc_action(sc); },
2✔
1642
        [](const subcmd_format_t& sf) { return sf.sf_action(sf); },
6✔
1643
        [](const subcmd_piper_t& sp) { return sp.sp_action(sp); },
2✔
UNCOV
1644
        [](const subcmd_regex101_t& sr) { return sr.sr_action(sr); },
×
1645
        [](const subcmd_crash_t& sc) { return sc.sc_action(sc); });
20✔
1646
}
1647

1648
}  // namespace lnav::management
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