1 // Functions used for implementing the complete builtin.
2 #include "config.h"  // IWYU pragma: keep
3 
4 #include <cstddef>
5 #include <cwchar>
6 #include <memory>
7 #include <string>
8 #include <vector>
9 
10 #include "builtin.h"
11 #include "color.h"
12 #include "common.h"
13 #include "complete.h"
14 #include "env.h"
15 #include "fallback.h"  // IWYU pragma: keep
16 #include "highlight.h"
17 #include "io.h"
18 #include "parse_constants.h"
19 #include "parse_util.h"
20 #include "parser.h"
21 #include "reader.h"
22 #include "wcstringutil.h"
23 #include "wgetopt.h"
24 #include "wutil.h"  // IWYU pragma: keep
25 
26 // builtin_complete_* are a set of rather silly looping functions that make sure that all the proper
27 // combinations of complete_add or complete_remove get called. This is needed since complete allows
28 // you to specify multiple switches on a single commandline, like 'complete -s a -s b -s c', but the
29 // complete_add function only accepts one short switch and one long switch.
30 
31 /// Silly function.
builtin_complete_add2(const wchar_t * cmd,bool cmd_is_path,const wchar_t * short_opt,const wcstring_list_t & gnu_opts,const wcstring_list_t & old_opts,completion_mode_t result_mode,const wchar_t * condition,const wchar_t * comp,const wchar_t * desc,int flags)32 static void builtin_complete_add2(const wchar_t *cmd, bool cmd_is_path, const wchar_t *short_opt,
33                                   const wcstring_list_t &gnu_opts, const wcstring_list_t &old_opts,
34                                   completion_mode_t result_mode, const wchar_t *condition,
35                                   const wchar_t *comp, const wchar_t *desc, int flags) {
36     for (const wchar_t *s = short_opt; *s; s++) {
37         complete_add(cmd, cmd_is_path, wcstring{*s}, option_type_short, result_mode, condition,
38                      comp, desc, flags);
39     }
40 
41     for (const wcstring &gnu_opt : gnu_opts) {
42         complete_add(cmd, cmd_is_path, gnu_opt, option_type_double_long, result_mode, condition,
43                      comp, desc, flags);
44     }
45 
46     for (const wcstring &old_opt : old_opts) {
47         complete_add(cmd, cmd_is_path, old_opt, option_type_single_long, result_mode, condition,
48                      comp, desc, flags);
49     }
50 
51     if (old_opts.empty() && gnu_opts.empty() && short_opt[0] == L'\0') {
52         complete_add(cmd, cmd_is_path, wcstring(), option_type_args_only, result_mode, condition,
53                      comp, desc, flags);
54     }
55 }
56 
57 /// Silly function.
builtin_complete_add(const wcstring_list_t & cmds,const wcstring_list_t & paths,const wchar_t * short_opt,const wcstring_list_t & gnu_opt,const wcstring_list_t & old_opt,completion_mode_t result_mode,const wchar_t * condition,const wchar_t * comp,const wchar_t * desc,int flags)58 static void builtin_complete_add(const wcstring_list_t &cmds, const wcstring_list_t &paths,
59                                  const wchar_t *short_opt, const wcstring_list_t &gnu_opt,
60                                  const wcstring_list_t &old_opt, completion_mode_t result_mode,
61                                  const wchar_t *condition, const wchar_t *comp, const wchar_t *desc,
62                                  int flags) {
63     for (const wcstring &cmd : cmds) {
64         builtin_complete_add2(cmd.c_str(), false /* not path */, short_opt, gnu_opt, old_opt,
65                               result_mode, condition, comp, desc, flags);
66     }
67 
68     for (const wcstring &path : paths) {
69         builtin_complete_add2(path.c_str(), true /* is path */, short_opt, gnu_opt, old_opt,
70                               result_mode, condition, comp, desc, flags);
71     }
72 }
73 
builtin_complete_remove_cmd(const wcstring & cmd,bool cmd_is_path,const wchar_t * short_opt,const wcstring_list_t & gnu_opt,const wcstring_list_t & old_opt)74 static void builtin_complete_remove_cmd(const wcstring &cmd, bool cmd_is_path,
75                                         const wchar_t *short_opt, const wcstring_list_t &gnu_opt,
76                                         const wcstring_list_t &old_opt) {
77     bool removed = false;
78     for (const wchar_t *s = short_opt; *s; s++) {
79         complete_remove(cmd, cmd_is_path, wcstring{*s}, option_type_short);
80         removed = true;
81     }
82 
83     for (const wcstring &opt : old_opt) {
84         complete_remove(cmd, cmd_is_path, opt, option_type_single_long);
85         removed = true;
86     }
87 
88     for (const wcstring &opt : gnu_opt) {
89         complete_remove(cmd, cmd_is_path, opt, option_type_double_long);
90         removed = true;
91     }
92 
93     if (!removed) {
94         // This means that all loops were empty.
95         complete_remove_all(cmd, cmd_is_path);
96     }
97 }
98 
builtin_complete_remove(const wcstring_list_t & cmds,const wcstring_list_t & paths,const wchar_t * short_opt,const wcstring_list_t & gnu_opt,const wcstring_list_t & old_opt)99 static void builtin_complete_remove(const wcstring_list_t &cmds, const wcstring_list_t &paths,
100                                     const wchar_t *short_opt, const wcstring_list_t &gnu_opt,
101                                     const wcstring_list_t &old_opt) {
102     for (const wcstring &cmd : cmds) {
103         builtin_complete_remove_cmd(cmd, false /* not path */, short_opt, gnu_opt, old_opt);
104     }
105 
106     for (const wcstring &path : paths) {
107         builtin_complete_remove_cmd(path, true /* is path */, short_opt, gnu_opt, old_opt);
108     }
109 }
110 
builtin_complete_print(const wcstring & cmd,io_streams_t & streams,parser_t & parser)111 static void builtin_complete_print(const wcstring &cmd, io_streams_t &streams, parser_t &parser) {
112     const wcstring repr = complete_print(cmd);
113 
114     // colorize if interactive
115     if (!streams.out_is_redirected && isatty(STDOUT_FILENO)) {
116         std::vector<highlight_spec_t> colors;
117         highlight_shell(repr, colors, parser.context());
118         streams.out.append(str2wcstring(colorize(repr, colors, parser.vars())));
119     } else {
120         streams.out.append(repr);
121     }
122 }
123 
124 /// The complete builtin. Used for specifying programmable tab-completions. Calls the functions in
125 // complete.cpp for any heavy lifting.
builtin_complete(parser_t & parser,io_streams_t & streams,const wchar_t ** argv)126 maybe_t<int> builtin_complete(parser_t &parser, io_streams_t &streams, const wchar_t **argv) {
127     ASSERT_IS_MAIN_THREAD();
128 
129     const wchar_t *cmd = argv[0];
130     int argc = builtin_count_args(argv);
131     completion_mode_t result_mode{};
132     int remove = 0;
133     wcstring short_opt;
134     wcstring_list_t gnu_opt, old_opt, subcommand;
135     const wchar_t *comp = L"", *desc = L"", *condition = L"";
136     bool do_complete = false;
137     bool have_do_complete_param = false;
138     wcstring do_complete_param;
139     wcstring_list_t cmd_to_complete;
140     wcstring_list_t path;
141     wcstring_list_t wrap_targets;
142     bool preserve_order = false;
143 
144     static const wchar_t *const short_options = L":a:c:p:s:l:o:d:fFrxeuAn:C::w:hk";
145     static const struct woption long_options[] = {
146         {L"exclusive", no_argument, nullptr, 'x'},
147         {L"no-files", no_argument, nullptr, 'f'},
148         {L"force-files", no_argument, nullptr, 'F'},
149         {L"require-parameter", no_argument, nullptr, 'r'},
150         {L"path", required_argument, nullptr, 'p'},
151         {L"command", required_argument, nullptr, 'c'},
152         {L"short-option", required_argument, nullptr, 's'},
153         {L"long-option", required_argument, nullptr, 'l'},
154         {L"old-option", required_argument, nullptr, 'o'},
155         {L"subcommand", required_argument, nullptr, 'S'},
156         {L"description", required_argument, nullptr, 'd'},
157         {L"arguments", required_argument, nullptr, 'a'},
158         {L"erase", no_argument, nullptr, 'e'},
159         {L"unauthoritative", no_argument, nullptr, 'u'},
160         {L"authoritative", no_argument, nullptr, 'A'},
161         {L"condition", required_argument, nullptr, 'n'},
162         {L"wraps", required_argument, nullptr, 'w'},
163         {L"do-complete", optional_argument, nullptr, 'C'},
164         {L"help", no_argument, nullptr, 'h'},
165         {L"keep-order", no_argument, nullptr, 'k'},
166         {nullptr, 0, nullptr, 0}};
167 
168     int opt;
169     wgetopter_t w;
170     while ((opt = w.wgetopt_long(argc, argv, short_options, long_options, nullptr)) != -1) {
171         switch (opt) {
172             case 'x': {
173                 result_mode.no_files = true;
174                 result_mode.requires_param = true;
175                 break;
176             }
177             case 'f': {
178                 result_mode.no_files = true;
179                 break;
180             }
181             case 'F': {
182                 result_mode.force_files = true;
183                 break;
184             }
185             case 'r': {
186                 result_mode.requires_param = true;
187                 break;
188             }
189             case 'k': {
190                 preserve_order = true;
191                 break;
192             }
193             case 'p':
194             case 'c': {
195                 wcstring tmp;
196                 if (unescape_string(w.woptarg, &tmp, UNESCAPE_SPECIAL)) {
197                     if (opt == 'p')
198                         path.push_back(tmp);
199                     else
200                         cmd_to_complete.push_back(tmp);
201                 } else {
202                     streams.err.append_format(_(L"%ls: Invalid token '%ls'\n"), cmd, w.woptarg);
203                     return STATUS_INVALID_ARGS;
204                 }
205                 break;
206             }
207             case 'd': {
208                 desc = w.woptarg;
209                 assert(desc);
210                 break;
211             }
212             case 'u': {
213                 // This option was removed in commit 1911298 and is now a no-op.
214                 break;
215             }
216             case 'A': {
217                 // This option was removed in commit 1911298 and is now a no-op.
218                 break;
219             }
220             case 's': {
221                 short_opt.append(w.woptarg);
222                 if (w.woptarg[0] == '\0') {
223                     streams.err.append_format(_(L"%ls: -s requires a non-empty string\n"), cmd);
224                     return STATUS_INVALID_ARGS;
225                 }
226                 break;
227             }
228             case 'l': {
229                 gnu_opt.push_back(w.woptarg);
230                 if (w.woptarg[0] == '\0') {
231                     streams.err.append_format(_(L"%ls: -l requires a non-empty string\n"), cmd);
232                     return STATUS_INVALID_ARGS;
233                 }
234                 break;
235             }
236             case 'o': {
237                 old_opt.push_back(w.woptarg);
238                 if (w.woptarg[0] == '\0') {
239                     streams.err.append_format(_(L"%ls: -o requires a non-empty string\n"), cmd);
240                     return STATUS_INVALID_ARGS;
241                 }
242                 break;
243             }
244             case 'S': {
245                 subcommand.push_back(w.woptarg);
246                 if (w.woptarg[0] == '\0') {
247                     streams.err.append_format(_(L"%ls: -S requires a non-empty string\n"), cmd);
248                     return STATUS_INVALID_ARGS;
249                 }
250                 break;
251             }
252             case 'a': {
253                 comp = w.woptarg;
254                 assert(comp);
255                 break;
256             }
257             case 'e': {
258                 remove = 1;
259                 break;
260             }
261             case 'n': {
262                 condition = w.woptarg;
263                 assert(condition);
264                 break;
265             }
266             case 'w': {
267                 wrap_targets.push_back(w.woptarg);
268                 break;
269             }
270             case 'C': {
271                 do_complete = true;
272                 have_do_complete_param = w.woptarg != nullptr;
273                 if (have_do_complete_param) do_complete_param = w.woptarg;
274                 break;
275             }
276             case 'h': {
277                 builtin_print_help(parser, streams, cmd);
278                 return STATUS_CMD_OK;
279             }
280             case ':': {
281                 builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
282                 return STATUS_INVALID_ARGS;
283             }
284             case '?': {
285                 builtin_unknown_option(parser, streams, cmd, argv[w.woptind - 1]);
286                 return STATUS_INVALID_ARGS;
287             }
288             default: {
289                 DIE("unexpected retval from wgetopt_long");
290             }
291         }
292     }
293 
294     if (result_mode.no_files && result_mode.force_files) {
295         streams.err.append_format(BUILTIN_ERR_COMBO2, L"complete",
296                                   L"'--no-files' and '--force-files'");
297         return STATUS_INVALID_ARGS;
298     }
299 
300     if (w.woptind != argc) {
301         // Use one left-over arg as the do-complete argument
302         // to enable `complete -C "git check"`.
303         if (do_complete && !have_do_complete_param && argc == w.woptind + 1) {
304             do_complete_param = argv[argc - 1];
305             have_do_complete_param = true;
306         } else if (!do_complete && cmd_to_complete.empty() && argc == w.woptind + 1) {
307             // Or use one left-over arg as the command to complete
308             cmd_to_complete.push_back(argv[argc - 1]);
309         } else {
310             streams.err.append_format(BUILTIN_ERR_TOO_MANY_ARGUMENTS, cmd);
311             builtin_print_error_trailer(parser, streams.err, cmd);
312             return STATUS_INVALID_ARGS;
313         }
314     }
315 
316     if (condition && std::wcslen(condition)) {
317         const wcstring condition_string = condition;
318         parse_error_list_t errors;
319         if (parse_util_detect_errors(condition_string, &errors)) {
320             streams.err.append_format(L"%ls: Condition '%ls' contained a syntax error", cmd,
321                                       condition);
322             for (const auto &error : errors) {
323                 streams.err.append_format(L"\n%ls: ", cmd);
324                 streams.err.append(error.describe(condition_string, parser.is_interactive()));
325             }
326             return STATUS_CMD_ERROR;
327         }
328     }
329 
330     if (comp && std::wcslen(comp)) {
331         wcstring prefix;
332         prefix.append(cmd);
333         prefix.append(L": ");
334 
335         if (maybe_t<wcstring> err_text = parse_util_detect_errors_in_argument_list(comp, prefix)) {
336             streams.err.append_format(L"%ls: Completion '%ls' contained a syntax error\n", cmd,
337                                       comp);
338             streams.err.append(*err_text);
339             streams.err.push_back(L'\n');
340             return STATUS_CMD_ERROR;
341         }
342     }
343 
344     if (do_complete) {
345         if (!have_do_complete_param) {
346             // No argument given, try to use the current commandline.
347             const wchar_t *cmd = reader_get_buffer();
348             if (cmd == nullptr) {
349                 // This corresponds to using 'complete -C' in non-interactive mode.
350                 // See #2361    .
351                 builtin_missing_argument(parser, streams, cmd, argv[w.woptind - 1]);
352                 return STATUS_INVALID_ARGS;
353             }
354             do_complete_param = cmd;
355         }
356         const wchar_t *token;
357 
358         parse_util_token_extent(do_complete_param.c_str(), do_complete_param.size(), &token,
359                                 nullptr, nullptr, nullptr);
360 
361         // Create a scoped transient command line, so that builtin_commandline will see our
362         // argument, not the reader buffer.
363         parser.libdata().transient_commandlines.push_back(do_complete_param);
364         cleanup_t remove_transient([&] { parser.libdata().transient_commandlines.pop_back(); });
365 
366         // Prevent accidental recursion (see #6171).
367         if (!parser.libdata().builtin_complete_current_commandline) {
368             if (!have_do_complete_param)
369                 parser.libdata().builtin_complete_current_commandline = true;
370 
371             completion_list_t comp =
372                 complete(do_complete_param,
373                          {completion_request_t::fuzzy_match, completion_request_t::descriptions},
374                          parser.context());
375 
376             for (const auto &next : comp) {
377                 // Make a fake commandline, and then apply the completion to it.
378                 const wcstring faux_cmdline = token;
379                 size_t tmp_cursor = faux_cmdline.size();
380                 wcstring faux_cmdline_with_completion = completion_apply_to_command_line(
381                     next.completion, next.flags, faux_cmdline, &tmp_cursor, false);
382 
383                 // completion_apply_to_command_line will append a space unless COMPLETE_NO_SPACE
384                 // is set. We don't want to set COMPLETE_NO_SPACE because that won't close
385                 // quotes. What we want is to close the quote, but not append the space. So we
386                 // just look for the space and clear it.
387                 if (!(next.flags & COMPLETE_NO_SPACE) &&
388                     string_suffixes_string(L" ", faux_cmdline_with_completion)) {
389                     faux_cmdline_with_completion.resize(faux_cmdline_with_completion.size() - 1);
390                 }
391 
392                 // The input data is meant to be something like you would have on the command
393                 // line, e.g. includes backslashes. The output should be raw, i.e. unescaped. So
394                 // we need to unescape the command line. See #1127.
395                 unescape_string_in_place(&faux_cmdline_with_completion, UNESCAPE_DEFAULT);
396                 streams.out.append(faux_cmdline_with_completion);
397 
398                 // Append any description.
399                 if (!next.description.empty()) {
400                     streams.out.push_back(L'\t');
401                     streams.out.append(next.description);
402                 }
403                 streams.out.push_back(L'\n');
404             }
405 
406             parser.libdata().builtin_complete_current_commandline = false;
407         }
408     } else if (path.empty() && gnu_opt.empty() && short_opt.empty() && old_opt.empty() && !remove &&
409                !*comp && !*desc && !*condition && wrap_targets.empty() && !result_mode.no_files &&
410                !result_mode.force_files && !result_mode.requires_param) {
411         // No arguments that would add or remove anything specified, so we print the definitions of
412         // all matching completions.
413         if (cmd_to_complete.empty()) {
414             builtin_complete_print(L"", streams, parser);
415         } else {
416             for (auto &cmd : cmd_to_complete) {
417                 builtin_complete_print(cmd, streams, parser);
418             }
419         }
420     } else {
421         int flags = COMPLETE_AUTO_SPACE;
422         if (preserve_order) {
423             flags |= COMPLETE_DONT_SORT;
424         }
425 
426         if (remove) {
427             builtin_complete_remove(cmd_to_complete, path, short_opt.c_str(), gnu_opt, old_opt);
428         } else {
429             builtin_complete_add(cmd_to_complete, path, short_opt.c_str(), gnu_opt, old_opt,
430                                  result_mode, condition, comp, desc, flags);
431         }
432 
433         // Handle wrap targets (probably empty). We only wrap commands, not paths.
434         for (const auto &wrap_target : wrap_targets) {
435             for (const auto &i : cmd_to_complete) {
436                 (remove ? complete_remove_wrapper : complete_add_wrapper)(i, wrap_target);
437             }
438         }
439     }
440 
441     return STATUS_CMD_OK;
442 }
443