dte test coverage


Directory: ./
File: src/options.c
Date: 2024-12-21 16:03:22
Exec Total Coverage
Lines: 395 483 81.8%
Functions: 60 66 90.9%
Branches: 182 273 66.7%

Line Branch Exec Source
1 #include <stdint.h>
2 #include <stdlib.h>
3 #include <string.h>
4 #include "options.h"
5 #include "buffer.h"
6 #include "command/serialize.h"
7 #include "editor.h"
8 #include "error.h"
9 #include "file-option.h"
10 #include "filetype.h"
11 #include "status.h"
12 #include "terminal/output.h"
13 #include "ui.h"
14 #include "util/arith.h"
15 #include "util/bsearch.h"
16 #include "util/intern.h"
17 #include "util/numtostr.h"
18 #include "util/str-array.h"
19 #include "util/str-util.h"
20 #include "util/string-view.h"
21 #include "util/strtonum.h"
22 #include "util/xmalloc.h"
23 #include "util/xstring.h"
24
25 typedef enum {
26 OPT_STR,
27 OPT_UINT,
28 OPT_ENUM,
29 OPT_BOOL,
30 OPT_FLAG,
31 OPT_REGEX,
32 OPT_FILESIZE,
33 } OptionType;
34
35 typedef union {
36 const char *str_val; // OPT_STR, OPT_REGEX
37 unsigned int uint_val; // OPT_UINT, OPT_ENUM, OPT_FLAG, OPT_FILESIZE
38 bool bool_val; // OPT_BOOL
39 } OptionValue;
40
41 typedef union {
42 struct {bool (*validate)(const char *value);} str_opt; // OPT_STR (optional)
43 struct {unsigned int min, max;} uint_opt; // OPT_UINT
44 struct {const char *const *values;} enum_opt; // OPT_ENUM, OPT_FLAG, OPT_BOOL
45 } OptionConstraint;
46
47 typedef struct {
48 const char name[22];
49 bool local;
50 bool global;
51 unsigned int offset;
52 OptionType type;
53 OptionConstraint u;
54 void (*on_change)(EditorState *e, bool global); // Optional
55 } OptionDesc;
56
57 #define STR_OPT(_name, OLG, _validate, _on_change) { \
58 OLG \
59 .name = _name, \
60 .type = OPT_STR, \
61 .u = {.str_opt = {.validate = _validate}}, \
62 .on_change = _on_change, \
63 }
64
65 #define UINT_OPT(_name, OLG, _min, _max, _on_change) { \
66 OLG \
67 .name = _name, \
68 .type = OPT_UINT, \
69 .u = {.uint_opt = { \
70 .min = _min, \
71 .max = _max, \
72 }}, \
73 .on_change = _on_change, \
74 }
75
76 #define ENUM_OPT(_name, OLG, _values, _on_change) { \
77 OLG \
78 .name = _name, \
79 .type = OPT_ENUM, \
80 .u = {.enum_opt = {.values = _values}}, \
81 .on_change = _on_change, \
82 }
83
84 #define FLAG_OPT(_name, OLG, _values, _on_change) { \
85 OLG \
86 .name = _name, \
87 .type = OPT_FLAG, \
88 .u = {.enum_opt = {.values = _values}}, \
89 .on_change = _on_change, \
90 }
91
92 #define BOOL_OPT(_name, OLG, _on_change) { \
93 OLG \
94 .name = _name, \
95 .type = OPT_BOOL, \
96 .u = {.enum_opt = {.values = bool_enum}}, \
97 .on_change = _on_change, \
98 }
99
100 #define REGEX_OPT(_name, OLG, _on_change) { \
101 OLG \
102 .name = _name, \
103 .type = OPT_REGEX, \
104 .on_change = _on_change, \
105 }
106
107 #define FSIZE_OPT(_name, OLG, _on_change) { \
108 .name = _name, \
109 .type = OPT_FILESIZE, \
110 OLG \
111 .on_change = _on_change, \
112 }
113
114 #define OLG(_offset, _local, _global) \
115 .offset = _offset, \
116 .local = _local, \
117 .global = _global, \
118
119 #define L(member) OLG(offsetof(LocalOptions, member), true, false)
120 #define G(member) OLG(offsetof(GlobalOptions, member), false, true)
121 #define C(member) OLG(offsetof(CommonOptions, member), true, true)
122
123 static void filetype_changed(EditorState *e, bool global)
124 {
125 BUG_ON(!e->buffer);
126 BUG_ON(global);
127 set_file_options(e, e->buffer);
128 buffer_update_syntax(e, e->buffer);
129 }
130
131 // TODO: Set a flag to be interpreted elsewhere, instead of emitting output here
132 3 static void set_window_title_changed(EditorState *e, bool global)
133 {
134 3 BUG_ON(!global);
135
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (e->flags & EFLAG_HEADLESS) {
136 return;
137 }
138
139 Terminal *term = &e->terminal;
140 if (e->options.set_window_title) {
141 if (e->status == EDITOR_RUNNING) {
142 const Buffer *b = e->buffer;
143 update_term_title(term, buffer_filename(b), buffer_modified(b));
144 }
145 } else {
146 term_restore_title(term);
147 term_save_title(term);
148 }
149 }
150
151 1 static void syntax_changed(EditorState *e, bool global)
152 {
153
2/4
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
1 if (e->buffer && !global) {
154 1 buffer_update_syntax(e, e->buffer);
155 }
156 1 }
157
158 static void overwrite_changed(EditorState *e, bool global)
159 {
160 if (!global) {
161 e->screen_update |= UPDATE_CURSOR_STYLE;
162 }
163 }
164
165 static void window_separator_changed(EditorState *e, bool global)
166 {
167 BUG_ON(!global);
168 if (e->root_frame && !e->root_frame->window) {
169 e->screen_update |= UPDATE_WINDOW_SEPARATORS;
170 }
171 }
172
173 4 static void redraw_buffer(EditorState *e, bool global)
174 {
175
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if (e->buffer && !global) {
176 4 mark_all_lines_changed(e->buffer);
177 }
178 4 }
179
180 3 static void redraw_screen(EditorState *e, bool global)
181 {
182 3 BUG_ON(!global);
183 3 e->screen_update |= UPDATE_ALL_WINDOWS;
184 3 }
185
186 19 static bool validate_statusline_format(const char *value)
187 {
188 19 size_t errpos = statusline_format_find_error(value);
189
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 17 times.
19 if (likely(errpos == 0)) {
190 return true;
191 }
192 2 char ch = value[errpos];
193
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 if (ch == '\0') {
194 1 return error_msg("Format character expected after '%%'");
195 }
196 1 return error_msg("Invalid format character '%c'", ch);
197 }
198
199 50 static bool validate_filetype(const char *value)
200 {
201
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 49 times.
50 if (!is_valid_filetype_name(value)) {
202 1 return error_msg("Invalid filetype name '%s'", value);
203 }
204 return true;
205 }
206
207 71 static OptionValue str_get(const OptionDesc* UNUSED_ARG(desc), void *ptr)
208 {
209 71 const char *const *strp = ptr;
210 71 return (OptionValue){.str_val = *strp};
211 }
212
213 1 static void str_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
214 {
215 1 const char **strp = ptr;
216 1 *strp = str_intern(v.str_val);
217 1 }
218
219 4 static bool str_parse(const OptionDesc *d, const char *str, OptionValue *v)
220 {
221
3/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 3 times.
4 bool valid = !d->u.str_opt.validate || d->u.str_opt.validate(str);
222 4 v->str_val = valid ? str : NULL;
223 4 return valid;
224 }
225
226 8 static const char *str_string(const OptionDesc* UNUSED_ARG(d), OptionValue v)
227 {
228 8 const char *s = v.str_val;
229
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 6 times.
8 return s ? s : "";
230 }
231
232 1 static bool str_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
233 {
234 1 const char **strp = ptr;
235 1 return xstreq(*strp, v.str_val);
236 }
237
238 51 static OptionValue re_get(const OptionDesc* UNUSED_ARG(desc), void *ptr)
239 {
240 51 const InternedRegexp *const *irp = ptr;
241
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 50 times.
51 return (OptionValue){.str_val = *irp ? (*irp)->str : NULL};
242 }
243
244 1 static void re_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
245 {
246 1 const InternedRegexp **irp = ptr;
247
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 *irp = v.str_val ? regexp_intern(v.str_val) : NULL;
248 1 }
249
250 37 static bool re_parse(const OptionDesc* UNUSED_ARG(d), const char *str, OptionValue *v)
251 {
252
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 37 times.
37 if (str[0] == '\0') {
253 v->str_val = NULL;
254 return true;
255 }
256
257
2/4
✓ Branch 0 taken 37 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 37 times.
✗ Branch 3 not taken.
37 bool valid = regexp_is_interned(str) || regexp_is_valid(str, REG_NEWLINE);
258 37 v->str_val = valid ? str : NULL;
259 37 return valid;
260 }
261
262 1 static bool re_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
263 {
264 1 const InternedRegexp **irp = ptr;
265
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 return *irp ? xstreq((*irp)->str, v.str_val) : !v.str_val;
266 }
267
268 428 static OptionValue uint_get(const OptionDesc* UNUSED_ARG(desc), void *ptr)
269 {
270 428 const unsigned int *valp = ptr;
271 428 return (OptionValue){.uint_val = *valp};
272 }
273
274 5 static void uint_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
275 {
276 5 unsigned int *valp = ptr;
277 5 *valp = v.uint_val;
278 5 }
279
280 11 static bool uint_parse(const OptionDesc *d, const char *str, OptionValue *v)
281 {
282 11 unsigned int val;
283
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 10 times.
11 if (!str_to_uint(str, &val)) {
284 1 return error_msg("Integer value for %s expected", d->name);
285 }
286
287 10 const unsigned int min = d->u.uint_opt.min;
288 10 const unsigned int max = d->u.uint_opt.max;
289
3/4
✓ Branch 0 taken 10 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 9 times.
10 if (val < min || val > max) {
290 1 return error_msg("Value for %s must be in %u-%u range", d->name, min, max);
291 }
292
293 9 v->uint_val = val;
294 9 return true;
295 }
296
297 11 static const char *uint_string(const OptionDesc* UNUSED_ARG(desc), OptionValue value)
298 {
299 11 return uint_to_str(value.uint_val);
300 }
301
302 17 static bool uint_equals(const OptionDesc* UNUSED_ARG(desc), void *ptr, OptionValue value)
303 {
304 17 const unsigned int *valp = ptr;
305 17 return *valp == value.uint_val;
306 }
307
308 636 static OptionValue bool_get(const OptionDesc* UNUSED_ARG(d), void *ptr)
309 {
310 636 const bool *valp = ptr;
311 636 return (OptionValue){.bool_val = *valp};
312 }
313
314 20 static void bool_set(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
315 {
316 20 bool *valp = ptr;
317 20 *valp = v.bool_val;
318 20 }
319
320 62 static bool bool_parse(const OptionDesc *d, const char *str, OptionValue *v)
321 {
322
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 26 times.
62 if (streq(str, "true")) {
323 36 v->bool_val = true;
324 36 return true;
325
2/2
✓ Branch 0 taken 25 times.
✓ Branch 1 taken 1 times.
26 } else if (streq(str, "false")) {
326 25 v->bool_val = false;
327 25 return true;
328 }
329 1 return error_msg("Invalid value for %s", d->name);
330 }
331
332 36 static const char *bool_string(const OptionDesc* UNUSED_ARG(d), OptionValue v)
333 {
334
2/2
✓ Branch 0 taken 18 times.
✓ Branch 1 taken 18 times.
36 return v.bool_val ? "true" : "false";
335 }
336
337 41 static bool bool_equals(const OptionDesc* UNUSED_ARG(d), void *ptr, OptionValue v)
338 {
339 41 const bool *valp = ptr;
340 41 return *valp == v.bool_val;
341 }
342
343 1 static bool enum_parse(const OptionDesc *d, const char *str, OptionValue *v)
344 {
345 1 const char *const *values = d->u.enum_opt.values;
346 1 unsigned int i;
347
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 1 times.
4 for (i = 0; values[i]; i++) {
348
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 3 times.
3 if (streq(values[i], str)) {
349 v->uint_val = i;
350 return true;
351 }
352 }
353
354 1 unsigned int val;
355
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
1 if (!str_to_uint(str, &val) || val >= i) {
356 1 return error_msg("Invalid value for %s", d->name);
357 }
358
359 v->uint_val = val;
360 return true;
361 }
362
363 8 static const char *enum_string(const OptionDesc *desc, OptionValue value)
364 {
365 8 return desc->u.enum_opt.values[value.uint_val];
366 }
367
368 58 static bool flag_parse(const OptionDesc *d, const char *str, OptionValue *v)
369 {
370 // "0" is allowed for compatibility and is the same as ""
371
3/4
✓ Branch 0 taken 6 times.
✓ Branch 1 taken 52 times.
✓ Branch 2 taken 6 times.
✗ Branch 3 not taken.
58 if (str[0] == '0' && str[1] == '\0') {
372 6 v->uint_val = 0;
373 6 return true;
374 }
375
376 52 const char *const *values = d->u.enum_opt.values;
377 52 unsigned int flags = 0;
378
379
2/2
✓ Branch 0 taken 340 times.
✓ Branch 1 taken 51 times.
391 for (size_t pos = 0, len = strlen(str); pos < len; ) {
380 340 const StringView flag = get_delim(str, &pos, len, ',');
381 340 size_t i;
382
2/2
✓ Branch 0 taken 1544 times.
✓ Branch 1 taken 1 times.
1885 for (i = 0; values[i]; i++) {
383
2/2
✓ Branch 0 taken 339 times.
✓ Branch 1 taken 1205 times.
1544 if (strview_equal_cstring(&flag, values[i])) {
384 339 flags |= 1u << i;
385 339 break;
386 }
387 }
388
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 339 times.
340 if (unlikely(!values[i])) {
389 1 int flen = (int)flag.length;
390 1 return error_msg("Invalid flag '%.*s' for %s", flen, flag.data, d->name);
391 }
392 }
393
394 51 v->uint_val = flags;
395 51 return true;
396 }
397
398 40 static const char *flag_string(const OptionDesc *desc, OptionValue value)
399 {
400 40 static char buf[128];
401 40 unsigned int flags = value.uint_val;
402
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 38 times.
40 if (!flags) {
403 2 buf[0] = '0';
404 2 buf[1] = '\0';
405 2 return buf;
406 }
407
408 38 char *ptr = buf;
409 38 const char *const *values = desc->u.enum_opt.values;
410
2/2
✓ Branch 0 taken 304 times.
✓ Branch 1 taken 38 times.
342 for (size_t i = 0, avail = sizeof(buf); values[i]; i++) {
411
2/2
✓ Branch 0 taken 290 times.
✓ Branch 1 taken 14 times.
304 if (flags & (1u << i)) {
412 290 char *next = memccpy(ptr, values[i], '\0', avail);
413 290 if (DEBUG >= 1) {
414 290 BUG_ON(!next);
415 290 avail -= (size_t)(next - ptr);
416 }
417 290 ptr = next;
418 290 ptr[-1] = ',';
419 }
420 }
421
422 38 BUG_ON(ptr == buf);
423 38 ptr[-1] = '\0';
424 38 return buf;
425 }
426
427 static bool fsize_parse(const OptionDesc* UNUSED_ARG(d), const char *str, OptionValue *value)
428 {
429 unsigned int x;
430 if (str_to_uint(str, &x)) {
431 // Values with no suffix are interpreted as MiB
432 value->uint_val = x;
433 return true;
434 }
435
436 intmax_t bytes = parse_filesize(str);
437 if (likely(bytes >= 0)) {
438 // Convert bytes to MiB and round up remainder
439 uintmax_t mib = (bytes >> 20) + !!(bytes & 0xFFFFF);
440 if (likely(mib <= UINT_MAX)) {
441 value->uint_val = mib;
442 return true;
443 }
444 }
445
446 error_msg("Invalid filesize: '%s'", str);
447 return false;
448 }
449
450 2 static const char *fsize_string(const OptionDesc* UNUSED_ARG(d), OptionValue value)
451 {
452 // TODO: Count trailing zeros and use GiB/TiB/etc. when appropriate
453 2 static char buf[DECIMAL_STR_MAX(value.uint_val) + 4];
454 2 size_t ndigits = buf_uint_to_str(value.uint_val, buf);
455 2 copyliteral(buf + ndigits, "MiB");
456 2 return buf;
457 }
458
459 static const struct {
460 OptionValue (*get)(const OptionDesc *desc, void *ptr);
461 void (*set)(const OptionDesc *desc, void *ptr, OptionValue value);
462 bool (*parse)(const OptionDesc *desc, const char *str, OptionValue *value);
463 const char *(*string)(const OptionDesc *desc, OptionValue value);
464 bool (*equals)(const OptionDesc *desc, void *ptr, OptionValue value);
465 } option_ops[] = {
466 [OPT_STR] = {str_get, str_set, str_parse, str_string, str_equals},
467 [OPT_UINT] = {uint_get, uint_set, uint_parse, uint_string, uint_equals},
468 [OPT_ENUM] = {uint_get, uint_set, enum_parse, enum_string, uint_equals},
469 [OPT_BOOL] = {bool_get, bool_set, bool_parse, bool_string, bool_equals},
470 [OPT_FLAG] = {uint_get, uint_set, flag_parse, flag_string, uint_equals},
471 [OPT_REGEX] = {re_get, re_set, re_parse, str_string, re_equals},
472 [OPT_FILESIZE] = {uint_get, uint_set, fsize_parse, fsize_string, uint_equals},
473 };
474
475 static const char *const bool_enum[] = {"false", "true", NULL};
476 static const char *const newline_enum[] = {"unix", "dos", NULL};
477 static const char *const tristate_enum[] = {"false", "true", "auto", NULL};
478 static const char *const save_unmodified_enum[] = {"none", "touch", "full", NULL};
479 static const char *const window_separator_enum[] = {"blank", "bar", NULL};
480
481 static const char *const detect_indent_values[] = {
482 "1", "2", "3", "4", "5", "6", "7", "8",
483 NULL
484 };
485
486 // Note: this must be kept in sync with WhitespaceErrorFlags
487 static const char *const ws_error_values[] = {
488 "space-indent",
489 "space-align",
490 "tab-indent",
491 "tab-after-indent",
492 "special",
493 "auto-indent",
494 "trailing",
495 "all-trailing",
496 NULL
497 };
498
499 static const OptionDesc option_desc[] = {
500 BOOL_OPT("auto-indent", C(auto_indent), NULL),
501 BOOL_OPT("brace-indent", L(brace_indent), NULL),
502 ENUM_OPT("case-sensitive-search", G(case_sensitive_search), tristate_enum, NULL),
503 FLAG_OPT("detect-indent", C(detect_indent), detect_indent_values, NULL),
504 BOOL_OPT("display-special", G(display_special), redraw_screen),
505 BOOL_OPT("editorconfig", C(editorconfig), NULL),
506 BOOL_OPT("emulate-tab", C(emulate_tab), NULL),
507 UINT_OPT("esc-timeout", G(esc_timeout), 0, 2000, NULL),
508 BOOL_OPT("expand-tab", C(expand_tab), redraw_buffer),
509 BOOL_OPT("file-history", C(file_history), NULL),
510 FSIZE_OPT("filesize-limit", G(filesize_limit), NULL),
511 STR_OPT("filetype", L(filetype), validate_filetype, filetype_changed),
512 BOOL_OPT("fsync", C(fsync), NULL),
513 REGEX_OPT("indent-regex", L(indent_regex), NULL),
514 UINT_OPT("indent-width", C(indent_width), 1, INDENT_WIDTH_MAX, NULL),
515 BOOL_OPT("lock-files", G(lock_files), NULL),
516 ENUM_OPT("newline", G(crlf_newlines), newline_enum, NULL),
517 BOOL_OPT("optimize-true-color", G(optimize_true_color), redraw_screen),
518 BOOL_OPT("overwrite", C(overwrite), overwrite_changed),
519 ENUM_OPT("save-unmodified", C(save_unmodified), save_unmodified_enum, NULL),
520 UINT_OPT("scroll-margin", G(scroll_margin), 0, 100, redraw_screen),
521 BOOL_OPT("select-cursor-char", G(select_cursor_char), redraw_screen),
522 BOOL_OPT("set-window-title", G(set_window_title), set_window_title_changed),
523 BOOL_OPT("show-line-numbers", G(show_line_numbers), redraw_screen),
524 STR_OPT("statusline-left", G(statusline_left), validate_statusline_format, NULL),
525 STR_OPT("statusline-right", G(statusline_right), validate_statusline_format, NULL),
526 BOOL_OPT("syntax", C(syntax), syntax_changed),
527 BOOL_OPT("tab-bar", G(tab_bar), redraw_screen),
528 UINT_OPT("tab-width", C(tab_width), 1, TAB_WIDTH_MAX, redraw_buffer),
529 UINT_OPT("text-width", C(text_width), 1, TEXT_WIDTH_MAX, NULL),
530 BOOL_OPT("utf8-bom", G(utf8_bom), NULL),
531 ENUM_OPT("window-separator", G(window_separator), window_separator_enum, window_separator_changed),
532 FLAG_OPT("ws-error", C(ws_error), ws_error_values, redraw_buffer),
533 };
534
535 364 static char *local_ptr(const OptionDesc *desc, LocalOptions *opt)
536 {
537 364 BUG_ON(!desc->local);
538 364 return (char*)opt + desc->offset;
539 }
540
541 612 static char *global_ptr(const OptionDesc *desc, GlobalOptions *opt)
542 {
543 612 BUG_ON(!desc->global);
544 612 return (char*)opt + desc->offset;
545 }
546
547 19 static char *get_option_ptr(EditorState *e, const OptionDesc *d, bool global)
548 {
549
2/2
✓ Branch 0 taken 9 times.
✓ Branch 1 taken 10 times.
19 return global ? global_ptr(d, &e->options) : local_ptr(d, &e->buffer->options);
550 }
551
552 303 static inline size_t count_enum_values(const OptionDesc *desc)
553 {
554 303 OptionType type = desc->type;
555 303 BUG_ON(type != OPT_ENUM && type != OPT_FLAG && type != OPT_BOOL);
556 303 const char *const *values = desc->u.enum_opt.values;
557 303 BUG_ON(!values);
558 303 return string_array_length((char**)values);
559 }
560
561 18 UNITTEST {
562 18 static const struct {
563 size_t alignment;
564 size_t size;
565 } map[] = {
566 [OPT_STR] = {ALIGNOF(const char*), sizeof(const char*)},
567 [OPT_UINT] = {ALIGNOF(unsigned int), sizeof(unsigned int)},
568 [OPT_ENUM] = {ALIGNOF(unsigned int), sizeof(unsigned int)},
569 [OPT_BOOL] = {ALIGNOF(bool), sizeof(bool)},
570 [OPT_FLAG] = {ALIGNOF(unsigned int), sizeof(unsigned int)},
571 [OPT_REGEX] = {ALIGNOF(const InternedRegexp*), sizeof(const InternedRegexp*)},
572 [OPT_FILESIZE] = {ALIGNOF(unsigned int), sizeof(unsigned int)},
573 };
574
575 18 static_assert(ARRAYLEN(map) == ARRAYLEN(option_ops));
576 18 static_assert(offsetof(CommonOptions, syntax) == offsetof(GlobalOptions, syntax));
577 18 static_assert(offsetof(CommonOptions, syntax) == offsetof(LocalOptions, syntax));
578 18 GlobalOptions gopts = {.tab_bar = true};
579 18 LocalOptions lopts = {.filetype = NULL};
580
581
2/2
✓ Branch 0 taken 594 times.
✓ Branch 1 taken 18 times.
612 for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
582 594 const OptionDesc *desc = &option_desc[i];
583 594 const OptionType type = desc->type;
584 594 BUG_ON(type >= ARRAYLEN(map));
585 594 size_t alignment = map[type].alignment;
586 594 size_t end = desc->offset + map[type].size;
587
2/2
✓ Branch 0 taken 540 times.
✓ Branch 1 taken 54 times.
594 if (desc->global) {
588 540 const char *ptr = global_ptr(desc, &gopts);
589 540 uintptr_t ptr_val = (uintptr_t)ptr;
590 540 BUG_ON(ptr_val % alignment != 0);
591 540 BUG_ON(end > sizeof(GlobalOptions));
592 }
593
2/2
✓ Branch 0 taken 306 times.
✓ Branch 1 taken 288 times.
594 if (desc->local) {
594 306 const char *ptr = local_ptr(desc, &lopts);
595 306 uintptr_t ptr_val = (uintptr_t)ptr;
596 306 BUG_ON(ptr_val % alignment != 0);
597 306 BUG_ON(end > sizeof(LocalOptions));
598 }
599
4/4
✓ Branch 0 taken 540 times.
✓ Branch 1 taken 54 times.
✓ Branch 2 taken 252 times.
✓ Branch 3 taken 288 times.
594 if (desc->global && desc->local) {
600 252 BUG_ON(end > sizeof(CommonOptions));
601 }
602
2/2
✓ Branch 0 taken 90 times.
✓ Branch 1 taken 504 times.
594 if (type == OPT_UINT) {
603 90 BUG_ON(desc->u.uint_opt.max <= desc->u.uint_opt.min);
604
2/2
✓ Branch 0 taken 306 times.
✓ Branch 1 taken 198 times.
504 } else if (type == OPT_BOOL) {
605 306 BUG_ON(desc->u.enum_opt.values != bool_enum);
606
2/2
✓ Branch 0 taken 72 times.
✓ Branch 1 taken 126 times.
198 } else if (type == OPT_ENUM) {
607 // NOLINTNEXTLINE(bugprone-assert-side-effect)
608 72 BUG_ON(count_enum_values(desc) < 2);
609
2/2
✓ Branch 0 taken 36 times.
✓ Branch 1 taken 90 times.
126 } else if (type == OPT_FLAG) {
610 36 size_t nvals = count_enum_values(desc);
611 36 OptionValue val = {.uint_val = -1};
612 36 BUG_ON(nvals < 2);
613 36 BUG_ON(nvals >= BITSIZE(val.uint_val));
614 36 const char *str = flag_string(desc, val);
615 36 BUG_ON(!str);
616 36 BUG_ON(str[0] == '\0');
617
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if (!flag_parse(desc, str, &val)) {
618 BUG("flag_parse() failed for string: %s", str);
619 }
620 36 unsigned int mask = (1u << nvals) - 1;
621
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 36 times.
36 if (val.uint_val != mask) {
622 BUG("values not equal: %u, %u", val.uint_val, mask);
623 }
624 }
625 }
626
627 // Ensure option_desc[] is properly sorted
628 18 CHECK_BSEARCH_ARRAY(option_desc, name, strcmp);
629 18 }
630
631 60 static bool desc_equals(const OptionDesc *desc, void *ptr, OptionValue value)
632 {
633 60 return option_ops[desc->type].equals(desc, ptr, value);
634 }
635
636 1186 static OptionValue desc_get(const OptionDesc *desc, void *ptr)
637 {
638 1186 return option_ops[desc->type].get(desc, ptr);
639 }
640
641 32 static void desc_set(EditorState *e, const OptionDesc *desc, void *ptr, bool global, OptionValue value)
642 {
643
2/2
✓ Branch 0 taken 27 times.
✓ Branch 1 taken 5 times.
32 if (desc_equals(desc, ptr, value)) {
644 return;
645 }
646
647 27 option_ops[desc->type].set(desc, ptr, value);
648
2/2
✓ Branch 0 taken 11 times.
✓ Branch 1 taken 16 times.
27 if (desc->on_change) {
649 11 desc->on_change(e, global);
650 }
651 }
652
653 137 static bool desc_parse(const OptionDesc *desc, const char *str, OptionValue *value)
654 {
655 137 return option_ops[desc->type].parse(desc, str, value);
656 }
657
658 69 static const char *desc_string(const OptionDesc *desc, OptionValue value)
659 {
660 69 return option_ops[desc->type].string(desc, value);
661 }
662
663 749 static int option_cmp(const void *key, const void *elem)
664 {
665 749 const char *name = key;
666 749 const OptionDesc *desc = elem;
667 749 return strcmp(name, desc->name);
668 }
669
670 166 static const OptionDesc *find_option(const char *name)
671 {
672 166 return BSEARCH(name, option_desc, option_cmp);
673 }
674
675 158 static const OptionDesc *must_find_option(const char *name)
676 {
677 158 const OptionDesc *desc = find_option(name);
678
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 157 times.
158 if (!desc) {
679 1 error_msg("No such option %s", name);
680 }
681 158 return desc;
682 }
683
684 static const OptionDesc *must_find_global_option(const char *name)
685 {
686 const OptionDesc *desc = must_find_option(name);
687 if (desc && !desc->global) {
688 error_msg("Option %s is not global", name);
689 return NULL;
690 }
691 return desc;
692 }
693
694 26 static bool do_set_option (
695 EditorState *e,
696 const OptionDesc *desc,
697 const char *value,
698 bool local,
699 bool global
700 ) {
701
4/4
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 13 times.
26 if (local && !desc->local) {
702 1 return error_msg("Option %s is not local", desc->name);
703 }
704
3/4
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 24 times.
✓ Branch 2 taken 1 times.
✗ Branch 3 not taken.
25 if (global && !desc->global) {
705 1 return error_msg("Option %s is not global", desc->name);
706 }
707
708 24 OptionValue val;
709
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 8 times.
24 if (!desc_parse(desc, value, &val)) {
710 return false;
711 }
712
713
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 13 times.
16 if (!local && !global) {
714 // Set both by default
715 3 local = desc->local;
716 3 global = desc->global;
717 }
718
719
2/2
✓ Branch 0 taken 14 times.
✓ Branch 1 taken 2 times.
16 if (local) {
720 14 desc_set(e, desc, local_ptr(desc, &e->buffer->options), false, val);
721 }
722
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 13 times.
16 if (global) {
723 3 desc_set(e, desc, global_ptr(desc, &e->options), true, val);
724 }
725
726 return true;
727 }
728
729 27 bool set_option(EditorState *e, const char *name, const char *value, bool local, bool global)
730 {
731 27 const OptionDesc *desc = must_find_option(name);
732
2/2
✓ Branch 0 taken 26 times.
✓ Branch 1 taken 1 times.
27 if (!desc) {
733 return false;
734 }
735 26 return do_set_option(e, desc, value, local, global);
736 }
737
738 1 bool set_bool_option(EditorState *e, const char *name, bool local, bool global)
739 {
740 1 const OptionDesc *desc = must_find_option(name);
741
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (!desc) {
742 return false;
743 }
744
1/2
✓ Branch 0 taken 1 times.
✗ Branch 1 not taken.
1 if (desc->type != OPT_BOOL) {
745 1 return error_msg("Option %s is not boolean", desc->name);
746 }
747 return do_set_option(e, desc, "true", local, global);
748 }
749
750 16 static const OptionDesc *find_toggle_option(const char *name, bool *global)
751 {
752
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (*global) {
753 return must_find_global_option(name);
754 }
755
756 // Toggle local value by default if option has both values
757 16 const OptionDesc *desc = must_find_option(name);
758
3/4
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 7 times.
✓ Branch 3 taken 9 times.
16 if (desc && !desc->local) {
759 7 *global = true;
760 }
761 return desc;
762 }
763
764 16 bool toggle_option(EditorState *e, const char *name, bool global, bool verbose)
765 {
766 16 const OptionDesc *desc = find_toggle_option(name, &global);
767
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 if (!desc) {
768 return false;
769 }
770
771 16 char *ptr = get_option_ptr(e, desc, global);
772 16 OptionValue value = desc_get(desc, ptr);
773 16 OptionType type = desc->type;
774
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 16 times.
16 if (type == OPT_ENUM) {
775 if (desc->u.enum_opt.values[value.uint_val + 1]) {
776 value.uint_val++;
777 } else {
778 value.uint_val = 0;
779 }
780
2/2
✓ Branch 0 taken 15 times.
✓ Branch 1 taken 1 times.
16 } else if (type == OPT_BOOL) {
781 15 value.bool_val = !value.bool_val;
782 } else {
783 1 return error_msg("Toggling %s requires arguments", name);
784 }
785
786 15 desc_set(e, desc, ptr, global, value);
787
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 15 times.
15 if (!verbose) {
788 return true;
789 }
790
791 const char *prefix = (global && desc->local) ? "[global] " : "";
792 const char *str = desc_string(desc, value);
793 return info_msg("%s%s = %s", prefix, desc->name, str);
794 }
795
796 bool toggle_option_values (
797 EditorState *e,
798 const char *name,
799 bool global,
800 bool verbose,
801 char **values,
802 size_t count
803 ) {
804 const OptionDesc *desc = find_toggle_option(name, &global);
805 if (!desc) {
806 return false;
807 }
808
809 BUG_ON(count == 0);
810 size_t current = 0;
811 bool error = false;
812 char *ptr = get_option_ptr(e, desc, global);
813 OptionValue *parsed_values = xnew(OptionValue, count);
814
815 for (size_t i = 0; i < count; i++) {
816 if (desc_parse(desc, values[i], &parsed_values[i])) {
817 if (desc_equals(desc, ptr, parsed_values[i])) {
818 current = i;
819 }
820 } else {
821 error = true;
822 }
823 }
824
825 if (!error) {
826 size_t i = size_increment_wrapped(current, count);
827 desc_set(e, desc, ptr, global, parsed_values[i]);
828 if (verbose) {
829 const char *prefix = (global && desc->local) ? "[global] " : "";
830 const char *str = desc_string(desc, parsed_values[i]);
831 info_msg("%s%s = %s", prefix, desc->name, str);
832 }
833 }
834
835 free(parsed_values);
836 return !error;
837 }
838
839 90 bool validate_local_options(char **strs)
840 {
841 90 size_t invalid = 0;
842
2/2
✓ Branch 0 taken 114 times.
✓ Branch 1 taken 90 times.
204 for (size_t i = 0; strs[i]; i += 2) {
843 114 const char *name = strs[i];
844 114 const char *value = strs[i + 1];
845 114 const OptionDesc *desc = must_find_option(name);
846
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 114 times.
114 if (unlikely(!desc)) {
847 invalid++;
848
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 114 times.
114 } else if (unlikely(!desc->local)) {
849 error_msg("%s is not local", name);
850 invalid++;
851
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 113 times.
114 } else if (unlikely(desc->on_change == filetype_changed)) {
852 1 error_msg("filetype cannot be set via option command");
853 1 invalid++;
854 } else {
855 113 OptionValue val;
856
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 113 times.
113 if (unlikely(!desc_parse(desc, value, &val))) {
857 invalid++;
858 }
859 }
860 }
861 90 return !invalid;
862 }
863
864 #if DEBUG >= 1
865 1073 static void sanity_check_option_value(const OptionDesc *desc, OptionValue val)
866 {
867 // NOLINTBEGIN(bugprone-assert-side-effect)
868
6/7
✓ Branch 0 taken 65 times.
✓ Branch 1 taken 187 times.
✓ Branch 2 taken 81 times.
✓ Branch 3 taken 114 times.
✓ Branch 4 taken 49 times.
✗ Branch 5 not taken.
✓ Branch 6 taken 577 times.
1073 switch (desc->type) {
869 65 case OPT_STR:
870 65 BUG_ON(!val.str_val);
871 65 BUG_ON(val.str_val != str_intern(val.str_val));
872
1/2
✓ Branch 0 taken 65 times.
✗ Branch 1 not taken.
65 if (desc->u.str_opt.validate) {
873 65 BUG_ON(!desc->u.str_opt.validate(val.str_val));
874 }
875 return;
876 187 case OPT_UINT:
877 187 BUG_ON(val.uint_val < desc->u.uint_opt.min);
878 187 BUG_ON(val.uint_val > desc->u.uint_opt.max);
879 return;
880 81 case OPT_ENUM:
881 81 BUG_ON(val.uint_val >= count_enum_values(desc));
882 return;
883 114 case OPT_FLAG: {
884 114 size_t nvals = count_enum_values(desc);
885 114 BUG_ON(nvals >= 32);
886 114 unsigned int mask = (1u << nvals) - 1;
887 114 unsigned int uint_val = val.uint_val;
888 114 BUG_ON((uint_val & mask) != uint_val);
889 }
890 return;
891 49 case OPT_REGEX:
892 49 BUG_ON(val.str_val && val.str_val[0] == '\0');
893 49 BUG_ON(val.str_val && !regexp_is_interned(val.str_val));
894 return;
895 case OPT_BOOL:
896 case OPT_FILESIZE:
897 return;
898 }
899 // NOLINTEND(bugprone-assert-side-effect)
900
901 BUG("unhandled option type");
902 }
903
904 57 static void sanity_check_options(const void *opts, bool global)
905 {
906
2/2
✓ Branch 0 taken 1881 times.
✓ Branch 1 taken 57 times.
1938 for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
907 1881 const OptionDesc *desc = &option_desc[i];
908 1881 BUG_ON(desc->type >= ARRAYLEN(option_ops));
909
6/6
✓ Branch 0 taken 1710 times.
✓ Branch 1 taken 171 times.
✓ Branch 2 taken 912 times.
✓ Branch 3 taken 798 times.
✓ Branch 4 taken 275 times.
✓ Branch 5 taken 808 times.
1881 if ((desc->global && desc->local) || global == desc->global) {
910 1073 OptionValue val = desc_get(desc, (char*)opts + desc->offset);
911 1073 sanity_check_option_value(desc, val);
912 }
913 }
914 57 }
915
916 8 void sanity_check_global_options(const GlobalOptions *gopts)
917 {
918 8 sanity_check_options(gopts, true);
919 8 }
920
921 49 void sanity_check_local_options(const LocalOptions *lopts)
922 {
923 49 sanity_check_options(lopts, false);
924 49 }
925 #endif
926
927 2 void collect_options(PointerArray *a, const char *prefix, bool local, bool global)
928 {
929
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 2 times.
68 for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
930 66 const OptionDesc *desc = &option_desc[i];
931
2/8
✗ Branch 0 not taken.
✓ Branch 1 taken 66 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
✗ Branch 4 not taken.
✓ Branch 5 taken 66 times.
✗ Branch 6 not taken.
✗ Branch 7 not taken.
66 if ((local && !desc->local) || (global && !desc->global)) {
932 continue;
933 }
934
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 32 times.
66 if (str_has_prefix(desc->name, prefix)) {
935 34 ptr_array_append(a, xstrdup(desc->name));
936 }
937 }
938 2 }
939
940 // Collect options that can be set via the "option" command
941 1 void collect_auto_options(PointerArray *a, const char *prefix)
942 {
943
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 1 times.
34 for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
944 33 const OptionDesc *desc = &option_desc[i];
945
4/4
✓ Branch 0 taken 17 times.
✓ Branch 1 taken 16 times.
✓ Branch 2 taken 1 times.
✓ Branch 3 taken 16 times.
33 if (!desc->local || desc->on_change == filetype_changed) {
946 17 continue;
947 }
948
1/2
✓ Branch 0 taken 16 times.
✗ Branch 1 not taken.
16 if (str_has_prefix(desc->name, prefix)) {
949 16 ptr_array_append(a, xstrdup(desc->name));
950 }
951 }
952 1 }
953
954 1 void collect_toggleable_options(PointerArray *a, const char *prefix, bool global)
955 {
956
2/2
✓ Branch 0 taken 33 times.
✓ Branch 1 taken 1 times.
34 for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
957 33 const OptionDesc *desc = &option_desc[i];
958
1/4
✗ Branch 0 not taken.
✓ Branch 1 taken 33 times.
✗ Branch 2 not taken.
✗ Branch 3 not taken.
33 if (global && !desc->global) {
959 continue;
960 }
961 33 OptionType type = desc->type;
962 33 bool toggleable = (type == OPT_ENUM || type == OPT_BOOL);
963
3/4
✓ Branch 0 taken 21 times.
✓ Branch 1 taken 12 times.
✓ Branch 2 taken 21 times.
✗ Branch 3 not taken.
33 if (toggleable && str_has_prefix(desc->name, prefix)) {
964 21 ptr_array_append(a, xstrdup(desc->name));
965 }
966 }
967 1 }
968
969 7 void collect_option_values(EditorState *e, PointerArray *a, const char *option, const char *prefix)
970 {
971 7 const OptionDesc *desc = find_option(option);
972
1/2
✓ Branch 0 taken 7 times.
✗ Branch 1 not taken.
7 if (!desc) {
973 return;
974 }
975
976
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 4 times.
7 if (prefix[0] == '\0') {
977 3 char *ptr = get_option_ptr(e, desc, !desc->local);
978 3 OptionValue value = desc_get(desc, ptr);
979 3 ptr_array_append(a, xstrdup(desc_string(desc, value)));
980 3 return;
981 }
982
983 4 OptionType type = desc->type;
984
2/4
✓ Branch 0 taken 4 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 4 times.
✗ Branch 3 not taken.
4 if (type == OPT_STR || type == OPT_UINT || type == OPT_REGEX || type == OPT_FILESIZE) {
985 return;
986 }
987
988 4 const char *const *values = desc->u.enum_opt.values;
989
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 2 times.
4 if (type == OPT_ENUM || type == OPT_BOOL) {
990
2/2
✓ Branch 0 taken 5 times.
✓ Branch 1 taken 2 times.
7 for (size_t i = 0; values[i]; i++) {
991
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 3 times.
5 if (str_has_prefix(values[i], prefix)) {
992 2 ptr_array_append(a, xstrdup(values[i]));
993 }
994 }
995 return;
996 }
997
998 2 BUG_ON(type != OPT_FLAG);
999 2 const char *comma = strrchr(prefix, ',');
1000
2/2
✓ Branch 0 taken 1 times.
✓ Branch 1 taken 1 times.
2 size_t prefix_len = comma ? ++comma - prefix : 0;
1001
2/2
✓ Branch 0 taken 16 times.
✓ Branch 1 taken 2 times.
18 for (size_t i = 0; values[i]; i++) {
1002 16 const char *str = values[i];
1003
2/2
✓ Branch 0 taken 3 times.
✓ Branch 1 taken 13 times.
16 if (str_has_prefix(str, prefix + prefix_len)) {
1004 3 ptr_array_append(a, xmemjoin(prefix, prefix_len, str, strlen(str) + 1));
1005 }
1006 }
1007 }
1008
1009 66 static void append_option(String *s, const OptionDesc *desc, void *ptr)
1010 {
1011 66 const OptionValue value = desc_get(desc, ptr);
1012 66 const char *value_str = desc_string(desc, value);
1013
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 66 times.
66 if (unlikely(value_str[0] == '-')) {
1014 string_append_literal(s, "-- ");
1015 }
1016 66 string_append_cstring(s, desc->name);
1017 66 string_append_byte(s, ' ');
1018 66 string_append_escaped_arg(s, value_str, true);
1019 66 string_append_byte(s, '\n');
1020 66 }
1021
1022 2 String dump_options(GlobalOptions *gopts, LocalOptions *lopts)
1023 {
1024 2 String buf = string_new(4096);
1025
2/2
✓ Branch 0 taken 66 times.
✓ Branch 1 taken 2 times.
68 for (size_t i = 0; i < ARRAYLEN(option_desc); i++) {
1026 66 const OptionDesc *desc = &option_desc[i];
1027
2/2
✓ Branch 0 taken 34 times.
✓ Branch 1 taken 32 times.
66 void *local = desc->local ? local_ptr(desc, lopts) : NULL;
1028
2/2
✓ Branch 0 taken 60 times.
✓ Branch 1 taken 6 times.
66 void *global = desc->global ? global_ptr(desc, gopts) : NULL;
1029
2/2
✓ Branch 0 taken 28 times.
✓ Branch 1 taken 38 times.
66 if (local && global) {
1030 28 const OptionValue global_value = desc_get(desc, global);
1031
1/2
✓ Branch 0 taken 28 times.
✗ Branch 1 not taken.
28 if (desc_equals(desc, local, global_value)) {
1032 28 string_append_literal(&buf, "set ");
1033 28 append_option(&buf, desc, local);
1034 } else {
1035 string_append_literal(&buf, "set -g ");
1036 append_option(&buf, desc, global);
1037 string_append_literal(&buf, "set -l ");
1038 append_option(&buf, desc, local);
1039 }
1040 } else {
1041 38 string_append_literal(&buf, "set ");
1042
2/2
✓ Branch 0 taken 32 times.
✓ Branch 1 taken 6 times.
70 append_option(&buf, desc, local ? local : global);
1043 }
1044 }
1045 2 return buf;
1046 }
1047
1048 1 const char *get_option_value_string(EditorState *e, const char *name)
1049 {
1050 1 const OptionDesc *desc = find_option(name);
1051
1/2
✗ Branch 0 not taken.
✓ Branch 1 taken 1 times.
1 if (!desc) {
1052 return NULL;
1053 }
1054 char *ptr = get_option_ptr(e, desc, !desc->local);
1055 return desc_string(desc, desc_get(desc, ptr));
1056 }
1057