master
cpp 742 lines 23 KB
Raw
1 /*++
2
3 Copyright (c) Microsoft. All rights reserved.
4
5 Module Name:
6
7 Command.cpp
8
9 Abstract:
10
11 Implementation of command execution logic.
12
13 --*/
14 #include "Argument.h"
15 #include "Command.h"
16 #include "Invocation.h"
17 #include "ArgumentParser.h"
18 #include "RootCommand.h"
19 #include "TableOutput.h"
20
21 #include <algorithm>
22 #include <typeinfo>
23
24 using namespace wsl::shared;
25 using namespace wsl::windows::common::wslutil;
26 using namespace wsl::windows::common::vt;
27 using namespace wsl::windows::wslc::execution;
28
29 namespace wsl::windows::wslc {
30
31 std::wstring s_ExecutableName = L"wslc";
32
33 namespace {
34 std::vector<std::wstring> WrapAliases(std::span<const std::wstring> aliases, std::optional<size_t> consoleWidth, size_t indent)
35 {
36 std::vector<std::wstring> lines;
37 std::wstring line(indent, L' ');
38
39 for (size_t i = 0; i < aliases.size(); ++i)
40 {
41 std::wstring token = aliases[i];
42 if (i + 1 < aliases.size())
43 {
44 token += L',';
45 }
46
47 const bool hasAlias = line.size() > indent;
48 const size_t requiredWidth = token.size() + (hasAlias ? 1 : 0);
49 if (hasAlias && consoleWidth.has_value() && line.size() + requiredWidth > *consoleWidth)
50 {
51 lines.emplace_back(std::move(line));
52 line.assign(indent, L' ');
53 }
54 else if (hasAlias)
55 {
56 line += L' ';
57 }
58
59 line += token;
60 }
61
62 if (line.size() > indent)
63 {
64 lines.emplace_back(std::move(line));
65 }
66
67 return lines;
68 }
69
70 std::wstring FormatCommandInvocation(const Command& command, std::wstring_view name)
71 {
72 std::wstring commandChain = command.FullName();
73 const auto firstSplit = commandChain.find_first_of(Command::ParentSplitChar);
74 if (firstSplit == std::wstring::npos)
75 {
76 return s_ExecutableName;
77 }
78
79 commandChain = commandChain.substr(firstSplit + 1);
80 const auto lastSplit = commandChain.find_last_of(Command::ParentSplitChar);
81 commandChain.replace(lastSplit == std::wstring::npos ? 0 : lastSplit + 1, std::wstring::npos, name);
82 std::ranges::replace(commandChain, Command::ParentSplitChar, L' ');
83
84 std::wstring invocation = s_ExecutableName;
85 invocation += L' ';
86 invocation += commandChain;
87 return invocation;
88 }
89
90 void AddCommandInvocations(const Command& command, std::vector<std::wstring>& invocations)
91 {
92 const auto addInvocation = [&](std::wstring_view name) {
93 auto invocation = FormatCommandInvocation(command, name);
94 if (std::ranges::find(invocations, invocation) == invocations.end())
95 {
96 invocations.emplace_back(std::move(invocation));
97 }
98 };
99
100 addInvocation(command.Name());
101 for (const auto alias : command.Aliases())
102 {
103 addInvocation(alias);
104 }
105 }
106
107 void FindCommandInvocations(const Command& target, const Command& parent, std::vector<std::wstring>& invocations)
108 {
109 for (const auto& command : parent.GetCommands())
110 {
111 if (typeid(target) == typeid(*command))
112 {
113 AddCommandInvocations(*command, invocations);
114 }
115
116 FindCommandInvocations(target, *command, invocations);
117 }
118 }
119
120 std::vector<std::wstring> GetCommandInvocations(const Command& command)
121 {
122 std::vector<std::wstring> invocations;
123 FindCommandInvocations(command, RootCommand(), invocations);
124
125 if (invocations.empty())
126 {
127 AddCommandInvocations(command, invocations);
128 }
129
130 if (invocations.size() == 1)
131 {
132 invocations.clear();
133 }
134
135 return invocations;
136 }
137 } // namespace
138
139 Command::Command(std::wstring_view name, std::vector<std::wstring_view>&& aliases, const std::wstring& parent) :
140 m_name(name), m_aliases(std::move(aliases))
141 {
142 if (!parent.empty())
143 {
144 m_fullName.reserve(parent.length() + 1 + name.length());
145 m_fullName = parent;
146 m_fullName += ParentSplitChar;
147 m_fullName += name;
148 }
149 else
150 {
151 m_fullName = name;
152 }
153 }
154
155 void Command::OutputHelp(Terminal& terminal, HelpOutput output, const CommandException* exception, std::span<const Argument> relevantArguments) const
156 {
157 constexpr size_t c_helpRowIndent = 2;
158 constexpr size_t c_helpColumnPadding = 2;
159 const bool fullHelp = output == HelpOutput::Full;
160 const bool commandHelp = output == HelpOutput::Command;
161 const bool argumentHelp = output == HelpOutput::Argument;
162 const auto helpLevel = fullHelp ? Terminal::Level::Output : Terminal::Level::Info;
163
164 // Emphasis sequences for help output.
165 static const auto& HelpHeadingEmphasis = Format::Bright;
166 static const auto& HelpCommandEmphasis = Format::Bright;
167 static const auto& HelpArgumentEmphasis = Format::Bright;
168 static const auto& HelpMetaEmphasis = Format::Dim;
169 static const auto& HelpPlaceholderEmphasis = Format::Fg::BrightCyan;
170
171 if (fullHelp)
172 {
173 terminal.Write(helpLevel, L"{}{}{}\n\n", HelpMetaEmphasis, Localization::WSLCCLI_CopyrightHeader(), Format::Default);
174 }
175
176 // Error if given
177 if (exception)
178 {
179 terminal.Error(L"{}\n\n", exception->Message());
180 }
181
182 if (fullHelp)
183 {
184 terminal.Write(helpLevel, L"{}\n\n", LongDescription());
185 }
186
187 // Build command chain from full name (replace ParentSplitChar with spaces, strip root).
188 std::wstring commandChain = FullName();
189 size_t firstSplit = commandChain.find_first_of(ParentSplitChar);
190 if (firstSplit == std::wstring::npos)
191 {
192 commandChain.clear();
193 }
194 else
195 {
196 commandChain = commandChain.substr(firstSplit + 1);
197 for (wchar_t& c : commandChain)
198 {
199 if (c == ParentSplitChar)
200 {
201 c = L' ';
202 }
203 }
204 }
205
206 std::vector<std::wstring> commandAliases;
207 if (fullHelp)
208 {
209 commandAliases = GetCommandInvocations(*this);
210 }
211 auto commands = GetCommands();
212 auto arguments = GetAllArguments();
213 std::vector<Argument> helpArguments;
214 if (fullHelp)
215 {
216 helpArguments = arguments;
217 }
218 else if (argumentHelp)
219 {
220 helpArguments.assign(relevantArguments.begin(), relevantArguments.end());
221 }
222
223 std::vector<Argument> standardArgs;
224 std::vector<Argument> positionalArgs;
225 std::vector<Argument> forwardArgs;
226 for (const auto& arg : arguments)
227 {
228 switch (arg.Kind())
229 {
230 case Kind::Flag:
231 case Kind::Value:
232 standardArgs.emplace_back(arg);
233 break;
234 case Kind::Positional:
235 positionalArgs.emplace_back(arg);
236 break;
237 case Kind::Forward:
238 forwardArgs.emplace_back(arg);
239 break;
240 }
241 }
242
243 const bool hasArguments = !positionalArgs.empty();
244 const bool hasOptions = !standardArgs.empty();
245 const bool hasForwardArgs = !forwardArgs.empty();
246
247 std::vector<Argument> helpStandardArgs;
248 std::vector<Argument> helpPositionalArgs;
249 std::vector<Argument> helpForwardArgs;
250 for (const auto& arg : helpArguments)
251 {
252 switch (arg.Kind())
253 {
254 case Kind::Flag:
255 case Kind::Value:
256 helpStandardArgs.emplace_back(arg);
257 break;
258 case Kind::Positional:
259 helpPositionalArgs.emplace_back(arg);
260 break;
261 case Kind::Forward:
262 helpForwardArgs.emplace_back(arg);
263 break;
264 }
265 }
266
267 const bool hasHelpArguments = !helpPositionalArgs.empty();
268 const bool hasHelpOptions = !helpStandardArgs.empty();
269 const bool hasHelpForwardArgs = !helpForwardArgs.empty();
270
271 auto globalArgs = RootCommand().GetGlobalArguments();
272
273 // Build usage line with Write calls for each segment.
274 {
275 std::wstring usageText = Localization::WSLCCLI_Usage(s_ExecutableName, std::wstring_view{commandChain});
276
277 while (!usageText.empty() && usageText.back() == L' ')
278 {
279 usageText.pop_back();
280 }
281
282 terminal.Write(helpLevel, L"{}{}{}", HelpHeadingEmphasis, usageText, Format::Default);
283
284 if (!commands.empty())
285 {
286 if (!arguments.empty())
287 {
288 terminal.Write(helpLevel, L" {}[{}", HelpMetaEmphasis, Format::Default);
289 }
290 else
291 {
292 terminal.Write(helpLevel, L" ");
293 }
294
295 terminal.Write(
296 helpLevel,
297 L"{}<{}{}{}{}{}>{}",
298 HelpMetaEmphasis,
299 Format::Default,
300 HelpPlaceholderEmphasis,
301 Localization::WSLCCLI_Command(),
302 Format::Default,
303 HelpMetaEmphasis,
304 Format::Default);
305 if (!arguments.empty())
306 {
307 terminal.Write(helpLevel, L"{}]{}", HelpMetaEmphasis, Format::Default);
308 }
309 }
310
311 if (hasOptions)
312 {
313 terminal.Write(
314 helpLevel,
315 L" {}[<{}{}{}{}{}>]{}",
316 HelpMetaEmphasis,
317 Format::Default,
318 HelpPlaceholderEmphasis,
319 Localization::WSLCCLI_Options(),
320 Format::Default,
321 HelpMetaEmphasis,
322 Format::Default);
323 }
324
325 for (const auto& arg : positionalArgs)
326 {
327 terminal.Write(helpLevel, L" ");
328 if (!arg.Required())
329 {
330 terminal.Write(helpLevel, L"{}[{}", HelpMetaEmphasis, Format::Default);
331 }
332
333 terminal.Write(
334 helpLevel, L"{}<{}{}{}{}{}>{}", HelpMetaEmphasis, Format::Default, HelpPlaceholderEmphasis, arg.Name(), Format::Default, HelpMetaEmphasis, Format::Default);
335 if (arg.IsUnlimited())
336 {
337 terminal.Write(helpLevel, L"{}...{}", HelpMetaEmphasis, Format::Default);
338 }
339
340 if (!arg.Required())
341 {
342 terminal.Write(helpLevel, L"{}]{}", HelpMetaEmphasis, Format::Default);
343 }
344 }
345
346 if (hasForwardArgs)
347 {
348 terminal.Write(
349 helpLevel,
350 L" {}[<{}{}{}{}{}>...]{}",
351 HelpMetaEmphasis,
352 Format::Default,
353 HelpPlaceholderEmphasis,
354 forwardArgs.front().Name(),
355 Format::Default,
356 HelpMetaEmphasis,
357 Format::Default);
358 }
359
360 terminal.Write(helpLevel, L"\n\n");
361 }
362
363 if (fullHelp && !commandAliases.empty())
364 {
365 terminal.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingAliases(), Format::Default);
366
367 std::optional<size_t> consoleWidth;
368 if (const auto width = terminal.GetConsoleWidth(helpLevel); width.has_value() && *width > 0)
369 {
370 consoleWidth = static_cast<size_t>(*width);
371 }
372
373 for (const auto& line : WrapAliases(commandAliases, consoleWidth, c_helpRowIndent))
374 {
375 terminal.Write(helpLevel, L"{}\n", line);
376 }
377 terminal.Write(helpLevel, L"\n");
378 }
379
380 // Col0: name/command
381 // Col1: description (word-wraps at computed column width)
382 const auto MakeHelpTable = [&terminal, helpLevel]() -> TableOutput<2> {
383 TableOutput<2> table{terminal, {L"", L""}, 50, c_helpColumnPadding, helpLevel};
384 table.SetShowHeader(false);
385 table.SetRowIndent(c_helpRowIndent);
386 table.SetColumnConfig(
387 1,
388 ColumnWidthConfig{
389 .MinWidth = ColumnWidthConfig::NoLimit,
390 .MaxWidth = ColumnWidthConfig::NoLimit,
391 .Overflow = ColumnOverflow::Wrap,
392 });
393 return table;
394 };
395
396 // Col0: short alias (e.g. "-f")
397 // Col1: long name (e.g. "--force")
398 // Col2: description (word-wraps at computed column width)
399 const auto MakeOptionsTable = [&terminal, helpLevel]() -> TableOutput<3> {
400 TableOutput<3> table{terminal, {L"", L"", L""}, {}, 50, c_helpColumnPadding, helpLevel};
401 table.SetShowHeader(false);
402 table.SetRowIndent(c_helpRowIndent);
403 table.SetColumnConfig(
404 2,
405 ColumnWidthConfig{
406 .MinWidth = ColumnWidthConfig::NoLimit,
407 .MaxWidth = ColumnWidthConfig::NoLimit,
408 .Overflow = ColumnOverflow::Wrap,
409 });
410 return table;
411 };
412
413 const auto AddArgumentRows = [](auto& table, const std::vector<Argument>& args) {
414 for (const auto& arg : args)
415 {
416 FormattedCell aliasCell{L""};
417 std::wstring name = arg.Name();
418 if (arg.Kind() == Kind::Flag || arg.Kind() == Kind::Value)
419 {
420 if (!arg.Alias().empty())
421 {
422 aliasCell = FormattedCell(std::wstring{WSLC_CLI_ARG_ID_CHAR} + arg.Alias(), HelpArgumentEmphasis);
423 }
424
425 name = std::wstring{WSLC_CLI_ARG_ID_CHAR} + std::wstring{WSLC_CLI_ARG_ID_CHAR} + name;
426 }
427
428 table.WriteRow({
429 std::move(aliasCell),
430 FormattedCell(std::move(name), HelpArgumentEmphasis),
431 FormattedCell(arg.Description()),
432 });
433 }
434 };
435
436 if ((fullHelp || commandHelp) && !commands.empty())
437 {
438 terminal.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingCommands(), Format::Default);
439
440 auto table = MakeHelpTable();
441 for (const auto& command : commands)
442 {
443 table.WriteRow({
444 FormattedCell(command->Name(), HelpCommandEmphasis),
445 FormattedCell(command->ShortDescription()),
446 });
447 }
448 table.Complete();
449
450 if (fullHelp)
451 {
452 terminal.Write(helpLevel, L"\n{} [{}]\n", Localization::WSLCCLI_HelpForDetails(), WSLC_CLI_HELP_ARG_STRING);
453 }
454 }
455
456 if (argumentHelp && !helpArguments.empty())
457 {
458 const bool onlyRelatedOptions = std::ranges::all_of(helpArguments, &Argument::IsOption);
459
460 terminal.Write(
461 helpLevel,
462 L"{}{}{}\n",
463 HelpHeadingEmphasis,
464 onlyRelatedOptions ? Localization::WSLCCLI_HeadingRelatedOptions() : Localization::WSLCCLI_HeadingRelatedArguments(),
465 Format::Default);
466
467 auto table = MakeOptionsTable();
468 AddArgumentRows(table, helpArguments);
469 table.Complete();
470 }
471 else if (fullHelp && !helpArguments.empty())
472 {
473 if (!commands.empty())
474 {
475 terminal.Write(helpLevel, L"\n");
476 }
477
478 // Arguments table: positional and forward args, name (emphasized) | description
479 if (hasHelpArguments || hasHelpForwardArgs)
480 {
481 terminal.Write(helpLevel, L"{}{}{}\n", HelpHeadingEmphasis, Localization::WSLCCLI_HeadingArguments(), Format::Default);
482
483 auto table = MakeHelpTable();
484
485 for (const auto& arg : helpPositionalArgs)
486 {
487 table.WriteRow({
488 FormattedCell(arg.Name(), HelpArgumentEmphasis),
489 FormattedCell(arg.Description()),
490 });
491 }
492
493 for (const auto& arg : helpForwardArgs)
494 {
495 table.WriteRow({
496 FormattedCell(arg.Name(), HelpArgumentEmphasis),
497 FormattedCell(arg.Description()),
498 });
499 }
500
501 table.Complete();
502 }
503 }
504
505 // Options table: alias (emphasized) | long name (emphasized) | description
506 // Global options are appended to the same table so column widths are shared.
507 if (fullHelp && (hasHelpOptions || !globalArgs.empty()))
508 {
509 if (hasHelpArguments || hasHelpForwardArgs)
510 {
511 terminal.Write(helpLevel, L"\n");
512 }
513 else if (fullHelp && !commands.empty() && helpArguments.empty())
514 {
515 terminal.Write(helpLevel, L"\n");
516 }
517
518 auto table = MakeOptionsTable();
519
520 if (hasHelpOptions)
521 {
522 table.WriteLine(FormattedCell(Localization::WSLCCLI_HeadingOptions(), HelpHeadingEmphasis));
523 AddArgumentRows(table, helpStandardArgs);
524 }
525
526 if (fullHelp && !globalArgs.empty())
527 {
528 if (hasHelpOptions)
529 {
530 table.WriteLine();
531 }
532 table.WriteLine(FormattedCell(Localization::WSLCCLI_HeadingGlobalOptions(), HelpHeadingEmphasis));
533 AddArgumentRows(table, globalArgs);
534 }
535
536 table.Complete();
537 }
538
539 if (!fullHelp)
540 {
541 if ((commandHelp && !commands.empty()) || (argumentHelp && !helpArguments.empty()))
542 {
543 terminal.Write(helpLevel, L"\n");
544 }
545
546 std::wstring helpCommand = s_ExecutableName;
547 if (!commandChain.empty())
548 {
549 helpCommand += L' ';
550 helpCommand += commandChain;
551 }
552
553 terminal.Write(helpLevel, L"{}\n", Localization::WSLCCLI_RunHelpForMoreInformation(helpCommand));
554 }
555 }
556
557 std::unique_ptr<Command> Command::FindSubCommand(Invocation& inv) const
558 {
559 auto itr = inv.begin();
560 if (itr == inv.end() || (*itr)[0] == WSLC_CLI_ARG_ID_CHAR)
561 {
562 // No more command arguments to check, so no command to find
563 return {};
564 }
565
566 auto commands = GetCommands();
567 if (commands.empty())
568 {
569 return {};
570 }
571
572 for (auto& command : commands)
573 {
574 if (wsl::shared::string::IsEqual(*itr, command->Name()))
575 {
576 inv.consume(itr);
577 return std::move(command);
578 }
579
580 for (const auto& alias : command->Aliases())
581 {
582 if (wsl::shared::string::IsEqual(*itr, alias))
583 {
584 inv.consume(itr);
585 return std::move(command);
586 }
587 }
588 }
589
590 throw CommandException(Localization::WSLCCLI_UnrecognizedCommandError(std::wstring_view{*itr}));
591 }
592
593 // Convert the invocation vector into a map of argument types and their associated values.
594 // Argument map is based on the arguments that the command defines and are stored as
595 // an enum -> variant multimap. This is parsing and value storage only, not validation of
596 // the argument data.
597 void Command::ParseArguments(
598 Invocation& inv, ArgMap& target, std::vector<Argument> definedArgs, bool optionsOnly, bool stopOnUnknown, const std::vector<Argument>& overridableDefaults) const
599 {
600 if (definedArgs.empty())
601 {
602 return;
603 }
604
605 ParseArgumentsStateMachine stateMachine{inv, target, std::move(definedArgs), optionsOnly, stopOnUnknown, overridableDefaults};
606
607 while (stateMachine.Step())
608 {
609 stateMachine.ThrowIfError();
610 }
611 stateMachine.ThrowIfError();
612
613 // Both modes leave the iterator at the first unconsumed token; sync inv.
614 if (optionsOnly || stopOnUnknown)
615 {
616 inv.consumeUntil(stateMachine.Position());
617 }
618 }
619
620 // Validates the ArgMap produced by ParseArguments. ArgMap is assumed to have
621 // been populated and parsed successfully from the invocation and now we are validating
622 // that the arguments provided meet the requirements of the command. This includes checking
623 // that all required arguments are present. Count limits are enforced during parsing
624 // (single-value args are last-wins), so they are not re-checked here.
625 // Any defined validation for specific ArgTypes are also run.
626 void Command::ValidateArguments(ArgMap& source, const std::vector<Argument>& definedArgs, bool runInternalHook) const
627 {
628 if (source.GetValue<ArgType::Help>())
629 {
630 return;
631 }
632
633 for (const auto& arg : definedArgs)
634 {
635 if (arg.Required() && !source.Contains(arg.Type()))
636 {
637 const auto name = arg.IsOption() ? std::wstring(2, WSLC_CLI_ARG_ID_CHAR) + arg.Name() : arg.Name();
638 throw ArgumentException(
639 arg.IsOption() ? Localization::WSLCCLI_RequiredArgumentOptionError(name)
640 : Localization::WSLCCLI_RequiredArgumentError(arg.Name()),
641 arg);
642 }
643
644 if (source.Contains(arg.Type()))
645 {
646 try
647 {
648 arg.Validate(source);
649 }
650 catch (const ArgumentException& exception)
651 {
652 std::vector<Argument> configuredArguments;
653 if (exception.Arguments().empty())
654 {
655 configuredArguments.emplace_back(arg);
656 }
657 else
658 {
659 configuredArguments.reserve(exception.Arguments().size());
660 for (const auto& exceptionArgument : exception.Arguments())
661 {
662 const auto configuredArgument = std::ranges::find(definedArgs, exceptionArgument.Type(), &Argument::Type);
663 configuredArguments.emplace_back(configuredArgument != definedArgs.end() ? *configuredArgument : exceptionArgument);
664 }
665 }
666
667 throw ArgumentException(exception.Message(), std::move(configuredArguments));
668 }
669 }
670 }
671
672 if (runInternalHook)
673 {
674 ValidateArgumentsInternal(source);
675 }
676 }
677
678 void Command::Execute(CLIExecutionContext& context) const
679 {
680 // If Help was part of the validated argument set, we will output help instead of executing.
681 if (context.Args.GetValue<ArgType::Help>())
682 {
683 OutputHelp(context.Terminal);
684 }
685 else
686 {
687 // Execute internal has the actual command execution path.
688 ExecuteInternal(context);
689 }
690 }
691
692 // External execution entry point called by the core execution flow.
693 void Execute(CLIExecutionContext& context, std::unique_ptr<Command>& command)
694 {
695 command->Execute(context);
696 }
697
698 void Command::ValidateArgumentsInternal(ArgMap&) const
699 {
700 // Commands may not need any extra validation; they'll override if they do.
701 }
702
703 std::vector<Argument> Command::GetArgumentsForHelp(std::initializer_list<ArgType> types) const
704 {
705 auto arguments = GetAllArguments();
706 auto globalArguments = RootCommand().GetGlobalArguments();
707 arguments.insert(arguments.end(), globalArguments.begin(), globalArguments.end());
708
709 std::vector<Argument> result;
710 result.reserve(types.size());
711
712 for (const auto type : types)
713 {
714 const auto argument = std::ranges::find(arguments, type, &Argument::Type);
715 THROW_HR_IF_MSG(E_INVALIDARG, argument == arguments.end(), "Argument type %zu is not configured for command", static_cast<size_t>(type));
716 result.emplace_back(*argument);
717 }
718
719 return result;
720 }
721
722 std::vector<Argument> Command::GetGlobalsAndEnvArguments() const
723 {
724 auto merged = GetGlobalArguments();
725 auto envOnly = GetEnvArguments();
726
727 // Globals listed first, so the loop below treats them as the winners.
728 merged.reserve(merged.size() + envOnly.size());
729 for (auto& arg : envOnly)
730 {
731 const auto type = arg.Type();
732 const bool alreadyPresent =
733 std::any_of(merged.begin(), merged.end(), [type](const Argument& existing) { return existing.Type() == type; });
734 if (!alreadyPresent)
735 {
736 merged.emplace_back(std::move(arg));
737 }
738 }
739
740 return merged;
741 }
742 } // namespace wsl::windows::wslc