| 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 |