dte test coverage


Directory: ./
File: src/exec.c
Date: 2025-10-16 19:09:21
Exec Total Coverage
Lines: 103 258 39.9%
Functions: 8 12 66.7%
Branches: 45 142 31.7%

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