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

tstack / lnav / 19561959745-2698

21 Nov 2025 06:05AM UTC coverage: 68.864%. Remained the same
19561959745-2698

push

github

tstack
[perf] improve stuff related to archive extraction

19 of 27 new or added lines in 4 files covered. (70.37%)

3 existing lines in 2 files now uncovered.

51106 of 74213 relevant lines covered (68.86%)

431629.27 hits per line

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

59.63
/src/cmds.io.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 <fnmatch.h>
31
#include <glob.h>
32

33
#include "base/attr_line.builder.hh"
34
#include "base/cell_container.hh"
35
#include "base/fs_util.hh"
36
#include "base/humanize.hh"
37
#include "base/humanize.network.hh"
38
#include "base/itertools.enumerate.hh"
39
#include "base/itertools.hh"
40
#include "base/paths.hh"
41
#include "bound_tags.hh"
42
#include "CLI/CLI.hpp"
43
#include "curl_looper.hh"
44
#include "external_opener.hh"
45
#include "field_overlay_source.hh"
46
#include "fmt/color.h"
47
#include "fmt/printf.h"
48
#include "lnav.hh"
49
#include "lnav_commands.hh"
50
#include "lnav_util.hh"
51
#include "scn/scan.h"
52
#include "service_tags.hh"
53
#include "shlex.hh"
54
#include "sql_util.hh"
55
#include "sysclip.hh"
56
#include "tailer/tailer.looper.hh"
57
#include "text_anonymizer.hh"
58
#include "url_handler.cfg.hh"
59
#include "url_loader.hh"
60
#include "vtab_module.hh"
61
#include "yajlpp/json_op.hh"
62

63
#if !CURL_AT_LEAST_VERSION(7, 80, 0)
64
extern "C"
65
{
66
const char* curl_url_strerror(CURLUcode error);
67
}
68
#endif
69

70
static bool
71
csv_needs_quoting(const std::string& str)
2,649✔
72
{
73
    return (str.find_first_of(",\"\r\n") != std::string::npos);
2,649✔
74
}
75

76
static std::string
77
csv_quote_string(const std::string& str)
52✔
78
{
79
    static const auto csv_column_quoter = lnav::pcre2pp::code::from_const("\"");
52✔
80

81
    auto retval = csv_column_quoter.replace(str, "\"\"");
52✔
82

83
    retval.insert(0, 1, '\"');
52✔
84
    retval.append(1, '\"');
52✔
85

86
    return retval;
52✔
87
}
×
88

89
static void
90
csv_write_string(FILE* outfile, const std::string& str)
2,649✔
91
{
92
    if (csv_needs_quoting(str)) {
2,649✔
93
        std::string quoted_str = csv_quote_string(str);
52✔
94

95
        fmt::fprintf(outfile, "%s", quoted_str);
52✔
96
    } else {
52✔
97
        fmt::fprintf(outfile, "%s", str);
2,597✔
98
    }
99
}
2,649✔
100

101
static void
102
yajl_writer(void* context, const char* str, size_t len)
19,800✔
103
{
104
    FILE* file = (FILE*) context;
19,800✔
105

106
    fwrite(str, len, 1, file);
19,800✔
107
}
19,800✔
108

109
static lnav::progress_result_t
110
write_progress(size_t row, size_t total)
×
111
{
112
    static const auto& ui_timer = ui_periodic_timer::singleton();
113
    static sig_atomic_t write_counter = 0;
114

115
    if (!ui_timer.time_to_update(write_counter)) {
×
116
        return lnav::progress_result_t::ok;
×
117
    }
118
    lnav_data.ld_bottom_source.update_loading(row, total, "Writing");
×
119
    lnav_data.ld_status[LNS_BOTTOM].set_needs_update();
×
120
    return lnav_data.ld_status_refresher(lnav::func::op_type::blocking);
×
121
}
122

123
static std::string
124
json_ptr_to_title(string_fragment ptr)
2✔
125
{
126
    std::string retval;
2✔
127

128
    while (!ptr.empty()) {
6✔
129
        const auto& [left, right] = ptr.split_when(string_fragment::tag1{'/'});
4✔
130
        stack_buf allocator;
4✔
131

132
        if (!retval.empty()) {
4✔
133
            retval.push_back(' ');
×
134
        }
135
        retval += json_ptr::decode(left, allocator);
4✔
136
        ptr = right;
4✔
137
    }
4✔
138
    return retval;
2✔
139
}
×
140

141
static void
142
json_write_cell(const db_label_source::header_meta& hm,
878✔
143
                yajlpp_container_base& container,
144
                const lnav::cell_container::cursor& cursor,
145
                lnav::text_anonymizer& ta,
146
                bool anonymize)
147
{
148
    switch (cursor.get_type()) {
878✔
149
        case lnav::cell_type::CT_NULL:
306✔
150
            container.gen();
306✔
151
            break;
306✔
152
        case lnav::cell_type::CT_INTEGER:
243✔
153
            container.gen(cursor.get_int());
243✔
154
            break;
243✔
155
        case lnav::cell_type::CT_FLOAT:
×
156
            container.gen(cursor.get_float());
×
157
            break;
×
158
        case lnav::cell_type::CT_TEXT: {
329✔
159
            if (hm.hm_sub_type == JSON_SUBTYPE) {
329✔
160
                unsigned char* err;
161
                json_ptr jp("");
67✔
162
                json_op jo(jp);
67✔
163

164
                jo.jo_ptr_callbacks = json_op::gen_callbacks;
67✔
165
                jo.jo_ptr_data = container.gen.yg_handle;
67✔
166
                auto parse_handle
167
                    = yajlpp::alloc_handle(&json_op::ptr_callbacks, &jo);
67✔
168

169
                const auto json_in = cursor.get_text();
67✔
170
                switch (yajl_parse(
67✔
171
                    parse_handle.in(), json_in.udata(), json_in.length()))
67✔
172
                {
173
                    case yajl_status_error:
×
174
                    case yajl_status_client_canceled: {
175
                        err = yajl_get_error(parse_handle.in(),
×
176
                                             0,
177
                                             json_in.udata(),
178
                                             json_in.length());
×
179
                        log_error("unable to parse JSON cell: %s", err);
×
180
                        container.gen(cursor.get_text());
×
181
                        yajl_free_error(parse_handle.in(), err);
×
182
                        return;
×
183
                    }
184
                    default:
67✔
185
                        break;
67✔
186
                }
187

188
                switch (yajl_complete_parse(parse_handle.in())) {
67✔
189
                    case yajl_status_error:
×
190
                    case yajl_status_client_canceled: {
191
                        err = yajl_get_error(parse_handle.in(),
×
192
                                             0,
193
                                             json_in.udata(),
194
                                             json_in.length());
×
195
                        log_error("unable to parse JSON cell: %s", err);
×
196
                        container.gen(cursor.get_text());
×
197
                        yajl_free_error(parse_handle.in(), err);
×
198
                        return;
×
199
                    }
200
                    default:
67✔
201
                        break;
67✔
202
                }
203
            } else if (anonymize) {
329✔
204
                container.gen(ta.next(cursor.get_text()));
×
205
            } else {
206
                container.gen(cursor.get_text());
262✔
207
            }
208
            break;
329✔
209
        }
210
    }
211
}
212

213
static void
214
json_write_row(exec_context& ec,
85✔
215
               yajl_gen handle,
216
               int row,
217
               lnav::text_anonymizer& ta,
218
               bool anonymize)
219
{
220
    auto& dls = *ec.ec_label_source_stack.back();
85✔
221
    yajlpp_map obj_map(handle);
85✔
222

223
    auto cursor = dls.dls_row_cursors[row].sync();
85✔
224
    for (size_t col = 0; col < dls.dls_headers.size();
858✔
225
         col++, cursor = cursor->next())
773✔
226
    {
227
        const auto& hm = dls.dls_headers[col];
773✔
228

229
        obj_map.gen(hm.hm_name);
773✔
230
        json_write_cell(hm, obj_map, cursor.value(), ta, anonymize);
773✔
231
    }
232
}
85✔
233

234
static Result<std::string, lnav::console::user_message>
235
com_save_to(exec_context& ec,
131✔
236
            std::string cmdline,
237
            std::vector<std::string>& args)
238
{
239
    static const intern_string_t SRC = intern_string::lookup("path");
367✔
240

241
    FILE *outfile = nullptr, *toclose = nullptr;
131✔
242
    const char* mode = "";
131✔
243
    std::string fn, retval;
131✔
244
    bool to_term = false;
131✔
245
    bool anonymize = false;
131✔
246
    int (*closer)(FILE*) = fclose;
131✔
247

248
    fn = trim(remaining_args(cmdline, args));
131✔
249

250
    shlex lexer(fn);
131✔
251

252
    auto split_args_res = lexer.split(ec.create_resolver());
131✔
253
    if (split_args_res.isErr()) {
131✔
254
        auto split_err = split_args_res.unwrapErr();
×
255
        auto um
256
            = lnav::console::user_message::error("unable to parse file name")
×
257
                  .with_reason(split_err.se_error.te_msg)
×
258
                  .with_snippet(lnav::console::snippet::from(
×
259
                      SRC, lexer.to_attr_line(split_err.se_error)))
×
260
                  .move();
×
261

262
        return Err(um);
×
263
    }
264

265
    auto split_args = split_args_res.unwrap()
262✔
266
        | lnav::itertools::map([](const auto& elem) { return elem.se_value; });
394✔
267
    auto anon_iter
268
        = std::find(split_args.begin(), split_args.end(), "--anonymize");
131✔
269
    if (anon_iter != split_args.end()) {
131✔
270
        split_args.erase(anon_iter);
2✔
271
        anonymize = true;
2✔
272
    }
273

274
    auto* tc = *lnav_data.ld_view_stack.top();
131✔
275
    auto opt_view_name = find_arg(split_args, "--view");
131✔
276
    if (opt_view_name) {
131✔
277
        auto opt_view_index = view_from_string(opt_view_name->c_str());
×
278

279
        if (!opt_view_index) {
×
280
            return ec.make_error("invalid view name: {}", *opt_view_name);
×
281
        }
282

283
        tc = &lnav_data.ld_views[*opt_view_index];
×
284
    }
285

286
    if (split_args.empty()) {
131✔
287
        return ec.make_error(
288
            "expecting file name or '-' to write to the terminal");
1✔
289
    }
290

291
    if (split_args.size() > 1) {
130✔
292
        return ec.make_error("more than one file name was matched");
×
293
    }
294

295
    if (args[0] == "append-to") {
130✔
296
        mode = "ae";
2✔
297
    } else {
298
        mode = "we";
128✔
299
    }
300

301
    auto& dls = *ec.ec_label_source_stack.back();
130✔
302
    bookmark_vector<vis_line_t> all_user_marks;
130✔
303
    lnav::text_anonymizer ta;
130✔
304

305
    if (args[0] == "write-csv-to" || args[0] == "write-json-to"
191✔
306
        || args[0] == "write-json-cols-to" || args[0] == "write-jsonlines-to"
34✔
307
        || args[0] == "write-cols-to" || args[0] == "write-table-to")
191✔
308
    {
309
        if (dls.dls_headers.empty()) {
109✔
310
            return ec.make_error(
311
                "no query result to write, use ';' to execute a query");
×
312
        }
313
    } else if (args[0] == "write-raw-to" && tc == &lnav_data.ld_views[LNV_DB]) {
21✔
314
    } else if (args[0] != "write-screen-to" && args[0] != "write-view-to") {
18✔
315
        all_user_marks = combined_user_marks(tc->get_bookmarks());
8✔
316
        if (all_user_marks.empty()) {
8✔
317
            return ec.make_error(
318
                "no lines marked to write, use 'm' to mark lines");
×
319
        }
320
    }
321

322
    if (ec.ec_dry_run) {
130✔
323
        outfile = tmpfile();
×
324
        toclose = outfile;
×
325
    } else if (split_args[0] == "-" || split_args[0] == "/dev/stdout") {
130✔
326
        auto ec_out = ec.get_output();
126✔
327

328
        if (!ec_out) {
126✔
329
            outfile = stdout;
×
330
            if (ec.ec_ui_callbacks.uc_pre_stdout_write) {
×
331
                ec.ec_ui_callbacks.uc_pre_stdout_write();
×
332
            }
333
            setvbuf(stdout, nullptr, _IONBF, 0);
×
334
            to_term = true;
×
335
            fprintf(outfile,
×
336
                    "\n---------------- Press any key to exit lo-fi display "
337
                    "----------------\n\n");
338
        } else {
339
            outfile = *ec_out;
126✔
340
        }
341
        if (outfile == stdout) {
126✔
342
            lnav_data.ld_stdout_used = true;
126✔
343
        }
344
    } else if (split_args[0] == "/dev/clipboard") {
4✔
345
        auto open_res = sysclip::open(sysclip::type_t::GENERAL);
×
346
        if (open_res.isErr()) {
×
347
            alerter::singleton().chime("cannot open clipboard");
×
348
            return ec.make_error("Unable to copy to clipboard: {}",
349
                                 open_res.unwrapErr());
×
350
        }
351
        auto holder = open_res.unwrap();
×
352
        toclose = outfile = holder.release();
×
353
        closer = holder.get_free_func<int (*)(FILE*)>();
×
354
    } else if (lnav_data.ld_flags & LNF_SECURE_MODE) {
4✔
355
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
2✔
356
    } else if ((outfile = fopen(split_args[0].c_str(), mode)) == nullptr) {
3✔
357
        return ec.make_error("unable to open file -- {}", split_args[0]);
×
358
    } else {
359
        toclose = outfile;
3✔
360
    }
361

362
    int line_count = 0;
129✔
363

364
    if (args[0] == "write-csv-to") {
129✔
365
        bool first = true;
69✔
366

367
        ec.set_output_format(text_format_t::TF_CSV);
69✔
368

369
        for (auto& dls_header : dls.dls_headers) {
504✔
370
            if (!first) {
435✔
371
                fprintf(outfile, ",");
366✔
372
            }
373
            csv_write_string(outfile, dls_header.hm_name);
435✔
374
            first = false;
435✔
375
        }
376
        fprintf(outfile, "\n");
69✔
377

378
        ArenaAlloc::Alloc<char> cell_alloc{1024};
69✔
379
        for (auto row = size_t{0}; row < dls.dls_row_cursors.size(); row++) {
441✔
380
            if (ec.ec_dry_run && line_count > 10) {
372✔
381
                break;
×
382
            }
383

384
            auto& row_cursor = dls.dls_row_cursors[row];
372✔
385
            first = true;
372✔
386
            auto cursor = row_cursor.sync();
372✔
387
            for (size_t lpc = 0; lpc < dls.dls_headers.size();
2,586✔
388
                 lpc++, cursor = cursor->next())
2,214✔
389
            {
390
                if (!first) {
2,214✔
391
                    fprintf(outfile, ",");
1,842✔
392
                }
393

394
                auto cell_sf = cursor->to_string_fragment(cell_alloc);
2,214✔
395
                auto cell_str = anonymize ? ta.next(cell_sf)
2,214✔
396
                                          : cell_sf.to_string();
2,214✔
397
                csv_write_string(outfile, cell_str);
2,214✔
398
                first = false;
2,214✔
399
                cell_alloc.reset();
2,214✔
400
            }
2,214✔
401
            fprintf(outfile, "\n");
372✔
402

403
            if (row > 0 && row % 1000 == 0) {
372✔
404
                if (write_progress(row, dls.dls_row_cursors.size())
×
405
                    == lnav::progress_result_t::interrupt)
×
406
                {
407
                    break;
×
408
                }
409
            }
410

411
            line_count += 1;
372✔
412
        }
413
    } else if (args[0] == "write-cols-to" || args[0] == "write-table-to") {
129✔
414
        bool first = true;
7✔
415
        auto tf = ec.get_output_format();
7✔
416

417
        if (tf != text_format_t::TF_MARKDOWN) {
7✔
418
            fprintf(outfile, "\u250f");
1✔
419
            for (const auto& hdr : dls.dls_headers) {
3✔
420
                auto cell_line = repeat("\u2501", hdr.hm_column_size);
4✔
421

422
                if (!first) {
2✔
423
                    fprintf(outfile, "\u2533");
1✔
424
                }
425
                fprintf(outfile, "%s", cell_line.c_str());
2✔
426
                first = false;
2✔
427
            }
2✔
428
            fprintf(outfile, "\u2513\n");
1✔
429
        }
430

431
        for (const auto& hdr : dls.dls_headers) {
43✔
432
            auto centered_hdr = center_str(hdr.hm_name, hdr.hm_column_size);
36✔
433
            auto style = fmt::text_style{};
36✔
434

435
            fprintf(outfile, tf == text_format_t::TF_MARKDOWN ? "|" : "\u2503");
36✔
436
            if (tf != text_format_t::TF_MARKDOWN) {
36✔
437
                style |= fmt::emphasis::bold;
2✔
438
            }
439
            fmt::print(outfile, style, FMT_STRING("{}"), centered_hdr);
108✔
440
        }
36✔
441
        fprintf(outfile, tf == text_format_t::TF_MARKDOWN ? "|\n" : "\u2503\n");
7✔
442

443
        first = true;
7✔
444
        fprintf(outfile, tf == text_format_t::TF_MARKDOWN ? "|" : "\u2521");
7✔
445
        for (const auto& hdr : dls.dls_headers) {
43✔
446
            auto cell_line
447
                = repeat(tf == text_format_t::TF_MARKDOWN ? "-" : "\u2501",
448
                         hdr.hm_column_size);
72✔
449

450
            if (tf == text_format_t::TF_MARKDOWN) {
36✔
451
                switch (hdr.hm_align) {
34✔
452
                    case text_align_t::start:
7✔
453
                        cell_line.front() = ':';
7✔
454
                        break;
7✔
455
                    case text_align_t::center:
×
456
                        cell_line.front() = ':';
×
457
                        cell_line.back() = ':';
×
458
                        break;
×
459
                    case text_align_t::end:
27✔
460
                        cell_line.back() = ':';
27✔
461
                        break;
27✔
462
                }
463
            }
464
            if (!first) {
36✔
465
                fprintf(outfile,
29✔
466
                        tf == text_format_t::TF_MARKDOWN ? "|" : "\u2547");
467
            }
468
            fprintf(outfile, "%s", cell_line.c_str());
36✔
469
            first = false;
36✔
470
        }
36✔
471
        fprintf(outfile, tf == text_format_t::TF_MARKDOWN ? "|\n" : "\u2529\n");
7✔
472

473
        ArenaAlloc::Alloc<char> cell_alloc{1024};
14✔
474
        for (size_t row = 0; row < dls.text_line_count(); row++) {
48✔
475
            if (ec.ec_dry_run && row > 10) {
41✔
476
                break;
×
477
            }
478

479
            auto cursor = dls.dls_row_cursors[row].sync();
41✔
480
            for (size_t col = 0; col < dls.dls_headers.size();
283✔
481
                 col++, cursor = cursor->next())
242✔
482
            {
483
                const auto& hdr = dls.dls_headers[col];
242✔
484

485
                fprintf(outfile,
242✔
486
                        tf == text_format_t::TF_MARKDOWN ? "|" : "\u2502");
487

488
                auto sf = cursor->to_string_fragment(cell_alloc);
242✔
489
                auto cell = attr_line_t::from_table_cell_content(sf, 200);
242✔
490
                if (anonymize) {
242✔
491
                    cell = ta.next(cell.al_string);
×
492
                }
493
                auto cell_length = cell.utf8_length_or_length();
242✔
494
                auto padding = anonymize ? 1 : hdr.hm_column_size - cell_length;
242✔
495
                auto rjust = hdr.hm_align == text_align_t::end;
242✔
496

497
                if (rjust) {
242✔
498
                    fprintf(outfile, "%s", std::string(padding, ' ').c_str());
350✔
499
                }
500
                fprintf(outfile, "%s", cell.al_string.c_str());
242✔
501
                if (!rjust) {
242✔
502
                    fprintf(outfile, "%s", std::string(padding, ' ').c_str());
134✔
503
                }
504
                cell_alloc.reset();
242✔
505
            }
242✔
506
            fprintf(outfile,
41✔
507
                    tf == text_format_t::TF_MARKDOWN ? "|\n" : "\u2502\n");
508

509
            if (row > 0 && row % 1000 == 0) {
41✔
510
                if (write_progress(row, dls.dls_row_cursors.size())
×
511
                    == lnav::progress_result_t::interrupt)
×
512
                {
513
                    break;
×
514
                }
515
            }
516

517
            line_count += 1;
41✔
518
        }
519

520
        if (tf != text_format_t::TF_MARKDOWN) {
7✔
521
            first = true;
1✔
522
            fprintf(outfile, "\u2514");
1✔
523
            for (const auto& hdr : dls.dls_headers) {
3✔
524
                auto cell_line = repeat("\u2501", hdr.hm_column_size);
4✔
525

526
                if (!first) {
2✔
527
                    fprintf(outfile, "\u2534");
1✔
528
                }
529
                fprintf(outfile, "%s", cell_line.c_str());
2✔
530
                first = false;
2✔
531
            }
2✔
532
            fprintf(outfile, "\u2518\n");
1✔
533
        }
534
    } else if (args[0] == "write-json-to") {
53✔
535
        yajlpp_gen gen;
26✔
536

537
        ec.set_output_format(text_format_t::TF_JSON);
26✔
538

539
        yajl_gen_config(gen, yajl_gen_beautify, 1);
26✔
540
        yajl_gen_config(gen, yajl_gen_print_callback, yajl_writer, outfile);
26✔
541

542
        {
543
            yajlpp_array root_array(gen);
26✔
544

545
            for (size_t row = 0; row < dls.dls_row_cursors.size(); row++) {
93✔
546
                if (ec.ec_dry_run && row > 10) {
67✔
547
                    break;
×
548
                }
549

550
                json_write_row(ec, gen, row, ta, anonymize);
67✔
551
                if (row > 0 && row % 1000 == 0) {
67✔
552
                    if (write_progress(row, dls.dls_row_cursors.size())
×
553
                        == lnav::progress_result_t::interrupt)
×
554
                    {
555
                        break;
×
556
                    }
557
                }
558
                line_count += 1;
67✔
559
            }
560
        }
26✔
561
    } else if (args[0] == "write-json-cols-to") {
53✔
562
        yajlpp_gen gen;
1✔
563

564
        ec.set_output_format(text_format_t::TF_JSON);
1✔
565

566
        yajl_gen_config(gen, yajl_gen_beautify, 0);
1✔
567
        yajl_gen_config(gen, yajl_gen_print_callback, yajl_writer, outfile);
1✔
568

569
        {
570
            yajlpp_array root_array(gen);
1✔
571

572
            fprintf(outfile, "\n    ");
1✔
573
            for (const auto& [curr_col, hdr] :
23✔
574
                 lnav::itertools::enumerate(dls.dls_headers))
24✔
575
            {
576
                if (hdr.hm_json_columns.empty()) {
22✔
577
                    yajlpp_map header_map(gen);
21✔
578

579
                    header_map.gen("name");
21✔
580
                    header_map.gen(hdr.hm_name);
21✔
581

582
                    header_map.gen("type");
21✔
583
                    switch (hdr.hm_column_type) {
21✔
584
                        case SQLITE_NULL:
8✔
585
                            header_map.gen("null");
8✔
586
                            break;
8✔
587
                        case SQLITE_INTEGER:
3✔
588
                            header_map.gen("integer");
3✔
589
                            break;
3✔
590
                        case SQLITE_FLOAT:
×
591
                            header_map.gen("float");
×
592
                            break;
×
593
                        case SQLITE3_TEXT:
10✔
594
                            if (hdr.hm_sub_type == JSON_SUBTYPE) {
10✔
595
                                header_map.gen("any");
×
596
                            } else {
597
                                header_map.gen("string");
10✔
598
                            }
599
                            break;
10✔
600
                    }
601

602
                    header_map.gen("width");
21✔
603
                    header_map.gen(hdr.hm_column_size);
21✔
604

605
                    header_map.gen("values");
21✔
606
                    {
607
                        yajlpp_array values_array(gen);
21✔
608
                        for (size_t row = 0; row < dls.dls_row_cursors.size();
126✔
609
                             row++)
610
                        {
611
                            if (ec.ec_dry_run && row > 10) {
105✔
612
                                break;
×
613
                            }
614

615
                            auto cursor = dls.dls_row_cursors[row].sync();
105✔
616
                            for (size_t col = 0; col < curr_col; col++) {
1,190✔
617
                                cursor = cursor->next();
1,085✔
618
                            }
619

620
                            if (cursor) {
105✔
621
                                json_write_cell(hdr,
210✔
622
                                                values_array,
623
                                                cursor.value(),
105✔
624
                                                ta,
625
                                                anonymize);
626
                            }
627
                        }
628
                    }
21✔
629
                } else {
21✔
630
                    std::vector<std::pair<
631
                        string_fragment,
632
                        db_label_source::header_meta::json_column_meta>>
633
                        json_columns;
1✔
634
                    for (const auto& json_col : hdr.hm_json_columns) {
3✔
635
                        json_columns.emplace_back(json_col.first,
2✔
636
                                                  json_col.second);
2✔
637
                    }
638
                    std::sort(json_columns.begin(),
1✔
639
                              json_columns.end(),
640
                              [](auto& lhs, auto& rhs) {
2✔
641
                                  return lhs.second.jcm_column_index
2✔
642
                                      < rhs.second.jcm_column_index;
2✔
643
                              });
644

645
                    for (const auto& [ptr, val] : json_columns) {
3✔
646
                        yajlpp_map header_map(gen);
2✔
647

648
                        header_map.gen("name");
2✔
649
                        header_map.gen(json_ptr_to_title(ptr));
2✔
650

651
                        header_map.gen("type");
2✔
652
                        header_map.gen("any");
2✔
653

654
                        header_map.gen("values");
2✔
655
                        {
656
                            yajlpp_array values_array(gen);
2✔
657
                            for (size_t row = 0;
12✔
658
                                 row < dls.dls_row_cursors.size();
12✔
659
                                 row++)
660
                            {
661
                                if (ec.ec_dry_run && row > 10) {
10✔
662
                                    break;
×
663
                                }
664

665
                                auto cursor = dls.dls_row_cursors[row].sync();
10✔
666
                                for (size_t col = 0; col < curr_col; col++) {
150✔
667
                                    cursor = cursor->next();
140✔
668
                                }
669

670
                                if (cursor
10✔
671
                                    && cursor->get_type()
10✔
672
                                        == lnav::cell_type::CT_TEXT)
10✔
673
                                {
674
                                    auto cell_sf = cursor->get_text();
10✔
675
                                    auto extract_res = extract_json_from(
676
                                        values_array.gen.yg_handle,
677
                                        cell_sf,
678
                                        ptr.data());
10✔
679

680
                                    if (extract_res.isErr()
10✔
681
                                        || extract_res.unwrap() == false)
10✔
682
                                    {
683
                                        values_array.gen();
4✔
684
                                    }
685
                                } else {
10✔
686
                                    values_array.gen();
×
687
                                }
688
                            }
689
                        }
2✔
690
                    }
2✔
691
                }
1✔
692
                line_count += 1;
22✔
693
                fprintf(outfile, "\n    ");
22✔
694
            }
695
        }
1✔
696
        fprintf(outfile, "\n");
1✔
697
    } else if (args[0] == "write-jsonlines-to") {
27✔
698
        ec.set_output_format(text_format_t::TF_JSON);
5✔
699

700
        yajlpp_gen gen;
5✔
701

702
        yajl_gen_config(gen, yajl_gen_beautify, 0);
5✔
703
        yajl_gen_config(gen, yajl_gen_print_callback, yajl_writer, outfile);
5✔
704

705
        for (size_t row = 0; row < dls.dls_row_cursors.size(); row++) {
23✔
706
            if (ec.ec_dry_run && row > 10) {
18✔
707
                break;
×
708
            }
709

710
            json_write_row(ec, gen, row, ta, anonymize);
18✔
711
            yajl_gen_reset(gen, "\n");
18✔
712
            if (row > 0 && row % 1000 == 0) {
18✔
713
                if (write_progress(row, dls.dls_row_cursors.size())
×
714
                    == lnav::progress_result_t::interrupt)
×
715
                {
716
                    break;
×
717
                }
718
            }
719
            line_count += 1;
18✔
720
        }
721
    } else if (args[0] == "write-screen-to") {
26✔
722
        bool wrapped = tc->get_word_wrap();
7✔
723
        auto orig_top = tc->get_top();
7✔
724
        auto inner_height = tc->get_inner_height();
7✔
725

726
        tc->set_word_wrap(to_term);
7✔
727

728
        auto top = tc->get_top();
7✔
729
        auto bottom = tc->get_bottom();
7✔
730
        if (lnav_data.ld_flags & LNF_HEADLESS && inner_height > 0_vl) {
7✔
731
            bottom = inner_height - 1_vl;
7✔
732
        }
733
        auto screen_height = inner_height == 0 ? 0 : bottom - top + 1;
7✔
734
        auto y = 0_vl;
7✔
735
        auto wrapped_count = 0_vl;
7✔
736
        std::vector<attr_line_t> rows(screen_height);
7✔
737
        auto dim = tc->get_dimensions();
7✔
738
        attr_line_t ov_al;
7✔
739

740
        auto* los = tc->get_overlay_source();
7✔
741
        while (los != nullptr
7✔
742
               && los->list_static_overlay(*tc,
24✔
743
                                           list_overlay_source::media_t::file,
744
                                           y,
745
                                           tc->get_inner_height(),
24✔
746
                                           ov_al))
747
        {
748
            write_line_to(outfile, ov_al);
5✔
749
            ov_al.clear();
5✔
750
            ++y;
5✔
751
        }
752
        tc->listview_value_for_rows(*tc, top, rows);
7✔
753
        for (auto& al : rows) {
26✔
754
            if (anonymize) {
19✔
755
                al.al_attrs.clear();
×
756
                al.al_string = ta.next(al.al_string);
×
757
            }
758
            auto cols_out = write_line_to(outfile, al);
19✔
759
            wrapped_count += vis_line_t(cols_out / (dim.second - 2));
19✔
760

761
            ++y;
19✔
762
            if (los != nullptr) {
19✔
763
                std::vector<attr_line_t> row_overlay_content;
19✔
764
                los->list_value_for_overlay(*tc, top, row_overlay_content);
19✔
765
                for (const auto& ov_row : row_overlay_content) {
21✔
766
                    write_line_to(outfile, ov_row);
2✔
767
                    line_count += 1;
2✔
768
                    ++y;
2✔
769
                }
770
            }
19✔
771
            line_count += 1;
19✔
772
            ++top;
19✔
773
        }
774

775
        tc->set_word_wrap(wrapped);
7✔
776
        tc->set_top(orig_top);
7✔
777

778
        if (!(lnav_data.ld_flags & LNF_HEADLESS)) {
7✔
779
            while (y + wrapped_count < dim.first + 2_vl) {
×
780
                fmt::print(outfile, FMT_STRING("\n"));
×
781
                ++y;
×
782
            }
783
        }
784
    } else if (args[0] == "write-raw-to") {
21✔
785
        if (tc == &lnav_data.ld_views[LNV_DB]) {
4✔
786
            ArenaAlloc::Alloc<char> cell_alloc{1024};
3✔
787
            for (auto row = size_t{0}; row < dls.dls_row_cursors.size(); row++)
18✔
788
            {
789
                if (ec.ec_dry_run && line_count > 10) {
15✔
790
                    break;
×
791
                }
792

793
                const auto& row_cursor = dls.dls_row_cursors[row];
15✔
794
                auto cursor = row_cursor.sync();
15✔
795
                for (size_t lpc = 0; lpc < dls.dls_headers.size();
31✔
796
                     lpc++, cursor = cursor->next())
16✔
797
                {
798
                    auto sf = cursor->to_string_fragment(cell_alloc);
16✔
799
                    if (anonymize) {
16✔
800
                        fputs(ta.next(sf).c_str(), outfile);
×
801
                    } else {
802
                        fwrite(sf.data(), sf.length(), 1, outfile);
16✔
803
                    }
804
                    cell_alloc.reset();
16✔
805
                }
806
                fprintf(outfile, "\n");
15✔
807

808
                if (row > 0 && row % 1000 == 0) {
15✔
809
                    if (write_progress(row, dls.dls_row_cursors.size())
×
810
                        == lnav::progress_result_t::interrupt)
×
811
                    {
812
                        break;
×
813
                    }
814
                }
815

816
                line_count += 1;
15✔
817
            }
818
        } else if (tc == &lnav_data.ld_views[LNV_LOG]) {
4✔
819
            std::optional<std::pair<logfile*, content_line_t>> last_line;
1✔
820
            bookmark_vector<vis_line_t> visited;
1✔
821
            auto& lss = lnav_data.ld_log_source;
1✔
822
            std::vector<attr_line_t> rows(1);
1✔
823
            std::string line;
1✔
824

825
            for (auto iter = all_user_marks.bv_tree.begin();
1✔
826
                 iter != all_user_marks.bv_tree.end();
4✔
827
                 ++iter)
3✔
828
            {
829
                if (ec.ec_dry_run && line_count > 10) {
3✔
830
                    break;
×
831
                }
832
                auto cl = lss.at(*iter);
3✔
833
                auto lf = lss.find(cl);
3✔
834
                auto lf_iter = lf->begin() + cl;
3✔
835

836
                while (lf_iter->get_sub_offset() != 0) {
6✔
837
                    --lf_iter;
3✔
838
                }
839

840
                auto line_pair = std::make_pair(
3✔
841
                    lf.get(),
3✔
842
                    content_line_t(std::distance(lf->begin(), lf_iter)));
6✔
843
                if (last_line && last_line.value() == line_pair) {
3✔
844
                    continue;
2✔
845
                }
846
                last_line = line_pair;
1✔
847
                auto read_res = lf->read_raw_message(lf_iter);
1✔
848
                if (read_res.isErr()) {
1✔
849
                    log_error("unable to read message: %s",
×
850
                              read_res.unwrapErr().c_str());
851
                    continue;
×
852
                }
853
                auto sbr = read_res.unwrap();
1✔
854
                if (anonymize) {
1✔
855
                    auto msg = ta.next(sbr.to_string_fragment().to_string());
×
856
                    fprintf(outfile, "%s\n", msg.c_str());
×
857
                } else {
×
858
                    fprintf(
2✔
859
                        outfile, "%.*s\n", (int) sbr.length(), sbr.get_data());
1✔
860
                }
861

862
                if (line_count > 0 && line_count % 1000 == 0) {
1✔
863
                    if (write_progress(line_count, all_user_marks.size())
×
864
                        == lnav::progress_result_t::interrupt)
×
865
                    {
866
                        break;
×
867
                    }
868
                }
869
                line_count += 1;
1✔
870
            }
3✔
871
        }
1✔
872
    } else if (args[0] == "write-view-to") {
10✔
873
        bool wrapped = tc->get_word_wrap();
3✔
874
        auto tss = tc->get_sub_source();
3✔
875

876
        tc->set_word_wrap(to_term);
3✔
877

878
        for (size_t lpc = 0; lpc < tss->text_line_count(); lpc++) {
32✔
879
            if (ec.ec_dry_run && lpc >= 10) {
29✔
880
                break;
×
881
            }
882

883
            std::string line;
29✔
884

885
            tss->text_value_for_line(*tc, lpc, line, text_sub_source::RF_RAW);
29✔
886
            if (anonymize) {
29✔
887
                line = ta.next(line);
26✔
888
            }
889
            fprintf(outfile, "%s\n", line.c_str());
29✔
890

891
            line_count += 1;
29✔
892
        }
29✔
893

894
        tc->set_word_wrap(wrapped);
3✔
895
    } else {
896
        auto* los = tc->get_overlay_source();
7✔
897
        auto* fos = dynamic_cast<field_overlay_source*>(los);
7✔
898
        std::vector<attr_line_t> rows(1);
7✔
899
        attr_line_t ov_al;
7✔
900

901
        if (fos != nullptr) {
7✔
902
            fos->fos_contexts.push(
14✔
903
                field_overlay_source::context{"", false, false, false});
21✔
904
        }
905

906
        auto y = 0_vl;
7✔
907
        while (los != nullptr
7✔
908
               && los->list_static_overlay(*tc,
14✔
909
                                           list_overlay_source::media_t::file,
910
                                           y,
911
                                           tc->get_inner_height(),
14✔
912
                                           ov_al))
913
        {
914
            write_line_to(outfile, ov_al);
×
915
            ov_al.clear();
×
916
            ++y;
×
917
        }
918
        for (auto iter = all_user_marks.bv_tree.begin();
7✔
919
             iter != all_user_marks.bv_tree.end();
17✔
920
             ++iter)
10✔
921
        {
922
            if (ec.ec_dry_run && line_count > 10) {
10✔
923
                break;
×
924
            }
925
            tc->listview_value_for_rows(*tc, *iter, rows);
10✔
926
            if (anonymize) {
10✔
927
                rows[0].al_attrs.clear();
×
928
                rows[0].al_string = ta.next(rows[0].al_string);
×
929
            }
930
            write_line_to(outfile, rows[0]);
10✔
931

932
            y = 0_vl;
10✔
933
            if (los != nullptr) {
10✔
934
                std::vector<attr_line_t> row_overlay_content;
10✔
935
                los->list_value_for_overlay(*tc, (*iter), row_overlay_content);
10✔
936
                for (const auto& ov_row : row_overlay_content) {
12✔
937
                    write_line_to(outfile, ov_row);
2✔
938
                    line_count += 1;
2✔
939
                    ++y;
2✔
940
                }
941
            }
10✔
942
            if (line_count > 0 && line_count % 1000 == 0) {
10✔
943
                if (write_progress(line_count, all_user_marks.size())
×
944
                    == lnav::progress_result_t::interrupt)
×
945
                {
946
                    break;
×
947
                }
948
            }
949
            line_count += 1;
10✔
950
        }
951

952
        if (fos != nullptr) {
7✔
953
            fos->fos_contexts.pop();
7✔
954
            ensure(!fos->fos_contexts.empty());
7✔
955
        }
956
    }
7✔
957

958
    fflush(outfile);
129✔
959

960
    if (to_term) {
129✔
961
        if (ec.ec_ui_callbacks.uc_post_stdout_write) {
×
962
            ec.ec_ui_callbacks.uc_post_stdout_write();
×
963
        } else {
964
            log_debug("no post stdout write callback");
×
965
        }
966
    }
967
    if (ec.ec_dry_run) {
129✔
968
        rewind(outfile);
×
969

970
        char buffer[32 * 1024];
971
        size_t rc = fread(buffer, 1, sizeof(buffer), outfile);
×
972

973
        attr_line_t al(std::string(buffer, rc));
×
974

975
        lnav_data.ld_preview_view[0].set_sub_source(
×
976
            &lnav_data.ld_preview_source[0]);
977
        lnav_data.ld_preview_source[0]
978
            .replace_with(al)
×
979
            .set_text_format(detect_text_format(al.get_string()))
×
980
            .truncate_to(10);
×
981
        lnav_data.ld_preview_status_source[0].get_description().set_value(
×
982
            "First lines of file: %s", split_args[0].c_str());
×
983
        lnav_data.ld_status[LNS_PREVIEW0].set_needs_update();
×
984
    } else {
×
985
        retval = fmt::format(FMT_STRING("info: Wrote {:L} rows to {}"),
516✔
986
                             line_count,
987
                             split_args[0]);
258✔
988
    }
989
    if (toclose != nullptr) {
129✔
990
        closer(toclose);
3✔
991
    }
992
    outfile = nullptr;
129✔
993

994
    lnav_data.ld_bottom_source.update_loading(0, 0);
129✔
995
    lnav_data.ld_status[LNS_BOTTOM].set_needs_update();
129✔
996
    lnav_data.ld_status_refresher(lnav::func::op_type::blocking);
129✔
997

998
    return Ok(retval);
129✔
999
}
131✔
1000

1001
static Result<std::string, lnav::console::user_message>
1002
com_open(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
14✔
1003
{
1004
    static const intern_string_t SRC = intern_string::lookup("path");
34✔
1005
    std::string retval;
14✔
1006

1007
    if (lnav_data.ld_flags & LNF_SECURE_MODE) {
14✔
1008
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
1009
    }
1010

1011
    if (args.size() < 2) {
14✔
1012
        return ec.make_error("expecting file name to open");
1✔
1013
    }
1014

1015
    file_collection fc;
13✔
1016
    auto pat = trim(remaining_args(cmdline, args));
13✔
1017

1018
    shlex lexer(pat);
13✔
1019
    auto split_args_res = lexer.split(ec.create_resolver());
13✔
1020
    if (split_args_res.isErr()) {
13✔
1021
        auto split_err = split_args_res.unwrapErr();
×
1022
        auto um
1023
            = lnav::console::user_message::error("unable to parse file names")
×
1024
                  .with_reason(split_err.se_error.te_msg)
×
1025
                  .with_snippet(lnav::console::snippet::from(
×
1026
                      SRC, lexer.to_attr_line(split_err.se_error)))
×
1027
                  .move();
×
1028

1029
        return Err(um);
×
1030
    }
1031

1032
    auto split_args = split_args_res.unwrap()
26✔
1033
        | lnav::itertools::map([](const auto& elem) { return elem.se_value; });
39✔
1034

1035
    std::vector<std::string> files_to_front;
13✔
1036
    std::vector<std::string> closed_files;
13✔
1037
    logfile_open_options loo;
13✔
1038

1039
    auto prov = ec.get_provenance<exec_context::file_open>();
13✔
1040
    if (prov) {
13✔
1041
        loo.with_filename(prov->fo_name);
4✔
1042
    }
1043
    loo.with_time_range(lnav_data.ld_default_time_range);
13✔
1044

1045
    CLI::App app{"open"};
39✔
1046
    std::vector<std::string> file_args;
13✔
1047
    std::string since_time;
13✔
1048
    std::string until_time;
13✔
1049

1050
    app.add_option("-S,--since", since_time, "start time for the log");
52✔
1051
    app.add_option("-U,--until", until_time, "end time for the log");
52✔
1052
    app.add_option("file", file_args, "files to open");
39✔
1053
    app.parse(split_args);
13✔
1054

1055
    if (!since_time.empty()) {
13✔
1056
        auto from_res = humanize::time::point::from(since_time);
×
1057
        if (from_res.isErr()) {
×
1058
            auto um = from_res.unwrapErr();
×
1059
            um.um_message = attr_line_t("invalid 'since' time ")
×
1060
                                .append_quoted(since_time);
×
1061
            return Err(um);
×
1062
        }
1063
        loo.loo_time_range.tr_begin = to_us(from_res.unwrap().get_point());
×
1064
    }
1065
    if (!until_time.empty()) {
13✔
1066
        auto from_res = humanize::time::point::from(until_time);
×
1067
        if (from_res.isErr()) {
×
1068
            auto um = from_res.unwrapErr();
×
1069
            um.um_message = attr_line_t("invalid 'until' time ")
×
1070
                                .append_quoted(until_time);
×
1071
            return Err(um);
×
1072
        }
1073
        loo.loo_time_range.tr_end = to_us(from_res.unwrap().get_point());
×
1074
    }
1075

1076
    for (auto fn : file_args) {
25✔
1077
        std::replace(fn.begin(), fn.end(), '\\', '/');
13✔
1078
        auto fn_path = std::filesystem::path{fn};
13✔
1079
        auto file_loc = file_location_t{default_for_text_format{}};
13✔
1080

1081
        if (fn_path.has_root_name() && fn_path.root_directory().empty()) {
13✔
1082
            return Err(
×
1083
                lnav::console::user_message::error("incomplete root name"));
×
1084
        }
1085

1086
        if (access(fn.c_str(), R_OK) != 0) {
13✔
1087
            auto pair = lnav::filesystem::split_file_location(fn);
10✔
1088
            fn = pair.first;
10✔
1089
            file_loc = pair.second;
10✔
1090
            loo.with_init_location(file_loc);
10✔
1091
        }
10✔
1092

1093
        auto file_iter = lnav_data.ld_active_files.fc_files.begin();
13✔
1094
        for (; file_iter != lnav_data.ld_active_files.fc_files.end();
13✔
1095
             ++file_iter)
×
1096
        {
1097
            auto lf = *file_iter;
1✔
1098

1099
            if (lf->get_filename() == fn) {
1✔
1100
                if (lf->get_format() != nullptr) {
1✔
1101
                    retval = "info: log file already loaded";
1✔
1102
                    break;
1✔
1103
                }
1104

1105
                lf->set_init_location(file_loc);
×
1106
                files_to_front.emplace_back(fn);
×
1107
                retval = "";
×
1108
                break;
×
1109
            }
1110
        }
1✔
1111
        if (file_iter == lnav_data.ld_active_files.fc_files.end()) {
13✔
1112
            auto_mem<char> abspath;
12✔
1113
            struct stat st;
1114
            size_t url_index;
1115

1116
#ifdef HAVE_LIBCURL
1117
            if (startswith(fn, "file:")) {
12✔
1118
                auto_mem<CURLU> cu(curl_url_cleanup);
×
1119
                cu = curl_url();
×
1120
                auto set_rc = curl_url_set(cu, CURLUPART_URL, fn.c_str(), 0);
×
1121
                if (set_rc != CURLUE_OK) {
×
1122
                    return Err(lnav::console::user_message::error(
×
1123
                                   attr_line_t("invalid URL: ")
×
1124
                                       .append(lnav::roles::file(fn)))
×
1125
                                   .with_reason(curl_url_strerror(set_rc)));
×
1126
                }
1127

1128
                auto_mem<char> path_part;
×
1129
                auto get_rc
1130
                    = curl_url_get(cu, CURLUPART_PATH, path_part.out(), 0);
×
1131
                if (get_rc != CURLUE_OK) {
×
1132
                    return Err(lnav::console::user_message::error(
×
1133
                                   attr_line_t("cannot get path from URL: ")
×
1134
                                       .append(lnav::roles::file(fn)))
×
1135
                                   .with_reason(curl_url_strerror(get_rc)));
×
1136
                }
1137
                auto_mem<char> frag_part;
×
1138
                get_rc
1139
                    = curl_url_get(cu, CURLUPART_FRAGMENT, frag_part.out(), 0);
×
1140
                if (get_rc != CURLUE_OK && get_rc != CURLUE_NO_FRAGMENT) {
×
1141
                    return Err(lnav::console::user_message::error(
×
1142
                                   attr_line_t("cannot get fragment from URL: ")
×
1143
                                       .append(lnav::roles::file(fn)))
×
1144
                                   .with_reason(curl_url_strerror(get_rc)));
×
1145
                }
1146

1147
                if (frag_part != nullptr && frag_part[0]) {
×
1148
                    fn = fmt::format(
×
1149
                        FMT_STRING("{}#{}"), path_part.in(), frag_part.in());
×
1150
                } else {
1151
                    fn = path_part;
×
1152
                }
1153
            }
1154
#endif
1155

1156
            if (lnav::filesystem::is_url(fn)) {
12✔
1157
#ifndef HAVE_LIBCURL
1158
                retval = "error: lnav was not compiled with libcurl";
1159
#else
1160
                if (!ec.ec_dry_run) {
×
1161
                    auto ul = std::make_shared<url_loader>(fn);
×
1162

1163
                    lnav_data.ld_active_files.fc_file_names[ul->get_path()]
×
1164
                        .with_filename(fn)
×
1165
                        .with_init_location(file_loc);
×
1166
                    lnav_data.ld_active_files.fc_files_generation += 1;
×
1167
                    isc::to<curl_looper&, services::curl_streamer_t>().send(
×
1168
                        [ul](auto& clooper) { clooper.add_request(ul); });
×
1169
                    lnav_data.ld_files_to_front.emplace_back(fn);
×
1170
                    closed_files.push_back(fn);
×
1171
                    retval = "info: opened URL";
×
1172
                } else {
×
1173
                    retval = "";
×
1174
                }
1175
#endif
1176
            } else if ((url_index = fn.find("://")) != std::string::npos) {
12✔
1177
                const auto& cfg
1178
                    = injector::get<const lnav::url_handler::config&>();
5✔
1179
                const auto HOST_REGEX
1180
                    = lnav::pcre2pp::code::from_const("://(?:\\?|$)");
5✔
1181

1182
                auto find_res = HOST_REGEX.find_in(fn).ignore_error();
5✔
1183
                if (find_res) {
5✔
1184
                    fn.insert(url_index + 3, "localhost");
×
1185
                }
1186

1187
                auto_mem<CURLU> cu(curl_url_cleanup);
5✔
1188
                cu = curl_url();
5✔
1189
                auto set_rc = curl_url_set(
5✔
1190
                    cu, CURLUPART_URL, fn.c_str(), CURLU_NON_SUPPORT_SCHEME);
1191
                if (set_rc != CURLUE_OK) {
5✔
1192
                    return Err(lnav::console::user_message::error(
×
1193
                                   attr_line_t("invalid URL: ")
×
1194
                                       .append(lnav::roles::file(fn)))
×
1195
                                   .with_reason(curl_url_strerror(set_rc)));
×
1196
                }
1197

1198
                auto_mem<char> scheme_part(curl_free);
5✔
1199
                auto get_rc
1200
                    = curl_url_get(cu, CURLUPART_SCHEME, scheme_part.out(), 0);
5✔
1201
                if (get_rc != CURLUE_OK) {
5✔
1202
                    return Err(lnav::console::user_message::error(
×
1203
                                   attr_line_t("cannot get scheme from URL: ")
×
1204
                                       .append(lnav::roles::file(fn)))
×
1205
                                   .with_reason(curl_url_strerror(set_rc)));
×
1206
                }
1207

1208
                auto proto_iter = cfg.c_schemes.find(scheme_part.in());
10✔
1209
                if (proto_iter == cfg.c_schemes.end()) {
5✔
1210
                    return Err(
×
1211
                        lnav::console::user_message::error(
×
1212
                            attr_line_t("no defined handler for URL scheme: ")
×
1213
                                .append(lnav::roles::file(scheme_part.in())))
×
1214
                            .with_reason(curl_url_strerror(set_rc)));
×
1215
                }
1216

1217
                auto path_and_args
1218
                    = fmt::format(FMT_STRING("{} {}"),
10✔
1219
                                  proto_iter->second.p_handler.pp_value,
5✔
1220
                                  fn);
×
1221

1222
                exec_context::provenance_guard pg(&ec,
1223
                                                  exec_context::file_open{fn});
5✔
1224

1225
                auto cb_guard = ec.push_callback(internal_sql_callback);
5✔
1226

1227
                auto exec_res = execute_file(ec, path_and_args);
5✔
1228
                if (exec_res.isErr()) {
5✔
1229
                    return exec_res;
×
1230
                }
1231

1232
                retval = "info: watching -- " + fn;
5✔
1233
            } else if (lnav::filesystem::is_glob(fn.c_str())) {
19✔
1234
                loo.with_init_location(file_loc);
4✔
1235
                fc.fc_file_names.insert2(fn, loo);
4✔
1236
                files_to_front.emplace_back(
4✔
1237
                    loo.loo_filename.empty() ? fn : loo.loo_filename);
4✔
1238
                retval = "info: watching -- " + fn;
4✔
1239
            } else if (stat(fn.c_str(), &st) == -1) {
3✔
1240
                if (fn.find(':') != std::string::npos) {
1✔
NEW
1241
                    fc.fc_file_names.insert2(fn, loo);
×
1242
                    retval = "info: watching -- " + fn;
×
1243
                } else {
1244
                    auto um = lnav::console::user_message::error(
×
1245
                                  attr_line_t("cannot open file: ")
1✔
1246
                                      .append(lnav::roles::file(fn)))
2✔
1247
                                  .with_errno_reason()
1✔
1248
                                  .with_snippets(ec.ec_source)
2✔
1249
                                  .with_help(
2✔
1250
                                      "make sure the file exists and is "
1251
                                      "accessible")
1252
                                  .move();
1✔
1253
                    return Err(um);
1✔
1254
                }
1✔
1255
            } else if (is_dev_null(st)) {
2✔
1256
                return ec.make_error("cannot open /dev/null");
×
1257
            } else if (S_ISFIFO(st.st_mode)) {
2✔
1258
                auto_fd fifo_fd;
×
1259

1260
                if ((fifo_fd = open(fn.c_str(), O_RDONLY)) == -1) {
×
1261
                    auto um = lnav::console::user_message::error(
×
1262
                                  attr_line_t("cannot open FIFO: ")
×
1263
                                      .append(lnav::roles::file(fn)))
×
1264
                                  .with_errno_reason()
×
1265
                                  .with_snippets(ec.ec_source)
×
1266
                                  .move();
×
1267
                    return Err(um);
×
1268
                } else if (ec.ec_dry_run) {
×
1269
                    retval = "";
×
1270
                } else {
1271
                    auto desc = fmt::format(FMT_STRING("FIFO [{}]"),
×
1272
                                            lnav_data.ld_fifo_counter++);
×
1273
                    if (prov) {
×
1274
                        desc = prov->fo_name;
×
1275
                    }
1276
                    auto create_piper_res = lnav::piper::create_looper(
1277
                        desc, std::move(fifo_fd), auto_fd{});
×
1278
                    if (create_piper_res.isErr()) {
×
1279
                        auto um = lnav::console::user_message::error(
×
1280
                                      attr_line_t("cannot create piper: ")
×
1281
                                          .append(lnav::roles::file(fn)))
×
1282
                                      .with_reason(create_piper_res.unwrapErr())
×
1283
                                      .with_snippets(ec.ec_source)
×
1284
                                      .move();
×
1285
                        return Err(um);
×
1286
                    }
1287
                    lnav_data.ld_active_files.fc_file_names[desc].with_piper(
×
1288
                        create_piper_res.unwrap());
×
1289
                }
1290
            } else if ((abspath = realpath(fn.c_str(), nullptr)) == nullptr) {
2✔
1291
                auto um = lnav::console::user_message::error(
×
1292
                              attr_line_t("cannot open file: ")
×
1293
                                  .append(lnav::roles::file(fn)))
×
1294
                              .with_errno_reason()
×
1295
                              .with_snippets(ec.ec_source)
×
1296
                              .with_help(
×
1297
                                  "make sure the file exists and is "
1298
                                  "accessible")
1299
                              .move();
×
1300
                return Err(um);
×
1301
            } else if (S_ISDIR(st.st_mode)) {
2✔
1302
                std::string dir_wild(abspath.in());
×
1303

1304
                if (dir_wild[dir_wild.size() - 1] == '/') {
×
1305
                    dir_wild.resize(dir_wild.size() - 1);
×
1306
                }
NEW
1307
                fc.fc_file_names.insert2(dir_wild + "/*", loo);
×
1308
                retval = "info: watching -- " + dir_wild;
×
1309
            } else if (!S_ISREG(st.st_mode)) {
2✔
1310
                auto um = lnav::console::user_message::error(
×
1311
                              attr_line_t("cannot open file: ")
×
1312
                                  .append(lnav::roles::file(fn)))
×
1313
                              .with_reason("not a regular file or directory")
×
1314
                              .with_snippets(ec.ec_source)
×
1315
                              .with_help(
×
1316
                                  "only regular files, directories, and FIFOs "
1317
                                  "can be opened")
1318
                              .move();
×
1319
                return Err(um);
×
1320
            } else if (access(fn.c_str(), R_OK) == -1) {
2✔
1321
                auto um = lnav::console::user_message::error(
×
1322
                              attr_line_t("cannot read file: ")
×
1323
                                  .append(lnav::roles::file(fn)))
×
1324
                              .with_errno_reason()
×
1325
                              .with_snippets(ec.ec_source)
×
1326
                              .with_help(
×
1327
                                  "make sure the file exists and is "
1328
                                  "accessible")
1329
                              .move();
×
1330
                return Err(um);
×
1331
            } else {
×
1332
                fn = abspath.in();
2✔
1333

1334
                file_iter = lnav_data.ld_active_files.fc_files.begin();
2✔
1335
                for (; file_iter != lnav_data.ld_active_files.fc_files.end();
2✔
1336
                     ++file_iter)
×
1337
                {
1338
                    auto lf = *file_iter;
×
1339

1340
                    if (lf->get_filename() == fn) {
×
1341
                        if (lf->get_format() != nullptr) {
×
1342
                            retval = "info: log file already loaded";
×
1343
                            break;
×
1344
                        }
1345

1346
                        lf->set_init_location(file_loc);
×
1347
                        files_to_front.emplace_back(fn);
×
1348
                        retval = "";
×
1349
                        break;
×
1350
                    }
1351
                }
1352
                if (file_iter == lnav_data.ld_active_files.fc_files.end()) {
2✔
1353
                    loo.with_init_location(file_loc);
2✔
1354
                    fc.fc_file_names.insert2(fn, loo);
2✔
1355
                    retval = "info: opened -- " + fn;
2✔
1356
                    files_to_front.emplace_back(fn);
2✔
1357

1358
                    closed_files.push_back(fn);
2✔
1359
                    if (!loo.loo_filename.empty()) {
2✔
1360
                        closed_files.push_back(loo.loo_filename);
×
1361
                    }
1362
                }
1363
#if 0
1364
                if (lnav_data.ld_rl_view != nullptr) {
1365
                    lnav_data.ld_rl_view->set_alt_value(
1366
                        HELP_MSG_1(X, "to close the file"));
1367
                }
1368
#endif
1369
            }
1370
        }
12✔
1371
    }
15✔
1372

1373
    if (ec.ec_dry_run) {
12✔
1374
        lnav_data.ld_preview_view[0].set_sub_source(
×
1375
            &lnav_data.ld_preview_source[0]);
1376
        lnav_data.ld_preview_source[0].clear();
×
1377
        if (!fc.fc_file_names.empty()) {
×
1378
            auto iter = fc.fc_file_names.begin();
×
1379
            std::string fn_str = iter->first;
×
1380

1381
            if (fn_str.find(':') != std::string::npos) {
×
1382
                auto id = lnav_data.ld_preview_generation;
×
1383
                lnav_data.ld_preview_status_source[0]
1384
                    .get_description()
×
1385
                    .set_cylon(true)
×
1386
                    .set_value("Loading %s...", fn_str.c_str());
×
1387
                lnav_data.ld_status[LNS_PREVIEW0].set_needs_update();
×
1388
                lnav_data.ld_preview_view[0].set_sub_source(
×
1389
                    &lnav_data.ld_preview_source[0]);
1390
                lnav_data.ld_preview_source[0].clear();
×
1391

1392
                isc::to<tailer::looper&, services::remote_tailer_t>().send(
×
1393
                    [id, fn_str](auto& tlooper) {
×
1394
                        auto rp_opt = humanize::network::path::from_str(fn_str);
×
1395
                        if (rp_opt) {
×
1396
                            tlooper.load_preview(id, *rp_opt);
×
1397
                        }
1398
                    });
×
1399
                lnav_data.ld_preview_view[0].set_needs_update();
×
1400
            } else if (lnav::filesystem::is_glob(fn_str)) {
×
1401
                static_root_mem<glob_t, globfree> gl;
1402

1403
                fn_str = lnav::filesystem::escape_glob_for_win(fn_str);
×
1404
                auto fn = std::filesystem::path(fn_str);
×
1405
                if (fn.has_root_name() && fn.root_directory().empty()) {
×
1406
                    log_debug("ignoring incomplete root name: %s", fn.c_str());
×
1407
                } else if (glob(fn_str.c_str(),
×
1408
                                GLOB_NOCHECK,
1409
                                nullptr,
1410
                                gl.inout())
1411
                           == 0)
×
1412
                {
1413
                    attr_line_t al;
×
1414

1415
                    for (size_t lpc = 0; lpc < gl->gl_pathc && lpc < 10; lpc++)
×
1416
                    {
1417
                        al.append(gl->gl_pathv[lpc]).append("\n");
×
1418
                    }
1419
                    if (gl->gl_pathc > 10) {
×
1420
                        al.append(" ... ")
×
1421
                            .append(lnav::roles::number(
×
1422
                                std::to_string(gl->gl_pathc - 10)))
×
1423
                            .append(" files not shown ...");
×
1424
                    }
1425
                    lnav_data.ld_preview_status_source[0]
1426
                        .get_description()
×
1427
                        .set_value("The following files will be loaded:"_frag);
×
1428
                    lnav_data.ld_status[LNS_PREVIEW0].set_needs_update();
×
1429
                    lnav_data.ld_preview_view[0].set_sub_source(
×
1430
                        &lnav_data.ld_preview_source[0]);
1431
                    lnav_data.ld_preview_source[0].replace_with(al);
×
1432
                } else {
×
1433
                    return ec.make_error("failed to evaluate glob -- {}",
1434
                                         fn_str);
×
1435
                }
1436
            } else {
×
1437
                auto fn = std::filesystem::path(fn_str);
×
1438
                auto detect_res = detect_file_format(fn);
×
1439
                attr_line_t al;
×
1440
                attr_line_builder alb(al);
×
1441

1442
                switch (detect_res.dffr_file_format) {
×
1443
                    case file_format_t::ARCHIVE: {
×
1444
                        auto describe_res = archive_manager::describe(fn);
×
1445

1446
                        if (describe_res.isOk()) {
×
1447
                            auto arc_res = describe_res.unwrap();
×
1448

1449
                            if (arc_res.is<archive_manager::archive_info>()) {
×
1450
                                auto ai
1451
                                    = arc_res
1452
                                          .get<archive_manager::archive_info>();
×
1453
                                auto lines_remaining = size_t{9};
×
1454

1455
                                al.append("Archive: ")
×
1456
                                    .append(
×
1457
                                        lnav::roles::symbol(ai.ai_format_name))
×
1458
                                    .append("\n");
×
1459
                                for (const auto& entry : ai.ai_entries) {
×
1460
                                    if (lines_remaining == 0) {
×
1461
                                        break;
×
1462
                                    }
1463
                                    lines_remaining -= 1;
×
1464

1465
                                    auto mtime_secs
1466
                                        = std::chrono::seconds{entry.e_mtime};
×
1467
                                    char timebuf[64];
1468
                                    sql_strftime(timebuf,
×
1469
                                                 sizeof(timebuf),
1470
                                                 mtime_secs,
1471
                                                 'T');
1472
                                    al.append("    ")
×
1473
                                        .append(entry.e_mode)
×
1474
                                        .append(" ")
×
1475
                                        .appendf(
×
1476
                                            FMT_STRING("{:>8}"),
×
1477
                                            humanize::file_size(
×
1478
                                                entry.e_size.value(),
×
1479
                                                humanize::alignment::columnar))
1480
                                        .append(" ")
×
1481
                                        .append(timebuf)
×
1482
                                        .append(" ")
×
1483
                                        .append(lnav::roles::file(entry.e_name))
×
1484
                                        .append("\n");
×
1485
                                }
1486
                            }
1487
                        } else {
×
1488
                            al.append(describe_res.unwrapErr());
×
1489
                        }
1490
                        break;
×
1491
                    }
1492
                    case file_format_t::MULTIPLEXED:
×
1493
                    case file_format_t::UNKNOWN: {
1494
                        auto open_res
1495
                            = lnav::filesystem::open_file(fn, O_RDONLY);
×
1496

1497
                        if (open_res.isErr()) {
×
1498
                            return ec.make_error("unable to open -- {}", fn);
×
1499
                        }
1500
                        auto preview_fd = open_res.unwrap();
×
1501
                        line_buffer lb;
×
1502
                        file_range range;
×
1503

1504
                        lb.set_fd(preview_fd);
×
1505
                        for (int lpc = 0; lpc < 100; lpc++) {
×
1506
                            auto load_result = lb.load_next_line(range);
×
1507

1508
                            if (load_result.isErr()) {
×
1509
                                break;
×
1510
                            }
1511

1512
                            auto li = load_result.unwrap();
×
1513

1514
                            range = li.li_file_range;
×
1515
                            if (!li.li_utf8_scan_result.is_valid()) {
×
1516
                                range.fr_size = 16;
×
1517
                            }
1518
                            auto read_result = lb.read_range(range);
×
1519
                            if (read_result.isErr()) {
×
1520
                                break;
×
1521
                            }
1522

1523
                            auto sbr = read_result.unwrap();
×
1524
                            auto sf = sbr.to_string_fragment();
×
1525
                            if (li.li_utf8_scan_result.is_valid()) {
×
1526
                                alb.append(sf);
×
1527
                            } else {
1528
                                {
1529
                                    auto ag = alb.with_attr(
1530
                                        VC_ROLE.value(role_t::VCR_FILE_OFFSET));
×
1531
                                    alb.appendf(FMT_STRING("{: >16x} "),
×
1532
                                                range.fr_offset);
1533
                                }
1534
                                alb.append_as_hexdump(sf);
×
1535
                                alb.append("\n");
×
1536
                            }
1537
                        }
1538
                        break;
×
1539
                    }
1540
                    case file_format_t::SQLITE_DB: {
×
1541
                        alb.append(fmt::to_string(detect_res.dffr_file_format));
×
1542
                        break;
×
1543
                    }
1544
                    case file_format_t::UNSUPPORTED:
×
1545
                    case file_format_t::REMOTE: {
1546
                        break;
×
1547
                    }
1548
                }
1549

1550
                auto tf = detect_text_format(al.get_string(), fn_str);
×
1551
                log_debug(":open preview text format: %s",
×
1552
                          fmt::to_string(tf).c_str());
1553

1554
                lnav_data.ld_preview_view[0].set_sub_source(
×
1555
                    &lnav_data.ld_preview_source[0]);
1556
                lnav_data.ld_preview_source[0].replace_with(al).set_text_format(
×
1557
                    tf);
1558
                lnav_data.ld_preview_status_source[0]
1559
                    .get_description()
×
1560
                    .set_value("For file: %s", fn.c_str());
×
1561
                lnav_data.ld_status[LNS_PREVIEW0].set_needs_update();
×
1562
            }
1563
        }
1564
    } else {
1565
        lnav_data.ld_files_to_front.insert(lnav_data.ld_files_to_front.end(),
12✔
1566
                                           files_to_front.begin(),
1567
                                           files_to_front.end());
1568
        for (const auto& fn : closed_files) {
14✔
1569
            lnav_data.ld_active_files.fc_closed_files.erase(fn);
2✔
1570
        }
1571

1572
        lnav_data.ld_active_files.merge(fc);
12✔
1573
    }
1574

1575
    return Ok(retval);
12✔
1576
}
14✔
1577

1578
static Result<std::string, lnav::console::user_message>
1579
com_xopen(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
×
1580
{
1581
    static const intern_string_t SRC = intern_string::lookup("path");
1582
    std::string retval;
×
1583

1584
    if (lnav_data.ld_flags & LNF_SECURE_MODE) {
×
1585
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
1586
    }
1587

1588
    if (args.size() < 2) {
×
1589
        return ec.make_error("expecting file name to open");
×
1590
    }
1591

1592
    if (ec.ec_dry_run) {
×
1593
        return Ok(retval);
×
1594
    }
1595

1596
    auto pat = trim(remaining_args(cmdline, args));
×
1597

1598
    shlex lexer(pat);
×
1599
    auto split_args_res = lexer.split(ec.create_resolver());
×
1600
    if (split_args_res.isErr()) {
×
1601
        auto split_err = split_args_res.unwrapErr();
×
1602
        auto um
1603
            = lnav::console::user_message::error("unable to parse file names")
×
1604
                  .with_reason(split_err.se_error.te_msg)
×
1605
                  .with_snippet(lnav::console::snippet::from(
×
1606
                      SRC, lexer.to_attr_line(split_err.se_error)))
×
1607
                  .move();
×
1608

1609
        return Err(um);
×
1610
    }
1611

1612
    auto split_args = split_args_res.unwrap()
×
1613
        | lnav::itertools::map([](const auto& elem) { return elem.se_value; });
×
1614
    for (const auto& fn : split_args) {
×
1615
        auto open_res = lnav::external_opener::for_href(fn);
×
1616
        if (open_res.isErr()) {
×
1617
            auto um = lnav::console::user_message::error(
×
1618
                          attr_line_t("Unable to open file: ")
×
1619
                              .append(lnav::roles::file(fn)))
×
1620
                          .with_reason(open_res.unwrapErr())
×
1621
                          .move();
×
1622
            return Err(um);
×
1623
        }
1624
    }
1625

1626
    return Ok(retval);
×
1627
}
1628

1629
static Result<std::string, lnav::console::user_message>
1630
com_close(exec_context& ec, std::string cmdline, std::vector<std::string>& args)
9✔
1631
{
1632
    static const intern_string_t SRC = intern_string::lookup("path");
25✔
1633
    std::string retval;
9✔
1634

1635
    auto* tc = *lnav_data.ld_view_stack.top();
9✔
1636
    std::vector<std::optional<std::filesystem::path>> actual_path_v;
9✔
1637
    std::vector<std::string> fn_v;
9✔
1638

1639
    if (args.size() > 1) {
9✔
1640
        auto lexer = shlex(cmdline);
1✔
1641

1642
        auto split_args_res = lexer.split(ec.create_resolver());
1✔
1643
        if (split_args_res.isErr()) {
1✔
1644
            auto split_err = split_args_res.unwrapErr();
×
1645
            auto um = lnav::console::user_message::error(
×
1646
                          "unable to parse file name")
1647
                          .with_reason(split_err.se_error.te_msg)
×
1648
                          .with_snippet(lnav::console::snippet::from(
×
1649
                              SRC, lexer.to_attr_line(split_err.se_error)))
×
1650
                          .move();
×
1651

1652
            return Err(um);
×
1653
        }
1654

1655
        auto args = split_args_res.unwrap()
2✔
1656
            | lnav::itertools::map(
1✔
1657
                        [](const auto& elem) { return elem.se_value; });
3✔
1658
        args.erase(args.begin());
1✔
1659

1660
        for (const auto& lf : lnav_data.ld_active_files.fc_files) {
2✔
1661
            if (lf.get() == nullptr) {
1✔
1662
                continue;
×
1663
            }
1664

1665
            auto find_iter
1666
                = find_if(args.begin(), args.end(), [&lf](const auto& arg) {
1✔
1667
                      return fnmatch(arg.c_str(), lf->get_filename().c_str(), 0)
1✔
1668
                          == 0;
1✔
1669
                  });
1670

1671
            if (find_iter == args.end()) {
1✔
1672
                continue;
×
1673
            }
1674

1675
            actual_path_v.push_back(lf->get_actual_path());
1✔
1676
            fn_v.emplace_back(lf->get_filename());
1✔
1677
            if (!ec.ec_dry_run) {
1✔
1678
                lnav_data.ld_active_files.request_close(lf);
1✔
1679
            }
1680
        }
1681
    } else if (tc == &lnav_data.ld_views[LNV_TEXT]) {
9✔
1682
        auto& tss = lnav_data.ld_text_source;
×
1683

1684
        if (tss.empty()) {
×
1685
            return ec.make_error("no text files are opened");
×
1686
        } else if (!ec.ec_dry_run) {
×
1687
            auto lf = tss.current_file();
×
1688
            actual_path_v.emplace_back(lf->get_actual_path());
×
1689
            fn_v.emplace_back(lf->get_filename());
×
1690
            lnav_data.ld_active_files.request_close(lf);
×
1691

1692
            if (tss.size() == 1) {
×
1693
                lnav_data.ld_view_stack.pop_back();
×
1694
            }
1695
        } else {
×
1696
            retval = fmt::format(FMT_STRING("closing -- {}"),
×
1697
                                 tss.current_file()->get_filename());
×
1698
        }
1699
    } else if (tc == &lnav_data.ld_views[LNV_LOG]) {
8✔
1700
        if (tc->get_inner_height() == 0) {
8✔
1701
            return ec.make_error("no log files loaded");
1✔
1702
        } else {
1703
            auto& lss = lnav_data.ld_log_source;
7✔
1704
            auto vl = tc->get_selection();
7✔
1705
            if (vl) {
7✔
1706
                auto cl = lss.at(vl.value());
7✔
1707
                auto lf = lss.find(cl);
7✔
1708

1709
                actual_path_v.push_back(lf->get_actual_path());
7✔
1710
                fn_v.emplace_back(lf->get_filename());
7✔
1711
                if (!ec.ec_dry_run) {
7✔
1712
                    lnav_data.ld_active_files.request_close(lf);
7✔
1713
                }
1714
            }
7✔
1715
        }
1716
    } else {
1717
        return ec.make_error("close must be run in the log or text file views");
×
1718
    }
1719
    if (!fn_v.empty()) {
8✔
1720
        if (ec.ec_dry_run) {
8✔
1721
            retval = "";
×
1722
        } else {
1723
            for (size_t lpc = 0; lpc < actual_path_v.size(); lpc++) {
16✔
1724
                const auto& fn = fn_v[lpc];
8✔
1725
                const auto& actual_path = actual_path_v[lpc];
8✔
1726

1727
                if (lnav::filesystem::is_url(fn)) {
8✔
1728
                    isc::to<curl_looper&, services::curl_streamer_t>().send(
×
1729
                        [fn](auto& clooper) { clooper.close_request(fn); });
×
1730
                }
1731
                if (actual_path) {
8✔
1732
                    lnav_data.ld_active_files.fc_file_names.erase(
8✔
1733
                        actual_path.value().string());
16✔
1734
                    lnav_data.ld_active_files.fc_closed_files.insert(
8✔
1735
                        actual_path->string());
16✔
1736
                }
1737
                lnav_data.ld_active_files.fc_closed_files.insert(fn);
8✔
1738
            }
1739
            retval = fmt::format(FMT_STRING("info: closed -- {}"),
32✔
1740
                                 fmt::join(fn_v, ", "));
24✔
1741
        }
1742
    }
1743

1744
    return Ok(retval);
8✔
1745
}
9✔
1746

1747
static Result<std::string, lnav::console::user_message>
1748
com_pipe_to(exec_context& ec,
7✔
1749
            std::string cmdline,
1750
            std::vector<std::string>& args)
1751
{
1752
    std::string retval;
7✔
1753

1754
    if (lnav_data.ld_flags & LNF_SECURE_MODE) {
7✔
1755
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
1756
    }
1757

1758
    if (args.size() < 2) {
7✔
1759
        return ec.make_error("expecting command to execute");
×
1760
    }
1761

1762
    if (ec.ec_dry_run) {
7✔
1763
        return Ok(std::string());
×
1764
    }
1765

1766
    auto* tc = *lnav_data.ld_view_stack.top();
7✔
1767
    auto bv = combined_user_marks(tc->get_bookmarks());
7✔
1768
    bool pipe_line_to = (args[0] == "pipe-line-to");
7✔
1769
    auto path_v = ec.ec_path_stack;
7✔
1770
    std::map<std::string, std::string> extra_env;
7✔
1771

1772
    if (pipe_line_to && tc == &lnav_data.ld_views[LNV_LOG]) {
7✔
1773
        log_data_helper ldh(lnav_data.ld_log_source);
6✔
1774
        char tmp_str[64];
1775

1776
        ldh.load_line(ec.ec_top_line, true);
6✔
1777
        ldh.parse_body();
6✔
1778
        auto format = ldh.ldh_file->get_format();
6✔
1779
        auto source_path = format->get_source_path();
6✔
1780
        path_v.insert(path_v.end(), source_path.begin(), source_path.end());
6✔
1781

1782
        extra_env["log_line"] = fmt::to_string((int) ec.ec_top_line);
18✔
1783
        sql_strftime(tmp_str, sizeof(tmp_str), ldh.ldh_line->get_timeval());
6✔
1784
        extra_env["log_time"] = tmp_str;
6✔
1785
        extra_env["log_path"] = ldh.ldh_file->get_filename();
18✔
1786
        extra_env["log_level"] = ldh.ldh_line->get_level_name().to_string();
18✔
1787
        if (ldh.ldh_line_values.lvv_opid_value) {
6✔
1788
            extra_env["log_opid"] = ldh.ldh_line_values.lvv_opid_value.value();
6✔
1789
        }
1790
        auto read_res = ldh.ldh_file->read_raw_message(ldh.ldh_line);
6✔
1791
        if (read_res.isOk()) {
6✔
1792
            auto raw_text = to_string(read_res.unwrap());
6✔
1793
            extra_env["log_raw_text"] = raw_text;
6✔
1794
        }
6✔
1795
        for (auto& ldh_line_value : ldh.ldh_line_values.lvv_values) {
48✔
1796
            extra_env[ldh_line_value.lv_meta.lvm_name.to_string()]
84✔
1797
                = ldh_line_value.to_string();
126✔
1798
        }
1799
        auto iter = ldh.ldh_parser->dp_pairs.begin();
6✔
1800
        for (size_t lpc = 0; lpc < ldh.ldh_parser->dp_pairs.size();
8✔
1801
             lpc++, ++iter)
2✔
1802
        {
1803
            std::string colname = ldh.ldh_parser->get_element_string(
1804
                iter->e_sub_elements->front());
2✔
1805
            colname = ldh.ldh_namer->add_column(colname).to_string();
2✔
1806
            std::string val = ldh.ldh_parser->get_element_string(
1807
                iter->e_sub_elements->back());
2✔
1808
            extra_env[colname] = val;
2✔
1809
        }
2✔
1810
    }
6✔
1811

1812
    std::string cmd = trim(remaining_args(cmdline, args));
7✔
1813
    auto for_child_res = auto_pipe::for_child_fds(STDIN_FILENO, STDOUT_FILENO);
7✔
1814

1815
    if (for_child_res.isErr()) {
7✔
1816
        return ec.make_error(FMT_STRING("unable to open pipe to child: {}"),
×
1817
                             for_child_res.unwrapErr());
×
1818
    }
1819

1820
    auto child_fds = for_child_res.unwrap();
7✔
1821

1822
    pid_t child_pid = fork();
7✔
1823

1824
    for (auto& child_fd : child_fds) {
21✔
1825
        child_fd.after_fork(child_pid);
14✔
1826
    }
1827

1828
    switch (child_pid) {
7✔
1829
        case -1:
×
1830
            return ec.make_error("unable to fork child process -- {}",
1831
                                 strerror(errno));
×
1832

1833
        case 0: {
×
1834
            const char* exec_args[] = {
×
1835
                "sh",
1836
                "-c",
1837
                cmd.c_str(),
×
1838
                nullptr,
1839
            };
1840
            std::string path;
×
1841

1842
            dup2(STDOUT_FILENO, STDERR_FILENO);
×
1843
            path_v.emplace_back(lnav::paths::dotlnav() / "formats/default");
×
1844

1845
            setenv("PATH", lnav::filesystem::build_path(path_v).c_str(), 1);
×
1846
            for (const auto& pair : extra_env) {
×
1847
                setenv(pair.first.c_str(), pair.second.c_str(), 1);
×
1848
            }
1849
            execvp(exec_args[0], (char* const*) exec_args);
×
1850
            _exit(1);
×
1851
            break;
1852
        }
×
1853

1854
        default: {
7✔
1855
            bookmark_vector<vis_line_t>::iterator iter;
7✔
1856
            std::string line;
7✔
1857

1858
            log_info("spawned pipe child %d -- %s", child_pid, cmd.c_str());
7✔
1859
            lnav_data.ld_children.push_back(child_pid);
7✔
1860

1861
            std::future<std::string> reader;
7✔
1862

1863
            if (child_fds[1].read_end() != -1) {
7✔
1864
                reader
1865
                    = ec.ec_pipe_callback(ec, cmdline, child_fds[1].read_end());
7✔
1866
            }
1867

1868
            if (pipe_line_to) {
7✔
1869
                if (tc->get_inner_height() == 0) {
6✔
1870
                    // Nothing to do
1871
                } else if (tc == &lnav_data.ld_views[LNV_LOG]) {
6✔
1872
                    auto& lss = lnav_data.ld_log_source;
6✔
1873
                    auto cl = lss.at(tc->get_top());
6✔
1874
                    auto lf = lss.find(cl);
6✔
1875
                    shared_buffer_ref sbr;
6✔
1876
                    lf->read_full_message(lf->message_start(lf->begin() + cl),
6✔
1877
                                          sbr);
1878
                    if (write(child_fds[0].write_end(),
12✔
1879
                              sbr.get_data(),
6✔
1880
                              sbr.length())
1881
                        == -1)
6✔
1882
                    {
1883
                        return ec.make_error("Unable to write to pipe -- {}",
1884
                                             lnav::from_errno());
×
1885
                    }
1886
                    log_perror(write(child_fds[0].write_end(), "\n", 1));
6✔
1887
                } else {
6✔
1888
                    tc->grep_value_for_line(tc->get_top(), line);
×
1889
                    if (write(
×
1890
                            child_fds[0].write_end(), line.c_str(), line.size())
×
1891
                        == -1)
×
1892
                    {
1893
                        return ec.make_error("Unable to write to pipe -- {}",
1894
                                             lnav::from_errno());
×
1895
                    }
1896
                    log_perror(write(child_fds[0].write_end(), "\n", 1));
×
1897
                }
1898
            } else {
1899
                for (iter = bv.bv_tree.begin(); iter != bv.bv_tree.end();
4✔
1900
                     ++iter)
3✔
1901
                {
1902
                    tc->grep_value_for_line(*iter, line);
3✔
1903
                    if (write(
6✔
1904
                            child_fds[0].write_end(), line.c_str(), line.size())
3✔
1905
                        == -1)
3✔
1906
                    {
1907
                        return ec.make_error("Unable to write to pipe -- {}",
1908
                                             lnav::from_errno());
×
1909
                    }
1910
                    log_perror(write(child_fds[0].write_end(), "\n", 1));
3✔
1911
                }
1912
            }
1913

1914
            child_fds[0].write_end().reset();
7✔
1915

1916
            if (reader.valid()) {
7✔
1917
                retval = reader.get();
7✔
1918
            } else {
1919
                retval = "";
×
1920
            }
1921
            break;
7✔
1922
        }
14✔
1923
    }
1924

1925
    return Ok(retval);
7✔
1926
}
7✔
1927

1928
static Result<std::string, lnav::console::user_message>
1929
com_redirect_to(exec_context& ec,
4✔
1930
                std::string cmdline,
1931
                std::vector<std::string>& args)
1932
{
1933
    static const intern_string_t SRC = intern_string::lookup("path");
6✔
1934

1935
    if (args.size() == 1) {
4✔
1936
        if (ec.ec_dry_run) {
2✔
1937
            return Ok(std::string("info: redirect will be cleared"));
×
1938
        }
1939

1940
        ec.clear_output();
2✔
1941
        return Ok(std::string("info: cleared redirect"));
4✔
1942
    }
1943

1944
    std::string fn = trim(remaining_args(cmdline, args));
2✔
1945
    shlex lexer(fn);
2✔
1946

1947
    auto split_args_res = lexer.split(ec.create_resolver());
2✔
1948
    if (split_args_res.isErr()) {
2✔
1949
        auto split_err = split_args_res.unwrapErr();
×
1950
        auto um
1951
            = lnav::console::user_message::error("unable to parse file name")
×
1952
                  .with_reason(split_err.se_error.te_msg)
×
1953
                  .with_snippet(lnav::console::snippet::from(
×
1954
                      SRC, lexer.to_attr_line(split_err.se_error)))
×
1955
                  .move();
×
1956

1957
        return Err(um);
×
1958
    }
1959
    auto split_args = split_args_res.unwrap()
4✔
1960
        | lnav::itertools::map([](const auto& elem) { return elem.se_value; });
6✔
1961
    if (split_args.size() > 1) {
2✔
1962
        return ec.make_error("more than one file name was matched");
×
1963
    }
1964

1965
    if (ec.ec_dry_run) {
2✔
1966
        return Ok("info: output will be redirected to -- " + split_args[0]);
×
1967
    }
1968

1969
    if (split_args[0] == "-") {
2✔
1970
        ec.clear_output();
×
1971
    } else if (split_args[0] == "/dev/clipboard") {
2✔
1972
        auto out = sysclip::open(sysclip::type_t::GENERAL);
×
1973
        if (out.isErr()) {
×
1974
            alerter::singleton().chime("cannot open clipboard");
×
1975
            return ec.make_error("Unable to copy to clipboard: {}",
1976
                                 out.unwrapErr());
×
1977
        }
1978

1979
        auto holder = out.unwrap();
×
1980
        ec.set_output(split_args[0],
×
1981
                      holder.release(),
1982
                      holder.get_free_func<int (*)(FILE*)>());
1983
    } else if (lnav_data.ld_flags & LNF_SECURE_MODE) {
2✔
1984
        return ec.make_error("{} -- unavailable in secure mode", args[0]);
×
1985
    } else {
1986
        FILE* file = fopen(split_args[0].c_str(), "w");
2✔
1987
        if (file == nullptr) {
2✔
1988
            return ec.make_error("unable to open file -- {}", split_args[0]);
×
1989
        }
1990

1991
        ec.set_output(split_args[0], file, fclose);
2✔
1992
    }
1993

1994
    return Ok("info: redirecting output to file -- " + split_args[0]);
2✔
1995
}
2✔
1996

1997
static readline_context::command_t IO_COMMANDS[] = {
1998
    {
1999
        "append-to",
2000
        com_save_to,
2001

2002
        help_text(":append-to")
2003
            .with_summary("Append marked lines in the current view to "
2004
                          "the given file")
2005
            .with_parameter(
2006
                help_text("path", "The path to the file to append to")
2007
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2008
            .with_tags({"io"})
2009
            .with_example({"To append marked lines to the file "
2010
                           "/tmp/interesting-lines.txt",
2011
                           "/tmp/interesting-lines.txt"}),
2012
    },
2013
    {
2014
        "write-to",
2015
        com_save_to,
2016

2017
        help_text(":write-to")
2018
            .with_summary("Overwrite the given file with any marked "
2019
                          "lines in the "
2020
                          "current view")
2021
            .with_parameter(
2022
                help_text("--anonymize", "Anonymize the lines").flag())
2023
            .with_parameter(
2024
                help_text("path", "The path to the file to write")
2025
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2026
            .with_tags({"io", "scripting"})
2027
            .with_example({"To write marked lines to the file "
2028
                           "/tmp/interesting-lines.txt",
2029
                           "/tmp/interesting-lines.txt"}),
2030
    },
2031
    {
2032
        "write-csv-to",
2033
        com_save_to,
2034

2035
        help_text(":write-csv-to")
2036
            .with_summary("Write SQL results to the given file in CSV format")
2037
            .with_parameter(
2038
                help_text("--anonymize", "Anonymize the row contents").flag())
2039
            .with_parameter(
2040
                help_text("path", "The path to the file to write")
2041
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2042
            .with_tags({"io", "scripting", "sql"})
2043
            .with_example({"To write SQL results as CSV to /tmp/table.csv",
2044
                           "/tmp/table.csv"}),
2045
    },
2046
    {
2047
        "write-json-to",
2048
        com_save_to,
2049

2050
        help_text(":write-json-to")
2051
            .with_summary("Write SQL results to the given file in JSON format")
2052
            .with_parameter(
2053
                help_text("--anonymize", "Anonymize the JSON values").flag())
2054
            .with_parameter(
2055
                help_text("path", "The path to the file to write")
2056
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2057
            .with_tags({"io", "scripting", "sql"})
2058
            .with_example({"To write SQL results as JSON to /tmp/table.json",
2059
                           "/tmp/table.json"}),
2060
    },
2061
    {
2062
        "write-json-cols-to",
2063
        com_save_to,
2064

2065
        help_text(":write-json-cols-to")
2066
            .with_summary("Write SQL results to the given file in a "
2067
                          "column-oriented JSON format.  In addition, columns "
2068
                          "that contain JSON values will be flattened to their "
2069
                          "own columns.  For example, a column containing "
2070
                          "values shaped like `{\"a\": 1, \"b\": 2}` will be "
2071
                          "split into two separate columns named 'a' and 'b'. "
2072
                          "This format can be useful for feeding into charting "
2073
                          "libraries.")
2074
            .with_parameter(
2075
                help_text("--anonymize", "Anonymize the JSON values").flag())
2076
            .with_parameter(
2077
                help_text("path", "The path to the file to write")
2078
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2079
            .with_tags({"io", "scripting", "sql"})
2080
            .with_example({"To write SQL results as JSON to /tmp/table.json",
2081
                           "/tmp/table.json"}),
2082
    },
2083
    {
2084
        "write-jsonlines-to",
2085
        com_save_to,
2086

2087
        help_text(":write-jsonlines-to")
2088
            .with_summary("Write SQL results to the given file in "
2089
                          "JSON Lines format")
2090
            .with_parameter(
2091
                help_text("--anonymize", "Anonymize the JSON values").flag())
2092
            .with_parameter(
2093
                help_text("path", "The path to the file to write")
2094
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2095
            .with_tags({"io", "scripting", "sql"})
2096
            .with_example({"To write SQL results as JSON Lines to "
2097
                           "/tmp/table.json",
2098
                           "/tmp/table.json"}),
2099
    },
2100
    {
2101
        "write-table-to",
2102
        com_save_to,
2103

2104
        help_text(":write-table-to")
2105
            .with_summary("Write SQL results to the given file in a "
2106
                          "tabular format")
2107
            .with_parameter(
2108
                help_text("--anonymize", "Anonymize the table contents").flag())
2109
            .with_parameter(
2110
                help_text("path", "The path to the file to write")
2111
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2112
            .with_tags({"io", "scripting", "sql"})
2113
            .with_example({"To write SQL results as text to /tmp/table.txt",
2114
                           "/tmp/table.txt"}),
2115
    },
2116
    {
2117
        "write-raw-to",
2118
        com_save_to,
2119

2120
        help_text(":write-raw-to")
2121
            .with_summary(
2122
                "In the log view, write the original log file content "
2123
                "of the marked messages to the file.  In the DB view, "
2124
                "the contents of the cells are written to the output "
2125
                "file.")
2126
            .with_parameter(
2127
                help_text("--view", "The view to use as the source of data")
2128
                    .optional()
2129
                    .with_enum_values({"log"_frag, "db"_frag}))
2130
            .with_parameter(
2131
                help_text("--anonymize", "Anonymize the lines").flag())
2132
            .with_parameter(
2133
                help_text("path", "The path to the file to write")
2134
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2135
            .with_tags({"io", "scripting", "sql"})
2136
            .with_example({"To write the marked lines in the log view "
2137
                           "to /tmp/table.txt",
2138
                           "/tmp/table.txt"}),
2139
    },
2140
    {
2141
        "write-view-to",
2142
        com_save_to,
2143

2144
        help_text(":write-view-to")
2145
            .with_summary("Write the text in the top view to the given file "
2146
                          "without any formatting")
2147
            .with_parameter(
2148
                help_text("--anonymize", "Anonymize the lines").flag())
2149
            .with_parameter(
2150
                help_text("path", "The path to the file to write")
2151
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2152
            .with_tags({"io", "scripting", "sql"})
2153
            .with_example(
2154
                {"To write the top view to /tmp/table.txt", "/tmp/table.txt"}),
2155
    },
2156
    {
2157
        "write-screen-to",
2158
        com_save_to,
2159

2160
        help_text(":write-screen-to")
2161
            .with_summary(
2162
                "Write the displayed text or SQL results to the given "
2163
                "file without any formatting")
2164
            .with_parameter(
2165
                help_text("--anonymize", "Anonymize the lines").flag())
2166
            .with_parameter(
2167
                help_text("path", "The path to the file to write")
2168
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2169
            .with_tags({"io", "scripting", "sql"})
2170
            .with_example({"To write only the displayed text to /tmp/table.txt",
2171
                           "/tmp/table.txt"}),
2172
    },
2173
    {
2174
        "open",
2175
        com_open,
2176

2177
        help_text(":open")
2178
            .with_summary("Open the given file(s) in lnav.  Opening files on "
2179
                          "machines "
2180
                          "accessible via SSH can be done using the syntax: "
2181
                          "[user@]host:/path/to/logs")
2182
            .with_parameter(help_text{"--since", "The low cutoff time"}
2183
                                .with_format(help_parameter_format_t::HPF_TEXT)
2184
                                .optional())
2185
            .with_parameter(help_text{"--until", "The high cutoff time"}
2186
                                .with_format(help_parameter_format_t::HPF_TEXT)
2187
                                .optional())
2188
            .with_parameter(
2189
                help_text{"path", "The path to the file to open"}
2190
                    .with_format(help_parameter_format_t::HPF_FILENAME)
2191
                    .one_or_more())
2192
            .with_example({"To open the file '/path/to/file'", "/path/to/file"})
2193
            .with_example({"To open the remote file '/var/log/syslog.log'",
2194
                           "dean@host1.example.com:/var/log/syslog.log"})
2195
            .with_tags({"io"}),
2196
    },
2197
    {
2198
        "xopen",
2199
        com_xopen,
2200

2201
        help_text(":xopen")
2202
            .with_summary("Use an external command to open the given file(s)")
2203
            .with_parameter(
2204
                help_text{"path", "The path to the file to open"}.one_or_more())
2205
            .with_example({"To open the file '/path/to/file'", "/path/to/file"})
2206
            .with_tags({"io"}),
2207
    },
2208
    {
2209
        "close",
2210
        com_close,
2211

2212
        help_text(":close")
2213
            .with_summary(
2214
                "Close the given file(s) or the focused file in the view")
2215
            .with_parameter(
2216
                help_text{"path",
2217
                          "A path or glob pattern that "
2218
                          "specifies the files to close"}
2219
                    .zero_or_more()
2220
                    .with_format(help_parameter_format_t::HPF_LOADED_FILE))
2221
            .with_opposites({"open"})
2222
            .with_tags({"io"}),
2223
    },
2224
    {
2225
        "pipe-to",
2226
        com_pipe_to,
2227

2228
        help_text(":pipe-to")
2229
            .with_summary("Pipe the marked lines to the given shell command")
2230
            .with_parameter(
2231
                help_text("shell-cmd", "The shell command-line to execute"))
2232
            .with_tags({"io"})
2233
            .with_example({"To write marked lines to 'sed' for processing",
2234
                           "sed -e s/foo/bar/g"}),
2235
    },
2236
    {
2237
        "pipe-line-to",
2238
        com_pipe_to,
2239

2240
        help_text(":pipe-line-to")
2241
            .with_summary("Pipe the focused line to the given shell "
2242
                          "command.  Any fields "
2243
                          "defined by the format will be set as "
2244
                          "environment variables.")
2245
            .with_parameter(
2246
                help_text("shell-cmd", "The shell command-line to execute"))
2247
            .with_tags({"io"})
2248
            .with_example({"To write the focused line to 'sed' for processing",
2249
                           "sed -e 's/foo/bar/g'"}),
2250
    },
2251
    {
2252
        "redirect-to",
2253
        com_redirect_to,
2254

2255
        help_text(":redirect-to")
2256
            .with_summary("Redirect the output of commands that write to "
2257
                          "stdout to the given file")
2258
            .with_parameter(
2259
                help_text("path",
2260
                          "The path to the file to write."
2261
                          "  If not specified, the current redirect "
2262
                          "will be cleared")
2263
                    .optional()
2264
                    .with_format(help_parameter_format_t::HPF_LOCAL_FILENAME))
2265
            .with_tags({"io", "scripting"})
2266
            .with_example({"To write the output of lnav commands to the file "
2267
                           "/tmp/script-output.txt",
2268
                           "/tmp/script-output.txt"}),
2269
    },
2270
};
2271

2272
void
2273
init_lnav_io_commands(readline_context::command_map_t& cmd_map)
620✔
2274
{
2275
    static auto WRITE_COLS_FRAG = "write-cols-to"_frag;
2276
    static auto WRITE_TABLE_FRAG = "write-table-to"_frag;
2277

2278
    for (auto& cmd : IO_COMMANDS) {
10,540✔
2279
        cmd.c_help.index_tags();
9,920✔
2280
        cmd_map[cmd.c_name] = &cmd;
9,920✔
2281
    }
2282
    cmd_map[WRITE_COLS_FRAG] = cmd_map[WRITE_TABLE_FRAG];
620✔
2283
}
620✔
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