dte test coverage


Directory: ./
File: src/exec.c
Date: 2025-02-14 16:55:22
Exec Total Coverage
Lines: 107 248 43.1%
Functions: 9 13 69.2%
Branches: 44 130 33.8%

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