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