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

tstack / lnav / 18478704608-2574

13 Oct 2025 09:28PM UTC coverage: 70.045% (-0.07%) from 70.119%
18478704608-2574

push

github

tstack
[md4c] add HTML generator

A bunch of external-access stuff.

69 of 167 new or added lines in 18 files covered. (41.32%)

1 existing line in 1 file now uncovered.

50324 of 71845 relevant lines covered (70.05%)

415329.15 hits per line

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

40.83
/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 <map>
32
#include <memory>
33
#include <string>
34
#include <string_view>
35
#include <vector>
36

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

64
#ifdef HAVE_RUST_DEPS
65
#    include "lnav_rs_ext.cxx.hh"
66
#endif
67

68
using namespace lnav::roles::literals;
69
using namespace std::string_view_literals;
70

71
static Result<std::string, lnav::console::user_message>
72
com_export_session_to(exec_context& ec,
5✔
73
                      std::string cmdline,
74
                      std::vector<std::string>& args)
75
{
76
    std::string retval;
5✔
77

78
    if (!ec.ec_dry_run) {
5✔
79
        auto_mem<FILE> outfile(fclose);
5✔
80
        auto fn = trim(remaining_args(cmdline, args));
5✔
81
        auto to_term = false;
5✔
82

83
        if (fn == "-" || fn == "/dev/stdout") {
5✔
84
            auto ec_out = ec.get_output();
2✔
85

86
            if (!ec_out) {
2✔
87
                outfile = auto_mem<FILE>::leak(stdout);
×
88

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

122
        auto export_res = lnav::session::export_to(outfile.in());
5✔
123

124
        fflush(outfile.in());
5✔
125
        if (to_term) {
5✔
126
            if (ec.ec_ui_callbacks.uc_post_stdout_write) {
×
127
                ec.ec_ui_callbacks.uc_post_stdout_write();
×
128
            }
129
        }
130
        if (export_res.isErr()) {
5✔
131
            return Err(export_res.unwrapErr());
×
132
        }
133

134
        retval = fmt::format(
5✔
135
            FMT_STRING("info: wrote session commands to -- {}"), fn);
20✔
136
    }
5✔
137

138
    return Ok(retval);
5✔
139
}
5✔
140

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

151
    return Ok(std::string());
9✔
152
}
153

154
static Result<std::string, lnav::console::user_message>
155
com_echo(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
50✔
156
{
157
    std::string retval = "error: expecting a message";
50✔
158

159
    if (args.size() >= 1) {
50✔
160
        bool lf = true;
50✔
161
        std::string src;
50✔
162

163
        if (args.size() > 2 && args[1] == "-n") {
50✔
164
            std::string::size_type index_in_cmdline = cmdline.find(args[1]);
1✔
165

166
            lf = false;
1✔
167
            src = cmdline.substr(index_in_cmdline + args[1].length() + 1);
1✔
168
        } else if (args.size() >= 2) {
49✔
169
            src = cmdline.substr(args[0].length() + 1);
31✔
170
        } else {
171
            src = "";
18✔
172
        }
173

174
        auto lexer = shlex(src);
50✔
175
        lexer.eval(retval, ec.create_resolver());
50✔
176

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

189
            if (outfile == stdout) {
43✔
190
                lnav_data.ld_stdout_used = true;
37✔
191
            }
192

193
            fprintf(outfile, "%s", retval.c_str());
43✔
194
            if (lf) {
43✔
195
                putc('\n', outfile);
42✔
196
            }
197
            fflush(outfile);
43✔
198

199
            retval = "";
43✔
200
        }
201
    }
50✔
202

203
    return Ok(retval);
100✔
204
}
50✔
205

206
static Result<std::string, lnav::console::user_message>
207
com_alt_msg(exec_context& ec,
4✔
208
            std::string cmdline,
209
            std::vector<std::string>& args)
210
{
211
    static auto& prompt = lnav::prompt::get();
4✔
212

213
    std::string retval;
4✔
214

215
    if (ec.ec_dry_run) {
4✔
216
        retval = "";
×
217
    } else if (args.size() == 1) {
4✔
218
        prompt.p_editor.clear_alt_value();
4✔
219
        retval = "";
4✔
220
    } else {
221
        std::string msg = remaining_args(cmdline, args);
×
222

223
        prompt.p_editor.set_alt_value(msg);
×
224
        retval = "";
×
225
    }
226

227
    return Ok(retval);
8✔
228
}
4✔
229

230
static Result<std::string, lnav::console::user_message>
231
com_eval(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
26✔
232
{
233
    std::string retval;
26✔
234

235
    if (args.size() > 1) {
26✔
236
        static intern_string_t EVAL_SRC = intern_string::lookup(":eval");
44✔
237

238
        std::string all_args = remaining_args(cmdline, args);
26✔
239
        std::string expanded_cmd;
26✔
240
        shlex lexer(all_args.c_str(), all_args.size());
26✔
241

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

253
        if (expanded_cmd.empty()) {
26✔
254
            return ec.make_error("empty result after evaluation");
×
255
        }
256

257
        if (ec.ec_dry_run) {
26✔
258
            attr_line_t al(expanded_cmd);
×
259

260
            lnav_data.ld_preview_status_source[0].get_description().set_value(
×
261
                "The command to be executed:"_frag);
×
262
            lnav_data.ld_status[LNS_PREVIEW0].set_needs_update();
×
263

264
            lnav_data.ld_preview_view[0].set_sub_source(
×
265
                &lnav_data.ld_preview_source[0]);
266
            lnav_data.ld_preview_source[0].replace_with(al);
×
267

268
            return Ok(std::string());
×
269
        }
270

271
        auto src_guard = ec.enter_source(EVAL_SRC, 1, expanded_cmd);
26✔
272
        auto content = string_fragment::from_str(expanded_cmd);
26✔
273
        multiline_executor me(ec, ":eval");
26✔
274
        for (auto line : content.split_lines()) {
114✔
275
            TRY(me.push_back(line));
88✔
276
        }
26✔
277
        TRY(me.final());
26✔
278
        retval = std::move(me.me_last_result);
26✔
279
    } else {
26✔
280
        return ec.make_error("expecting a command or query to evaluate");
×
281
    }
282

283
    return Ok(retval);
26✔
284
}
26✔
285

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

291
    if (lnav_data.ld_flags & LNF_SECURE_MODE) {
4✔
292
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
293
    }
294

295
    std::vector<std::string> word_exp;
4✔
296
    std::string pat;
4✔
297

298
    pat = trim(remaining_args(cmdline, args));
4✔
299

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

311
        return Err(um);
×
312
    }
313

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

317
    if (split_args.size() != 1) {
4✔
318
        return ec.make_error("expecting a single argument");
×
319
    }
320

321
    struct stat st;
322

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

328
    if (!S_ISDIR(st.st_mode)) {
3✔
329
        return ec.make_error("{} is not a directory", split_args[0]);
2✔
330
    }
331

332
    if (!ec.ec_dry_run) {
2✔
333
        chdir(split_args[0].c_str());
2✔
334
        setenv("PWD", split_args[0].c_str(), 1);
2✔
335
    }
336

337
    return Ok(std::string());
2✔
338
}
4✔
339

340
static Result<std::string, lnav::console::user_message>
341
com_sh(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
5✔
342
{
343
    if (lnav_data.ld_flags & LNF_SECURE_MODE) {
5✔
344
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
345
    }
346

347
    static size_t EXEC_COUNT = 0;
348

349
    if (!ec.ec_dry_run) {
5✔
350
        std::optional<std::string> name_flag;
5✔
351

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

360
                if (startswith(flags[1].se_value, NAME_FLAG)) {
5✔
361
                    name_flag = flags[1].se_value.substr(strlen(NAME_FLAG));
×
362
                    cmd_start = flags[1].se_origin.sf_end;
×
363
                }
364
            }
365
        }
5✔
366

367
        auto carg = trim(cmdline.substr(cmd_start));
5✔
368

369
        log_info("executing: %s", carg.c_str());
5✔
370

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

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

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

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

429
            execvp(exec_args[0], (char**) exec_args);
×
430
            _exit(EXIT_FAILURE);
×
431
        }
432

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

449
        auto name_base = display_name;
5✔
450
        size_t name_counter = 0;
5✔
451

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

463
        auto create_piper_res
464
            = lnav::piper::create_looper(display_name,
465
                                         std::move(child_fds[0].read_end()),
5✔
466
                                         std::move(child_fds[1].read_end()));
10✔
467

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

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

486
        return Ok(fmt::format(FMT_STRING("info: executing -- {}"), carg));
20✔
487
    }
5✔
488

489
    return Ok(std::string());
×
490
}
491

492
#ifdef HAVE_RUST_DEPS
493

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

518
    return retval;
1,280✔
519
}
640✔
520

521
DIST_SLICE(prog_reps) lnav::progress_reporter_t EXT_PROG_REP = ext_prog_rep;
522

523
namespace lnav_rs_ext {
524

525
LnavLogLevel
526
get_lnav_log_level()
6,468✔
527
{
528
    switch (lnav_log_level) {
6,468✔
529
        case lnav_log_level_t::TRACE:
×
530
            return LnavLogLevel::trace;
×
531
        case lnav_log_level_t::DEBUG:
6,468✔
532
            return LnavLogLevel::debug;
6,468✔
533
        case lnav_log_level_t::INFO:
×
534
            return LnavLogLevel::info;
×
535
        case lnav_log_level_t::WARNING:
×
536
            return LnavLogLevel::warning;
×
537
        case lnav_log_level_t::ERROR:
×
538
            return LnavLogLevel::error;
×
539
    }
540

541
    return LnavLogLevel::info;
×
542
}
543

544
void
545
log_msg(LnavLogLevel level, ::rust::Str file, uint32_t line, ::rust::Str msg)
5,495✔
546
{
547
    auto ln_level = static_cast<lnav_log_level_t>(level);
5,495✔
548

549
    ::log_msg(ln_level,
10,990✔
550
              ((std::string) file).c_str(),
10,990✔
551
              line,
552
              "%.*s",
553
              msg.size(),
554
              msg.data());
555
}
5,495✔
556

557
::rust::String
558
version_info()
×
559
{
560
    yajlpp_gen gen;
×
561
    {
562
        yajlpp_map root(gen);
×
563

564
        root.gen("product");
×
565
        root.gen(PACKAGE);
×
566
        root.gen("version");
×
567
        root.gen(PACKAGE_VERSION);
×
568
    }
569

570
    return gen.to_string_fragment().to_string();
×
571
}
572

573
static void
NEW
574
process_md_out(const MD_CHAR* data, MD_SIZE len, void* user_data)
×
575
{
NEW
576
    auto& buffer = *((::rust::Vec<uint8_t>*) user_data);
×
NEW
577
    auto sv = std::string_view(data, len);
×
578

NEW
579
    std::copy(sv.begin(), sv.end(), std::back_inserter(buffer));
×
580
}
581

582
struct static_file_not_found_t {};
583

584
using static_file_t
585
    = mapbox::util::variant<const bin_src_file*, static_file_not_found_t>;
586

587
static std::optional<const bin_src_file*>
NEW
588
find_static_src_file(const std::string& path)
×
589
{
NEW
590
    for (const auto& file : lnav_static_files) {
×
NEW
591
        if (file.get_name() == path) {
×
NEW
592
            return &file;
×
593
        }
594
    }
595

NEW
596
    return std::nullopt;
×
597
}
598

599
static static_file_t
NEW
600
find_static_file(const std::string& path)
×
601
{
NEW
602
    auto exact_match = find_static_src_file(path);
×
NEW
603
    if (exact_match) {
×
NEW
604
        return static_file_t{exact_match.value()};
×
605
    }
606

NEW
607
    return static_file_t{static_file_not_found_t{}};
×
608
}
609

610
static static_file_t
NEW
611
resolve_static_src_file(std::string path)
×
612
{
613
    static const auto HTML_EXT = lnav::pcre2pp::code::from_const("\\.html$");
614

NEW
615
    auto exact_match = find_static_file(path);
×
NEW
616
    if (!exact_match.is<static_file_not_found_t>()) {
×
NEW
617
        return exact_match;
×
618
    }
619

NEW
620
    if (!path.empty() && !endswith(path, "/")) {
×
NEW
621
        path += "/";
×
622
    }
NEW
623
    path += "index.html";
×
NEW
624
    auto index_match = find_static_file(path);
×
NEW
625
    if (!index_match.is<static_file_not_found_t>()) {
×
NEW
626
        return index_match;
×
627
    }
628

NEW
629
    path = HTML_EXT.replace(path, ".md");
×
NEW
630
    auto md_match = find_static_file(path);
×
NEW
631
    if (!md_match.is<static_file_not_found_t>()) {
×
NEW
632
        return md_match;
×
633
    }
634

NEW
635
    return static_file_t{static_file_not_found_t{}};
×
636
}
637

638
static void
NEW
639
render_markdown(const bin_src_file& file, ::rust::Vec<uint8_t>& dst)
×
640
{
641
    static const auto HEADER = R"(
642
<html>
643
<head>
644
<title>{title}</title>
645
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/themes/prism.min.css">
646
</head>
647
<body>
648
)";
649
    static const auto FOOTER = R"(
650
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/prism.min.js"></script>
651
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/components/prism-sql.min.js"></script>
652
<script src="/assets/js/prism-lnav.js"></script>
653
<script src="https://cdnjs.cloudflare.com/ajax/libs/prism/1.30.0/plugins/autoloader/prism-autoloader.min.js"></script>
654
<script src="/assets/js/lnav-client.js"></script>
655
</body>
656
</html>
657
)"sv;
658

NEW
659
    auto content = file.to_string_fragment_producer()->to_string();
×
NEW
660
    auto md_file = md4cpp::parse_file(file.get_name().to_string(), content);
×
NEW
661
    auto title = fmt::format(FMT_STRING("lnav: {}"), file.get_name());
×
662

NEW
663
    if (md_file.f_frontmatter_format == text_format_t::TF_YAML) {
×
664
        auto tree = ryml::parse_in_arena(
NEW
665
            lnav::ryml::to_csubstr(file.get_name()),
×
NEW
666
            lnav::ryml::to_csubstr(md_file.f_frontmatter));
×
NEW
667
        tree["title"] >> title;
×
668
    }
669

NEW
670
    auto head = fmt::format(HEADER, fmt::arg("title", title));
×
NEW
671
    std::copy(head.begin(), head.end(), std::back_inserter(dst));
×
NEW
672
    md_html(md_file.f_body.data(),
×
NEW
673
            md_file.f_body.length(),
×
674
            process_md_out,
675
            &dst,
676
            MD_DIALECT_GITHUB | MD_FLAG_UNDERLINE | MD_FLAG_STRIKETHROUGH,
677
            0);
NEW
678
    std::copy(FOOTER.begin(), FOOTER.end(), std::back_inserter(dst));
×
679
}
680

681
void
NEW
682
get_static_file(::rust::Str path, ::rust::Vec<uint8_t>& dst)
×
683
{
NEW
684
    auto path_str = (std::string) path;
×
685

NEW
686
    if (startswith(path_str, "/")) {
×
NEW
687
        path_str.erase(0, 1);
×
688
    }
NEW
689
    log_info("static file request: %s", path_str.c_str());
×
NEW
690
    auto matched_file = resolve_static_src_file(path_str);
×
NEW
691
    matched_file.match(
×
NEW
692
        [&dst](const bin_src_file* file) {
×
NEW
693
            auto name = file->get_name();
×
NEW
694
            log_info("  matched static source file: %.*s",
×
695
                     name.length(),
696
                     name.data());
NEW
697
            if (name.endswith(".md")) {
×
NEW
698
                render_markdown(*file, dst);
×
699
            } else {
NEW
700
                auto prod = file->to_string_fragment_producer();
×
NEW
701
                prod->for_each([&dst](const auto& sf) {
×
NEW
702
                    std::copy(sf.begin(), sf.end(), std::back_inserter(dst));
×
NEW
703
                    return Ok();
×
704
                });
705
            }
NEW
706
        },
×
NEW
707
        [](const static_file_not_found_t&) {
×
NEW
708
            log_info("  static file not found");
×
NEW
709
        });
×
710
}
711

712
ExecResult
713
execute_external_command(::rust::String rs_src,
×
714
                         ::rust::String rs_script,
715
                         ::rust::String hdrs)
716
{
717
    auto src = (std::string) rs_src;
×
718
    auto script = (std::string) rs_script;
×
719
    auto retval = std::make_shared<ExecResult>();
×
720

721
    log_debug("sending remote command to main looper");
×
722
    isc::to<main_looper&, services::main_t>().send_and_wait(
×
723
        [src, script, hdrs, &retval](auto& mlooper) {
×
724
            log_debug("executing remote command from: %s", src.c_str());
×
725
            db_label_source ext_db_source;
×
726
            auto& ec = lnav_data.ld_exec_context;
×
727
            // XXX we should still allow an external command to update the
728
            // regular DB view.
729
            auto dsg = ec.enter_db_source(&ext_db_source);
×
730
            auto* outfile = tmpfile();
×
731
            auto ec_out = exec_context::output_t{outfile, fclose};
×
732
            auto og = exec_context::output_guard{ec, "default", ec_out};
×
733
            auto me = multiline_executor{ec, src};
×
734
            auto pg = ec.with_provenance(exec_context::external_access{src});
×
735
            ec.ec_local_vars.push(std::map<std::string, scoped_value_t>{
×
736
                {"headers", scoped_value_t{(std::string) hdrs}}});
×
737
            auto script_frag = string_fragment::from_str(script);
×
738
            for (const auto& line : script_frag.split_lines()) {
×
739
                auto res = me.push_back(line);
×
740
                if (res.isErr()) {
×
741
                    auto um = res.unwrapErr();
×
742
                    retval->error.msg = um.um_message.al_string;
×
743
                    retval->error.reason = um.um_reason.al_string;
×
744
                    retval->error.help = um.um_help.al_string;
×
745
                    ec.ec_local_vars.pop();
×
746
                    return;
×
747
                }
748
            }
749
            auto res = me.final();
×
750
            if (res.isErr()) {
×
751
                auto um = res.unwrapErr();
×
752
                retval->error.msg = um.um_message.al_string;
×
753
                retval->error.reason = um.um_reason.al_string;
×
754
                retval->error.help = um.um_help.al_string;
×
755
            } else {
×
756
                fseek(ec_out.first, 0, SEEK_SET);
×
757
                retval->status = me.me_last_result;
×
758
                retval->content_type = fmt::to_string(ec.get_output_format());
×
759
                retval->content_fd = dup(fileno(ec_out.first));
×
760
            }
761
            ec.ec_local_vars.pop();
×
762
        });
×
763

764
    return *retval;
×
765
}
766

767
}  // namespace lnav_rs_ext
768
#endif
769

770
static Result<std::string, lnav::console::user_message>
771
com_external_access(exec_context& ec,
×
772
                    std::string cmdline,
773
                    std::vector<std::string>& args)
774
{
775
#ifdef HAVE_RUST_DEPS
776
    if (args.size() != 3) {
×
777
        return ec.make_error("Expecting port number and API key");
×
778
    }
779

780
    if (lnav_data.ld_flags & LNF_SECURE_MODE) {
×
781
        return ec.make_error("External access is not available in secure mode");
×
782
    }
783

784
    std::string retval;
×
785
    if (ec.ec_dry_run) {
×
786
        return Ok(retval);
×
787
    }
788

789
    auto scan_res = scn::scan_int<uint16_t>(args[1]);
×
790
    if (!scan_res || !scan_res.value().range().empty()) {
×
791
        return ec.make_error(FMT_STRING("port value is not a number: {}"),
×
792
                             args[1]);
×
793
    }
794
    auto port = scan_res->value();
×
795

796
    auto buf = auto_buffer::alloc((args[2].size() * 5) / 3);
×
797
    auto outlen = buf.capacity();
×
798
    base64_encode(args[2].data(), args[2].size(), buf.in(), &outlen, 0);
×
799
    auto start_res
800
        = lnav_rs_ext::start_ext_access(port, ::rust::String(buf.in(), outlen));
×
801
    if (start_res.port == 0) {
×
802
        return ec.make_error(FMT_STRING("unable to start external access: {}"),
×
803
                             (std::string) start_res.error);
×
804
    }
805

806
    retval = fmt::format(FMT_STRING("info: started external access on port {}"),
×
807
                         start_res.port);
×
808
    setenv("LNAV_EXTERNAL_PORT", fmt::to_string(start_res.port).c_str(), 1);
×
809
    auto url = fmt::format(FMT_STRING("http://127.0.0.1:{}"), start_res.port);
×
810
    setenv("LNAV_EXTERNAL_URL", url.c_str(), 1);
×
811

812
    {
NEW
813
        auto top_source = injector::get<std::shared_ptr<top_status_source>>();
×
814

NEW
815
        auto& sf = top_source->statusview_value_for_field(
×
816
            top_status_source::TSF_EXT_ACCESS);
817

NEW
818
        sf.set_width(3);
×
NEW
819
        sf.set_value("\xF0\x9F\x8C\x90");
×
NEW
820
        sf.on_click = [](auto& top_source) {
×
821
            static const intern_string_t SRC
822
                = intern_string::lookup("internal");
823
            static const auto LOC = source_location{SRC};
824

NEW
825
            auto& ec = lnav_data.ld_exec_context;
×
NEW
826
            ec.execute(LOC, ":external-access-login");
×
827
        };
828
    }
829

UNCOV
830
    return Ok(retval);
×
831
#else
832
    return ec.make_error("lnav was compiled without Rust extensions");
833
#endif
834
}
835

836
static Result<std::string, lnav::console::user_message>
NEW
837
com_external_access_login(exec_context& ec,
×
838
                          std::string cmdline,
839
                          std::vector<std::string>& args)
840
{
841
#ifdef HAVE_RUST_DEPS
NEW
842
    auto url = getenv("LNAV_EXTERNAL_URL");
×
NEW
843
    if (url == nullptr) {
×
NEW
844
        auto um = lnav::console::user_message::error(
×
845
                      "external-access is not enabled")
NEW
846
                      .with_help(attr_line_t("Use the ")
×
NEW
847
                                     .append(":external-access"_keyword)
×
NEW
848
                                     .append(" command to enable"));
×
849

NEW
850
        return Err(um);
×
851
    }
852

NEW
853
    auto otp = (std::string) lnav_rs_ext::set_one_time_password();
×
NEW
854
    auto url_with_otp = fmt::format(FMT_STRING("{}/login?otp={}"), url, otp);
×
NEW
855
    auto open_res = lnav::external_opener::for_href(url_with_otp);
×
NEW
856
    if (open_res.isErr()) {
×
NEW
857
        auto err = open_res.unwrapErr();
×
NEW
858
        auto um = lnav::console::user_message::error(
×
859
                      "unable to open external access URL")
NEW
860
                      .with_reason(err);
×
861

NEW
862
        return Err(um);
×
863
    }
864

NEW
865
    return Ok(std::string());
×
866
#else
867
    return ec.make_error("lnav was compiled without Rust extensions");
868
#endif
869
}
870

871
static readline_context::command_t SCRIPTING_COMMANDS[] = {
872
    {
873
        "export-session-to",
874
        com_export_session_to,
875

876
        help_text(":export-session-to")
877
            .with_summary("Export the current lnav state to an executable lnav "
878
                          "script file that contains the commands needed to "
879
                          "restore the current session")
880
            .with_parameter(
881
                help_text("path", "The path to the file to write")
882
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
883
            .with_tags({"io", "scripting"}),
884
    },
885
    {
886
        "rebuild",
887
        com_rebuild,
888
        help_text(":rebuild")
889
            .with_summary("Forcefully rebuild file indexes")
890
            .with_tags({"scripting"}),
891
    },
892
    {
893
        "echo",
894
        com_echo,
895

896
        help_text(":echo")
897
            .with_summary(
898
                "Echo the given message to the screen or, if "
899
                ":redirect-to has "
900
                "been called, to output file specified in the "
901
                "redirect.  "
902
                "Variable substitution is performed on the message.  "
903
                "Use a "
904
                "backslash to escape any special characters, like '$'")
905
            .with_parameter(help_text("-n",
906
                                      "Do not print a line-feed at "
907
                                      "the end of the output")
908
                                .optional()
909
                                .with_format(help_parameter_format_t::HPF_TEXT))
910
            .with_parameter(help_text("msg", "The message to display"))
911
            .with_tags({"io", "scripting"})
912
            .with_example({"To output 'Hello, World!'", "Hello, World!"}),
913
    },
914
    {
915
        "alt-msg",
916
        com_alt_msg,
917

918
        help_text(":alt-msg")
919
            .with_summary("Display a message in the alternate command position")
920
            .with_parameter(help_text("msg", "The message to display")
921
                                .with_format(help_parameter_format_t::HPF_TEXT))
922
            .with_tags({"scripting"})
923
            .with_example({"To display 'Press t to switch to the text view' on "
924
                           "the bottom right",
925
                           "Press t to switch to the text view"}),
926
    },
927
    {
928
        "eval",
929
        com_eval,
930

931
        help_text(":eval")
932
            .with_summary("Evaluate the given command/query after doing "
933
                          "environment variable substitution")
934
            .with_parameter(help_text(
935
                "command", "The command or query to perform substitution on."))
936
            .with_tags({"scripting"})
937
            .with_examples({{"To substitute the table name from a variable",
938
                             ";SELECT * FROM ${table}"}}),
939
    },
940
    {
941
        "sh",
942
        com_sh,
943

944
        help_text(":sh")
945
            .with_summary("Execute the given command-line and display the "
946
                          "captured output")
947
            .with_parameter(help_text(
948
                "--name=<name>", "The name to give to the captured output"))
949
            .with_parameter(
950
                help_text("cmdline", "The command-line to execute."))
951
            .with_tags({"scripting"}),
952
    },
953
    {
954
        "cd",
955
        com_cd,
956

957
        help_text(":cd")
958
            .with_summary("Change the current directory")
959
            .with_parameter(
960
                help_text("dir", "The new current directory")
961
                    .with_format(help_parameter_format_t::HPF_DIRECTORY))
962
            .with_tags({"scripting"}),
963
    },
964
    {
965
        "external-access",
966
        com_external_access,
967
        help_text(":external-access")
968
            .with_summary(
969
                "Open a port to give remote access to this lnav instance")
970
            .with_parameter(
971
                help_text("port", "The port number to listen on")
972
                    .with_format(help_parameter_format_t::HPF_NUMBER))
973
            .with_parameter(
974
                help_text("api-key", "The API key")
975
                    .with_format(help_parameter_format_t::HPF_STRING))
976
            .with_tags({"scripting"}),
977
    },
978
    {
979
        "external-access-login",
980
        com_external_access_login,
981
        help_text(":external-access-login")
982
            .with_summary("Use the external-opener to open a URL that refers "
983
                          "to lnav's external-access server")
984
            .with_tags({"scripting"}),
985
    },
986
};
987

988
void
989
init_lnav_scripting_commands(readline_context::command_map_t& cmd_map)
589✔
990
{
991
    for (auto& cmd : SCRIPTING_COMMANDS) {
5,890✔
992
        cmd.c_help.index_tags();
5,301✔
993
        cmd_map[cmd.c_name] = &cmd;
15,903✔
994
    }
995
}
589✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc