dte test coverage


Directory: ./
File: src/exec.c
Date: 2024-12-21 16:03:22
Exec Total Coverage
Lines: 105 241 43.6%
Functions: 9 13 69.2%
Branches: 43 128 33.6%

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