Command.cpp (43311B)
1 // Copyright (c) Microsoft Corporation. 2 // Licensed under the MIT License. 3 #include "pch.h" 4 #include "Command.h" 5 #include "Resources.h" 6 #include "Sixel.h" 7 #include <winget/UserSettings.h> 8 #include <AppInstallerRuntime.h> 9 #include <winget/Locale.h> 10 #include <winget/Reboot.h> 11 #include <winget/Authentication.h> 12 13 using namespace std::string_view_literals; 14 using namespace AppInstaller::Utility::literals; 15 using namespace AppInstaller::Settings; 16 17 namespace AppInstaller::CLI 18 { 19 namespace 20 { 21 constexpr Utility::LocIndView s_Command_ArgName_SilentAndInteractive = "silent|interactive"_liv; 22 23 void LaunchLogsIfRequested(Execution::Context& context) 24 { 25 try 26 { 27 if (context.Args.Contains(Execution::Args::Type::OpenLogs)) 28 { 29 // TODO: Consider possibly adding functionality that if the context contains 'Execution::Args::Type::Log' to open the path provided for the log 30 // The above was omitted initially as a security precaution to ensure that user input to '--log' wouldn't be passed directly to ShellExecute 31 ShellExecute(NULL, NULL, Runtime::GetPathTo(Runtime::PathName::DefaultLogLocation).wstring().c_str(), NULL, NULL, SW_SHOWNORMAL); 32 } 33 } 34 CATCH_LOG(); 35 } 36 } 37 38 Command::Command( 39 std::string_view name, 40 std::vector<std::string_view> aliases, 41 std::string_view parent, 42 Command::Visibility visibility, 43 Settings::ExperimentalFeature::Feature feature, 44 Settings::TogglePolicy::Policy groupPolicy, 45 CommandOutputFlags outputFlags) : 46 m_name(name), m_aliases(std::move(aliases)), m_visibility(visibility), m_feature(feature), m_groupPolicy(groupPolicy), m_outputFlags(outputFlags) 47 { 48 if (!parent.empty()) 49 { 50 m_fullName.reserve(parent.length() + 1 + name.length()); 51 m_fullName = parent; 52 m_fullName += ParentSplitChar; 53 m_fullName += name; 54 } 55 else 56 { 57 m_fullName = name; 58 } 59 } 60 61 void Command::OutputIntroHeader(Execution::Reporter& reporter) const 62 { 63 auto infoOut = reporter.Info(); 64 VirtualTerminal::ConstructedSequence indent; 65 66 if (reporter.SixelsEnabled()) 67 { 68 try 69 { 70 std::filesystem::path imagePath = Runtime::GetPathTo(Runtime::PathName::ImageAssets); 71 72 if (!imagePath.empty()) 73 { 74 // This image matches the target pixel size. If changing the target size, choose the most appropriate image. 75 imagePath /= "AppList.targetsize-40.png"; 76 77 VirtualTerminal::Sixel::Image wingetIcon{ imagePath }; 78 79 // Using a height of 2 to match the two lines of header. 80 UINT imageHeightCells = 2; 81 UINT imageWidthCells = 2 * imageHeightCells; 82 83 wingetIcon.RenderSizeInCells(imageWidthCells, imageHeightCells); 84 wingetIcon.RenderTo(infoOut); 85 86 indent = VirtualTerminal::Cursor::Position::Forward(static_cast<int16_t>(imageWidthCells)); 87 infoOut << VirtualTerminal::Cursor::Position::Up(static_cast<int16_t>(imageHeightCells) - 1); 88 } 89 } 90 CATCH_LOG(); 91 } 92 93 auto productName = Runtime::IsReleaseBuild() ? Resource::String::WindowsPackageManager : Resource::String::WindowsPackageManagerPreview; 94 infoOut << indent << productName(Runtime::GetClientVersion()) << std::endl 95 << indent << Resource::String::MainCopyrightNotice << std::endl; 96 } 97 98 void Command::OutputHelp(Execution::Reporter& reporter, const CommandException* exception) const 99 { 100 // Header 101 OutputIntroHeader(reporter); 102 reporter.EmptyLine(); 103 104 // Error if given 105 if (exception) 106 { 107 reporter.Error() << exception->Message() << std::endl << std::endl; 108 } 109 110 // Description 111 auto infoOut = reporter.Info(); 112 infoOut << 113 LongDescription() << std::endl << 114 std::endl; 115 116 // Example usage for this command 117 // First create the command chain for output 118 std::string commandChain = FullName(); 119 size_t firstSplit = commandChain.find_first_of(ParentSplitChar); 120 if (firstSplit == std::string::npos) 121 { 122 commandChain.clear(); 123 } 124 else 125 { 126 commandChain = commandChain.substr(firstSplit + 1); 127 for (char& c : commandChain) 128 { 129 if (c == ParentSplitChar) 130 { 131 c = ' '; 132 } 133 } 134 } 135 136 // Output the command preamble and command chain 137 infoOut << Resource::String::Usage("winget"_liv, Utility::LocIndView{ commandChain }); 138 139 auto commandAliases = Aliases(); 140 auto commands = GetVisibleCommands(); 141 auto arguments = GetVisibleArguments(); 142 143 bool hasArguments = false; 144 bool hasOptions = false; 145 146 // Output the command token, made optional if arguments are present. 147 if (!commands.empty()) 148 { 149 infoOut << ' '; 150 151 if (!arguments.empty()) 152 { 153 infoOut << '['; 154 } 155 156 infoOut << '<' << Resource::String::Command << '>'; 157 158 if (!arguments.empty()) 159 { 160 infoOut << ']'; 161 } 162 } 163 164 // Arguments are required by a test to have all positionals first. 165 for (const auto& arg : arguments) 166 { 167 if (arg.Type() == ArgumentType::Positional) 168 { 169 hasArguments = true; 170 171 infoOut << ' '; 172 173 if (!arg.Required()) 174 { 175 infoOut << '['; 176 } 177 178 infoOut << '['; 179 180 if (arg.Alias() == ArgumentCommon::NoAlias) 181 { 182 infoOut << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << arg.Name(); 183 } 184 else 185 { 186 infoOut << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << arg.Alias(); 187 } 188 189 infoOut << "] <"_liv << arg.Name() << '>'; 190 191 if (arg.Limit() > 1) 192 { 193 infoOut << "..."_liv; 194 } 195 196 if (!arg.Required()) 197 { 198 infoOut << ']'; 199 } 200 } 201 else 202 { 203 hasOptions = true; 204 infoOut << " [<"_liv << Resource::String::Options << ">]"_liv; 205 break; 206 } 207 } 208 209 infoOut << 210 std::endl << 211 std::endl; 212 213 if (!commandAliases.empty()) 214 { 215 infoOut << Resource::String::AvailableCommandAliases << std::endl; 216 217 for (const auto& commandAlias : commandAliases) 218 { 219 infoOut << " "_liv << Execution::HelpCommandEmphasis << commandAlias << std::endl; 220 } 221 infoOut << std::endl; 222 } 223 224 if (!commands.empty()) 225 { 226 if (Name() == FullName()) 227 { 228 infoOut << Resource::String::AvailableCommands << std::endl; 229 } 230 else 231 { 232 infoOut << Resource::String::AvailableSubcommands << std::endl; 233 } 234 235 size_t maxCommandNameLength = 0; 236 for (const auto& command : commands) 237 { 238 maxCommandNameLength = std::max(maxCommandNameLength, command->Name().length()); 239 } 240 241 for (const auto& command : commands) 242 { 243 size_t fillChars = (maxCommandNameLength - command->Name().length()) + 2; 244 infoOut << " "_liv << Execution::HelpCommandEmphasis << command->Name() << Utility::LocIndString{ std::string(fillChars, ' ') } << command->ShortDescription() << std::endl; 245 } 246 247 infoOut << 248 std::endl << 249 Resource::String::HelpForDetails 250 << " ["_liv << APPINSTALLER_CLI_HELP_ARGUMENT << ']' << std::endl; 251 } 252 253 if (!arguments.empty()) 254 { 255 if (!commands.empty()) 256 { 257 infoOut << std::endl; 258 } 259 260 std::vector<std::string> argNames; 261 size_t maxArgNameLength = 0; 262 for (const auto& arg : arguments) 263 { 264 argNames.emplace_back(arg.GetUsageString()); 265 maxArgNameLength = std::max(maxArgNameLength, argNames.back().length()); 266 } 267 268 if (hasArguments) 269 { 270 infoOut << Resource::String::AvailableArguments << std::endl; 271 272 size_t i = 0; 273 for (const auto& arg : arguments) 274 { 275 const std::string& argName = argNames[i++]; 276 if (arg.Type() == ArgumentType::Positional) 277 { 278 size_t fillChars = (maxArgNameLength - argName.length()) + 2; 279 infoOut << " "_liv << Execution::HelpArgumentEmphasis << argName << Utility::LocIndString{ std::string(fillChars, ' ') } << arg.Description() << std::endl; 280 } 281 } 282 } 283 284 if (hasOptions) 285 { 286 if (hasArguments) 287 { 288 infoOut << std::endl; 289 } 290 291 infoOut << Resource::String::AvailableOptions << std::endl; 292 293 size_t i = 0; 294 for (const auto& arg : arguments) 295 { 296 const std::string& argName = argNames[i++]; 297 if (arg.Type() != ArgumentType::Positional) 298 { 299 size_t fillChars = (maxArgNameLength - argName.length()) + 2; 300 infoOut << " "_liv << Execution::HelpArgumentEmphasis << argName << Utility::LocIndString{ std::string(fillChars, ' ') } << arg.Description() << std::endl; 301 } 302 } 303 } 304 } 305 306 // Finally, the link to the documentation pages 307 auto helpLink = HelpLink(); 308 if (!helpLink.empty()) 309 { 310 infoOut << std::endl << Resource::String::HelpLinkPreamble(helpLink) << std::endl; 311 } 312 } 313 314 std::unique_ptr<Command> Command::FindSubCommand(Invocation& inv) const 315 { 316 auto itr = inv.begin(); 317 if (itr == inv.end() || (*itr)[0] == APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR) 318 { 319 // No more command arguments to check, so no command to find 320 return {}; 321 } 322 323 auto commands = GetCommands(); 324 if (commands.empty()) 325 { 326 // No more subcommands 327 return {}; 328 } 329 330 for (auto& command : commands) 331 { 332 if ( 333 Utility::CaseInsensitiveEquals(*itr, command->Name()) || 334 Utility::CaseInsensitiveContains(command->Aliases(), *itr) 335 ) 336 { 337 if (!ExperimentalFeature::IsEnabled(command->Feature())) 338 { 339 auto feature = ExperimentalFeature::GetFeature(command->Feature()); 340 AICLI_LOG(CLI, Error, << "Trying to use command: " << *itr << " without enabling feature " << feature.JsonName()); 341 throw CommandException(Resource::String::FeatureDisabledMessage(feature.JsonName())); 342 } 343 344 if (!Settings::GroupPolicies().IsEnabled(command->GroupPolicy())) 345 { 346 auto policy = TogglePolicy::GetPolicy(command->GroupPolicy()); 347 AICLI_LOG(CLI, Error, << "Trying to use command: " << *itr << " disabled by group policy " << policy.RegValueName()); 348 throw GroupPolicyException(command->GroupPolicy()); 349 } 350 351 AICLI_LOG(CLI, Info, << "Found subcommand: " << *itr); 352 inv.consume(itr); 353 return std::move(command); 354 } 355 } 356 357 // The command has opted-in to be executed when it has subcommands and the next token is a positional parameter value 358 if (m_selectCurrentCommandIfUnrecognizedSubcommandFound) 359 { 360 return {}; 361 } 362 363 // TODO: If we get to a large number of commands, do a fuzzy search much like git 364 throw CommandException(Resource::String::UnrecognizedCommand(Utility::LocIndView{ *itr })); 365 } 366 367 // The argument parsing state machine. 368 // It is broken out to enable completion to process arguments, ignore errors, 369 // and determine the likely state of the word to be completed. 370 struct ParseArgumentsStateMachine 371 { 372 ParseArgumentsStateMachine(Invocation& inv, Execution::Args& execArgs, std::vector<CLI::Argument> arguments); 373 374 ParseArgumentsStateMachine(const ParseArgumentsStateMachine&) = delete; 375 ParseArgumentsStateMachine& operator=(const ParseArgumentsStateMachine&) = delete; 376 377 ParseArgumentsStateMachine(ParseArgumentsStateMachine&&) = default; 378 ParseArgumentsStateMachine& operator=(ParseArgumentsStateMachine&&) = default; 379 380 // Processes the next argument from the invocation. 381 // Returns true if there was an argument to process; 382 // returns false if there were none. 383 bool Step(); 384 385 // Throws if there was an error during the prior step. 386 void ThrowIfError() const; 387 388 // The current state of the state machine. 389 // An empty state indicates that the next argument can be anything. 390 struct State 391 { 392 State() = default; 393 State(Execution::Args::Type type, std::string_view arg) : m_type(type), m_arg(arg) {} 394 State(CommandException ce) : m_exception(std::move(ce)) {} 395 396 // If set, indicates that the next argument is a value for this type. 397 const std::optional<Execution::Args::Type>& Type() const { return m_type; } 398 399 // The actual argument string associated with Type. 400 const Utility::LocIndString& Arg() const { return m_arg; } 401 402 // If set, indicates that the last argument produced an error. 403 const std::optional<CommandException>& Exception() const { return m_exception; } 404 405 private: 406 std::optional<Execution::Args::Type> m_type; 407 Utility::LocIndString m_arg; 408 std::optional<CommandException> m_exception; 409 }; 410 411 const State& GetState() const { return m_state; } 412 413 bool OnlyPositionalRemain() const { return m_onlyPositionalArgumentsRemain; } 414 415 // Gets the next positional argument, or nullptr if there is not one. 416 const CLI::Argument* NextPositional(); 417 418 const std::vector<CLI::Argument>& Arguments() const { return m_arguments; } 419 420 private: 421 State StepInternal(); 422 423 void ProcessAdjoinedValue(Execution::Args::Type type, std::string_view value); 424 425 Invocation& m_invocation; 426 Execution::Args& m_executionArgs; 427 std::vector<CLI::Argument> m_arguments; 428 429 Invocation::iterator m_invocationItr; 430 std::vector<CLI::Argument>::iterator m_positionalSearchItr; 431 bool m_onlyPositionalArgumentsRemain = false; 432 433 State m_state; 434 }; 435 436 ParseArgumentsStateMachine::ParseArgumentsStateMachine(Invocation& inv, Execution::Args& execArgs, std::vector<CLI::Argument> arguments) : 437 m_invocation(inv), 438 m_executionArgs(execArgs), 439 m_arguments(std::move(arguments)), 440 m_invocationItr(m_invocation.begin()), 441 m_positionalSearchItr(m_arguments.begin()) 442 { 443 } 444 445 bool ParseArgumentsStateMachine::Step() 446 { 447 if (m_invocationItr == m_invocation.end()) 448 { 449 return false; 450 } 451 452 m_state = StepInternal(); 453 return true; 454 } 455 456 void ParseArgumentsStateMachine::ThrowIfError() const 457 { 458 if (m_state.Exception()) 459 { 460 throw m_state.Exception().value(); 461 } 462 // If the next argument was to be a value, but none was provided, convert it to an exception. 463 else if (m_state.Type() && m_invocationItr == m_invocation.end()) 464 { 465 throw CommandException(Resource::String::MissingArgumentError(m_state.Arg())); 466 } 467 } 468 469 const CLI::Argument* ParseArgumentsStateMachine::NextPositional() 470 { 471 // Find the next appropriate positional arg if the current itr isn't one or has hit its limit. 472 while (m_positionalSearchItr != m_arguments.end() && 473 (m_positionalSearchItr->Type() != ArgumentType::Positional || m_executionArgs.GetCount(m_positionalSearchItr->ExecArgType()) == m_positionalSearchItr->Limit())) 474 { 475 ++m_positionalSearchItr; 476 } 477 478 if (m_positionalSearchItr == m_arguments.end()) 479 { 480 return nullptr; 481 } 482 483 return &*m_positionalSearchItr; 484 } 485 486 // Parse arguments as such: 487 // 1. If argument starts with a single -, only the single character alias is considered. 488 // a. If the named argument alias (a) needs a VALUE, it can be provided in these ways: 489 // -a=VALUE 490 // -a VALUE 491 // b. If the argument is a flag, additional characters after are treated as if they start 492 // with a -, repeatedly until the end of the argument is reached. Fails if non-flags hit. 493 // 2. If the argument starts with a double --, only the full name is considered. 494 // a. If the named argument (arg) needs a VALUE, it can be provided in these ways: 495 // --arg=VALUE 496 // --arg VALUE 497 // 3. If the argument does not start with any -, it is considered the next positional argument. 498 // 4. If the argument is only a double --, all further arguments are only considered as positional. 499 ParseArgumentsStateMachine::State ParseArgumentsStateMachine::StepInternal() 500 { 501 auto currArg = Utility::LocIndView{ *m_invocationItr }; 502 ++m_invocationItr; 503 504 // If the previous step indicated a value was needed, set it and forget it. 505 if (m_state.Type()) 506 { 507 m_executionArgs.AddArg(m_state.Type().value(), currArg); 508 return {}; 509 } 510 511 // This is a positional argument 512 if (m_onlyPositionalArgumentsRemain || currArg.empty() || currArg[0] != APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR) 513 { 514 const CLI::Argument* nextPositional = NextPositional(); 515 if (!nextPositional) 516 { 517 return CommandException(Resource::String::ExtraPositionalError(currArg)); 518 } 519 520 m_executionArgs.AddArg(nextPositional->ExecArgType(), currArg); 521 } 522 // The currentArg must not be empty, and starts with a - 523 else if (currArg.length() == 1) 524 { 525 return CommandException(Resource::String::InvalidArgumentSpecifierError(currArg)); 526 } 527 // Now it must be at least 2 chars 528 else if (currArg[1] != APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR) 529 { 530 // Parse the single character alias argument 531 char currChar = currArg[1]; 532 533 auto itr = std::find_if(m_arguments.begin(), m_arguments.end(), [&](const Argument& arg) { return (currChar == arg.Alias()); }); 534 if (itr == m_arguments.end()) 535 { 536 return CommandException(Resource::String::InvalidAliasError(currArg)); 537 } 538 539 if (itr->Type() == ArgumentType::Flag) 540 { 541 m_executionArgs.AddArg(itr->ExecArgType()); 542 543 for (size_t i = 2; i < currArg.length(); ++i) 544 { 545 currChar = currArg[i]; 546 547 auto itr2 = std::find_if(m_arguments.begin(), m_arguments.end(), [&](const Argument& arg) { return (currChar == arg.Alias()); }); 548 if (itr2 == m_arguments.end()) 549 { 550 return CommandException(Resource::String::AdjoinedNotFoundError(currArg)); 551 } 552 else if (itr2->Type() != ArgumentType::Flag) 553 { 554 return CommandException(Resource::String::AdjoinedNotFlagError(currArg)); 555 } 556 else 557 { 558 m_executionArgs.AddArg(itr2->ExecArgType()); 559 } 560 } 561 } 562 else if (currArg.length() > 2) 563 { 564 if (currArg[2] == APPINSTALLER_CLI_ARGUMENT_SPLIT_CHAR) 565 { 566 ProcessAdjoinedValue(itr->ExecArgType(), currArg.substr(3)); 567 } 568 else 569 { 570 return CommandException(Resource::String::SingleCharAfterDashError(currArg)); 571 } 572 } 573 else 574 { 575 return { itr->ExecArgType(), currArg }; 576 } 577 } 578 // The currentArg is at least 2 chars, both of which are -- 579 else if (currArg.length() == 2) 580 { 581 m_onlyPositionalArgumentsRemain = true; 582 } 583 // The currentArg is more than 2 chars, both of which are -- 584 else 585 { 586 // This is an arg name, find it and process its value if needed. 587 // Skip the double arg identifier chars. 588 size_t argStart = currArg.find_first_not_of(APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR); 589 std::string_view argName = currArg.substr(argStart); 590 bool argFound = false; 591 592 bool hasValue = false; 593 std::string_view argValue; 594 size_t splitChar = argName.find_first_of(APPINSTALLER_CLI_ARGUMENT_SPLIT_CHAR); 595 if (splitChar != std::string::npos) 596 { 597 hasValue = true; 598 argValue = argName.substr(splitChar + 1); 599 argName = argName.substr(0, splitChar); 600 } 601 602 for (const auto& arg : m_arguments) 603 { 604 if ( 605 Utility::CaseInsensitiveEquals(argName, arg.Name()) || 606 Utility::CaseInsensitiveEquals(argName, arg.AlternateName()) 607 ) 608 { 609 if (arg.Type() == ArgumentType::Flag) 610 { 611 if (hasValue) 612 { 613 return CommandException(Resource::String::FlagContainAdjoinedError(currArg)); 614 } 615 616 m_executionArgs.AddArg(arg.ExecArgType()); 617 } 618 else if (hasValue) 619 { 620 ProcessAdjoinedValue(arg.ExecArgType(), argValue); 621 } 622 else 623 { 624 return { arg.ExecArgType(), currArg }; 625 } 626 argFound = true; 627 break; 628 } 629 } 630 631 if (!argFound) 632 { 633 return CommandException(Resource::String::InvalidNameError(currArg)); 634 } 635 } 636 637 // If we get here, the next argument can be anything again. 638 return {}; 639 } 640 641 void ParseArgumentsStateMachine::ProcessAdjoinedValue(Execution::Args::Type type, std::string_view value) 642 { 643 // If the adjoined value is wrapped in quotes, strip them off. 644 if (value.length() >= 2 && value[0] == '"' && value[value.length() - 1] == '"') 645 { 646 value = value.substr(1, value.length() - 2); 647 } 648 649 m_executionArgs.AddArg(type, std::string{ value }); 650 } 651 652 void Command::ParseArguments(Invocation& inv, Execution::Args& execArgs) const 653 { 654 auto definedArgs = GetArguments(); 655 Argument::GetCommon(definedArgs); 656 657 ParseArgumentsStateMachine stateMachine{ inv, execArgs, std::move(definedArgs) }; 658 659 while (stateMachine.Step()) 660 { 661 stateMachine.ThrowIfError(); 662 } 663 664 // Special handling for multi-query arguments: 665 execArgs.MakeMultiQueryContainUniqueValues(); 666 execArgs.MoveMultiQueryToSingleQueryIfNeeded(); 667 } 668 669 void Command::ValidateArguments(Execution::Args& execArgs) const 670 { 671 // If help is asked for, don't bother validating anything else 672 if (execArgs.Contains(Execution::Args::Type::Help)) 673 { 674 return; 675 } 676 677 // Common arguments need to be validated with command arguments, as there may be common arguments blocked by Experimental Feature or Group Policy 678 auto allArgs = GetArguments(); 679 Argument::GetCommon(allArgs); 680 681 for (const auto& arg : allArgs) 682 { 683 if (!Settings::GroupPolicies().IsEnabled(arg.GroupPolicy()) && execArgs.Contains(arg.ExecArgType())) 684 { 685 auto policy = TogglePolicy::GetPolicy(arg.GroupPolicy()); 686 AICLI_LOG(CLI, Error, << "Trying to use argument: " << arg.Name() << " disabled by group policy " << policy.RegValueName()); 687 throw GroupPolicyException(arg.GroupPolicy()); 688 } 689 690 if (arg.AdminSetting() != BoolAdminSetting::Unknown && !Settings::IsAdminSettingEnabled(arg.AdminSetting()) && execArgs.Contains(arg.ExecArgType())) 691 { 692 auto setting = Settings::AdminSettingToString(arg.AdminSetting()); 693 AICLI_LOG(CLI, Error, << "Trying to use argument: " << arg.Name() << " disabled by admin setting " << setting); 694 throw CommandException(Resource::String::FeatureDisabledByAdminSettingMessage(setting)); 695 } 696 697 if (!ExperimentalFeature::IsEnabled(arg.Feature()) && execArgs.Contains(arg.ExecArgType())) 698 { 699 auto feature = ExperimentalFeature::GetFeature(arg.Feature()); 700 AICLI_LOG(CLI, Error, << "Trying to use argument: " << arg.Name() << " without enabling feature " << feature.JsonName()); 701 throw CommandException(Resource::String::FeatureDisabledMessage(feature.JsonName())); 702 } 703 704 if (arg.Required() && !execArgs.Contains(arg.ExecArgType())) 705 { 706 throw CommandException(Resource::String::RequiredArgError(arg.Name())); 707 } 708 709 if (arg.Limit() < execArgs.GetCount(arg.ExecArgType())) 710 { 711 throw CommandException(Resource::String::TooManyArgError(arg.Name())); 712 } 713 } 714 715 if (execArgs.Contains(Execution::Args::Type::Silent) && execArgs.Contains(Execution::Args::Type::Interactive)) 716 { 717 throw CommandException(Resource::String::TooManyBehaviorsError(s_Command_ArgName_SilentAndInteractive)); 718 } 719 720 if (execArgs.Contains(Execution::Args::Type::CustomHeader) && !execArgs.Contains(Execution::Args::Type::Source) && 721 !execArgs.Contains(Execution::Args::Type::SourceName)) 722 { 723 throw CommandException(Resource::String::HeaderArgumentNotApplicableWithoutSource(Argument::ForType(Execution::Args::Type::CustomHeader).Name())); 724 } 725 726 if (execArgs.Contains(Execution::Args::Type::Count)) 727 { 728 try 729 { 730 int countRequested = std::stoi(std::string(execArgs.GetArg(Execution::Args::Type::Count))); 731 if (countRequested < 1 || countRequested > 1000) 732 { 733 throw CommandException(Resource::String::CountOutOfBoundsError); 734 } 735 } 736 catch (...) 737 { 738 throw CommandException(Resource::String::CountOutOfBoundsError); 739 } 740 } 741 742 if (execArgs.Contains(Execution::Args::Type::InstallArchitecture)) 743 { 744 Utility::Architecture selectedArch = Utility::ConvertToArchitectureEnum(std::string(execArgs.GetArg(Execution::Args::Type::InstallArchitecture))); 745 if ((selectedArch == Utility::Architecture::Unknown) || (Utility::IsApplicableArchitecture(selectedArch) == Utility::InapplicableArchitecture)) 746 { 747 std::vector<Utility::LocIndString> applicableArchitectures; 748 for (Utility::Architecture i : Utility::GetApplicableArchitectures()) 749 { 750 applicableArchitectures.emplace_back(Utility::ToString(i)); 751 } 752 753 auto validOptions = Utility::Join(", "_liv, applicableArchitectures); 754 throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::InstallArchitecture).Name(), validOptions)); 755 } 756 } 757 758 if (execArgs.Contains(Execution::Args::Type::InstallerArchitecture)) 759 { 760 Utility::Architecture selectedArch = Utility::ConvertToArchitectureEnum(std::string(execArgs.GetArg(Execution::Args::Type::InstallerArchitecture))); 761 if (selectedArch == Utility::Architecture::Unknown) 762 { 763 std::vector<Utility::LocIndString> applicableArchitectures; 764 for (Utility::Architecture i : Utility::GetAllArchitectures()) 765 { 766 applicableArchitectures.emplace_back(Utility::ToString(i)); 767 } 768 769 auto validOptions = Utility::Join(", "_liv, applicableArchitectures); 770 throw CommandException(Resource::String::InvalidArgumentValueError(Argument::ForType(Execution::Args::Type::InstallerArchitecture).Name(), validOptions)); 771 } 772 } 773 774 if (execArgs.Contains(Execution::Args::Type::Locale)) 775 { 776 if (!Locale::IsWellFormedBcp47Tag(execArgs.GetArg(Execution::Args::Type::Locale))) 777 { 778 throw CommandException(Resource::String::InvalidArgumentValueErrorWithoutValidValues(Argument::ForType(Execution::Args::Type::Locale).Name())); 779 } 780 } 781 782 if (execArgs.Contains(Execution::Args::Type::InstallScope)) 783 { 784 if (Manifest::ConvertToScopeEnum(execArgs.GetArg(Execution::Args::Type::InstallScope)) == Manifest::ScopeEnum::Unknown) 785 { 786 auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{ "user"_lis, "machine"_lis }); 787 throw CommandException(Resource::String::InvalidArgumentValueError(ArgumentCommon::ForType(Execution::Args::Type::InstallScope).Name, validOptions)); 788 } 789 } 790 791 if (execArgs.Contains(Execution::Args::Type::InstallerType)) 792 { 793 Manifest::InstallerTypeEnum selectedInstallerType = Manifest::ConvertToInstallerTypeEnum(std::string(execArgs.GetArg(Execution::Args::Type::InstallerType))); 794 if (selectedInstallerType == Manifest::InstallerTypeEnum::Unknown) 795 { 796 throw CommandException(Resource::String::InvalidArgumentValueErrorWithoutValidValues(Argument::ForType(Execution::Args::Type::InstallerType).Name())); 797 } 798 } 799 800 if (execArgs.Contains(Execution::Args::Type::AuthenticationMode)) 801 { 802 if (Authentication::ConvertToAuthenticationMode(execArgs.GetArg(Execution::Args::Type::AuthenticationMode)) == Authentication::AuthenticationMode::Unknown) 803 { 804 auto validOptions = Utility::Join(", "_liv, std::vector<Utility::LocIndString>{ "interactive"_lis, "silentPreferred"_lis, "silent"_lis }); 805 throw CommandException(Resource::String::InvalidArgumentValueError(ArgumentCommon::ForType(Execution::Args::Type::AuthenticationMode).Name, validOptions)); 806 } 807 } 808 809 Argument::ValidateExclusiveArguments(execArgs); 810 811 ValidateArgumentsInternal(execArgs); 812 } 813 814 // Completion can produce one of several things if the completion context is appropriate: 815 // 1. Sub commands, if the context is immediately after this command. 816 // 2. Argument names, if a value is not expected. 817 // 3. Argument values, if one is expected. 818 void Command::Complete(Execution::Context& context) const 819 { 820 CompletionData& data = context.Get<Execution::Data::CompletionData>(); 821 const std::string& word = data.Word(); 822 823 // The word we are to complete is directly after the command, thus it's sub-commands are potentials. 824 if (data.BeforeWord().begin() == data.BeforeWord().end()) 825 { 826 for (const auto& command : GetCommands()) 827 { 828 if (word.empty() || Utility::CaseInsensitiveStartsWith(command->Name(), word)) 829 { 830 context.Reporter.Completion() << command->Name() << std::endl; 831 } 832 // Allow for command aliases to be auto-completed 833 if (!(command->Aliases()).empty() && !word.empty()) 834 { 835 for (const auto& commandAlias : command->Aliases()) 836 { 837 if (Utility::CaseInsensitiveStartsWith(commandAlias, word)) 838 { 839 context.Reporter.Completion() << commandAlias << std::endl; 840 } 841 } 842 } 843 } 844 } 845 846 // Consume what remains, if any, of the preceding values to determine what type the word is. 847 auto definedArgs = GetArguments(); 848 Argument::GetCommon(definedArgs); 849 850 ParseArgumentsStateMachine stateMachine{ data.BeforeWord(), context.Args, std::move(definedArgs) }; 851 852 // We don't care if there are errors along the way, just do the best that can be done and try to 853 // complete whatever would be next if the bad strings were simply ignored. To do that we just spin 854 // through the state until we reach our word. 855 while (stateMachine.Step()); 856 857 const auto& state = stateMachine.GetState(); 858 859 // This means that anything is possible, so argument names are on the table. 860 if (!state.Type() && !stateMachine.OnlyPositionalRemain()) 861 { 862 // Use argument names if: 863 // 1. word is empty 864 // 2. word is just "-" 865 // 3. word starts with "--" 866 if (word.empty() || 867 word == APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_STRING || 868 Utility::CaseInsensitiveStartsWith(word, APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_STRING APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_STRING)) 869 { 870 for (const auto& arg : stateMachine.Arguments()) 871 { 872 if (word.length() <= 2 || Utility::CaseInsensitiveStartsWith(arg.Name(), word.substr(2))) 873 { 874 context.Reporter.Completion() << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << arg.Name() << std::endl; 875 } 876 } 877 } 878 // Use argument aliases if the word is already one; allow cycling through them. 879 else if (Utility::CaseInsensitiveStartsWith(word, APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_STRING) && word.length() == 2) 880 { 881 for (const auto& arg : stateMachine.Arguments()) 882 { 883 if (arg.Alias() != ArgumentCommon::NoAlias) 884 { 885 context.Reporter.Completion() << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << arg.Alias() << std::endl; 886 } 887 } 888 } 889 } 890 891 std::optional<Execution::Args::Type> typeToComplete = state.Type(); 892 893 // We are not waiting on an argument value, so the next could be a positional if the incoming word is not an argument name. 894 // If there is one, offer to complete it. 895 if (!typeToComplete && (word.empty() || word[0] != APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR)) 896 { 897 const auto* nextPositional = stateMachine.NextPositional(); 898 if (nextPositional) 899 { 900 typeToComplete = nextPositional->ExecArgType(); 901 } 902 } 903 904 // To enable more complete scenarios, also attempt to parse any arguments after the word to complete. 905 // This will allow these later values to affect the result of the completion (for instance, if a specific source is listed). 906 { 907 ParseArgumentsStateMachine afterWordStateMachine{ data.AfterWord(), context.Args, stateMachine.Arguments() }; 908 while (afterWordStateMachine.Step()); 909 } 910 911 // Let the derived command take over supplying context sensitive argument value. 912 if (typeToComplete) 913 { 914 Complete(context, typeToComplete.value()); 915 } 916 } 917 918 void Command::Complete(Execution::Context&, Execution::Args::Type) const 919 { 920 // Derived commands must supply context sensitive argument values. 921 } 922 923 void Command::Execute(Execution::Context& context) const 924 { 925 // Block any execution if winget is disabled by policy. 926 // Override the function to bypass this. 927 if (!Settings::GroupPolicies().IsEnabled(Settings::TogglePolicy::Policy::WinGet)) 928 { 929 AICLI_LOG(CLI, Error, << "WinGet is disabled by group policy"); 930 throw Settings::GroupPolicyException::GroupPolicyException(Settings::TogglePolicy::Policy::WinGet); 931 } 932 933 AICLI_LOG(CLI, Info, << "Executing command: " << Name()); 934 if (context.Args.Contains(Execution::Args::Type::Help)) 935 { 936 OutputHelp(context.Reporter); 937 } 938 else 939 { 940 ExecuteInternal(context); 941 } 942 943 // NOTE: Reboot logic will still run even if the context is terminated (not including unhandled exceptions). 944 if (context.Args.Contains(Execution::Args::Type::AllowReboot) && 945 WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::RebootRequired)) 946 { 947 context.Reporter.Warn() << Resource::String::InitiatingReboot << std::endl; 948 949 if (context.Args.Contains(Execution::Args::Type::Wait)) 950 { 951 context.Reporter.PromptForEnter(); 952 } 953 954 if (WI_IsFlagSet(context.GetFlags(), Execution::ContextFlag::RegisterResume)) 955 { 956 // RegisterResume context flag assumes we already wrote to the RunOnce registry. 957 // Since we are about to initiate a restart, this is no longer needed as a safety net. 958 Reboot::UnregisterRestartForWER(); 959 960 context.ClearFlags(Execution::ContextFlag::RegisterResume); 961 } 962 963 context.ClearFlags(Execution::ContextFlag::RebootRequired); 964 965 if (!Reboot::InitiateReboot()) 966 { 967 context.Reporter.Error() << Resource::String::FailedToInitiateReboot << std::endl; 968 } 969 } 970 else 971 { 972 LaunchLogsIfRequested(context); 973 974 if (context.Args.Contains(Execution::Args::Type::Wait)) 975 { 976 context.Reporter.PromptForEnter(); 977 } 978 } 979 } 980 981 void Command::Resume(Execution::Context& context) const 982 { 983 context.Reporter.Error() << Resource::String::CommandDoesNotSupportResumeMessage << std::endl; 984 AICLI_TERMINATE_CONTEXT(E_NOTIMPL); 985 } 986 987 void Command::SelectCurrentCommandIfUnrecognizedSubcommandFound(bool value) 988 { 989 m_selectCurrentCommandIfUnrecognizedSubcommandFound = value; 990 } 991 992 void Command::ValidateArgumentsInternal(Execution::Args&) const 993 { 994 // Do nothing by default. 995 // Commands may not need any extra validation. 996 } 997 998 void Command::ExecuteInternal(Execution::Context& context) const 999 { 1000 context.Reporter.Error() << Resource::String::PendingWorkError << std::endl; 1001 THROW_HR(E_NOTIMPL); 1002 } 1003 1004 Command::Visibility Command::GetVisibility() const 1005 { 1006 if (!ExperimentalFeature::IsEnabled(m_feature)) 1007 { 1008 return Command::Visibility::Hidden; 1009 } 1010 1011 if (!Settings::GroupPolicies().IsEnabled(m_groupPolicy)) 1012 { 1013 return Command::Visibility::Hidden; 1014 } 1015 1016 return m_visibility; 1017 } 1018 1019 std::vector<std::unique_ptr<Command>> Command::GetVisibleCommands() const 1020 { 1021 auto commands = GetCommands(); 1022 1023 commands.erase( 1024 std::remove_if( 1025 commands.begin(), commands.end(), 1026 [](const std::unique_ptr<Command>& command) { return command->GetVisibility() == Command::Visibility::Hidden; }), 1027 commands.end()); 1028 1029 return commands; 1030 } 1031 1032 std::vector<Argument> Command::GetVisibleArguments() const 1033 { 1034 auto arguments = GetArguments(); 1035 Argument::GetCommon(arguments); 1036 1037 arguments.erase( 1038 std::remove_if( 1039 arguments.begin(), arguments.end(), 1040 [](const Argument& arg) { return arg.GetVisibility() == Argument::Visibility::Hidden; }), 1041 arguments.end()); 1042 1043 return arguments; 1044 } 1045 1046 void ExecuteWithoutLoggingSuccess(Execution::Context& context, Command* command) 1047 { 1048 try 1049 { 1050 if (!Settings::User().GetWarnings().empty() && 1051 !WI_IsFlagSet(command->GetOutputFlags(), CommandOutputFlags::IgnoreSettingsWarnings)) 1052 { 1053 context.Reporter.Warn() << Resource::String::SettingsWarnings << std::endl; 1054 } 1055 1056 command->Execute(context); 1057 } 1058 catch (...) 1059 { 1060 context.SetTerminationHR(Workflow::HandleException(context, std::current_exception())); 1061 1062 LaunchLogsIfRequested(context); 1063 } 1064 } 1065 1066 int Execute(Execution::Context& context, std::unique_ptr<Command>& command) 1067 { 1068 ExecuteWithoutLoggingSuccess(context, command.get()); 1069 1070 if (SUCCEEDED(context.GetTerminationHR())) 1071 { 1072 Logging::Telemetry().LogCommandSuccess(command->FullName()); 1073 } 1074 1075 return context.GetTerminationHR(); 1076 } 1077 }