dte test coverage


Directory: ./
File: src/exec.c
Date: 2025-06-04 06:50:24
Exec Total Coverage
Lines: 109 266 41.0%
Functions: 9 13 69.2%
Branches: 44 140 31.4%

Line Branch Exec Source
1 #include <stdint.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include <unistd.h>
5 #include "exec.h"
6 #include "block-iter.h"
7 #include "buffer.h"
8 #include "change.h"
9 #include "command/error.h"
10 #include "command/macro.h"
11 #include "commands.h"
12 #include "ctags.h"
13 #include "move.h"
14 #include "msg.h"
15 #include "selection.h"
16 #include "tag.h"
17 #include "terminal/mode.h"
18 #include "util/bsearch.h"
19 #include "util/debug.h"
20 #include "util/numtostr.h"
21 #include "util/ptr-array.h"
22 #include "util/str-util.h"
23 #include "util/string-view.h"
24 #include "util/string.h"
25 #include "util/strtonum.h"
26 #include "util/xsnprintf.h"
27 #include "view.h"
28 #include "window.h"
29
30 enum {
31 IN = 1 << 0,
32 OUT = 1 << 1,
33 ERR = 1 << 2,
34 ALL = IN | OUT | ERR,
35 };
36
37 static const struct {
38 char name[11];
39 uint8_t flags;
40 } exec_map[] = {
41 [EXEC_BUFFER] = {"buffer", IN | OUT},
42 [EXEC_COMMAND] = {"command", IN},
43 [EXEC_ECHO] = {"echo", OUT},
44 [EXEC_ERRMSG] = {"errmsg", ERR},
45 [EXEC_EVAL] = {"eval", OUT},
46 [EXEC_LINE] = {"line", IN},
47 [EXEC_MSG] = {"msg", IN | OUT},
48 [EXEC_MSG_A] = {"msgA", IN | OUT},
49 [EXEC_MSG_B] = {"msgB", IN | OUT},
50 [EXEC_MSG_C] = {"msgC", IN | OUT},
51 [EXEC_NULL] = {"null", ALL},
52 [EXEC_OPEN] = {"open", IN | OUT},
53 [EXEC_OPEN_REL] = {"open-rel", IN},
54 [EXEC_SEARCH] = {"search", IN},
55 [EXEC_TAG] = {"tag", OUT},
56 [EXEC_TAG_A] = {"tagA", OUT},
57 [EXEC_TAG_B] = {"tagB", OUT},
58 [EXEC_TAG_C] = {"tagC", OUT},
59 [EXEC_TTY] = {"tty", ALL},
60 [EXEC_WORD] = {"word", IN},
61 };
62
63 22 UNITTEST {
64 22 CHECK_BSEARCH_ARRAY(exec_map, name);
65 22 }
66
67 18 ExecAction lookup_exec_action(const char *name, int fd)
68 {
69 18 BUG_ON(fd < 0 || fd > 2);
70 18 ssize_t i = BSEARCH_IDX(name, exec_map, vstrcmp);
71
3/4
✓ Branch 0 (5→6) taken 15 times.
✓ Branch 1 (5→8) taken 3 times.
✓ Branch 2 (6→7) taken 15 times.
✗ Branch 3 (6→8) not taken.
18 return (i >= 0 && (exec_map[i].flags & 1u << fd)) ? i : EXEC_INVALID;
72 }
73
74 1 void collect_exec_actions(PointerArray *a, const char *prefix, int fd)
75 {
76
1/2
✓ Branch 0 (2→3) taken 1 times.
✗ Branch 1 (2→10) not taken.
1 if (unlikely(fd < 0 || fd > 2)) {
77 return;
78 }
79
80 1 size_t prefix_len = strlen(prefix);
81 1 unsigned int flag = 1u << fd;
82
2/2
✓ Branch 0 (9→4) taken 20 times.
✓ Branch 1 (9→10) taken 1 times.
21 for (size_t i = 0; i < ARRAYLEN(exec_map); i++) {
83 20 const char *action = exec_map[i].name;
84
3/4
✓ Branch 0 (4→5) taken 14 times.
✓ Branch 1 (4→8) taken 6 times.
✓ Branch 2 (5→6) taken 14 times.
✗ Branch 3 (5→8) not taken.
20 if ((exec_map[i].flags & flag) && str_has_strn_prefix(action, prefix, prefix_len)) {
85 14 ptr_array_append(a, xstrdup(action));
86 }
87 }
88 }
89
90 static void open_files_from_string(EditorState *e, const String *str)
91 {
92 PointerArray filenames = PTR_ARRAY_INIT;
93 for (size_t pos = 0, size = str->len; pos < size; ) {
94 char *filename = buf_next_line(str->buffer, &pos, size);
95 if (filename[0] != '\0') {
96 ptr_array_append(&filenames, filename);
97 }
98 }
99
100 if (filenames.count == 0) {
101 return;
102 }
103
104 ptr_array_append(&filenames, NULL);
105 window_open_files(e->window, (char**)filenames.ptrs, NULL);
106
107 // TODO: re-enable this when the todo in allow_macro_recording() is done
108 // macro_command_hook(&e->macro, "open", (char**)filenames.ptrs);
109
110 ptr_array_free_array(&filenames);
111 }
112
113 static void parse_and_activate_message(EditorState *e, const String *str, ExecAction action)
114 {
115 if (unlikely(str->len == 0)) {
116 error_msg(&e->err, "child produced no output");
117 return;
118 }
119
120 size_t msgs_idx = (action == EXEC_MSG) ? 0 : action - EXEC_MSG_A;
121 BUG_ON(msgs_idx >= ARRAYLEN(e->messages));
122 MessageArray *msgs = &e->messages[msgs_idx];
123 size_t count = msgs->array.count;
124 size_t x;
125
126 if (!count || !buf_parse_size(str->buffer, str->len, &x) || !x) {
127 return;
128 }
129
130 msgs->pos = MIN(x - 1, count - 1);
131 activate_current_message(msgs, e->window);
132 }
133
134 static void parse_and_activate_tags(EditorState *e, const String *str, ExecAction action)
135 {
136 ErrorBuffer *ebuf = &e->err;
137 if (unlikely(str->len == 0)) {
138 error_msg(ebuf, "child produced no output");
139 return;
140 }
141
142 char cwd[8192];
143 if (unlikely(!getcwd(cwd, sizeof cwd))) {
144 error_msg_errno(ebuf, "getcwd() failed");
145 return;
146 }
147
148 const StringView dir = strview_from_cstring(cwd);
149 const char *buffer_filename = e->buffer->abs_filename;
150 TagFile *tf = &e->tagfile;
151 enum {NOT_LOADED, LOADED, FAILED} tf_status = NOT_LOADED;
152
153 size_t msgs_idx = (action == EXEC_TAG) ? e->options.msg_tag : action - EXEC_TAG_A;
154 BUG_ON(msgs_idx >= ARRAYLEN(e->messages));
155 MessageArray *msgs = &e->messages[msgs_idx];
156 clear_messages(msgs);
157
158 for (size_t pos = 0, len = str->len; pos < len; ) {
159 Tag tag;
160 StringView line = buf_slice_next_line(str->buffer, &pos, len);
161 if (line.length == 0) {
162 continue;
163 }
164
165 bool parsed = parse_ctags_line(&tag, line.data, line.length);
166 if (parsed) {
167 // `line` is a valid tags(5) file entry; handle it directly
168 add_message_for_tag(msgs, &tag, &dir);
169 continue;
170 }
171
172 // Treat `line` as a simple tag name (look it up in the tags(5) file)
173 switch (tf_status) {
174 case NOT_LOADED:
175 tf_status = load_tag_file(tf, ebuf) ? LOADED : FAILED;
176 if (tf_status == FAILED) {
177 continue;
178 }
179 // Fallthrough
180 case LOADED:
181 tag_lookup(tf, msgs, ebuf, &line, buffer_filename);
182 continue;
183 case FAILED:
184 continue;
185 }
186 }
187
188 activate_current_message_save(msgs, &e->bookmarks, e->view);
189 }
190
191 static void insert_to_selection (
192 View *view,
193 const String *output,
194 const SelectionInfo *info
195 ) {
196 size_t del_count = info->eo - info->so;
197 buffer_replace_bytes(view, del_count, output->buffer, output->len);
198
199 if (output->len == 0) {
200 // If the selection was replaced with 0 bytes then there's nothing
201 // new to select, so just unselect instead
202 unselect(view);
203 return;
204 }
205
206 // Keep the selection and adjust the size to the newly inserted text
207 size_t so = info->so;
208 size_t eo = so + (output->len - 1);
209 block_iter_goto_offset(&view->cursor, info->swapped ? so : eo);
210 view->sel_so = info->swapped ? eo : so;
211 view->sel_eo = SEL_EO_RECALC;
212 }
213
214 5 static const char **lines_and_columns_env(const Window *window)
215 {
216 5 static char lines[DECIMAL_STR_MAX(window->edit_h)];
217 5 static char columns[DECIMAL_STR_MAX(window->edit_w)];
218 5 static const char *vars[] = {
219 "LINES", lines,
220 "COLUMNS", columns,
221 NULL,
222 };
223
224 5 buf_uint_to_str(window->edit_h, lines);
225 5 buf_uint_to_str(window->edit_w, columns);
226 5 return vars;
227 }
228
229 4 static void show_spawn_error_msg(ErrorBuffer *ebuf, const String *errstr, int err)
230 {
231
2/2
✓ Branch 0 (2→3) taken 1 times.
✓ Branch 1 (2→4) taken 3 times.
4 if (err <= 0) {
232 // spawn() returned an error code instead of an exit code, which
233 // indicates that something failed before (or during) the child
234 // process exec(3p), or that an error occurred in wait_child().
235 // In this case, error_msg() has already been called.
236 1 return;
237 }
238
239 3 char msg[512];
240 3 msg[0] = '\0';
241
2/2
✓ Branch 0 (4→5) taken 1 times.
✓ Branch 1 (4→10) taken 2 times.
3 if (errstr->len) {
242 1 size_t pos = 0;
243 1 StringView line = buf_slice_next_line(errstr->buffer, &pos, errstr->len);
244 1 BUG_ON(pos == 0);
245 1 size_t len = MIN(line.length, sizeof(msg) - 8);
246 1 xsnprintf(msg, sizeof(msg), ": \"%.*s\"", (int)len, line.data);
247 }
248
249
2/2
✓ Branch 0 (10→11) taken 1 times.
✓ Branch 1 (10→15) taken 2 times.
3 if (err >= 256) {
250 1 int sig = err >> 8;
251 1 const char *str = strsignal(sig);
252
1/2
✗ Branch 0 (12→13) not taken.
✓ Branch 1 (12→14) taken 1 times.
1 error_msg(ebuf, "Child received signal %d (%s)%s", sig, str ? str : "??", msg);
253 2 } else if (err) {
254 2 error_msg(ebuf, "Child returned %d%s", err, msg);
255 }
256 }
257
258 45 static SpawnAction spawn_action_from_exec_action(ExecAction action)
259 {
260 45 BUG_ON(action == EXEC_INVALID);
261
1/2
✓ Branch 0 (4→5) taken 45 times.
✗ Branch 1 (4→7) not taken.
45 if (action == EXEC_NULL) {
262 return SPAWN_NULL;
263
2/2
✓ Branch 0 (5→6) taken 15 times.
✓ Branch 1 (5→7) taken 30 times.
45 } else if (action == EXEC_TTY) {
264 return SPAWN_TTY;
265 } else {
266 15 return SPAWN_PIPE;
267 }
268 }
269
270 // NOLINTNEXTLINE(readability-function-size)
271 15 ssize_t handle_exec (
272 EditorState *e,
273 const char **argv,
274 ExecAction actions[3],
275 ExecFlags exec_flags
276 ) {
277 15 View *view = e->view;
278 15 const BlockIter saved_cursor = view->cursor;
279 15 const ssize_t saved_sel_so = view->sel_so;
280 15 const ssize_t saved_sel_eo = view->sel_eo;
281 15 char *alloc = NULL;
282 15 bool output_to_buffer = (actions[STDOUT_FILENO] == EXEC_BUFFER);
283 15 bool input_from_buffer = false;
284 15 bool replace_unselected_input = false;
285 15 bool quiet = (exec_flags & EXECFLAG_QUIET);
286
287 75 SpawnContext ctx = {
288 .argv = argv,
289 .outputs = {STRING_INIT, STRING_INIT},
290 .quiet = quiet,
291 15 .ebuf = &e->err,
292
2/2
✓ Branch 0 (2→3) taken 5 times.
✓ Branch 1 (2→4) taken 10 times.
15 .env = output_to_buffer ? lines_and_columns_env(e->window) : NULL,
293 .actions = {
294 15 spawn_action_from_exec_action(actions[0]),
295 15 spawn_action_from_exec_action(actions[1]),
296 15 spawn_action_from_exec_action(actions[2]),
297 },
298 };
299
300 15 ExecAction in_action = actions[STDIN_FILENO];
301
2/10
✗ Branch 0 (7→8) not taken.
✓ Branch 1 (7→12) taken 3 times.
✗ Branch 2 (7→17) not taken.
✗ Branch 3 (7→25) not taken.
✗ Branch 4 (7→30) not taken.
✗ Branch 5 (7→32) not taken.
✗ Branch 6 (7→34) not taken.
✗ Branch 7 (7→36) not taken.
✗ Branch 8 (7→38) not taken.
✓ Branch 9 (7→39) taken 12 times.
15 switch (in_action) {
302 case EXEC_LINE:
303 input_from_buffer = true;
304 if (!view->selection) {
305 move_bol(view, BOL_SIMPLE);
306 StringView line = block_iter_get_line(&view->cursor);
307 ctx.input.length = line.length;
308 replace_unselected_input = true;
309 }
310 break;
311 3 case EXEC_BUFFER:
312 3 input_from_buffer = true;
313
1/2
✓ Branch 0 (12→13) taken 3 times.
✗ Branch 1 (12→39) not taken.
3 if (!view->selection) {
314 3 const Block *blk;
315
2/2
✓ Branch 0 (15→14) taken 3 times.
✓ Branch 1 (15→16) taken 3 times.
6 block_for_each(blk, &view->buffer->blocks) {
316 3 ctx.input.length += blk->size;
317 }
318 3 move_bof(view);
319 3 replace_unselected_input = true;
320 }
321 break;
322 case EXEC_WORD:
323 input_from_buffer = true;
324 if (!view->selection) {
325 StringView line;
326 size_t offset = get_current_line_and_offset(&view->cursor, &line);
327 size_t start = offset;
328 size_t end = get_bounds_for_word_under_cursor(line, &start);
329 if (end == 0) {
330 break;
331 }
332
333 // If `start` is less than `offset` here, the subtraction wraps
334 // but nevertheless works as intended
335 view->cursor.offset += start - offset; // == view->cursor.offset -= (offset - start)
336
337 ctx.input.length = end - start;
338 BUG_ON(view->cursor.offset >= view->cursor.blk->size);
339 replace_unselected_input = true;
340 }
341 break;
342 case EXEC_MSG:
343 case EXEC_MSG_A:
344 case EXEC_MSG_B:
345 case EXEC_MSG_C: {
346 size_t msgs_idx = (in_action == EXEC_MSG) ? 0 : in_action - EXEC_MSG_A;
347 BUG_ON(msgs_idx >= ARRAYLEN(e->messages));
348 String messages = dump_messages(&e->messages[msgs_idx]);
349 ctx.input = strview_from_string(&messages);
350 alloc = messages.buffer;
351 break;
352 }
353 case EXEC_COMMAND: {
354 String hist = history_dump(&e->command_history);
355 ctx.input = strview_from_string(&hist);
356 alloc = hist.buffer;
357 break;
358 }
359 case EXEC_SEARCH: {
360 String hist = history_dump(&e->search_history);
361 ctx.input = strview_from_string(&hist);
362 alloc = hist.buffer;
363 break;
364 }
365 case EXEC_OPEN: {
366 String hist = file_history_dump(&e->file_history);
367 ctx.input = strview_from_string(&hist);
368 alloc = hist.buffer;
369 break;
370 }
371 case EXEC_OPEN_REL: {
372 String hist = file_history_dump_relative(&e->file_history);
373 ctx.input = strview_from_string(&hist);
374 alloc = hist.buffer;
375 break;
376 }
377 case EXEC_NULL:
378 case EXEC_TTY:
379 break;
380 // These can't be used as input actions and should be prevented by
381 // the validity checks in cmd_exec():
382 case EXEC_TAG:
383 case EXEC_TAG_A:
384 case EXEC_TAG_B:
385 case EXEC_TAG_C:
386 case EXEC_ECHO:
387 case EXEC_EVAL:
388 case EXEC_ERRMSG:
389 case EXEC_INVALID:
390 default:
391 BUG("unhandled action");
392 return -1;
393 }
394
395 // This could be left uninitialized, but doing so makes some old compilers
396 // produce false-positive "-Wmaybe-uninitialized" warnings
397 15 SelectionInfo info = {.si = view->cursor};
398
399
1/4
✗ Branch 0 (39→40) not taken.
✓ Branch 1 (39→44) taken 15 times.
✗ Branch 2 (40→41) not taken.
✗ Branch 3 (40→47) not taken.
15 if (view->selection && (input_from_buffer || output_to_buffer)) {
400 info = init_selection(view);
401 view->cursor = info.si;
402 if (input_from_buffer) {
403 ctx.input.length = info.eo - info.so;
404 }
405 }
406
407
2/2
✓ Branch 0 (44→45) taken 3 times.
✓ Branch 1 (44→47) taken 12 times.
15 if (input_from_buffer) {
408 3 alloc = block_iter_get_bytes(&view->cursor, ctx.input.length);
409 3 ctx.input.data = alloc;
410 }
411
412 15 yield_terminal(e, quiet);
413 15 int err = spawn(&ctx);
414
3/4
✓ Branch 0 (49→50) taken 14 times.
✓ Branch 1 (49→51) taken 1 times.
✓ Branch 2 (50→51) taken 14 times.
✗ Branch 3 (50→52) not taken.
15 bool prompt = (err >= 0) && (exec_flags & EXECFLAG_PROMPT);
415 15 resume_terminal(e, quiet, prompt);
416 15 free(alloc);
417
418
2/2
✓ Branch 0 (53→54) taken 4 times.
✓ Branch 1 (53→58) taken 11 times.
15 if (err != 0) {
419 4 show_spawn_error_msg(&e->err, &ctx.outputs[1], err);
420 4 string_free(&ctx.outputs[0]);
421 4 string_free(&ctx.outputs[1]);
422 4 view->cursor = saved_cursor;
423 4 return -1;
424 }
425
426 11 string_free(&ctx.outputs[1]);
427 11 String *output = &ctx.outputs[0];
428 11 bool strip_trailing_newline = (exec_flags & EXECFLAG_STRIP_NL);
429 11 if (
430 strip_trailing_newline
431
1/2
✗ Branch 0 (59→60) not taken.
✓ Branch 1 (59→65) taken 11 times.
11 && output_to_buffer
432 && output->len > 0
433 && output->buffer[output->len - 1] == '\n'
434 ) {
435 output->len--;
436 if (output->len > 0 && output->buffer[output->len - 1] == '\r') {
437 output->len--;
438 }
439 }
440
441
2/2
✓ Branch 0 (65→66) taken 6 times.
✓ Branch 1 (65→67) taken 5 times.
11 if (!output_to_buffer) {
442 6 view->cursor = saved_cursor;
443 6 view->sel_so = saved_sel_so;
444 6 view->sel_eo = saved_sel_eo;
445 6 mark_all_lines_changed(view->buffer);
446 }
447
448 11 ExecAction out_action = actions[STDOUT_FILENO];
449
2/8
✓ Branch 0 (67→68) taken 5 times.
✗ Branch 1 (67→73) not taken.
✗ Branch 2 (67→77) not taken.
✗ Branch 3 (67→78) not taken.
✗ Branch 4 (67→80) not taken.
✗ Branch 5 (67→82) not taken.
✗ Branch 6 (67→84) not taken.
✓ Branch 7 (67→86) taken 6 times.
11 switch (out_action) {
450 5 case EXEC_BUFFER:
451
1/2
✗ Branch 0 (68→69) not taken.
✓ Branch 1 (68→70) taken 5 times.
5 if (view->selection) {
452 insert_to_selection(view, output, &info);
453 } else {
454
2/2
✓ Branch 0 (70→71) taken 2 times.
✓ Branch 1 (70→72) taken 3 times.
5 size_t del_count = replace_unselected_input ? ctx.input.length : 0;
455 5 buffer_replace_bytes(view, del_count, output->buffer, output->len);
456 }
457 break;
458 case EXEC_ECHO:
459 if (output->len) {
460 size_t pos = 0;
461 StringView line = buf_slice_next_line(output->buffer, &pos, output->len);
462 info_msg(&e->err, "%.*s", (int)line.length, line.data);
463 }
464 break;
465 case EXEC_MSG:
466 case EXEC_MSG_A:
467 case EXEC_MSG_B:
468 case EXEC_MSG_C:
469 parse_and_activate_message(e, output, out_action);
470 break;
471 case EXEC_OPEN:
472 open_files_from_string(e, output);
473 break;
474 case EXEC_TAG:
475 case EXEC_TAG_A:
476 case EXEC_TAG_B:
477 case EXEC_TAG_C:
478 parse_and_activate_tags(e, output, out_action);
479 break;
480 case EXEC_EVAL:
481 exec_normal_config(e, strview_from_string(output));
482 break;
483 case EXEC_NULL:
484 case EXEC_TTY:
485 break;
486 // These can't be used as output actions
487 case EXEC_COMMAND:
488 case EXEC_ERRMSG:
489 case EXEC_LINE:
490 case EXEC_OPEN_REL:
491 case EXEC_SEARCH:
492 case EXEC_WORD:
493 case EXEC_INVALID:
494 default:
495 BUG("unhandled action");
496 return -1;
497 }
498
499 11 size_t output_len = output->len;
500 11 string_free(output);
501 11 return output_len;
502 }
503
504 15 void yield_terminal(EditorState *e, bool quiet)
505 {
506
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→7) taken 15 times.
15 if (e->flags & EFLAG_HEADLESS) {
507 return;
508 }
509
510 if (quiet) {
511 term_raw_isig();
512 } else {
513 e->child_controls_terminal = true;
514 ui_end(e, false);
515 term_cooked();
516 }
517 }
518
519 15 void resume_terminal(EditorState *e, bool quiet, bool prompt)
520 {
521
1/2
✗ Branch 0 (2→3) not taken.
✓ Branch 1 (2→10) taken 15 times.
15 if (e->flags & EFLAG_HEADLESS) {
522 return;
523 }
524
525 term_raw();
526 if (!quiet && e->child_controls_terminal) {
527 if (prompt) {
528 any_key(&e->terminal, e->options.esc_timeout);
529 }
530 ui_start(e);
531 e->child_controls_terminal = false;
532 }
533 }
534