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

tstack / lnav / 21219323786-2766

21 Jan 2026 05:28PM UTC coverage: 68.961% (-0.04%) from 68.999%
21219323786-2766

push

github

tstack
[log_format] display an error if there is a JSON timestamp property, but it does not parse correctly

41 of 50 new or added lines in 3 files covered. (82.0%)

40 existing lines in 6 files now uncovered.

51838 of 75170 relevant lines covered (68.96%)

437089.4 hits per line

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

28.54
/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 <functional>
31

32
#include "lnav.management_cli.hh"
33

34
#include <glob.h>
35
#include <pwd.h>
36

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

60
using namespace std::string_view_literals;
61
using namespace lnav::roles::literals;
62

63
namespace lnav::management {
64

65
struct no_subcmd_t {
66
    CLI::App* ns_root_app{nullptr};
67
};
68

69
static auto DEFAULT_WRAPPING
70
    = text_wrap_settings{}.with_padding_indent(4).with_width(60);
71

72
inline attr_line_t&
73
symbol_reducer(const std::string& elem, attr_line_t& accum)
×
74
{
75
    if (!accum.empty()) {
×
76
        accum.append(", ");
×
77
    }
78
    return accum.append(lnav::roles::symbol(elem));
×
79
}
80

81
inline attr_line_t&
82
subcmd_reducer(const CLI::App* app, attr_line_t& accum)
×
83
{
84
    return accum.append("\n ")
×
85
        .append("\u2022"_list_glyph)
×
86
        .append(" ")
×
87
        .append(lnav::roles::keyword(app->get_name()))
×
88
        .append(": ")
×
89
        .append(app->get_description());
×
90
}
91

92
static const auto INDEX_MD_TEMPLATE = R"(---
93
title: lnav app: {title}
94
---
95

96
# {title}
97

98
The following text was generated dynamically by an lnav code block in this
99
Markdown file:
100

101
``` {{ .lnav .eval-and-replace }}
102
:echo <blockquote>Hello, ${{USER}}!</blockquote>
103
```
104

105

106
Check out the API test app to learn more about what is possible.  Then,
107
edit this file and reload the page to see the results.
108

109
)"sv;
110

111
struct subcmd_apps_t {
112
    using action_t = std::function<perform_result_t(const subcmd_apps_t&)>;
113

114
    CLI::App* sa_apps_app{nullptr};
115
    action_t sa_action;
116
    std::string sa_name;
117

118
    static perform_result_t default_action(const subcmd_apps_t& sc)
119
    {
120
        auto um
121
            = console::user_message::error(
×
122
                  "expecting an operation related to lnav apps")
123
                  .with_help(sc.sa_apps_app->get_subcommands({})
×
124
                             | lnav::itertools::fold(
×
125
                                 subcmd_reducer,
126
                                 attr_line_t{"the available operations are:"}))
×
127
                  .move();
×
128

129
        return {std::move(um)};
×
130
    }
131

132
    static perform_result_t create_action(const subcmd_apps_t& sa)
133
    {
134
        auto configs_dir = lnav::paths::dotlnav() / "configs";
×
135
        std::string publisher;
×
136

137
        auto* user_env = getenv("USER");
×
138
        if (user_env) {
×
139
            publisher = user_env;
×
140
        } else {
141
            auto* user_id = getpwuid(getuid());
×
142
            if (user_id) {
×
143
                publisher = user_id->pw_name;
×
144
            }
145
        }
146
        if (publisher.empty()) {
×
147
            auto um = console::user_message::error(
×
148
                          "Unable to determine the publisher for the app")
149
                          .with_help(attr_line_t("Set the ")
×
150
                                         .append("USER"_variable)
×
151
                                         .append(" environment variable"));
×
152
            return {std::move(um)};
×
153
        }
154

155
        auto dst_dir = configs_dir / sa.sa_name;
×
156
        if (std::filesystem::exists(dst_dir)) {
×
157
            auto um = console::user_message::error(
×
158
                          attr_line_t("app directory already exists: ")
×
159
                              .append(lnav::roles::file(dst_dir.string())))
×
160
                          .with_help(attr_line_t("Delete the directory or "
×
161
                                                 "choose a different name"));
×
162
            return {std::move(um)};
×
163
        }
164

165
        yajlpp_gen gen;
×
166
        {
167
            yajlpp_map root_map(gen);
×
168

169
            root_map.gen("$schema");
×
170
            root_map.gen(DEFAULT_CONFIG_SCHEMA);
×
171

172
            root_map.gen("apps");
×
173
            {
174
                yajlpp_map apps_map(gen);
×
175

176
                apps_map.gen(publisher);
×
177
                {
178
                    yajlpp_map publisher_map(gen);
×
179

180
                    publisher_map.gen(sa.sa_name);
×
181
                    {
182
                        yajlpp_map app_map(gen);
×
183

184
                        apps_map.gen("root");
×
185
                        apps_map.gen("app-files");
×
186

187
                        apps_map.gen("description");
×
188
                        apps_map.gen("My fancy new lnav app");
×
189
                    }
190
                }
191
            }
192
        }
193

194
        auto app_files_dir = dst_dir / "app-files";
×
195
        std::error_code ec;
×
196
        std::filesystem::create_directories(app_files_dir, ec);
×
197
        if (ec) {
×
198
            auto um = console::user_message::error(
×
199
                          attr_line_t("cannot create app directory: ")
×
200
                              .append(lnav::roles::file(dst_dir.string())))
×
201
                          .with_reason(ec.message());
×
202

203
            return {std::move(um)};
×
204
        }
205

206
        auto app_config_path = dst_dir / "app.json";
×
207
        auto config_write_res = lnav::filesystem::write_file(
208
            app_config_path, gen.to_string_fragment());
×
209
        if (config_write_res.isErr()) {
×
210
            std::filesystem::remove_all(dst_dir, ec);
×
211
            auto um
212
                = console::user_message::error(
×
213
                      attr_line_t("cannot write app config file: ")
×
214
                          .append(lnav::roles::file(app_config_path.string())))
×
215
                      .with_reason(config_write_res.unwrapErr());
×
216
            return {std::move(um)};
×
217
        }
218

219
        auto index_md
220
            = fmt::format(INDEX_MD_TEMPLATE, fmt::arg("title", sa.sa_name));
×
221
        auto index_write_res = lnav::filesystem::write_file(
222
            app_files_dir / "index.md", index_md);
×
223
        if (index_write_res.isErr()) {
×
224
            std::filesystem::remove_all(dst_dir, ec);
×
225
            auto um
226
                = console::user_message::error(
×
227
                      attr_line_t("cannot write app config file: ")
×
228
                          .append(lnav::roles::file(app_config_path.string())))
×
229
                      .with_reason(index_write_res.unwrapErr());
×
230
            return {std::move(um)};
×
231
        }
232

233
        auto um
234
            = lnav::console::user_message::info(
×
235
                  attr_line_t("created app directory: ")
×
236
                      .append(lnav::roles::file(dst_dir.string())))
×
237
                  .with_note(
×
238
                      "edit the app.json file in the directory to configure "
239
                      "the app")
240
                  .with_note(
×
241
                      "edit the app-files/index.md file to implement the app");
×
242

243
        return {std::move(um)};
×
244
    }
245

246
    subcmd_apps_t& set_action(action_t act)
×
247
    {
248
        if (!this->sa_action) {
×
249
            this->sa_action = std::move(act);
×
250
        }
251
        return *this;
×
252
    }
253
};
254

255
struct subcmd_config_t {
256
    using action_t = std::function<perform_result_t(const subcmd_config_t&)>;
257

258
    CLI::App* sc_config_app{nullptr};
259
    action_t sc_action;
260
    std::string sc_path;
261

262
    static perform_result_t default_action(const subcmd_config_t& sc)
263
    {
264
        auto um
265
            = console::user_message::error(
×
266
                  "expecting an operation related to lnav configuration")
267
                  .with_help(sc.sc_config_app->get_subcommands({})
×
268
                             | lnav::itertools::fold(
×
269
                                 subcmd_reducer,
270
                                 attr_line_t{"the available operations are:"}))
×
271
                  .move();
×
272

273
        return {std::move(um)};
×
274
    }
275

276
    static perform_result_t get_action(const subcmd_config_t&)
1✔
277
    {
278
        auto config_str = dump_config();
1✔
279
        auto um = console::user_message::raw(config_str);
1✔
280

281
        return {std::move(um)};
4✔
282
    }
2✔
283

284
    static perform_result_t blame_action(const subcmd_config_t&)
1✔
285
    {
286
        auto blame = attr_line_t();
1✔
287

288
        for (const auto& pair : lnav_config_locations) {
1,536✔
289
            blame.appendf(FMT_STRING("{} -> {}:{}\n"),
4,605✔
290
                          pair.first,
1,535✔
291
                          pair.second.sl_source,
1,535✔
292
                          pair.second.sl_line_number);
1,535✔
293
        }
294

295
        auto um = console::user_message::raw(blame.rtrim());
1✔
296

297
        return {std::move(um)};
4✔
298
    }
2✔
299

300
    static perform_result_t file_options_action(const subcmd_config_t& sc)
301
    {
302
        auto& safe_options_hier
303
            = injector::get<lnav::safe_file_options_hier&>();
×
304

305
        if (sc.sc_path.empty()) {
×
306
            auto um = lnav::console::user_message::error(
307
                "Expecting a file path to check for options");
×
308

309
            return {std::move(um)};
×
310
        }
311

312
        safe::ReadAccess<lnav::safe_file_options_hier> options_hier(
313
            safe_options_hier);
×
314

315
        auto realpath_res = lnav::filesystem::realpath(sc.sc_path);
×
316
        if (realpath_res.isErr()) {
×
317
            auto um = lnav::console::user_message::error(
×
318
                          attr_line_t("Unable to get full path for file: ")
×
319
                              .append(lnav::roles::file(sc.sc_path)))
×
320
                          .with_reason(realpath_res.unwrapErr())
×
321
                          .move();
×
322

323
            return {std::move(um)};
×
324
        }
325
        auto full_path = realpath_res.unwrap();
×
326
        auto file_opts = options_hier->match(full_path);
×
327
        if (file_opts) {
×
328
            auto content = attr_line_t()
×
329
                               .append(file_opts->second.to_json_string()
×
330
                                           .to_string_fragment())
×
331
                               .move();
×
332
            auto um = lnav::console::user_message::raw(content);
×
333
            perform_result_t retval;
×
334

335
            retval.emplace_back(um);
×
336

337
            return retval;
×
338
        }
339

340
        auto um
341
            = lnav::console::user_message::info(
×
342
                  attr_line_t("no options found for file: ")
×
343
                      .append(lnav::roles::file(full_path.string())))
×
344
                  .with_help(
×
345
                      attr_line_t("Use the ")
×
346
                          .append(":set-file-timezone"_symbol)
×
347
                          .append(
×
348
                              " command to set the zone for messages in files "
349
                              "that do not include a zone in the timestamp"))
350
                  .move();
×
351

352
        return {std::move(um)};
×
353
    }
354

355
    subcmd_config_t& set_action(action_t act)
4✔
356
    {
357
        if (!this->sc_action) {
4✔
358
            this->sc_action = std::move(act);
2✔
359
        }
360
        return *this;
4✔
361
    }
362
};
363

364
struct subcmd_format_t {
365
    using action_t = std::function<perform_result_t(const subcmd_format_t&)>;
366

367
    CLI::App* sf_format_app{nullptr};
368
    std::string sf_name;
369
    std::string sf_path;
370
    CLI::App* sf_regex_app{nullptr};
371
    std::string sf_regex_name;
372
    CLI::App* sf_regex101_app{nullptr};
373
    action_t sf_action;
374

375
    subcmd_format_t& set_action(action_t act)
12✔
376
    {
377
        if (!this->sf_action) {
12✔
378
            this->sf_action = std::move(act);
6✔
379
        }
380
        return *this;
12✔
381
    }
382

383
    Result<std::shared_ptr<log_format>, console::user_message> validate_format()
6✔
384
        const
385
    {
386
        if (this->sf_name.empty()) {
6✔
387
            auto um
388
                = console::user_message::error(
×
389
                      "expecting a format name to operate on")
390
                      .with_note(
×
391
                          (log_format::get_root_formats()
×
392
                           | lnav::itertools::map(&log_format::get_name)
×
393
                           | lnav::itertools::sort_with(
×
394
                               intern_string_t::case_lt)
395
                           | lnav::itertools::map(&intern_string_t::to_string)
×
396
                           | lnav::itertools::fold(symbol_reducer,
×
397
                                                   attr_line_t{}))
×
398
                              .add_header("the available formats are: ")
×
399
                              .wrap_with(&DEFAULT_WRAPPING))
×
400
                      .move();
×
401

402
            return Err(um);
×
403
        }
404

405
        auto lformat = log_format::find_root_format(this->sf_name.c_str());
6✔
406
        if (lformat == nullptr) {
6✔
407
            auto um
408
                = console::user_message::error(
×
409
                      attr_line_t("unknown format: ")
×
410
                          .append(lnav::roles::symbol(this->sf_name)))
×
411
                      .with_note(
×
412
                          (log_format::get_root_formats()
×
413
                           | lnav::itertools::map(&log_format::get_name)
×
414
                           | lnav::itertools::similar_to(this->sf_name)
×
415
                           | lnav::itertools::map(&intern_string_t::to_string)
×
416
                           | lnav::itertools::fold(symbol_reducer,
×
417
                                                   attr_line_t{}))
×
418
                              .add_header(
×
419
                                  "did you mean one of the following?\n")
420
                              .wrap_with(&DEFAULT_WRAPPING))
×
421
                      .move();
×
422

423
            return Err(um);
×
424
        }
425

426
        return Ok(lformat);
6✔
427
    }
6✔
428

429
    Result<external_log_format*, console::user_message>
430
    validate_external_format() const
6✔
431
    {
432
        auto lformat = TRY(this->validate_format());
6✔
433
        auto* ext_lformat = dynamic_cast<external_log_format*>(lformat.get());
6✔
434

435
        if (ext_lformat == nullptr) {
6✔
436
            return Err(console::user_message::error(
×
437
                attr_line_t()
×
438
                    .append_quoted(lnav::roles::symbol(this->sf_name))
×
439
                    .append(" is an internal format that is not defined in a "
×
440
                            "configuration file")));
×
441
        }
442

443
        return Ok(ext_lformat);
6✔
444
    }
6✔
445

446
    Result<std::pair<external_log_format*,
447
                     std::shared_ptr<external_log_format::pattern>>,
448
           console::user_message>
449
    validate_regex() const
×
450
    {
451
        auto* ext_lformat = TRY(this->validate_external_format());
×
452

453
        if (this->sf_regex_name.empty()) {
×
454
            auto um
455
                = console::user_message::error(
×
456
                      "expecting a regex name to operate on")
457
                      .with_note(
×
458
                          ext_lformat->elf_pattern_order
×
459
                          | lnav::itertools::map(
×
460
                              &external_log_format::pattern::p_name)
461
                          | lnav::itertools::map(&intern_string_t::to_string)
×
462
                          | lnav::itertools::fold(
×
463
                              symbol_reducer,
464
                              attr_line_t{"the available regexes are: "}))
×
465
                      .move();
×
466

467
            return Err(um);
×
468
        }
469

470
        for (const auto& pat : ext_lformat->elf_pattern_order) {
×
471
            if (pat->p_name == this->sf_regex_name) {
×
472
                return Ok(std::make_pair(ext_lformat, pat));
×
473
            }
474
        }
475

476
        auto um
477
            = console::user_message::error(
×
478
                  attr_line_t("unknown regex: ")
×
479
                      .append(lnav::roles::symbol(this->sf_regex_name)))
×
480
                  .with_note(
×
481
                      (ext_lformat->elf_pattern_order
×
482
                       | lnav::itertools::map(
×
483
                           &external_log_format::pattern::p_name)
484
                       | lnav::itertools::map(&intern_string_t::to_string)
×
485
                       | lnav::itertools::similar_to(this->sf_regex_name)
×
486
                       | lnav::itertools::fold(symbol_reducer, attr_line_t{}))
×
487
                          .add_header("did you mean one of the following?\n"))
×
488
                  .move();
×
489

490
        return Err(um);
×
491
    }
492

493
    static perform_result_t default_action(const subcmd_format_t& sf)
494
    {
495
        auto validate_res = sf.validate_format();
×
496
        if (validate_res.isErr()) {
×
497
            return {validate_res.unwrapErr()};
×
498
        }
499

500
        auto lformat = validate_res.unwrap();
×
501
        auto* ext_format = dynamic_cast<external_log_format*>(lformat.get());
×
502

503
        attr_line_t ext_details;
×
504
        if (ext_format != nullptr) {
×
505
            ext_details.append("\n   ")
×
506
                .append("Regexes"_h3)
×
507
                .append(": ")
×
508
                .join(ext_format->elf_pattern_order
×
509
                          | lnav::itertools::map(
×
510
                              &external_log_format::pattern::p_name)
511
                          | lnav::itertools::map(&intern_string_t::to_string),
×
512
                      VC_ROLE.value(role_t::VCR_SYMBOL),
×
513
                      ", ");
514
        }
515

516
        auto um
517
            = console::user_message::error(
×
518
                  attr_line_t("expecting an operation to perform on the ")
×
519
                      .append(lnav::roles::symbol(sf.sf_name))
×
520
                      .append(" format"))
×
521
                  .with_note(attr_line_t()
×
522
                                 .append(lnav::roles::symbol(sf.sf_name))
×
523
                                 .append(": ")
×
524
                                 .append(lformat->lf_description)
×
525
                                 .append(ext_details))
×
526
                  .with_help(sf.sf_format_app->get_subcommands({})
×
527
                             | lnav::itertools::fold(
×
528
                                 subcmd_reducer,
529
                                 attr_line_t{"the available operations are:"}))
×
530
                  .move();
×
531

532
        return {std::move(um)};
×
533
    }
534

535
    static perform_result_t default_regex_action(const subcmd_format_t& sf)
536
    {
537
        auto validate_res = sf.validate_regex();
×
538

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

543
        auto um = console::user_message::error(
×
544
                      attr_line_t("expecting an operation to perform on the ")
×
545
                          .append(lnav::roles::symbol(sf.sf_regex_name))
×
546
                          .append(" regular expression"))
×
547
                      .with_help(
×
548
                          attr_line_t{"the available subcommands are:"}.append(
×
549
                              sf.sf_regex_app->get_subcommands({})
×
550
                              | lnav::itertools::fold(subcmd_reducer,
×
551
                                                      attr_line_t{})))
×
552
                      .move();
×
553

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

557
    static perform_result_t get_action(const subcmd_format_t& sf)
558
    {
559
        auto validate_res = sf.validate_format();
×
560

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

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

567
        auto um = console::user_message::raw(
568
            attr_line_t()
×
569
                .append(lnav::roles::symbol(sf.sf_name))
×
570
                .append(": ")
×
571
                .append(on_blank(format->lf_description, "<no description>")));
×
572

573
        return {std::move(um)};
×
574
    }
575

576
    static perform_result_t test_action(const subcmd_format_t& sf)
6✔
577
    {
578
        auto validate_res = sf.validate_external_format();
6✔
579
        if (validate_res.isErr()) {
6✔
580
            return {validate_res.unwrapErr()};
×
581
        }
582
        auto* format = validate_res.unwrap();
6✔
583

584
        if (sf.sf_path.empty()) {
6✔
585
            auto um = lnav::console::user_message::error(
586
                "Expecting a file path to test");
×
587

588
            return {std::move(um)};
×
589
        }
590
        auto stat_res = lnav::filesystem::stat_file(sf.sf_path);
6✔
591
        if (stat_res.isErr()) {
6✔
592
            auto um = lnav::console::user_message::error(
×
593
                          attr_line_t("Unable to stat file: ")
1✔
594
                              .append(lnav::roles::file(sf.sf_path)))
2✔
595
                          .with_reason(stat_res.unwrapErr());
1✔
596

597
            return {std::move(um)};
3✔
598
        }
1✔
599
        auto st = stat_res.unwrap();
5✔
600
        if (!S_ISREG(st.st_mode)) {
5✔
601
            auto um = lnav::console::user_message::error(
×
602
                          attr_line_t("Unable to test with path: ")
1✔
603
                              .append(lnav::roles::file(sf.sf_path)))
2✔
604
                          .with_reason("not a regular file");
1✔
605

606
            return {std::move(um)};
3✔
607
        }
1✔
608

609
        auto open_res = lnav::filesystem::open_file(sf.sf_path, O_RDONLY);
4✔
610
        if (open_res.isErr()) {
4✔
611
            auto um = lnav::console::user_message::error(
×
612
                          attr_line_t("Unable to open file")
×
613
                              .append(lnav::roles::file(sf.sf_path)))
×
614
                          .with_reason(open_res.unwrapErr());
×
615

616
            return {std::move(um)};
×
617
        }
618
        auto test_file_fd = open_res.unwrap();
4✔
619

620
        line_buffer lb;
4✔
621
        lb.set_fd(test_file_fd);
4✔
622

623
        auto load_res = lb.load_next_line();
4✔
624
        if (load_res.isErr()) {
4✔
625
            auto um = lnav::console::user_message::error(
×
626
                          attr_line_t("Unable load line from file")
×
627
                              .append(lnav::roles::file(sf.sf_path)))
×
628
                          .with_reason(load_res.unwrapErr());
×
629

630
            return {std::move(um)};
×
631
        }
632
        auto li = load_res.unwrap();
4✔
633
        auto read_res = lb.read_range(li.li_file_range);
4✔
634
        if (read_res.isErr()) {
4✔
635
            auto um = lnav::console::user_message::error(
×
636
                          attr_line_t("Unable read line from file")
×
637
                              .append(lnav::roles::file(sf.sf_path)))
×
638
                          .with_reason(read_res.unwrapErr());
×
639

640
            return {std::move(um)};
×
641
        }
642
        auto sbr = read_res.unwrap();
4✔
643

644
        auto sample = log_format::sample_t{{
4✔
645
            intern_string::lookup("/"),
646
            source_location{
647
                intern_string::lookup(sf.sf_path),
4✔
648
                1,
649
            },
650
            sbr.to_string_fragment().rtrim("\n").to_string(),
4✔
651
        }};
8✔
652
        perform_result_t retval;
4✔
653
        auto test_res = format->test_line(sample, retval);
4✔
654
        test_res.match(
4✔
655
            [&retval, &sample](const log_format::scan_no_match& nope) {
×
656
                if (retval.empty()) {
2✔
657
                    auto um = lnav::console::user_message::error(
658
                        attr_line_t("test file did not match any of this "
2✔
659
                                    "format's patterns"));
1✔
660
                    if (nope.snm_reason != nullptr) {
1✔
661
                        um.with_reason(nope.snm_reason)
2✔
662
                            .with_snippet(lnav::console::snippet::from(
2✔
663
                                sample.s_line.pp_location,
1✔
664
                                sample.s_line.pp_value));
1✔
665
                    }
666
                    retval.emplace_back(um);
1✔
667
                }
1✔
668
            },
2✔
NEW
669
            [&retval, &sample](const log_format::scan_error& se) {
×
NEW
670
                if (retval.empty()) {
×
671
                    auto um = lnav::console::user_message::error(
NEW
672
                        attr_line_t("test file did not match due to an error"));
×
NEW
673
                    um.with_reason(se.se_message)
×
NEW
674
                        .with_snippet(lnav::console::snippet::from(
×
NEW
675
                            sample.s_line.pp_location, sample.s_line.pp_value));
×
NEW
676
                    retval.emplace_back(um);
×
677
                }
NEW
678
            },
×
UNCOV
679
            [&retval, &sample](const log_format::scan_match& yep) mutable {
×
680
                auto al = attr_line_t("test file matched format");
2✔
681
                if (!sample.s_matched_regexes.empty()) {
2✔
682
                    al.append(" pattern ")
1✔
683
                        .append(lnav::roles::symbol(
1✔
684
                            *sample.s_matched_regexes.begin()));
2✔
685
                }
686

687
                auto um = lnav::console::user_message::ok(al).with_snippet(
4✔
688
                    lnav::console::snippet::from(sample.s_line.pp_location,
4✔
689
                                                 sample.s_line.pp_value));
4✔
690
                if (sample.s_matched_regexes.empty()) {
2✔
691
                    auto note_al = attr_line_t("quality value of ")
1✔
692
                                       .append(lnav::roles::number(
1✔
693
                                           fmt::to_string(yep.sm_quality)))
2✔
694
                                       .append(" and ")
1✔
695
                                       .append(lnav::roles::number(
2✔
696
                                           fmt::to_string(yep.sm_strikes)))
2✔
697
                                       .append(" strikes");
1✔
698
                    um.with_note(note_al);
1✔
699
                }
1✔
700
                retval.emplace_back(um);
2✔
701
            },
2✔
702
            [&retval](const log_format::scan_incomplete& inc) {
4✔
703
                auto um = lnav::console::user_message::error(attr_line_t(
×
704
                    "test file did not have enough data to match against"));
×
705
                retval.emplace_back(um);
×
706
            });
×
707

708
        return retval;
4✔
709
    }
8✔
710

711
    static perform_result_t source_action(const subcmd_format_t& sf)
712
    {
713
        auto validate_res = sf.validate_external_format();
×
714

715
        if (validate_res.isErr()) {
×
716
            return {validate_res.unwrapErr()};
×
717
        }
718

719
        auto* format = validate_res.unwrap();
×
720

721
        if (format->elf_format_source_order.empty()) {
×
722
            return {
723
                console::user_message::error(
724
                    "format is builtin, there is no source file"),
725
            };
726
        }
727

728
        auto um = console::user_message::raw(
729
            format->elf_format_source_order[0].string());
×
730

731
        return {std::move(um)};
×
732
    }
733

734
    static perform_result_t sources_action(const subcmd_format_t& sf)
735
    {
736
        auto validate_res = sf.validate_external_format();
×
737

738
        if (validate_res.isErr()) {
×
739
            return {validate_res.unwrapErr()};
×
740
        }
741

742
        auto* format = validate_res.unwrap();
×
743

744
        if (format->elf_format_source_order.empty()) {
×
745
            return {
746
                console::user_message::error(
747
                    "format is builtin, there is no source file"),
748
            };
749
        }
750

751
        auto um = console::user_message::raw(
752
            attr_line_t().join(format->elf_format_source_order,
×
753
                               VC_ROLE.value(role_t::VCR_TEXT),
×
754
                               "\n"));
×
755

756
        return {std::move(um)};
×
757
    }
758

759
    static perform_result_t regex101_pull_action(const subcmd_format_t& sf)
760
    {
761
        auto validate_res = sf.validate_regex();
×
762
        if (validate_res.isErr()) {
×
763
            return {validate_res.unwrapErr()};
×
764
        }
765

766
        auto format_regex_pair = validate_res.unwrap();
×
767
        auto get_meta_res
768
            = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
×
769

770
        return get_meta_res.match(
771
            [&sf](
×
772
                const lnav::session::regex101::error& err) -> perform_result_t {
773
                return {
774
                    console::user_message::error(
×
775
                        attr_line_t("unable to get DB entry for: ")
×
776
                            .append(lnav::roles::symbol(sf.sf_name))
×
777
                            .append("/")
×
778
                            .append(lnav::roles::symbol(sf.sf_regex_name)))
×
779
                        .with_reason(err.e_msg),
×
780
                };
781
            },
×
782
            [&sf](
×
783
                const lnav::session::regex101::no_entry&) -> perform_result_t {
784
                return {
785
                    console::user_message::error(
×
786
                        attr_line_t("regex ")
×
787
                            .append_quoted(
×
788
                                lnav::roles::symbol(sf.sf_regex_name))
×
789
                            .append(" of format ")
×
790
                            .append_quoted(lnav::roles::symbol(sf.sf_name))
×
791
                            .append(" has not been pushed to regex101.com"))
×
792
                        .with_help(
×
793
                            attr_line_t("use the ")
×
794
                                .append_quoted("push"_keyword)
×
795
                                .append(" subcommand to create the regex on "
×
796
                                        "regex101.com for easy editing")),
797
                };
798
            },
×
799
            [&](const lnav::session::regex101::entry& en) -> perform_result_t {
×
800
                auto retrieve_res = regex101::client::retrieve(en.re_permalink);
×
801

802
                return retrieve_res.match(
803
                    [&](const console::user_message& um) -> perform_result_t {
×
804
                        return {
805
                            console::user_message::error(
×
806
                                attr_line_t("unable to retrieve entry ")
×
807
                                    .append_quoted(
×
808
                                        lnav::roles::symbol(en.re_permalink))
×
809
                                    .append(" from regex101.com"))
×
810
                                .with_reason(um),
×
811
                        };
812
                    },
×
813
                    [&](const regex101::client::no_entry&) -> perform_result_t {
×
814
                        lnav::session::regex101::delete_entry(sf.sf_name,
×
815
                                                              sf.sf_regex_name);
×
816
                        return {
817
                            console::user_message::error(
×
818
                                attr_line_t("entry ")
×
819
                                    .append_quoted(
×
820
                                        lnav::roles::symbol(en.re_permalink))
×
821
                                    .append(
×
822
                                        " no longer exists on regex101.com"))
823
                                .with_help(attr_line_t("use the ")
×
824
                                               .append_quoted("delete"_keyword)
×
825
                                               .append(" subcommand to delete "
×
826
                                                       "the association")),
827
                        };
828
                    },
×
829
                    [&](const regex101::client::entry& remote_entry)
×
830
                        -> perform_result_t {
831
                        auto curr_entry = regex101::convert_format_pattern(
832
                            format_regex_pair.first, format_regex_pair.second);
×
833

834
                        if (curr_entry.e_regex == remote_entry.e_regex) {
×
835
                            return {
836
                                console::user_message::ok(
×
837
                                    attr_line_t("local regex is in sync "
×
838
                                                "with entry ")
839
                                        .append_quoted(lnav::roles::symbol(
×
840
                                            en.re_permalink))
×
841
                                        .append(" on regex101.com"))
×
842
                                    .with_help(
×
843
                                        attr_line_t("make edits on ")
×
844
                                            .append_quoted(lnav::roles::file(
×
845
                                                regex101::client::to_edit_url(
×
846
                                                    en.re_permalink)))
×
847
                                            .append(" and then run this "
×
848
                                                    "command again to update "
849
                                                    "the local values")),
850
                            };
851
                        }
852

853
                        auto patch_res
854
                            = regex101::patch(format_regex_pair.first,
×
855
                                              sf.sf_regex_name,
×
856
                                              remote_entry);
×
857

858
                        if (patch_res.isErr()) {
×
859
                            return {
860
                                console::user_message::error(
×
861
                                    attr_line_t(
×
862
                                        "unable to patch format regex: ")
863
                                        .append(lnav::roles::symbol(sf.sf_name))
×
864
                                        .append("/")
×
865
                                        .append(lnav::roles::symbol(
×
866
                                            sf.sf_regex_name)))
×
867
                                    .with_reason(patch_res.unwrapErr()),
×
868
                            };
869
                        }
870

871
                        auto um = console::user_message::ok(
872
                            attr_line_t("format patch file written to: ")
×
873
                                .append(lnav::roles::file(
×
874
                                    patch_res.unwrap().string())));
×
875
                        if (!format_regex_pair.first->elf_builtin_format) {
×
876
                            um.with_help(
×
877
                                attr_line_t("once the regex has been found "
×
878
                                            "to be working correctly, move the "
879
                                            "contents of the patch file to the "
880
                                            "original file at:\n   ")
881
                                    .append(lnav::roles::file(
×
882
                                        format_regex_pair.first
×
883
                                            ->elf_format_source_order.front()
×
884
                                            .string())));
×
885
                        }
886

887
                        return {std::move(um)};
×
888
                    });
×
889
            });
×
890
    }
891

892
    static perform_result_t regex101_default_action(const subcmd_format_t& sf)
893
    {
894
        auto validate_res = sf.validate_regex();
×
895

896
        if (validate_res.isErr()) {
×
897
            return {validate_res.unwrapErr()};
×
898
        }
899

900
        auto um = console::user_message::error(
901
            attr_line_t("expecting an operation to perform on the ")
×
902
                .append(lnav::roles::symbol(sf.sf_regex_name))
×
903
                .append(" regex using regex101.com"));
×
904

905
        auto get_res
906
            = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
×
907
        if (get_res.is<lnav::session::regex101::entry>()) {
×
908
            auto local_entry = get_res.get<lnav::session::regex101::entry>();
×
909
            um.with_note(
×
910
                attr_line_t("this regex is currently associated with the "
×
911
                            "following regex101.com entry:\n   ")
912
                    .append(lnav::roles::file(regex101::client::to_edit_url(
×
913
                        local_entry.re_permalink))));
914
        }
915

916
        um.with_help(attr_line_t{"the available subcommands are:"}.append(
×
917
            sf.sf_regex101_app->get_subcommands({})
×
918
            | lnav::itertools::fold(subcmd_reducer, attr_line_t{})));
×
919

920
        return {std::move(um)};
×
921
    }
922

923
    static perform_result_t regex101_push_action(const subcmd_format_t& sf)
924
    {
925
        auto validate_res = sf.validate_regex();
×
926
        if (validate_res.isErr()) {
×
927
            return {validate_res.unwrapErr()};
×
928
        }
929

930
        auto format_regex_pair = validate_res.unwrap();
×
931
        auto entry = regex101::convert_format_pattern(format_regex_pair.first,
×
932
                                                      format_regex_pair.second);
×
933
        auto get_meta_res
934
            = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
×
935

936
        if (get_meta_res.is<lnav::session::regex101::entry>()) {
×
937
            auto entry_meta
938
                = get_meta_res.get<lnav::session::regex101::entry>();
×
939
            auto retrieve_res
940
                = regex101::client::retrieve(entry_meta.re_permalink);
×
941

942
            if (retrieve_res.is<regex101::client::entry>()) {
×
943
                auto remote_entry = retrieve_res.get<regex101::client::entry>();
×
944

945
                if (remote_entry == entry) {
×
946
                    return {
947
                        console::user_message::ok(
948
                            attr_line_t("regex101 entry ")
×
949
                                .append(lnav::roles::symbol(
×
950
                                    entry_meta.re_permalink))
951
                                .append(" is already up-to-date")),
×
952
                    };
953
                }
954
            } else if (retrieve_res.is<console::user_message>()) {
×
955
                return {
956
                    retrieve_res.get<console::user_message>(),
×
957
                };
958
            }
959

960
            entry.e_permalink_fragment = entry_meta.re_permalink;
×
961
        }
962

963
        auto upsert_res = regex101::client::upsert(entry);
×
964
        auto upsert_info = upsert_res.unwrap();
×
965

966
        if (get_meta_res.is<lnav::session::regex101::no_entry>()) {
×
967
            lnav::session::regex101::insert_entry({
×
968
                format_regex_pair.first->get_name().to_string(),
×
969
                format_regex_pair.second->p_name.to_string(),
×
970
                upsert_info.cr_permalink_fragment,
971
                upsert_info.cr_delete_code,
972
            });
973
        }
974

975
        return {
976
            console::user_message::ok(
×
977
                attr_line_t("pushed regex to -- ")
×
978
                    .append(lnav::roles::file(regex101::client::to_edit_url(
×
979
                        upsert_info.cr_permalink_fragment))))
980
                .with_help(attr_line_t("use the ")
×
981
                               .append_quoted("pull"_keyword)
×
982
                               .append(" subcommand to update the format after "
×
983
                                       "you make changes on regex101.com")),
984
        };
985
    }
986

987
    static perform_result_t regex101_delete_action(const subcmd_format_t& sf)
988
    {
989
        auto get_res
990
            = lnav::session::regex101::get_entry(sf.sf_name, sf.sf_regex_name);
×
991

992
        return get_res.match(
993
            [&sf](
×
994
                const lnav::session::regex101::entry& en) -> perform_result_t {
995
                {
996
                    auto validate_res = sf.validate_external_format();
×
997

998
                    if (validate_res.isOk()) {
×
999
                        auto ppath = regex101::patch_path(validate_res.unwrap(),
×
1000
                                                          en.re_permalink);
×
1001

1002
                        if (std::filesystem::exists(ppath)) {
×
1003
                            return {
1004
                                console::user_message::error(
×
1005
                                    attr_line_t("cannot delete regex101 entry "
×
1006
                                                "while patch file exists"))
1007
                                    .with_note(attr_line_t("  ").append(
×
1008
                                        lnav::roles::file(ppath.string())))
×
1009
                                    .with_help(attr_line_t(
×
1010
                                        "move the contents of the patch file "
1011
                                        "to the main log format and then "
1012
                                        "delete the file to continue")),
1013
                            };
1014
                        }
1015
                    }
1016
                }
1017

1018
                perform_result_t retval;
×
1019
                if (en.re_delete_code.empty()) {
×
1020
                    retval.emplace_back(
×
1021
                        console::user_message::warning(
×
1022
                            attr_line_t("not deleting regex101 entry ")
×
1023
                                .append_quoted(
×
1024
                                    lnav::roles::symbol(en.re_permalink)))
×
1025
                            .with_reason(
×
1026
                                "delete code is not known for this entry")
1027
                            .with_note(
1028
                                "formats created by importing a regex101.com "
1029
                                "entry will not have a delete code"));
1030
                } else {
1031
                    auto delete_res
1032
                        = regex101::client::delete_entry(en.re_delete_code);
×
1033

1034
                    if (delete_res.isErr()) {
×
1035
                        return {
1036
                            console::user_message::error(
×
1037
                                "unable to delete regex101 entry")
1038
                                .with_reason(delete_res.unwrapErr()),
×
1039
                        };
1040
                    }
1041
                }
1042

1043
                lnav::session::regex101::delete_entry(sf.sf_name,
×
1044
                                                      sf.sf_regex_name);
×
1045

1046
                retval.emplace_back(console::user_message::ok(
×
1047
                    attr_line_t("deleted regex101 entry: ")
×
1048
                        .append(lnav::roles::symbol(en.re_permalink))));
×
1049

1050
                return retval;
×
1051
            },
×
1052
            [&sf](
×
1053
                const lnav::session::regex101::no_entry&) -> perform_result_t {
1054
                return {
1055
                    console::user_message::error(
1056
                        attr_line_t("no regex101 entry for ")
×
1057
                            .append(lnav::roles::symbol(sf.sf_name))
×
1058
                            .append("/")
×
1059
                            .append(lnav::roles::symbol(sf.sf_regex_name))),
×
1060
                };
1061
            },
×
1062
            [&sf](
×
1063
                const lnav::session::regex101::error& err) -> perform_result_t {
1064
                return {
1065
                    console::user_message::error(
×
1066
                        attr_line_t("unable to get regex101 entry for ")
×
1067
                            .append(lnav::roles::symbol(sf.sf_name))
×
1068
                            .append("/")
×
1069
                            .append(lnav::roles::symbol(sf.sf_regex_name)))
×
1070
                        .with_reason(err.e_msg),
×
1071
                };
1072
            });
×
1073
    }
1074
};
1075

1076
struct subcmd_piper_t {
1077
    using action_t = std::function<perform_result_t(const subcmd_piper_t&)>;
1078

1079
    CLI::App* sp_app{nullptr};
1080
    action_t sp_action;
1081

1082
    subcmd_piper_t& set_action(action_t act)
4✔
1083
    {
1084
        if (!this->sp_action) {
4✔
1085
            this->sp_action = std::move(act);
2✔
1086
        }
1087
        return *this;
4✔
1088
    }
1089

1090
    static perform_result_t default_action(const subcmd_piper_t& sp)
1091
    {
1092
        auto um
1093
            = console::user_message::error(
×
1094
                  "expecting an operation related to piper storage")
1095
                  .with_help(sp.sp_app->get_subcommands({})
×
1096
                             | lnav::itertools::fold(
×
1097
                                 subcmd_reducer,
1098
                                 attr_line_t{"the available operations are:"}))
×
1099
                  .move();
×
1100

1101
        return {std::move(um)};
×
1102
    }
1103

1104
    static perform_result_t list_action(const subcmd_piper_t&)
2✔
1105
    {
1106
        static const intern_string_t SRC = intern_string::lookup("piper");
6✔
1107
        static const auto DOT_HEADER = std::filesystem::path(".header");
2✔
1108

1109
        struct item {
1110
            lnav::piper::header i_header;
1111
            std::string i_url;
1112
            file_size_t i_total_size{0};
1113
        };
1114

1115
        file_size_t grand_total{0};
2✔
1116
        std::vector<item> items;
2✔
1117
        std::error_code ec;
2✔
1118

1119
        for (const auto& instance_dir : std::filesystem::directory_iterator(
2✔
1120
                 lnav::piper::storage_path(), ec))
4✔
1121
        {
1122
            if (!instance_dir.is_directory()) {
2✔
1123
                log_warning("piper directory entry is not a directory: %s",
×
1124
                            instance_dir.path().c_str());
1125
                continue;
×
1126
            }
1127

1128
            std::optional<lnav::piper::header> hdr_opt;
2✔
1129
            auto url = fmt::format(FMT_STRING("piper://{}"),
6✔
1130
                                   instance_dir.path().filename().string());
4✔
1131
            file_size_t total_size{0};
2✔
1132
            auto hdr_path = instance_dir / DOT_HEADER;
2✔
1133
            if (std::filesystem::exists(hdr_path)) {
2✔
1134
                auto hdr_read_res = lnav::filesystem::read_file(hdr_path);
×
1135
                if (hdr_read_res.isOk()) {
×
1136
                    auto hdr = string_fragment::from_str(hdr_read_res.unwrap());
×
1137
                    auto parse_res
1138
                        = lnav::piper::header_handlers.parser_for(SRC).of(hdr);
×
1139
                    if (parse_res.isOk()) {
×
1140
                        hdr_opt = parse_res.unwrap();
×
1141
                    } else {
1142
                        log_error("failed to parse header: %s -- %s",
×
1143
                                  hdr_path.c_str(),
1144
                                  parse_res.unwrapErr()[0]
1145
                                      .to_attr_line()
1146
                                      .get_string()
1147
                                      .c_str());
1148
                    }
1149
                } else {
×
1150
                    log_error("failed to read header file: %s -- %s",
×
1151
                              hdr_path.c_str(),
1152
                              hdr_read_res.unwrapErr().c_str());
1153
                }
1154
            }
1155

1156
            for (const auto& entry :
2✔
1157
                 std::filesystem::directory_iterator(instance_dir.path()))
4✔
1158
            {
1159
                if (entry.path().filename() == DOT_HEADER) {
2✔
1160
                    continue;
×
1161
                }
1162

1163
                total_size += entry.file_size();
2✔
1164
                char buffer[lnav::piper::HEADER_SIZE];
1165

1166
                auto entry_open_res
1167
                    = lnav::filesystem::open_file(entry.path(), O_RDONLY);
2✔
1168
                if (entry_open_res.isErr()) {
2✔
1169
                    log_warning("unable to open piper file: %s -- %s",
×
1170
                                entry.path().c_str(),
1171
                                entry_open_res.unwrapErr().c_str());
1172
                    continue;
×
1173
                }
1174

1175
                auto entry_fd = entry_open_res.unwrap();
2✔
1176
                if (read(entry_fd, buffer, sizeof(buffer)) != sizeof(buffer)) {
2✔
1177
                    log_warning("piper file is too small: %s",
×
1178
                                entry.path().c_str());
1179
                    continue;
×
1180
                }
1181
                auto hdr_bits_opt = lnav::piper::read_header(entry_fd, buffer);
2✔
1182
                if (!hdr_bits_opt) {
2✔
1183
                    log_warning("could not read piper header: %s",
×
1184
                                entry.path().c_str());
1185
                    continue;
×
1186
                }
1187

1188
                auto hdr_buf = std::move(hdr_bits_opt.value());
2✔
1189

1190
                total_size -= hdr_buf.size();
2✔
1191
                auto hdr_sf
1192
                    = string_fragment::from_bytes(hdr_buf.in(), hdr_buf.size());
2✔
1193
                auto hdr_parse_res
1194
                    = lnav::piper::header_handlers.parser_for(SRC).of(hdr_sf);
2✔
1195
                if (hdr_parse_res.isErr()) {
2✔
1196
                    log_error("failed to parse piper header: %s",
×
1197
                              hdr_parse_res.unwrapErr()[0]
1198
                                  .to_attr_line()
1199
                                  .get_string()
1200
                                  .c_str());
1201
                    continue;
×
1202
                }
1203

1204
                auto hdr = hdr_parse_res.unwrap();
2✔
1205

1206
                if (!hdr_opt || hdr < hdr_opt.value()) {
2✔
1207
                    hdr_opt = hdr;
2✔
1208
                }
1209
            }
4✔
1210

1211
            if (hdr_opt) {
2✔
1212
                items.emplace_back(item{hdr_opt.value(), url, total_size});
2✔
1213
            }
1214

1215
            grand_total += total_size;
2✔
1216
        }
4✔
1217

1218
        if (ec && ec.value() != ENOENT) {
2✔
1219
            auto um = lnav::console::user_message::error(
×
1220
                          attr_line_t("unable to access piper directory: ")
×
1221
                              .append(lnav::roles::file(
×
1222
                                  lnav::piper::storage_path().string())))
×
1223
                          .with_reason(ec.message())
×
1224
                          .move();
×
1225
            return {std::move(um)};
×
1226
        }
1227

1228
        if (items.empty()) {
2✔
1229
            if (verbosity != verbosity_t::quiet) {
×
1230
                auto um
1231
                    = lnav::console::user_message::info(
×
1232
                          attr_line_t("no piper captures were found in:\n\t")
×
1233
                              .append(lnav::roles::file(
×
1234
                                  lnav::piper::storage_path().string())))
×
1235
                          .with_help(
×
1236
                              attr_line_t("You can create a capture by "
×
1237
                                          "piping data into ")
1238
                                  .append(lnav::roles::file("lnav"))
×
1239
                                  .append(" or using the ")
×
1240
                                  .append_quoted(lnav::roles::symbol(":sh"))
×
1241
                                  .append(" command"))
×
1242
                          .move();
×
1243
                return {std::move(um)};
×
1244
            }
1245

1246
            return {};
×
1247
        }
1248

1249
        auto txt
1250
            = items
1251
            | lnav::itertools::sort_with([](const item& lhs, const item& rhs) {
4✔
1252
                  if (lhs.i_header < rhs.i_header) {
×
1253
                      return true;
×
1254
                  }
1255

1256
                  if (rhs.i_header < lhs.i_header) {
×
1257
                      return false;
×
1258
                  }
1259

1260
                  return lhs.i_url < rhs.i_url;
×
1261
              })
1262
            | lnav::itertools::map([](const item& it) {
6✔
1263
                  auto ago = humanize::time::point::from_tv(it.i_header.h_ctime)
2✔
1264
                                 .as_time_ago();
2✔
1265
                  auto retval = attr_line_t()
2✔
1266
                                    .append(lnav::roles::list_glyph(
2✔
1267
                                        fmt::format(FMT_STRING("{:>18}"), ago)))
10✔
1268
                                    .append("  ")
2✔
1269
                                    .append(lnav::roles::file(it.i_url))
4✔
1270
                                    .append(" ")
2✔
1271
                                    .append(lnav::roles::number(fmt::format(
4✔
1272
                                        FMT_STRING("{:>8}"),
6✔
1273
                                        humanize::file_size(
2✔
1274
                                            it.i_total_size,
2✔
1275
                                            humanize::alignment::columnar))))
1276
                                    .append(" ")
2✔
1277
                                    .append_quoted(lnav::roles::comment(
4✔
1278
                                        it.i_header.h_name))
2✔
1279
                                    .append("\n");
2✔
1280
                  if (verbosity == verbosity_t::verbose) {
2✔
1281
                      auto env_al
1282
                          = it.i_header.h_env
×
1283
                          | lnav::itertools::map([](const auto& pair) {
×
1284
                                return attr_line_t()
×
1285
                                    .append(lnav::roles::identifier(pair.first))
×
1286
                                    .append("=")
×
1287
                                    .append(pair.second)
×
1288
                                    .append("\n");
×
1289
                            })
1290
                          | lnav::itertools::fold(
×
1291
                                [](const auto& elem, auto& accum) {
×
1292
                                    if (!accum.empty()) {
×
1293
                                        accum.append(28, ' ');
×
1294
                                    }
1295
                                    return accum.append(elem);
×
1296
                                },
1297
                                attr_line_t());
×
1298

1299
                      retval.append(23, ' ')
×
1300
                          .append("cwd: ")
×
1301
                          .append(lnav::roles::file(it.i_header.h_cwd))
×
1302
                          .append("\n")
×
1303
                          .append(23, ' ')
×
1304
                          .append("env: ")
×
1305
                          .append(env_al);
×
1306
                  }
1307
                  return retval;
4✔
1308
              })
2✔
1309
            | lnav::itertools::fold(
4✔
1310
                  [](const auto& elem, auto& accum) {
2✔
1311
                      return accum.append(elem);
2✔
1312
                  },
1313
                  attr_line_t{});
6✔
1314
        txt.rtrim();
2✔
1315

1316
        perform_result_t retval;
2✔
1317
        if (verbosity != verbosity_t::quiet) {
2✔
1318
            auto extra_um
1319
                = lnav::console::user_message::info(
×
1320
                      attr_line_t(
1✔
1321
                          "the following piper captures were found in:\n\t")
1322
                          .append(lnav::roles::file(
2✔
1323
                              lnav::piper::storage_path().string())))
2✔
1324
                      .with_note(
2✔
1325
                          attr_line_t("The captures currently consume ")
2✔
1326
                              .append(lnav::roles::number(humanize::file_size(
2✔
1327
                                  grand_total, humanize::alignment::none)))
1328
                              .append(" of disk space.  File sizes include "
1✔
1329
                                      "associated metadata."))
1330
                      .with_help(
2✔
1331
                          "You can reopen a capture by passing the piper URL "
1332
                          "to lnav")
1333
                      .move();
1✔
1334
            retval.emplace_back(extra_um);
1✔
1335
        }
1✔
1336
        retval.emplace_back(lnav::console::user_message::raw(txt));
2✔
1337

1338
        return retval;
2✔
1339
    }
2✔
1340

1341
    static perform_result_t clean_action(const subcmd_piper_t&)
1342
    {
1343
        std::error_code ec;
×
1344

1345
        std::filesystem::remove_all(lnav::piper::storage_path(), ec);
×
1346
        if (ec) {
×
1347
            return {
1348
                lnav::console::user_message::error(
×
1349
                    "unable to remove piper storage directory")
1350
                    .with_reason(ec.message()),
×
1351
            };
1352
        }
1353

1354
        return {};
×
1355
    }
1356
};
1357

1358
struct subcmd_regex101_t {
1359
    using action_t = std::function<perform_result_t(const subcmd_regex101_t&)>;
1360

1361
    CLI::App* sr_app{nullptr};
1362
    action_t sr_action;
1363
    std::string sr_import_url;
1364
    std::string sr_import_name;
1365
    std::string sr_import_regex_name{"std"};
1366

1367
    subcmd_regex101_t& set_action(action_t act)
×
1368
    {
1369
        if (!this->sr_action) {
×
1370
            this->sr_action = std::move(act);
×
1371
        }
1372
        return *this;
×
1373
    }
1374

1375
    static perform_result_t default_action(const subcmd_regex101_t& sr)
1376
    {
1377
        auto um
1378
            = console::user_message::error(
×
1379
                  "expecting an operation related to the regex101.com "
1380
                  "integration")
1381
                  .with_help(sr.sr_app->get_subcommands({})
×
1382
                             | lnav::itertools::fold(
×
1383
                                 subcmd_reducer,
1384
                                 attr_line_t{"the available operations are:"}))
×
1385
                  .move();
×
1386

1387
        return {std::move(um)};
×
1388
    }
1389

1390
    static perform_result_t list_action(const subcmd_regex101_t&)
1391
    {
1392
        auto get_res = lnav::session::regex101::get_entries();
×
1393

1394
        if (get_res.isErr()) {
×
1395
            return {
1396
                console::user_message::error(
×
1397
                    "unable to read regex101 entries from DB")
1398
                    .with_reason(get_res.unwrapErr()),
×
1399
            };
1400
        }
1401

1402
        auto entries = get_res.unwrap()
×
1403
            | lnav::itertools::map([](const auto& elem) {
×
1404
                           return fmt::format(
1405
                               FMT_STRING("   format {} regex {} regex101\n"),
×
1406
                               elem.re_format_name,
×
1407
                               elem.re_regex_name);
×
1408
                       })
1409
            | lnav::itertools::fold(
×
1410
                           [](const auto& elem, auto& accum) {
×
1411
                               return accum.append(elem);
×
1412
                           },
1413
                           attr_line_t{});
×
1414

1415
        auto um = console::user_message::ok(
1416
            entries.add_header("the following regex101 entries were found:\n")
×
1417
                .with_default("no regex101 entries found"));
×
1418

1419
        return {std::move(um)};
×
1420
    }
1421

1422
    static perform_result_t import_action(const subcmd_regex101_t& sr)
1423
    {
1424
        auto import_res = regex101::import(
1425
            sr.sr_import_url, sr.sr_import_name, sr.sr_import_regex_name);
×
1426

1427
        if (import_res.isOk()) {
×
1428
            return {
1429
                lnav::console::user_message::ok(
×
1430
                    attr_line_t("converted regex101 entry to format file: ")
×
1431
                        .append(lnav::roles::file(import_res.unwrap())))
×
1432
                    .with_note("the converted format may still have errors")
×
1433
                    .with_help(
×
1434
                        attr_line_t(
×
1435
                            "use the following command to patch the regex as "
1436
                            "more changes are made on regex101.com:\n")
1437
                            .appendf(FMT_STRING("   lnav -m format {} regex {} "
×
1438
                                                "regex101 pull"),
1439
                                     sr.sr_import_name,
×
1440
                                     sr.sr_import_regex_name)),
×
1441
            };
1442
        }
1443

1444
        return {
1445
            import_res.unwrapErr(),
1446
        };
1447
    }
1448
};
1449

1450
struct subcmd_crash_t {
1451
    using action_t = std::function<perform_result_t(const subcmd_crash_t&)>;
1452

1453
    CLI::App* sc_app{nullptr};
1454
    action_t sc_action;
1455

1456
    subcmd_crash_t& set_action(action_t act)
×
1457
    {
1458
        if (!this->sc_action) {
×
1459
            this->sc_action = std::move(act);
×
1460
        }
1461
        return *this;
×
1462
    }
1463

1464
    static perform_result_t default_action(const subcmd_crash_t& sc)
1465
    {
1466
        auto um
1467
            = console::user_message::error(
×
1468
                  "expecting an operation related to crash logs")
1469
                  .with_help(sc.sc_app->get_subcommands({})
×
1470
                             | lnav::itertools::fold(
×
1471
                                 subcmd_reducer,
1472
                                 attr_line_t{"the available operations are:"}))
×
1473
                  .move();
×
1474

1475
        return {std::move(um)};
×
1476
    }
1477

1478
    static perform_result_t upload_action(const subcmd_crash_t&)
1479
    {
1480
        static constexpr char SPINNER_CHARS[] = "-\\|/";
1481
        constexpr size_t SPINNER_SIZE = sizeof(SPINNER_CHARS) - 1;
×
1482

1483
        static_root_mem<glob_t, globfree> gl;
1484
        const auto path = lnav::paths::dotlnav() / "crash" / "crash-*.log";
×
1485
        perform_result_t retval;
×
1486

1487
        auto glob_rc = glob(path.c_str(), 0, nullptr, gl.inout());
×
1488
        if (glob_rc == GLOB_NOMATCH) {
×
1489
            auto um = console::user_message::info("no crash logs to upload");
×
1490
            return {std::move(um)};
×
1491
        }
1492
        if (glob_rc != 0) {
×
1493
            auto um = console::user_message::error("unable to find crash logs");
×
1494
            return {std::move(um)};
×
1495
        }
1496

1497
        for (size_t lpc = 0; lpc < gl->gl_pathc; lpc++) {
×
1498
            auto crash_file = std::filesystem::path(gl->gl_pathv[lpc]);
×
1499
            int spinner_index = 0;
×
1500

1501
            log_info("uploading crash log: %s", crash_file.c_str());
×
1502
            printf("~");
×
1503
            fflush(stdout);
×
1504
            auto upload_res = crashd::client::upload(
1505
                crash_file,
1506
                [&spinner_index](double dltotal,
×
1507
                                 double dlnow,
1508
                                 double ultotal,
1509
                                 double ulnow) {
1510
                    printf("\b%c", SPINNER_CHARS[spinner_index % SPINNER_SIZE]);
×
1511
                    spinner_index += 1;
×
1512
                    fflush(stdout);
×
1513
                    return crashd::client::progress_result_t::ok;
×
1514
                });
×
1515
            if (spinner_index > 0) {
×
1516
                printf("\b");
×
1517
            }
1518
            printf(".");
×
1519
            fflush(stdout);
×
1520
            if (upload_res.isErr()) {
×
1521
                retval.push_back(upload_res.unwrapErr());
×
1522
            } else {
1523
                std::error_code ec;
×
1524

1525
                std::filesystem::remove(crash_file, ec);
×
1526
            }
1527
        }
1528

1529
        printf("\n");
×
1530
        auto um = console::user_message::ok(
1531
            attr_line_t("uploaded ")
×
1532
                .append(lnav::roles::number(fmt::to_string(gl->gl_pathc)))
×
1533
                .append(" crash logs, thank you!"));
×
1534
        retval.push_back(um);
×
1535

1536
        return retval;
×
1537
    }
1538
};
1539

1540
using operations_v = mapbox::util::variant<no_subcmd_t,
1541
                                           subcmd_apps_t,
1542
                                           subcmd_config_t,
1543
                                           subcmd_format_t,
1544
                                           subcmd_piper_t,
1545
                                           subcmd_regex101_t,
1546
                                           subcmd_crash_t>;
1547

1548
class operations {
1549
public:
1550
    operations_v o_ops;
1551
};
1552

1553
std::shared_ptr<operations>
1554
describe_cli(CLI::App& app, int argc, char* argv[])
10✔
1555
{
1556
    auto retval = std::make_shared<operations>();
10✔
1557

1558
    retval->o_ops = no_subcmd_t{
20✔
1559
        &app,
1560
    };
10✔
1561

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

1564
    subcmd_apps_t apps_args;
10✔
1565
    subcmd_config_t config_args;
10✔
1566
    subcmd_format_t format_args;
10✔
1567
    subcmd_piper_t piper_args;
10✔
1568
    subcmd_regex101_t regex101_args;
10✔
1569
    subcmd_crash_t crash_args;
10✔
1570

1571
    {
1572
        auto* subcmd_apps
1573
            = app.add_subcommand("apps", "manage lnav apps")->callback([&] {
30✔
1574
                  apps_args.set_action(subcmd_apps_t::default_action);
×
1575
                  retval->o_ops = apps_args;
×
1576
              });
×
1577
        apps_args.sa_apps_app = subcmd_apps;
10✔
1578
        auto* sub_create_options
1579
            = subcmd_apps->add_subcommand("create", "create a new app")
1580
                  ->callback([&] {
40✔
1581
                      apps_args.set_action(subcmd_apps_t::create_action);
×
1582
                  });
×
1583
        sub_create_options->add_option(
50✔
1584
            "name", apps_args.sa_name, "name of the app");
1585
    }
1586

1587
    {
1588
        auto* subcmd_config
1589
            = app.add_subcommand("config",
1590
                                 "perform operations on the lnav configuration")
1591
                  ->callback([&]() {
30✔
1592
                      config_args.set_action(subcmd_config_t::default_action);
2✔
1593
                      retval->o_ops = config_args;
2✔
1594
                  });
2✔
1595
        config_args.sc_config_app = subcmd_config;
10✔
1596

1597
        subcmd_config->add_subcommand("get", "print the current configuration")
1598
            ->callback(
50✔
1599
                [&]() { config_args.set_action(subcmd_config_t::get_action); });
21✔
1600

1601
        subcmd_config
1602
            ->add_subcommand("blame",
1603
                             "print the configuration options and their source")
1604
            ->callback([&]() {
40✔
1605
                config_args.set_action(subcmd_config_t::blame_action);
1✔
1606
            });
1✔
1607

1608
        auto* sub_file_options = subcmd_config->add_subcommand(
40✔
1609
            "file-options", "print the options applied to specific files");
1610

1611
        sub_file_options->add_option(
40✔
1612
            "path", config_args.sc_path, "the path to the file");
1613
        sub_file_options->callback([&]() {
10✔
1614
            config_args.set_action(subcmd_config_t::file_options_action);
×
1615
        });
×
1616
    }
1617

1618
    {
1619
        auto* subcmd_format
1620
            = app.add_subcommand("format",
1621
                                 "perform operations on log file formats")
1622
                  ->callback([&]() {
30✔
1623
                      format_args.set_action(subcmd_format_t::default_action);
6✔
1624
                      retval->o_ops = format_args;
6✔
1625
                  });
6✔
1626
        format_args.sf_format_app = subcmd_format;
10✔
1627
        subcmd_format
1628
            ->add_option(
1629
                "format_name", format_args.sf_name, "the name of the format")
1630
            ->expected(1);
40✔
1631

1632
        {
1633
            subcmd_format
1634
                ->add_subcommand("get", "print information about a format")
1635
                ->callback([&]() {
40✔
1636
                    format_args.set_action(subcmd_format_t::get_action);
×
1637
                });
×
1638
        }
1639

1640
        {
1641
            auto* subcmd_format_test
1642
                = subcmd_format
1643
                      ->add_subcommand("test",
1644
                                       "test this format against a file")
1645
                      ->callback([&]() {
40✔
1646
                          format_args.set_action(subcmd_format_t::test_action);
6✔
1647
                      });
6✔
1648
            subcmd_format_test->add_option(
40✔
1649
                "path", format_args.sf_path, "the path to the file to test");
1650
        }
1651

1652
        {
1653
            subcmd_format
1654
                ->add_subcommand("source",
1655
                                 "print the path of the first source file "
1656
                                 "containing this format")
1657
                ->callback([&]() {
40✔
1658
                    format_args.set_action(subcmd_format_t::source_action);
×
1659
                });
×
1660
        }
1661

1662
        {
1663
            subcmd_format
1664
                ->add_subcommand("sources",
1665
                                 "print the paths of all source files "
1666
                                 "containing this format")
1667
                ->callback([&]() {
40✔
1668
                    format_args.set_action(subcmd_format_t::sources_action);
×
1669
                });
×
1670
        }
1671

1672
        {
1673
            auto* subcmd_format_regex
1674
                = subcmd_format
1675
                      ->add_subcommand(
1676
                          "regex",
1677
                          "operate on the format's regular expressions")
1678
                      ->callback([&]() {
40✔
1679
                          format_args.set_action(
×
1680
                              subcmd_format_t::default_regex_action);
1681
                      });
×
1682
            format_args.sf_regex_app = subcmd_format_regex;
10✔
1683
            subcmd_format_regex->add_option(
40✔
1684
                "regex-name",
1685
                format_args.sf_regex_name,
1686
                "the name of the regular expression to operate on");
1687

1688
            {
1689
                auto* subcmd_format_regex_regex101
1690
                    = subcmd_format_regex
1691
                          ->add_subcommand("regex101",
1692
                                           "use regex101.com to edit this "
1693
                                           "regular expression")
1694
                          ->callback([&]() {
40✔
1695
                              format_args.set_action(
×
1696
                                  subcmd_format_t::regex101_default_action);
1697
                          });
×
1698
                format_args.sf_regex101_app = subcmd_format_regex_regex101;
10✔
1699

1700
                {
1701
                    subcmd_format_regex_regex101
1702
                        ->add_subcommand("push",
1703
                                         "create/update an entry for "
1704
                                         "this regex on regex101.com")
1705
                        ->callback([&]() {
40✔
1706
                            format_args.set_action(
×
1707
                                subcmd_format_t::regex101_push_action);
1708
                        });
×
1709
                    subcmd_format_regex_regex101
1710
                        ->add_subcommand(
1711
                            "pull",
1712
                            "create a patch format file for this "
1713
                            "regular expression based on the entry in "
1714
                            "regex101.com")
1715
                        ->callback([&]() {
40✔
1716
                            format_args.set_action(
×
1717
                                subcmd_format_t::regex101_pull_action);
1718
                        });
×
1719
                    subcmd_format_regex_regex101
1720
                        ->add_subcommand(
1721
                            "delete",
1722
                            "delete the entry regex101.com that was "
1723
                            "created by a push operation")
1724
                        ->callback([&]() {
50✔
1725
                            format_args.set_action(
×
1726
                                subcmd_format_t::regex101_delete_action);
1727
                        });
×
1728
                }
1729
            }
1730
        }
1731
    }
1732

1733
    {
1734
        auto* subcmd_piper
1735
            = app.add_subcommand("piper", "perform operations on piper storage")
1736
                  ->callback([&]() {
30✔
1737
                      piper_args.set_action(subcmd_piper_t::default_action);
2✔
1738
                      retval->o_ops = piper_args;
2✔
1739
                  });
2✔
1740
        piper_args.sp_app = subcmd_piper;
10✔
1741

1742
        subcmd_piper
1743
            ->add_subcommand("list", "print the available piper captures")
1744
            ->callback(
50✔
1745
                [&]() { piper_args.set_action(subcmd_piper_t::list_action); });
22✔
1746

1747
        subcmd_piper->add_subcommand("clean", "remove all piper captures")
1748
            ->callback(
60✔
1749
                [&]() { piper_args.set_action(subcmd_piper_t::clean_action); });
20✔
1750
    }
1751

1752
    {
1753
        auto* subcmd_regex101
1754
            = app.add_subcommand("regex101",
1755
                                 "create and edit log message regular "
1756
                                 "expressions using regex101.com")
1757
                  ->callback([&]() {
30✔
1758
                      regex101_args.set_action(
×
1759
                          subcmd_regex101_t::default_action);
1760
                      retval->o_ops = regex101_args;
×
1761
                  });
×
1762
        regex101_args.sr_app = subcmd_regex101;
10✔
1763

1764
        {
1765
            subcmd_regex101
1766
                ->add_subcommand("list",
1767
                                 "list the log format regular expression "
1768
                                 "linked to entries on regex101.com")
1769
                ->callback([&]() {
40✔
1770
                    regex101_args.set_action(subcmd_regex101_t::list_action);
×
1771
                });
×
1772
        }
1773
        {
1774
            auto* subcmd_regex101_import
1775
                = subcmd_regex101
1776
                      ->add_subcommand("import",
1777
                                       "create a new format from a regular "
1778
                                       "expression on regex101.com")
1779
                      ->callback([&]() {
40✔
1780
                          regex101_args.set_action(
×
1781
                              subcmd_regex101_t::import_action);
1782
                      });
×
1783

1784
            subcmd_regex101_import->add_option(
40✔
1785
                "url",
1786
                regex101_args.sr_import_url,
1787
                "The regex101.com url to construct a log format from");
1788
            subcmd_regex101_import->add_option("name",
40✔
1789
                                               regex101_args.sr_import_name,
1790
                                               "The name for the log format");
1791
            subcmd_regex101_import
1792
                ->add_option("regex-name",
60✔
1793
                             regex101_args.sr_import_regex_name,
1794
                             "The name for the new regex")
1795
                ->always_capture_default();
10✔
1796
        }
1797
    }
1798

1799
    {
1800
        auto* subcmd_crash
1801
            = app.add_subcommand("crash", "manage crash logs")->callback([&]() {
30✔
1802
                  crash_args.set_action(subcmd_crash_t::default_action);
×
1803
                  retval->o_ops = crash_args;
×
1804
              });
×
1805
        crash_args.sc_app = subcmd_crash;
10✔
1806

1807
        {
1808
            subcmd_crash->add_subcommand("upload", "upload crash logs")
1809
                ->callback([&]() {
40✔
1810
                    crash_args.set_action(subcmd_crash_t::upload_action);
×
1811
                });
×
1812
        }
1813
    }
1814

1815
    app.parse(argc, argv);
10✔
1816

1817
    return retval;
20✔
1818
}
10✔
1819

1820
perform_result_t
1821
perform(std::shared_ptr<operations> opts)
10✔
1822
{
1823
    return opts->o_ops.match(
10✔
1824
        [](const no_subcmd_t& ns) -> perform_result_t {
×
1825
            auto um = console::user_message::error(
×
1826
                          attr_line_t("expecting an operation to perform"))
×
1827
                          .with_help(
×
1828
                              ns.ns_root_app->get_subcommands({})
×
1829
                              | lnav::itertools::fold(
×
1830
                                  subcmd_reducer,
1831
                                  attr_line_t{"the available operations are:"}))
×
1832
                          .move();
×
1833

1834
            return {std::move(um)};
×
1835
        },
×
1836
        [](const subcmd_apps_t& sa) { return sa.sa_action(sa); },
×
1837
        [](const subcmd_config_t& sc) { return sc.sc_action(sc); },
2✔
1838
        [](const subcmd_format_t& sf) { return sf.sf_action(sf); },
6✔
1839
        [](const subcmd_piper_t& sp) { return sp.sp_action(sp); },
2✔
1840
        [](const subcmd_regex101_t& sr) { return sr.sr_action(sr); },
×
1841
        [](const subcmd_crash_t& sc) { return sc.sc_action(sc); });
20✔
1842
}
1843

1844
}  // 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