1 // Functions for executing builtin functions.
2 //
3 // How to add a new builtin function:
4 //
5 // 1). Create a function in builtin.c with the following signature:
6 //
7 //     <tt>static maybe_t<int> builtin_NAME(parser_t &parser, io_streams_t &streams, wchar_t
8 //     **argv)</tt>
9 //
10 // where NAME is the name of the builtin, and args is a zero-terminated list of arguments.
11 //
12 // 2). Add a line like { L"NAME", &builtin_NAME, N_(L"Bla bla bla") }, to the builtin_data_t
13 // variable. The description is used by the completion system. Note that this array is sorted.
14 //
15 // 3). Create a file doc_src/NAME.rst, containing the manual for the builtin in
16 // reStructuredText-format. Check the other builtin manuals for proper syntax.
17 //
18 // 4). Use 'git add doc_src/NAME.txt' to start tracking changes to the documentation file.
19 #include "config.h"  // IWYU pragma: keep
20 
21 #include "builtin.h"
22 
23 #include <unistd.h>
24 
25 #include <algorithm>
26 #include <cerrno>
27 #include <cstdlib>
28 #include <cstring>
29 #include <cwchar>
30 #include <memory>
31 #include <string>
32 
33 #include "builtin_argparse.h"
34 #include "builtin_bg.h"
35 #include "builtin_bind.h"
36 #include "builtin_block.h"
37 #include "builtin_builtin.h"
38 #include "builtin_cd.h"
39 #include "builtin_command.h"
40 #include "builtin_commandline.h"
41 #include "builtin_complete.h"
42 #include "builtin_contains.h"
43 #include "builtin_disown.h"
44 #include "builtin_echo.h"
45 #include "builtin_emit.h"
46 #include "builtin_eval.h"
47 #include "builtin_exit.h"
48 #include "builtin_fg.h"
49 #include "builtin_functions.h"
50 #include "builtin_history.h"
51 #include "builtin_jobs.h"
52 #include "builtin_math.h"
53 #include "builtin_printf.h"
54 #include "builtin_pwd.h"
55 #include "builtin_random.h"
56 #include "builtin_read.h"
57 #include "builtin_realpath.h"
58 #include "builtin_return.h"
59 #include "builtin_set.h"
60 #include "builtin_set_color.h"
61 #include "builtin_source.h"
62 #include "builtin_status.h"
63 #include "builtin_string.h"
64 #include "builtin_test.h"
65 #include "builtin_type.h"
66 #include "builtin_ulimit.h"
67 #include "builtin_wait.h"
68 #include "common.h"
69 #include "complete.h"
70 #include "exec.h"
71 #include "fallback.h"  // IWYU pragma: keep
72 #include "flog.h"
73 #include "intern.h"
74 #include "io.h"
75 #include "parse_constants.h"
76 #include "parse_util.h"
77 #include "parser.h"
78 #include "proc.h"
79 #include "reader.h"
80 #include "wgetopt.h"
81 #include "wutil.h"  // IWYU pragma: keep
82 
operator <(const wcstring & other) const83 bool builtin_data_t::operator<(const wcstring &other) const {
84     return std::wcscmp(this->name, other.c_str()) < 0;
85 }
86 
operator <(const builtin_data_t * other) const87 bool builtin_data_t::operator<(const builtin_data_t *other) const {
88     return std::wcscmp(this->name, other->name) < 0;
89 }
90 
91 /// Counts the number of arguments in the specified null-terminated array
builtin_count_args(const wchar_t * const * argv)92 int builtin_count_args(const wchar_t *const *argv) {
93     int argc;
94     for (argc = 1; argv[argc] != nullptr;) {
95         argc++;
96     }
97 
98     assert(argv[argc] == nullptr);
99     return argc;
100 }
101 
102 /// This function works like wperror, but it prints its result into the streams.err string instead
103 /// to stderr. Used by the builtin commands.
builtin_wperror(const wchar_t * s,io_streams_t & streams)104 void builtin_wperror(const wchar_t *s, io_streams_t &streams) {
105     char *err = std::strerror(errno);
106     if (s != nullptr) {
107         streams.err.append(s);
108         streams.err.append(L": ");
109     }
110     if (err != nullptr) {
111         const wcstring werr = str2wcstring(err);
112         streams.err.append(werr);
113         streams.err.push_back(L'\n');
114     }
115 }
116 
117 static const wchar_t *const short_options = L"+:h";
118 static const struct woption long_options[] = {{L"help", no_argument, nullptr, 'h'},
119                                               {nullptr, 0, nullptr, 0}};
120 
parse_help_only_cmd_opts(struct help_only_cmd_opts_t & opts,int * optind,int argc,const wchar_t ** argv,parser_t & parser,io_streams_t & streams)121 int parse_help_only_cmd_opts(struct help_only_cmd_opts_t &opts, int *optind, int argc,
122                              const wchar_t **argv, parser_t &parser, io_streams_t &streams) {
123     const wchar_t *cmd = argv[0];
124     int opt;
125     wgetopter_t w;
126     while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
127         switch (opt) {  //!OCLINT(too few branches)
128             case 'h': {
129                 opts.print_help = true;
130                 break;
131             }
132             case ':': {
133                 builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
134                 return STATUS_INVALID_ARGS;
135             }
136             case '?': {
137                 builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
138                 return STATUS_INVALID_ARGS;
139             }
140             default: {
141                 DIE("unexpected retval from wgetopt_long");
142             }
143         }
144     }
145 
146     *optind = w.woptind;
147     return STATUS_CMD_OK;
148 }
149 
150 /// Display help/usage information for the specified builtin or function from manpage
151 ///
152 /// @param  name
153 ///    builtin or function name to get up help for
154 ///
155 /// Process and print help for the specified builtin or function.
builtin_print_help(parser_t & parser,const io_streams_t & streams,const wchar_t * name,wcstring * error_message)156 void builtin_print_help(parser_t &parser, const io_streams_t &streams, const wchar_t *name,
157                         wcstring *error_message) {
158     // This won't ever work if no_exec is set.
159     if (no_exec()) return;
160     const wcstring name_esc = escape_string(name, ESCAPE_ALL);
161     wcstring cmd = format_string(L"__fish_print_help %ls ", name_esc.c_str());
162     io_chain_t ios;
163     if (error_message) {
164         cmd.append(escape_string(*error_message, ESCAPE_ALL));
165         // If it's an error, redirect the output of __fish_print_help to stderr
166         ios.push_back(std::make_shared<io_fd_t>(STDOUT_FILENO, STDERR_FILENO));
167     }
168     auto res = parser.eval(cmd, ios);
169     if (res.status.exit_code() == 2) {
170         streams.err.append_format(BUILTIN_ERR_MISSING_HELP, name_esc.c_str(), name_esc.c_str());
171     }
172 }
173 
174 /// Perform error reporting for encounter with unknown option.
builtin_unknown_option(parser_t & parser,io_streams_t & streams,const wchar_t * cmd,const wchar_t * opt)175 void builtin_unknown_option(parser_t &parser, io_streams_t &streams, const wchar_t *cmd,
176                             const wchar_t *opt) {
177     streams.err.append_format(BUILTIN_ERR_UNKNOWN, cmd, opt);
178     builtin_print_error_trailer(parser, streams.err, cmd);
179 }
180 
181 /// Perform error reporting for encounter with missing argument.
builtin_missing_argument(parser_t & parser,io_streams_t & streams,const wchar_t * cmd,const wchar_t * opt,bool print_hints)182 void builtin_missing_argument(parser_t &parser, io_streams_t &streams, const wchar_t *cmd,
183                               const wchar_t *opt, bool print_hints) {
184     if (opt[0] == L'-' && opt[1] != L'-') {
185         opt += std::wcslen(opt) - 1;
186     }
187     streams.err.append_format(BUILTIN_ERR_MISSING, cmd, opt);
188     if (print_hints) {
189         builtin_print_error_trailer(parser, streams.err, cmd);
190     }
191 }
192 
193 /// Print the backtrace and call for help that we use at the end of error messages.
builtin_print_error_trailer(parser_t & parser,output_stream_t & b,const wchar_t * cmd)194 void builtin_print_error_trailer(parser_t &parser, output_stream_t &b, const wchar_t *cmd) {
195     b.append(L"\n");
196     const wcstring stacktrace = parser.current_line();
197     // Don't print two empty lines if we don't have a stacktrace.
198     if (!stacktrace.empty()) {
199         b.append(stacktrace);
200         b.append(L"\n");
201     }
202     b.append_format(_(L"(Type 'help %ls' for related documentation)\n"), cmd);
203 }
204 
205 /// A generic bultin that only supports showing a help message. This is only a placeholder that
206 /// prints the help message. Useful for commands that live in the parser.
builtin_generic(parser_t & parser,io_streams_t & streams,const wchar_t ** argv)207 static maybe_t<int> builtin_generic(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
208     const wchar_t *cmd = argv[0];
209     int argc = builtin_count_args(argv);
210     help_only_cmd_opts_t opts;
211     int optind;
212     int retval = parse_help_only_cmd_opts(opts, &optind, argc, argv, parser, streams);
213     if (retval != STATUS_CMD_OK) return retval;
214 
215     if (opts.print_help) {
216         builtin_print_help(parser, streams, cmd);
217         return STATUS_CMD_OK;
218     }
219 
220     // Hackish - if we have no arguments other than the command, we are a "naked invocation" and we
221     // just print help.
222     if (argc == 1 || wcscmp(cmd, L"time") == 0) {
223         builtin_print_help(parser, streams, cmd);
224         return STATUS_INVALID_ARGS;
225     }
226 
227     return STATUS_CMD_ERROR;
228 }
229 
230 // How many bytes we read() at once.
231 // Since this is just for counting, it can be massive.
232 #define COUNT_CHUNK_SIZE (512 * 256)
233 /// Implementation of the builtin count command, used to count the number of arguments sent to it.
builtin_count(parser_t & parser,io_streams_t & streams,const wchar_t ** argv)234 static maybe_t<int> builtin_count(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
235     UNUSED(parser);
236     int argc = 0;
237 
238     // Count the newlines coming in via stdin like `wc -l`.
239     if (streams.stdin_is_directly_redirected) {
240         assert(streams.stdin_fd >= 0 &&
241                "Should have a valid fd since stdin is directly redirected");
242         char buf[COUNT_CHUNK_SIZE];
243         while (true) {
244             long n = read_blocked(streams.stdin_fd, buf, COUNT_CHUNK_SIZE);
245             if (n == 0) {
246                 break;
247             } else if (n < 0) {
248                 wperror(L"read");
249                 return STATUS_CMD_ERROR;
250             }
251             for (int i = 0; i < n; i++) {
252                 if (buf[i] == L'\n') {
253                     argc++;
254                 }
255             }
256         }
257     }
258 
259     // Always add the size of argv.
260     // That means if you call `something | count a b c`, you'll get the count of something _plus 3_.
261     argc += builtin_count_args(argv) - 1;
262     streams.out.append_format(L"%d\n", argc);
263     return argc == 0 ? STATUS_CMD_ERROR : STATUS_CMD_OK;
264 }
265 
266 /// This function handles both the 'continue' and the 'break' builtins that are used for loop
267 /// control.
builtin_break_continue(parser_t & parser,io_streams_t & streams,const wchar_t ** argv)268 static maybe_t<int> builtin_break_continue(parser_t &parser, io_streams_t &streams,
269                                            const wchar_t **argv) {
270     int is_break = (std::wcscmp(argv[0], L"break") == 0);
271     int argc = builtin_count_args(argv);
272 
273     if (argc != 1) {
274         wcstring error_message = format_string(BUILTIN_ERR_UNKNOWN, argv[0], argv[1]);
275         builtin_print_help(parser, streams, argv[0], &error_message);
276         return STATUS_INVALID_ARGS;
277     }
278 
279     // Paranoia: ensure we have a real loop.
280     bool has_loop = false;
281     for (const auto &b : parser.blocks()) {
282         if (b.type() == block_type_t::while_block || b.type() == block_type_t::for_block) {
283             has_loop = true;
284             break;
285         }
286         if (b.is_function_call()) break;
287     }
288     if (!has_loop) {
289         wcstring error_message = format_string(_(L"%ls: Not inside of loop\n"), argv[0]);
290         builtin_print_help(parser, streams, argv[0], &error_message);
291         return STATUS_CMD_ERROR;
292     }
293 
294     // Mark the status in the libdata.
295     parser.libdata().loop_status = is_break ? loop_status_t::breaks : loop_status_t::continues;
296     return STATUS_CMD_OK;
297 }
298 
299 /// Implementation of the builtin breakpoint command, used to launch the interactive debugger.
builtin_breakpoint(parser_t & parser,io_streams_t & streams,const wchar_t ** argv)300 static maybe_t<int> builtin_breakpoint(parser_t &parser, io_streams_t &streams,
301                                        const wchar_t **argv) {
302     const wchar_t *cmd = argv[0];
303     if (argv[1] != nullptr) {
304         streams.err.append_format(BUILTIN_ERR_ARG_COUNT1, cmd, 0, builtin_count_args(argv) - 1);
305         return STATUS_INVALID_ARGS;
306     }
307 
308     // If we're not interactive then we can't enter the debugger. So treat this command as a no-op.
309     if (!parser.is_interactive()) {
310         return STATUS_CMD_ERROR;
311     }
312 
313     // Ensure we don't allow creating a breakpoint at an interactive prompt. There may be a simpler
314     // or clearer way to do this but this works.
315     const block_t *block1 = parser.block_at_index(1);
316     if (!block1 || block1->type() == block_type_t::breakpoint) {
317         streams.err.append_format(_(L"%ls: Command not valid at an interactive prompt\n"), cmd);
318         return STATUS_ILLEGAL_CMD;
319     }
320 
321     const block_t *bpb = parser.push_block(block_t::breakpoint_block());
322     reader_read(parser, STDIN_FILENO, streams.io_chain ? *streams.io_chain : io_chain_t());
323     parser.pop_block(bpb);
324     return parser.get_last_status();
325 }
326 
builtin_true(parser_t & parser,io_streams_t & streams,const wchar_t ** argv)327 maybe_t<int> builtin_true(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
328     UNUSED(parser);
329     UNUSED(streams);
330     UNUSED(argv);
331     return STATUS_CMD_OK;
332 }
333 
builtin_false(parser_t & parser,io_streams_t & streams,const wchar_t ** argv)334 maybe_t<int> builtin_false(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
335     UNUSED(parser);
336     UNUSED(streams);
337     UNUSED(argv);
338     return STATUS_CMD_ERROR;
339 }
340 
builtin_gettext(parser_t & parser,io_streams_t & streams,const wchar_t ** argv)341 maybe_t<int> builtin_gettext(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
342     UNUSED(parser);
343     UNUSED(streams);
344     for (int i = 1; i < builtin_count_args(argv); i++) {
345         streams.out.append(_(argv[i]));
346     }
347     return STATUS_CMD_OK;
348 }
349 
350 // END OF BUILTIN COMMANDS
351 // Below are functions for handling the builtin commands.
352 // THESE MUST BE SORTED BY NAME! Completion lookup uses binary search.
353 
354 // Data about all the builtin commands in fish.
355 // Functions that are bound to builtin_generic are handled directly by the parser.
356 // NOTE: These must be kept in sorted order!
357 static const builtin_data_t builtin_datas[] = {
358     {L".", &builtin_source, N_(L"Evaluate contents of file")},
359     {L":", &builtin_true, N_(L"Return a successful result")},
360     {L"[", &builtin_test, N_(L"Test a condition")},
361     {L"_", &builtin_gettext, N_(L"Translate a string")},
362     {L"and", &builtin_generic, N_(L"Execute command if previous command succeeded")},
363     {L"argparse", &builtin_argparse, N_(L"Parse options in fish script")},
364     {L"begin", &builtin_generic, N_(L"Create a block of code")},
365     {L"bg", &builtin_bg, N_(L"Send job to background")},
366     {L"bind", &builtin_bind, N_(L"Handle fish key bindings")},
367     {L"block", &builtin_block, N_(L"Temporarily block delivery of events")},
368     {L"break", &builtin_break_continue, N_(L"Stop the innermost loop")},
369     {L"breakpoint", &builtin_breakpoint,
370      N_(L"Temporarily halt execution of a script and launch an interactive debug prompt")},
371     {L"builtin", &builtin_builtin, N_(L"Run a builtin command instead of a function")},
372     {L"case", &builtin_generic, N_(L"Conditionally execute a block of commands")},
373     {L"cd", &builtin_cd, N_(L"Change working directory")},
374     {L"command", &builtin_command, N_(L"Run a program instead of a function or builtin")},
375     {L"commandline", &builtin_commandline, N_(L"Set or get the commandline")},
376     {L"complete", &builtin_complete, N_(L"Edit command specific completions")},
377     {L"contains", &builtin_contains, N_(L"Search for a specified string in a list")},
378     {L"continue", &builtin_break_continue,
379      N_(L"Skip the rest of the current lap of the innermost loop")},
380     {L"count", &builtin_count, N_(L"Count the number of arguments")},
381     {L"disown", &builtin_disown, N_(L"Remove job from job list")},
382     {L"echo", &builtin_echo, N_(L"Print arguments")},
383     {L"else", &builtin_generic, N_(L"Evaluate block if condition is false")},
384     {L"emit", &builtin_emit, N_(L"Emit an event")},
385     {L"end", &builtin_generic, N_(L"End a block of commands")},
386     {L"eval", &builtin_eval, N_(L"Evaluate a string as a statement")},
387     {L"exec", &builtin_generic, N_(L"Run command in current process")},
388     {L"exit", &builtin_exit, N_(L"Exit the shell")},
389     {L"false", &builtin_false, N_(L"Return an unsuccessful result")},
390     {L"fg", &builtin_fg, N_(L"Send job to foreground")},
391     {L"for", &builtin_generic, N_(L"Perform a set of commands multiple times")},
392     {L"function", &builtin_generic, N_(L"Define a new function")},
393     {L"functions", &builtin_functions, N_(L"List or remove functions")},
394     {L"history", &builtin_history, N_(L"History of commands executed by user")},
395     {L"if", &builtin_generic, N_(L"Evaluate block if condition is true")},
396     {L"jobs", &builtin_jobs, N_(L"Print currently running jobs")},
397     {L"math", &builtin_math, N_(L"Evaluate math expressions")},
398     {L"not", &builtin_generic, N_(L"Negate exit status of job")},
399     {L"or", &builtin_generic, N_(L"Execute command if previous command failed")},
400     {L"printf", &builtin_printf, N_(L"Prints formatted text")},
401     {L"pwd", &builtin_pwd, N_(L"Print the working directory")},
402     {L"random", &builtin_random, N_(L"Generate random number")},
403     {L"read", &builtin_read, N_(L"Read a line of input into variables")},
404     {L"realpath", &builtin_realpath, N_(L"Convert path to absolute path without symlinks")},
405     {L"return", &builtin_return, N_(L"Stop the currently evaluated function")},
406     {L"set", &builtin_set, N_(L"Handle environment variables")},
407     {L"set_color", &builtin_set_color, N_(L"Set the terminal color")},
408     {L"source", &builtin_source, N_(L"Evaluate contents of file")},
409     {L"status", &builtin_status, N_(L"Return status information about fish")},
410     {L"string", &builtin_string, N_(L"Manipulate strings")},
411     {L"switch", &builtin_generic, N_(L"Conditionally execute a block of commands")},
412     {L"test", &builtin_test, N_(L"Test a condition")},
413     {L"time", &builtin_generic, N_(L"Measure how long a command or block takes")},
414     {L"true", &builtin_true, N_(L"Return a successful result")},
415     {L"type", &builtin_type, N_(L"Check if a thing is a thing")},
416     {L"ulimit", &builtin_ulimit, N_(L"Set or get the shells resource usage limits")},
417     {L"wait", &builtin_wait, N_(L"Wait for background processes completed")},
418     {L"while", &builtin_generic, N_(L"Perform a command multiple times")},
419 };
420 
421 #define BUILTIN_COUNT (sizeof builtin_datas / sizeof *builtin_datas)
422 
423 /// Look up a builtin_data_t for a specified builtin
424 ///
425 /// @param  name
426 ///    Name of the builtin
427 ///
428 /// @return
429 ///    Pointer to a builtin_data_t
430 ///
builtin_lookup(const wcstring & name)431 static const builtin_data_t *builtin_lookup(const wcstring &name) {
432     const builtin_data_t *array_end = builtin_datas + BUILTIN_COUNT;
433     const builtin_data_t *found = std::lower_bound(builtin_datas, array_end, name);
434     if (found != array_end && name == found->name) {
435         return found;
436     }
437     return nullptr;
438 }
439 
440 /// Initialize builtin data.
builtin_init()441 void builtin_init() {
442     for (size_t i = 0; i < BUILTIN_COUNT; i++) {
443         const wchar_t *name = builtin_datas[i].name;
444         intern_static(name);
445         assert((i == 0 || std::wcscmp(builtin_datas[i - 1].name, name) < 0) &&
446                "builtins are not sorted alphabetically");
447     }
448 }
449 
450 /// Is there a builtin command with the given name?
builtin_exists(const wcstring & cmd)451 bool builtin_exists(const wcstring &cmd) { return static_cast<bool>(builtin_lookup(cmd)); }
452 
453 /// Is the command a keyword we need to special-case the handling of `-h` and `--help`.
454 static const wchar_t *const help_builtins[] = {L"for", L"while",  L"function", L"if",
455                                                L"end", L"switch", L"case"};
cmd_needs_help(const wcstring & cmd)456 static bool cmd_needs_help(const wcstring &cmd) { return contains(help_builtins, cmd); }
457 
458 /// Execute a builtin command
builtin_run(parser_t & parser,const wcstring_list_t & argv,io_streams_t & streams)459 proc_status_t builtin_run(parser_t &parser, const wcstring_list_t &argv, io_streams_t &streams) {
460     if (argv.empty()) return proc_status_t::from_exit_code(STATUS_INVALID_ARGS);
461     const wcstring &cmdname = argv.front();
462 
463     // We can be handed a keyword by the parser as if it was a command. This happens when the user
464     // follows the keyword by `-h` or `--help`. Since it isn't really a builtin command we need to
465     // handle displaying help for it here.
466     if (argv.size() == 2 && parse_util_argument_is_help(argv[1]) && cmd_needs_help(cmdname)) {
467         builtin_print_help(parser, streams, cmdname.c_str());
468         return proc_status_t::from_exit_code(STATUS_CMD_OK);
469     }
470 
471     if (const builtin_data_t *data = builtin_lookup(cmdname)) {
472         // Construct the permutable argv array which the builtin expects, and execute the builtin.
473         null_terminated_array_t<wchar_t> argv_arr(argv);
474         maybe_t<int> builtin_ret = data->func(parser, streams, argv_arr.get());
475 
476         // Flush our out and error streams, and check for their errors.
477         int out_ret = streams.out.flush_and_check_error();
478         int err_ret = streams.err.flush_and_check_error();
479 
480         // Resolve our status code.
481         // If the builtin itself produced an error, use that error.
482         // Otherwise use any errors from writing to out and writing to err, in that order.
483         int code = builtin_ret ? *builtin_ret : 0;
484         if (code == 0) code = out_ret;
485         if (code == 0) code = err_ret;
486 
487         // The exit code is cast to an 8-bit unsigned integer, so saturate to 255. Otherwise,
488         // multiples of 256 are reported as 0.
489         if (code > 255) code = 255;
490 
491         // Handle the case of an empty status.
492         if (code == 0 && !builtin_ret.has_value()) {
493             return proc_status_t::empty();
494         }
495         return proc_status_t::from_exit_code(code);
496     }
497 
498     FLOGF(error, UNKNOWN_BUILTIN_ERR_MSG, cmdname.c_str());
499     return proc_status_t::from_exit_code(STATUS_CMD_ERROR);
500 }
501 
502 /// Returns a list of all builtin names.
builtin_get_names()503 wcstring_list_t builtin_get_names() {
504     wcstring_list_t result;
505     result.reserve(BUILTIN_COUNT);
506     for (const auto &builtin_data : builtin_datas) {
507         result.push_back(builtin_data.name);
508     }
509     return result;
510 }
511 
512 /// Insert all builtin names into list.
builtin_get_names(completion_list_t * list)513 void builtin_get_names(completion_list_t *list) {
514     assert(list != nullptr);
515     list->reserve(list->size() + BUILTIN_COUNT);
516     for (const auto &builtin_data : builtin_datas) {
517         append_completion(list, builtin_data.name);
518     }
519 }
520 
521 /// Return a one-line description of the specified builtin.
builtin_get_desc(const wcstring & name)522 const wchar_t *builtin_get_desc(const wcstring &name) {
523     const wchar_t *result = L"";
524     const builtin_data_t *builtin = builtin_lookup(name);
525     if (builtin) {
526         result = _(builtin->desc);
527     }
528     return result;
529 }
530