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

tstack / lnav / 20037697486-2737

08 Dec 2025 05:52PM UTC coverage: 68.909% (-0.008%) from 68.917%
20037697486-2737

push

github

tstack
[tidy] use lnav::enums::bitset

53 of 59 new or added lines in 12 files covered. (89.83%)

55 existing lines in 3 files now uncovered.

51508 of 74748 relevant lines covered (68.91%)

435428.14 hits per line

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

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

30
#include <algorithm>
31
#include <filesystem>
32
#include <map>
33
#include <memory>
34
#include <string>
35
#include <string_view>
36
#include <vector>
37

38
#include "apps.cfg.hh"
39
#include "apps.hh"
40
#include "base/itertools.hh"
41
#include "base/lnav.console.hh"
42
#include "base/lnav.ryml.hh"
43
#include "base/result.h"
44
#include "bound_tags.hh"
45
#include "command_executor.hh"
46
#include "config.h"
47
#include "external_opener.hh"
48
#include "itertools.similar.hh"
49
#include "libbase64.h"
50
#include "lnav.hh"
51
#include "lnav.indexing.hh"
52
#include "lnav.prompt.hh"
53
#include "lnav_commands.hh"
54
#include "md4c/md4c-html.h"
55
#include "md4cpp.hh"
56
#include "pcrepp/pcre2pp.hh"
57
#include "readline_context.hh"
58
#include "scn/scan.h"
59
#include "service_tags.hh"
60
#include "session.export.hh"
61
#include "shlex.hh"
62
#include "static-files.h"
63
#include "sysclip.hh"
64
#include "text_format.hh"
65
#include "top_status_source.hh"
66
#include "yajlpp/yajlpp.hh"
67

68
#ifdef HAVE_RUST_DEPS
69
#    include "lnav_rs_ext.cxx.hh"
70
#endif
71

72
using namespace lnav::roles::literals;
73
using namespace std::string_view_literals;
74

75
static Result<std::string, lnav::console::user_message>
76
com_export_session_to(exec_context& ec,
6✔
77
                      std::string cmdline,
78
                      std::vector<std::string>& args)
79
{
80
    std::string retval;
6✔
81

82
    if (!ec.ec_dry_run) {
6✔
83
        auto_mem<FILE> outfile(fclose);
6✔
84
        auto fn = trim(remaining_args(cmdline, args));
6✔
85
        auto to_term = false;
6✔
86

87
        if (fn == "-" || fn == "/dev/stdout") {
6✔
88
            auto ec_out = ec.get_output();
3✔
89

90
            if (!ec_out) {
3✔
91
                outfile = auto_mem<FILE>::leak(stdout);
×
92

93
                if (ec.ec_ui_callbacks.uc_pre_stdout_write) {
×
94
                    ec.ec_ui_callbacks.uc_pre_stdout_write();
×
95
                }
96
                setvbuf(stdout, nullptr, _IONBF, 0);
×
97
                to_term = true;
×
98
                fprintf(outfile,
×
99
                        "\n---------------- Press any key to exit "
100
                        "lo-fi "
101
                        "display "
102
                        "----------------\n\n");
103
            } else {
104
                outfile = auto_mem<FILE>::leak(ec_out.value());
3✔
105
            }
106
            if (outfile.in() == stdout) {
3✔
107
                lnav_data.ld_stdout_used = true;
3✔
108
            }
109
        } else if (fn == "/dev/clipboard") {
3✔
110
            auto open_res = sysclip::open(sysclip::type_t::GENERAL);
×
111
            if (open_res.isErr()) {
×
112
                alerter::singleton().chime("cannot open clipboard");
×
113
                return ec.make_error("Unable to copy to clipboard: {}",
114
                                     open_res.unwrapErr());
×
115
            }
116
            outfile = open_res.unwrap();
×
117
        } else if (lnav_data.ld_flags.is_set<lnav_flags::secure_mode>()) {
3✔
118
            return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
119
        } else {
120
            if ((outfile = fopen(fn.c_str(), "we")) == nullptr) {
3✔
121
                return ec.make_error("unable to open file -- {}", fn);
×
122
            }
123
            fchmod(fileno(outfile.in()), S_IRWXU);
3✔
124
        }
125

126
        auto export_res = lnav::session::export_to(outfile.in());
6✔
127

128
        fflush(outfile.in());
6✔
129
        if (to_term) {
6✔
130
            if (ec.ec_ui_callbacks.uc_post_stdout_write) {
×
131
                ec.ec_ui_callbacks.uc_post_stdout_write();
×
132
            }
133
        }
134
        if (export_res.isErr()) {
6✔
135
            return Err(export_res.unwrapErr());
×
136
        }
137

138
        retval = fmt::format(
6✔
139
            FMT_STRING("info: wrote session commands to -- {}"), fn);
24✔
140
    }
6✔
141

142
    return Ok(retval);
6✔
143
}
6✔
144

145
static Result<std::string, lnav::console::user_message>
146
com_rebuild(exec_context& ec,
9✔
147
            std::string cmdline,
148
            std::vector<std::string>& args)
149
{
150
    if (!ec.ec_dry_run) {
9✔
151
        rescan_files(true);
9✔
152
        rebuild_indexes_repeatedly();
9✔
153
    }
154

155
    return Ok(std::string());
9✔
156
}
157

158
static Result<std::string, lnav::console::user_message>
159
com_echo(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
45✔
160
{
161
    std::string retval = "error: expecting a message";
45✔
162

163
    if (args.size() >= 1) {
45✔
164
        bool lf = true;
45✔
165
        std::string src;
45✔
166

167
        if (args.size() > 2 && args[1] == "-n") {
45✔
168
            std::string::size_type index_in_cmdline = cmdline.find(args[1]);
1✔
169

170
            lf = false;
1✔
171
            src = cmdline.substr(index_in_cmdline + args[1].length() + 1);
1✔
172
        } else if (args.size() >= 2) {
44✔
173
            src = cmdline.substr(args[0].length() + 1);
30✔
174
        } else {
175
            src = "";
14✔
176
        }
177

178
        auto lexer = shlex(src);
45✔
179
        lexer.eval(retval, ec.create_resolver());
45✔
180

181
        auto ec_out = ec.get_output();
45✔
182
        if (ec.ec_dry_run) {
45✔
183
            lnav_data.ld_preview_status_source[0].get_description().set_value(
×
184
                "The text to output:"_frag);
×
185
            lnav_data.ld_status[LNS_PREVIEW0].set_needs_update();
×
186
            lnav_data.ld_preview_view[0].set_sub_source(
×
187
                &lnav_data.ld_preview_source[0]);
188
            lnav_data.ld_preview_source[0].replace_with(attr_line_t(retval));
×
189
            retval = "";
×
190
        } else if (ec_out) {
45✔
191
            FILE* outfile = *ec_out;
43✔
192

193
            if (outfile == stdout) {
43✔
194
                lnav_data.ld_stdout_used = true;
37✔
195
            }
196

197
            fprintf(outfile, "%s", retval.c_str());
43✔
198
            if (lf) {
43✔
199
                putc('\n', outfile);
42✔
200
            }
201
            fflush(outfile);
43✔
202

203
            retval = "";
43✔
204
        }
205
    }
45✔
206

207
    return Ok(retval);
90✔
208
}
45✔
209

210
static Result<std::string, lnav::console::user_message>
211
com_alt_msg(exec_context& ec,
×
212
            std::string cmdline,
213
            std::vector<std::string>& args)
214
{
215
    static auto& prompt = lnav::prompt::get();
216

217
    std::string retval;
×
218

219
    if (ec.ec_dry_run) {
×
220
        retval = "";
×
221
    } else if (args.size() == 1) {
×
222
        prompt.p_editor.clear_alt_value();
×
223
        retval = "";
×
224
    } else {
225
        std::string msg = remaining_args(cmdline, args);
×
226

227
        prompt.p_editor.set_alt_value(msg);
×
228
        retval = "";
×
229
    }
230

231
    return Ok(retval);
×
232
}
233

234
static Result<std::string, lnav::console::user_message>
235
com_eval(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
22✔
236
{
237
    std::string retval;
22✔
238

239
    if (args.size() > 1) {
22✔
240
        static intern_string_t EVAL_SRC = intern_string::lookup(":eval");
32✔
241

242
        std::string all_args = remaining_args(cmdline, args);
22✔
243
        std::string expanded_cmd;
22✔
244
        shlex lexer(all_args.c_str(), all_args.size());
22✔
245

246
        log_debug("Evaluating: %s", all_args.c_str());
22✔
247
        if (!lexer.eval(expanded_cmd,
22✔
248
                        {
249
                            &ec.ec_local_vars.top(),
22✔
250
                            &ec.ec_global_vars,
22✔
251
                        }))
252
        {
253
            return ec.make_error("invalid arguments");
×
254
        }
255
        log_debug("Expanded command to evaluate: %s", expanded_cmd.c_str());
22✔
256

257
        if (expanded_cmd.empty()) {
22✔
258
            return ec.make_error("empty result after evaluation");
×
259
        }
260

261
        if (ec.ec_dry_run) {
22✔
262
            attr_line_t al(expanded_cmd);
×
263

264
            lnav_data.ld_preview_status_source[0].get_description().set_value(
×
265
                "The command to be executed:"_frag);
×
266
            lnav_data.ld_status[LNS_PREVIEW0].set_needs_update();
×
267

268
            lnav_data.ld_preview_view[0].set_sub_source(
×
269
                &lnav_data.ld_preview_source[0]);
270
            lnav_data.ld_preview_source[0].replace_with(al);
×
271

272
            return Ok(std::string());
×
273
        }
274

275
        auto src_guard = ec.enter_source(EVAL_SRC, 1, expanded_cmd);
22✔
276
        auto content = string_fragment::from_str(expanded_cmd);
22✔
277
        multiline_executor me(ec, ":eval");
22✔
278
        for (auto line : content.split_lines()) {
106✔
279
            TRY(me.push_back(line));
84✔
280
        }
22✔
281
        TRY(me.final());
22✔
282
        retval = std::move(me.me_last_result);
22✔
283
    } else {
22✔
284
        return ec.make_error("expecting a command or query to evaluate");
×
285
    }
286

287
    return Ok(retval);
22✔
288
}
22✔
289

290
static Result<std::string, lnav::console::user_message>
291
com_cd(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
4✔
292
{
293
    static const intern_string_t SRC = intern_string::lookup("path");
12✔
294

295
    if (lnav_data.ld_flags.is_set<lnav_flags::secure_mode>()) {
4✔
296
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
297
    }
298

299
    std::vector<std::string> word_exp;
4✔
300
    std::string pat;
4✔
301

302
    pat = trim(remaining_args(cmdline, args));
4✔
303

304
    shlex lexer(pat);
4✔
305
    auto split_args_res = lexer.split(ec.create_resolver());
4✔
306
    if (split_args_res.isErr()) {
4✔
307
        auto split_err = split_args_res.unwrapErr();
×
308
        auto um
309
            = lnav::console::user_message::error("unable to parse file name")
×
310
                  .with_reason(split_err.se_error.te_msg)
×
311
                  .with_snippet(lnav::console::snippet::from(
×
312
                      SRC, lexer.to_attr_line(split_err.se_error)))
×
313
                  .move();
×
314

315
        return Err(um);
×
316
    }
317

318
    auto split_args = split_args_res.unwrap()
8✔
319
        | lnav::itertools::map([](const auto& elem) { return elem.se_value; });
12✔
320

321
    if (split_args.size() != 1) {
4✔
322
        return ec.make_error("expecting a single argument");
×
323
    }
324

325
    struct stat st;
326

327
    if (stat(split_args[0].c_str(), &st) != 0) {
4✔
328
        return Err(ec.make_error_msg("cannot access -- {}", split_args[0])
3✔
329
                       .with_errno_reason());
1✔
330
    }
331

332
    if (!S_ISDIR(st.st_mode)) {
3✔
333
        return ec.make_error("{} is not a directory", split_args[0]);
2✔
334
    }
335

336
    if (!ec.ec_dry_run) {
2✔
337
        chdir(split_args[0].c_str());
2✔
338
        setenv("PWD", split_args[0].c_str(), 1);
2✔
339
    }
340

341
    return Ok(std::string());
2✔
342
}
4✔
343

344
static Result<std::string, lnav::console::user_message>
345
com_sh(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
5✔
346
{
347
    if (lnav_data.ld_flags.is_set<lnav_flags::secure_mode>()) {
5✔
348
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
349
    }
350

351
    static size_t EXEC_COUNT = 0;
352

353
    if (!ec.ec_dry_run) {
5✔
354
        std::optional<std::string> name_flag;
5✔
355

356
        shlex lexer(cmdline);
5✔
357
        auto cmd_start = args[0].size();
5✔
358
        auto split_res = lexer.split(ec.create_resolver());
5✔
359
        if (split_res.isOk()) {
5✔
360
            auto flags = split_res.unwrap();
5✔
361
            if (flags.size() >= 2) {
5✔
362
                static const char* NAME_FLAG = "--name=";
363

364
                if (startswith(flags[1].se_value, NAME_FLAG)) {
5✔
365
                    name_flag = flags[1].se_value.substr(strlen(NAME_FLAG));
×
366
                    cmd_start = flags[1].se_origin.sf_end;
×
367
                }
368
            }
369
        }
5✔
370

371
        auto carg = trim(cmdline.substr(cmd_start));
5✔
372

373
        log_info("executing: %s", carg.c_str());
5✔
374

375
        auto child_fds_res
376
            = auto_pipe::for_child_fds(STDOUT_FILENO, STDERR_FILENO);
5✔
377
        if (child_fds_res.isErr()) {
5✔
378
            auto um = lnav::console::user_message::error(
×
379
                          "unable to create child pipes")
380
                          .with_reason(child_fds_res.unwrapErr())
×
381
                          .move();
×
382
            ec.add_error_context(um);
×
383
            return Err(um);
×
384
        }
385
        auto child_res = lnav::pid::from_fork();
5✔
386
        if (child_res.isErr()) {
5✔
387
            auto um
388
                = lnav::console::user_message::error("unable to fork() child")
×
389
                      .with_reason(child_res.unwrapErr())
×
390
                      .move();
×
391
            ec.add_error_context(um);
×
392
            return Err(um);
×
393
        }
394

395
        auto child_fds = child_fds_res.unwrap();
5✔
396
        auto child = child_res.unwrap();
5✔
397
        for (auto& child_fd : child_fds) {
15✔
398
            child_fd.after_fork(child.in());
10✔
399
        }
400
        if (child.in_child()) {
5✔
401
            auto dev_null = open("/dev/null", O_RDONLY | O_CLOEXEC);
×
402

403
            dup2(dev_null, STDIN_FILENO);
×
404
            const char* exec_args[] = {
×
405
                getenv_opt("SHELL").value_or("bash"),
×
406
                "-c",
407
                carg.c_str(),
×
408
                nullptr,
409
            };
410

411
            for (const auto& pair : ec.ec_local_vars.top()) {
×
412
                pair.second.match(
×
413
                    [&pair](const std::string& val) {
×
414
                        setenv(pair.first.c_str(), val.c_str(), 1);
×
415
                    },
×
416
                    [&pair](const string_fragment& sf) {
×
417
                        setenv(pair.first.c_str(), sf.to_string().c_str(), 1);
×
418
                    },
×
419
                    [](null_value_t) {},
×
420
                    [&pair](int64_t val) {
×
421
                        setenv(
×
422
                            pair.first.c_str(), fmt::to_string(val).c_str(), 1);
×
423
                    },
×
424
                    [&pair](double val) {
×
425
                        setenv(
×
426
                            pair.first.c_str(), fmt::to_string(val).c_str(), 1);
×
427
                    },
×
428
                    [&pair](bool val) {
×
429
                        setenv(pair.first.c_str(), val ? "1" : "0", 1);
×
430
                    });
×
431
            }
432

433
            execvp(exec_args[0], (char**) exec_args);
×
434
            _exit(EXIT_FAILURE);
×
435
        }
436

437
        std::string display_name;
5✔
438
        auto open_prov = ec.get_provenance<exec_context::file_open>();
5✔
439
        if (open_prov) {
5✔
440
            if (name_flag) {
1✔
441
                display_name = fmt::format(
×
442
                    FMT_STRING("{}/{}"), open_prov->fo_name, name_flag.value());
×
443
            } else {
444
                display_name = open_prov->fo_name;
1✔
445
            }
446
        } else if (name_flag) {
4✔
447
            display_name = name_flag.value();
×
448
        } else {
449
            display_name
450
                = fmt::format(FMT_STRING("sh-{} {}"), EXEC_COUNT++, carg);
16✔
451
        }
452

453
        auto name_base = display_name;
5✔
454
        size_t name_counter = 0;
5✔
455

456
        while (true) {
457
            auto fn_iter
458
                = lnav_data.ld_active_files.fc_file_names.find(display_name);
5✔
459
            if (fn_iter == lnav_data.ld_active_files.fc_file_names.end()) {
5✔
460
                break;
5✔
461
            }
462
            name_counter += 1;
×
463
            display_name
464
                = fmt::format(FMT_STRING("{} [{}]"), name_base, name_counter);
×
465
        }
466

467
        auto create_piper_res
468
            = lnav::piper::create_looper(display_name,
469
                                         std::move(child_fds[0].read_end()),
5✔
470
                                         std::move(child_fds[1].read_end()));
10✔
471

472
        if (create_piper_res.isErr()) {
5✔
473
            auto um
474
                = lnav::console::user_message::error("unable to create piper")
×
475
                      .with_reason(create_piper_res.unwrapErr())
×
476
                      .move();
×
477
            ec.add_error_context(um);
×
478
            return Err(um);
×
479
        }
480

481
        lnav_data.ld_active_files.fc_file_names[display_name].with_piper(
10✔
482
            create_piper_res.unwrap());
10✔
483
        lnav_data.ld_child_pollers.emplace_back(child_poller{
15✔
484
            display_name,
485
            std::move(child),
5✔
486
            [](auto& fc, auto& child) {},
5✔
487
        });
488
        lnav_data.ld_files_to_front.emplace_back(display_name);
5✔
489

490
        return Ok(fmt::format(FMT_STRING("info: executing -- {}"), carg));
20✔
491
    }
5✔
492

493
    return Ok(std::string());
×
494
}
495

496
#ifdef HAVE_RUST_DEPS
497

498
static lnav::task_progress
499
ext_prog_rep()
1,641✔
500
{
501
    auto ext = lnav_rs_ext::get_status();
1,641✔
502
    auto status = ext.status == lnav_rs_ext::Status::idle
1,641✔
503
        ? lnav::progress_status_t::idle
1,641✔
504
        : lnav::progress_status_t::working;
505
    std::vector<lnav::console::user_message> msgs_out;
1,641✔
506
    for (const auto& err : ext.messages) {
1,642✔
507
        auto um = lnav::console::user_message::error((std::string) err.error)
2✔
508
                      .with_reason((std::string) err.source)
2✔
509
                      .with_help((std::string) err.help);
1✔
510
        msgs_out.emplace_back(um);
1✔
511
    }
1✔
512
    auto retval = lnav::task_progress{
513
        (std::string) ext.id,
514
        status,
515
        ext.version,
1,641✔
516
        (std::string) ext.current_step,
517
        ext.completed,
1,641✔
518
        ext.total,
1,641✔
519
        std::move(msgs_out),
1,641✔
520
    };
1,641✔
521

522
    return retval;
3,282✔
523
}
1,641✔
524

525
DIST_SLICE(prog_reps) lnav::progress_reporter_t EXT_PROG_REP = ext_prog_rep;
526

527
namespace lnav_rs_ext {
528

529
LnavLogLevel
530
get_lnav_log_level()
6,466✔
531
{
532
    switch (lnav_log_level) {
6,466✔
533
        case lnav_log_level_t::TRACE:
×
534
            return LnavLogLevel::trace;
×
535
        case lnav_log_level_t::DEBUG:
6,466✔
536
            return LnavLogLevel::debug;
6,466✔
537
        case lnav_log_level_t::INFO:
×
538
            return LnavLogLevel::info;
×
539
        case lnav_log_level_t::WARNING:
×
540
            return LnavLogLevel::warning;
×
541
        case lnav_log_level_t::ERROR:
×
542
            return LnavLogLevel::error;
×
543
    }
544

545
    return LnavLogLevel::info;
×
546
}
547

548
void
549
log_msg(LnavLogLevel level, ::rust::Str file, uint32_t line, ::rust::Str msg)
5,458✔
550
{
551
    auto ln_level = static_cast<lnav_log_level_t>(level);
5,458✔
552

553
    ::log_msg(ln_level,
10,916✔
554
              ((std::string) file).c_str(),
10,916✔
555
              line,
556
              "%.*s",
557
              (int) msg.size(),
5,458✔
558
              msg.data());
559
}
5,458✔
560

561
::rust::String
562
version_info()
×
563
{
564
    yajlpp_gen gen;
×
565
    {
566
        yajlpp_map root(gen);
×
567

568
        root.gen("product");
×
569
        root.gen(PACKAGE);
×
570
        root.gen("version");
×
571
        root.gen(PACKAGE_VERSION);
×
572
        root.gen("pid");
×
573
        root.gen(getpid());
×
574
    }
575

576
    return gen.to_string_fragment().to_string();
×
577
}
578

579
static void
580
process_md_out(const MD_CHAR* data, MD_SIZE len, void* user_data)
×
581
{
582
    auto& buffer = *((::rust::Vec<uint8_t>*) user_data);
×
583
    auto sv = std::string_view(data, len);
×
584

585
    std::copy(sv.begin(), sv.end(), std::back_inserter(buffer));
×
586
}
587

588
struct static_file_not_found_t {};
589

590
struct static_app_file {
591
    std::filesystem::path saf_path;
592
};
593

594
using static_file_t = mapbox::util::
595
    variant<const bin_src_file*, static_app_file, static_file_not_found_t>;
596

597
static std::optional<const bin_src_file*>
598
find_static_src_file(const std::string& path)
×
599
{
600
    for (const auto& file : lnav_static_files) {
×
601
        if (file.get_name() == path) {
×
602
            return &file;
×
603
        }
604
    }
605

606
    return std::nullopt;
×
607
}
608

609
static static_file_t
610
find_static_file(const std::string& path)
×
611
{
612
    static const auto APP_PATH = lnav::pcre2pp::code::from_const(
613
        "apps/(?<pub>[^/]+)/(?<name>[^/]+)?(?<path>.*)");
×
614
    static const auto app_cfg = injector::get<lnav::apps::config&>();
615
    static const auto ROOT_PATH = std::filesystem::path("/");
616
    thread_local auto match_data = lnav::pcre2pp::match_data::unitialized();
×
617

618
    auto exact_match = find_static_src_file(path);
×
619
    if (exact_match) {
×
620
        return static_file_t{exact_match.value()};
×
621
    }
622

623
    auto matcher = APP_PATH.capture_from(path).into(match_data);
×
624
    if (matcher.found_p()) {
×
625
        auto pub = match_data["pub"]->to_string();
×
626
        auto name = match_data["name"]->to_string();
×
627
        auto path = std::filesystem::path(match_data["path"]->to_string())
×
628
                        .lexically_normal()
×
629
                        .lexically_relative(ROOT_PATH);
×
630

631
        log_info("static file request: %s/%s %s",
×
632
                 pub.c_str(),
633
                 name.c_str(),
634
                 path.c_str());
635
        auto pub_iter = app_cfg.c_publishers.find(pub);
×
636
        if (pub_iter != app_cfg.c_publishers.end()) {
×
637
            auto app_iter = pub_iter->second.pd_apps.find(name);
×
638
            if (app_iter != pub_iter->second.pd_apps.end()) {
×
639
                auto file_path = app_iter->second.get_root_path() / path;
×
640
                log_trace("  full path: %s", file_path.c_str());
×
641
                std::error_code ec;
×
642
                if (std::filesystem::is_regular_file(file_path, ec)) {
×
643
                    return static_app_file{file_path};
×
644
                }
645
            }
646
        }
647
    }
648

649
    return static_file_t{static_file_not_found_t{}};
×
650
}
651

652
static static_file_t
653
resolve_static_src_file(std::string path)
×
654
{
655
    static const auto HTML_EXT = lnav::pcre2pp::code::from_const("\\.html$");
656

657
    auto exact_match = find_static_file(path);
×
658
    if (!exact_match.is<static_file_not_found_t>()) {
×
659
        return exact_match;
×
660
    }
661

662
    if (!path.empty() && !endswith(path, "/")) {
×
663
        path += "/";
×
664
    }
665
    path += "index.html";
×
666
    auto index_match = find_static_file(path);
×
667
    if (!index_match.is<static_file_not_found_t>()) {
×
668
        return index_match;
×
669
    }
670

671
    path = HTML_EXT.replace(path, ".md");
×
672
    auto md_match = find_static_file(path);
×
673
    if (!md_match.is<static_file_not_found_t>()) {
×
674
        return md_match;
×
675
    }
676

677
    return static_file_t{static_file_not_found_t{}};
×
678
}
679

680
static void
681
render_markdown(const std::filesystem::path& src,
×
682
                const std::string& content,
683
                ::rust::Vec<uint8_t>& dst)
684
{
685
    static const auto HEADER = R"(
686
<html>
687
<head>
688
<title>{title}</title>
689
<link rel="stylesheet" href="/assets/css/main.css">
690
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism.min.css">
691
<script src="/assets/js/lnav-client.js"></script>
692
</head>
693
<body>
694
<main class="page-content">
695
<div class="wrapper">
696
)";
697
    static const auto FOOTER = R"(
698
<script src="/assets/js/lnav-markdown-magic.js"></script>
699
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js"></script>
700
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-sql.min.js"></script>
701
<script src="/assets/js/prism-lnav.js"></script>
702
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js"></script>
703
</div>
704
</main>
705
<footer class="site-footer">
706
</footer>
707
</body>
708
</html>
709
)"sv;
710

711
    auto md_file = md4cpp::parse_file(src, content);
×
712
    auto title = fmt::format(FMT_STRING("lnav: {}"), src);
×
713

714
    if (md_file.f_frontmatter_format == text_format_t::TF_YAML) {
×
715
        auto tree = ryml::parse_in_arena(
716
            lnav::ryml::to_csubstr(src.string()),
×
717
            lnav::ryml::to_csubstr(md_file.f_frontmatter));
×
718
        tree["title"] >> title;
×
719
    }
720

721
    auto head = fmt::format(HEADER, fmt::arg("title", title));
×
722
    std::copy(head.begin(), head.end(), std::back_inserter(dst));
×
723
    md_html(md_file.f_body.data(),
×
724
            md_file.f_body.length(),
×
725
            process_md_out,
726
            &dst,
727
            MD_DIALECT_GITHUB | MD_FLAG_UNDERLINE | MD_FLAG_STRIKETHROUGH,
728
            0);
729
    std::copy(FOOTER.begin(), FOOTER.end(), std::back_inserter(dst));
×
730
}
731

732
void
733
get_static_file(::rust::Str path, ::rust::Vec<uint8_t>& dst)
×
734
{
735
    auto path_str = (std::string) path;
×
736

737
    if (startswith(path_str, "/")) {
×
738
        path_str.erase(0, 1);
×
739
    }
740
    log_info("static file request: %s", path_str.c_str());
×
741
    auto matched_file = resolve_static_src_file(path_str);
×
742
    matched_file.match(
×
743
        [&dst](const bin_src_file* file) {
×
744
            auto name = file->get_name();
×
745
            log_info("  matched static source file: %.*s",
×
746
                     name.length(),
747
                     name.data());
748
            if (name.endswith(".md")) {
×
749
                render_markdown(
×
750
                    file->get_name().to_string(),
×
751
                    file->to_string_fragment_producer()->to_string(),
×
752
                    dst);
753
            } else {
754
                auto prod = file->to_string_fragment_producer();
×
755
                prod->for_each([&dst](const auto& sf) {
×
756
                    std::copy(sf.begin(), sf.end(), std::back_inserter(dst));
×
757
                    return Ok();
×
758
                });
759
            }
760
        },
×
761
        [&dst](const static_app_file& file) {
×
762
            log_info("  matched app file: %s", file.saf_path.c_str());
×
763
            if (file.saf_path.extension() == ".md") {
×
764
                auto read_res = lnav::filesystem::read_file(file.saf_path);
×
765
                if (read_res.isOk()) {
×
766
                    render_markdown(file.saf_path, read_res.unwrap(), dst);
×
767
                } else {
768
                    log_error("failed to read app file: %s - %s",
×
769
                              file.saf_path.c_str(),
770
                              read_res.unwrapErr().c_str());
771
                }
772
                return;
×
773
            }
774
            auto open_res
775
                = lnav::filesystem::open_file(file.saf_path, O_RDONLY);
×
776
            if (open_res.isOk()) {
×
777
                auto fd = open_res.unwrap();
×
778
                char buffer[1024];
779

780
                while (true) {
781
                    auto read_res = read(fd, buffer, sizeof(buffer));
×
782
                    if (read_res < 0) {
×
783
                        log_error("read of app file failed: %s - %s",
×
784
                                  file.saf_path.c_str(),
785
                                  strerror(errno));
786
                        break;
×
787
                    }
788
                    if (read_res == 0) {
×
789
                        break;
×
790
                    }
791
                    std::copy(
×
792
                        buffer, buffer + read_res, std::back_inserter(dst));
×
793
                }
794
            } else {
×
795
                log_error("failed to open app file: %s - %s",
×
796
                          file.saf_path.c_str(),
797
                          open_res.unwrapErr().c_str());
798
            }
799
        },
×
800
        [](const static_file_not_found_t&) {
×
801
            log_info("  static file not found");
×
802
        });
×
803
}
804

805
ExecResult
806
execute_external_command(::rust::String rs_src,
×
807
                         ::rust::String rs_script,
808
                         ::rust::String hdrs,
809
                         ::rust::Vec<VarPair> vars)
810
{
811
    auto src = (std::string) rs_src;
×
812
    auto script = (std::string) rs_script;
×
813
    auto retval = std::make_shared<ExecResult>();
×
814

815
    log_debug("sending remote command to main looper");
×
816
    isc::to<main_looper&, services::main_t>().send_and_wait(
×
817
        [src, script, hdrs, vars, &retval](auto& mlooper) {
×
818
            log_debug("executing remote command from: %s", src.c_str());
×
819
            db_label_source ext_db_source;
×
820
            auto& ec = lnav_data.ld_exec_context;
×
821
            // XXX we should still allow an external command to update the
822
            // regular DB view.
823
            auto dsg = ec.enter_db_source(&ext_db_source);
×
824
            auto* outfile = tmpfile();
×
825
            auto ec_out = exec_context::output_t{outfile, fclose};
×
826
            auto og = exec_context::output_guard{ec, "default", ec_out};
×
827
            auto me = multiline_executor{ec, src};
×
828
            auto pg = ec.with_provenance(exec_context::external_access{src});
×
829
            ec.ec_local_vars.push(std::map<std::string, scoped_value_t>{
×
830
                {"headers", scoped_value_t{(std::string) hdrs}}});
×
831
            log_trace("var count: %zu", vars.size());
×
832
            for (const auto& var : vars) {
×
833
                auto buf = auto_buffer::alloc(var.value.length());
×
834
                auto outlen = buf.capacity();
×
835
                base64_decode(
×
836
                    var.value.data(), var.value.length(), buf.in(), &outlen, 0);
837
                buf.resize(outlen);
×
838
                auto key_str = (std::string) var.expr;
×
839
                log_trace(
×
840
                    "  %s=%.*s", key_str.c_str(), (int) buf.size(), buf.data());
841
                ec.ec_local_vars.top()[key_str]
×
842
                    = scoped_value_t{buf.to_string()};
×
843
            }
844
            auto script_frag = string_fragment::from_str(script);
×
845
            for (const auto& line : script_frag.split_lines()) {
×
846
                auto res = me.push_back(line);
×
847
                if (res.isErr()) {
×
848
                    auto um = res.unwrapErr();
×
849
                    retval->error.msg = um.um_message.al_string;
×
850
                    retval->error.reason = um.um_reason.al_string;
×
851
                    retval->error.help = um.um_help.al_string;
×
852
                    ec.ec_local_vars.pop();
×
853
                    return;
×
854
                }
855
            }
856
            auto res = me.final();
×
857
            if (res.isErr()) {
×
858
                auto um = res.unwrapErr();
×
859
                retval->error.msg = um.um_message.al_string;
×
860
                retval->error.reason = um.um_reason.al_string;
×
861
                retval->error.help = um.um_help.al_string;
×
862
            } else {
×
863
                fseek(ec_out.first, 0, SEEK_SET);
×
864
                retval->status = me.me_last_result;
×
865
                retval->content_type = fmt::to_string(ec.get_output_format());
×
866
                retval->content_fd = dup(fileno(ec_out.first));
×
867
            }
868
            ec.ec_local_vars.pop();
×
869
        });
×
870

871
    return *retval;
×
872
}
873

874
}  // namespace lnav_rs_ext
875
#endif
876

877
static Result<std::string, lnav::console::user_message>
878
com_external_access(exec_context& ec,
×
879
                    std::string cmdline,
880
                    std::vector<std::string>& args)
881
{
882
#ifdef HAVE_RUST_DEPS
883
    if (args.size() != 3) {
×
884
        return ec.make_error("Expecting port number and API key");
×
885
    }
886

NEW
887
    if (lnav_data.ld_flags.is_set<lnav_flags::secure_mode>()) {
×
888
        return ec.make_error("External access is not available in secure mode");
×
889
    }
890

891
    std::string retval;
×
892
    if (ec.ec_dry_run) {
×
893
        return Ok(retval);
×
894
    }
895

896
    auto scan_res = scn::scan_int<uint16_t>(args[1]);
×
897
    if (!scan_res || !scan_res.value().range().empty()) {
×
898
        return ec.make_error(FMT_STRING("port value is not a number: {}"),
×
899
                             args[1]);
×
900
    }
901
    auto port = scan_res->value();
×
902

903
    auto buf = auto_buffer::alloc((args[2].size() * 5) / 3);
×
904
    auto outlen = buf.capacity();
×
905
    base64_encode(args[2].data(), args[2].size(), buf.in(), &outlen, 0);
×
906
    auto start_res
907
        = lnav_rs_ext::start_ext_access(port, ::rust::String(buf.in(), outlen));
×
908
    if (start_res.port == 0) {
×
909
        return ec.make_error(FMT_STRING("unable to start external access: {}"),
×
910
                             (std::string) start_res.error);
×
911
    }
912

913
    retval = fmt::format(FMT_STRING("info: started external access on port {}"),
×
914
                         start_res.port);
×
915
    setenv("LNAV_EXTERNAL_PORT", fmt::to_string(start_res.port).c_str(), 1);
×
916
    auto url = fmt::format(FMT_STRING("http://127.0.0.1:{}"), start_res.port);
×
917
    setenv("LNAV_EXTERNAL_URL", url.c_str(), 1);
×
918

919
    {
920
        auto top_source = injector::get<std::shared_ptr<top_status_source>>();
×
921

922
        auto& sf = top_source->statusview_value_for_field(
×
923
            top_status_source::TSF_EXT_ACCESS);
924

925
        sf.set_width(3);
×
926
        sf.set_value("\xF0\x9F\x8C\x90");
×
927
        sf.on_click = [](auto& top_source) {
×
928
            auto& ec = lnav_data.ld_exec_context;
×
929
            ec.execute(INTERNAL_SRC_LOC, ":external-access-login");
×
930
        };
931
    }
932

933
    return Ok(retval);
×
934
#else
935
    return ec.make_error("lnav was compiled without Rust extensions");
936
#endif
937
}
938

939
static Result<std::string, lnav::console::user_message>
940
com_external_access_login(exec_context& ec,
×
941
                          std::string cmdline,
942
                          std::vector<std::string>& args)
943
{
944
#ifdef HAVE_RUST_DEPS
945
    auto url = getenv("LNAV_EXTERNAL_URL");
×
946
    if (url == nullptr) {
×
947
        auto um = lnav::console::user_message::error(
×
948
                      "external-access is not enabled")
949
                      .with_help(attr_line_t("Use the ")
×
950
                                     .append(":external-access"_keyword)
×
951
                                     .append(" command to enable"));
×
952

953
        return Err(um);
×
954
    }
955

956
    std::string retval;
×
957
    std::string target;
×
958
    if (args.size() == 2) {
×
959
        auto app_sf = string_fragment::from_str(args[1]);
×
960
        auto split_res = app_sf.split_pair(string_fragment::tag1{'/'});
×
961
        if (!split_res) {
×
962
            auto um
963
                = lnav::console::user_message::error(
×
964
                      attr_line_t("invalid app name ").append_quoted(app_sf))
×
965
                      .with_reason(
×
966
                          attr_line_t("Expecting an argument of the form ")
×
967
                              .append("publisher"_variable)
×
968
                              .append("/")
×
969
                              .append("app"_variable));
×
970
            return Err(um);
×
971
        }
972

973
        auto poss_apps = lnav::apps::get_app_names();
×
974
        auto iter = std::find(poss_apps.begin(), poss_apps.end(), app_sf);
×
975
        if (iter == poss_apps.end()) {
×
976
            auto similar_apps
977
                = poss_apps | lnav::itertools::similar_to(app_sf.to_string());
×
978
            auto um = lnav::console::user_message::error(
979
                attr_line_t("unknown app ").append_quoted(app_sf));
×
980
            if (!similar_apps.empty()) {
×
981
                auto note = attr_line_t("Did you mean one of the following: ")
×
982
                                .join(similar_apps, ", ");
×
983
                um.with_note(note);
×
984
            }
985
            return Err(um);
×
986
        }
987

988
        target = fmt::format(FMT_STRING("&target=/apps/{}/"), app_sf);
×
989
    }
990

991
    if (ec.ec_dry_run) {
×
992
        return Ok(retval);
×
993
    }
994

995
    auto otp = (std::string) lnav_rs_ext::set_one_time_password();
×
996
    auto url_with_otp
997
        = fmt::format(FMT_STRING("{}/login?otp={}{}"), url, otp, target);
×
998
    auto open_res = lnav::external_opener::for_href(url_with_otp);
×
999
    if (open_res.isErr()) {
×
1000
        auto err = open_res.unwrapErr();
×
1001
        auto um = lnav::console::user_message::error(
×
1002
                      "unable to open external access URL")
1003
                      .with_reason(err);
×
1004

1005
        return Err(um);
×
1006
    }
1007

1008
    return Ok(retval);
×
1009
#else
1010
    return ec.make_error("lnav was compiled without Rust extensions");
1011
#endif
1012
}
1013

1014
static readline_context::command_t SCRIPTING_COMMANDS[] = {
1015
    {
1016
        "export-session-to",
1017
        com_export_session_to,
1018

1019
        help_text(":export-session-to")
1020
            .with_summary("Export the current lnav state to an executable lnav "
1021
                          "script file that contains the commands needed to "
1022
                          "restore the current session")
1023
            .with_parameter(
1024
                help_text("path", "The path to the file to write")
1025
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
1026
            .with_tags({"io", "scripting"}),
1027
    },
1028
    {
1029
        "rebuild",
1030
        com_rebuild,
1031
        help_text(":rebuild")
1032
            .with_summary("Forcefully rebuild file indexes")
1033
            .with_tags({"scripting"}),
1034
    },
1035
    {
1036
        "echo",
1037
        com_echo,
1038

1039
        help_text(":echo")
1040
            .with_summary(
1041
                "Echo the given message to the screen or, if "
1042
                ":redirect-to has "
1043
                "been called, to output file specified in the "
1044
                "redirect.  "
1045
                "Variable substitution is performed on the message.  "
1046
                "Use a "
1047
                "backslash to escape any special characters, like '$'")
1048
            .with_parameter(help_text("-n",
1049
                                      "Do not print a line-feed at "
1050
                                      "the end of the output")
1051
                                .optional()
1052
                                .with_format(help_parameter_format_t::HPF_TEXT))
1053
            .with_parameter(help_text("msg", "The message to display"))
1054
            .with_tags({"io", "scripting"})
1055
            .with_example({"To output 'Hello, World!'", "Hello, World!"}),
1056
    },
1057
    {
1058
        "alt-msg",
1059
        com_alt_msg,
1060

1061
        help_text(":alt-msg")
1062
            .with_summary("Display a message in the alternate command position")
1063
            .with_parameter(help_text("msg", "The message to display")
1064
                                .with_format(help_parameter_format_t::HPF_TEXT))
1065
            .with_tags({"scripting"})
1066
            .with_example({"To display 'Press t to switch to the text view' on "
1067
                           "the bottom right",
1068
                           "Press t to switch to the text view"}),
1069
    },
1070
    {
1071
        "eval",
1072
        com_eval,
1073

1074
        help_text(":eval")
1075
            .with_summary("Evaluate the given command/query after doing "
1076
                          "environment variable substitution")
1077
            .with_parameter(help_text(
1078
                "command", "The command or query to perform substitution on."))
1079
            .with_tags({"scripting"})
1080
            .with_examples({{"To substitute the table name from a variable",
1081
                             ";SELECT * FROM ${table}"}}),
1082
    },
1083
    {
1084
        "sh",
1085
        com_sh,
1086

1087
        help_text(":sh")
1088
            .with_summary("Execute the given command-line and display the "
1089
                          "captured output")
1090
            .with_parameter(help_text(
1091
                "--name=<name>", "The name to give to the captured output"))
1092
            .with_parameter(
1093
                help_text("cmdline", "The command-line to execute."))
1094
            .with_tags({"scripting"}),
1095
    },
1096
    {
1097
        "cd",
1098
        com_cd,
1099

1100
        help_text(":cd")
1101
            .with_summary("Change the current directory")
1102
            .with_parameter(
1103
                help_text("dir", "The new current directory")
1104
                    .with_format(help_parameter_format_t::HPF_DIRECTORY))
1105
            .with_tags({"scripting"}),
1106
    },
1107
    {
1108
        "external-access",
1109
        com_external_access,
1110
        help_text(":external-access")
1111
            .with_summary(
1112
                "Open a port to give remote access to this lnav instance")
1113
            .with_parameter(
1114
                help_text("port", "The port number to listen on")
1115
                    .with_format(help_parameter_format_t::HPF_NUMBER))
1116
            .with_parameter(
1117
                help_text("api-key", "The API key")
1118
                    .with_format(help_parameter_format_t::HPF_STRING))
1119
            .with_tags({"scripting"}),
1120
    },
1121
    {
1122
        "external-access-login",
1123
        com_external_access_login,
1124
        help_text(":external-access-login")
1125
            .with_summary("Use the external-opener to open a URL that refers "
1126
                          "to lnav's external-access server")
1127
            .with_parameter(
1128
                help_text{"app", "The app to launch"}
1129
                    .with_format(help_parameter_format_t::HPF_KNOWN_APP)
1130
                    .optional())
1131
            .with_tags({"scripting"}),
1132
    },
1133
};
1134

1135
void
1136
init_lnav_scripting_commands(readline_context::command_map_t& cmd_map)
622✔
1137
{
1138
    for (auto& cmd : SCRIPTING_COMMANDS) {
6,220✔
1139
        cmd.c_help.index_tags();
5,598✔
1140
        cmd_map[cmd.c_name] = &cmd;
5,598✔
1141
    }
1142
}
622✔
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