winget-cli

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

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 }