commit 71036bdab7a231cc75c2e551be20106b564c7b19 parent 30b652b6bb96116d61ebf05f9589d0c8bdc1a3ec Author: JohnMcPMS <johnmcp@microsoft.com> Date: Mon, 30 Mar 2020 14:56:20 -0700 Update command line parsing and help (#71) Diffstat:
30 files changed, 1124 insertions(+), 281 deletions(-)
diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj @@ -172,6 +172,7 @@ </Link> </ItemDefinitionGroup> <ItemGroup> + <ClInclude Include="Argument.h" /> <ClInclude Include="Command.h" /> <ClInclude Include="Commands\HashCommand.h" /> <ClInclude Include="Commands\SearchCommand.h" /> @@ -199,6 +200,7 @@ <ClInclude Include="Workflows\WorkflowBase.h" /> </ItemGroup> <ItemGroup> + <ClCompile Include="Argument.cpp" /> <ClCompile Include="Command.cpp" /> <ClCompile Include="Commands\HashCommand.cpp" /> <ClCompile Include="Commands\SearchCommand.cpp" /> diff --git a/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters b/src/AppInstallerCLICore/AppInstallerCLICore.vcxproj.filters @@ -99,6 +99,9 @@ <ClInclude Include="VTSupport.h"> <Filter>Header Files</Filter> </ClInclude> + <ClInclude Include="Argument.h"> + <Filter>Header Files</Filter> + </ClInclude> </ItemGroup> <ItemGroup> <ClCompile Include="pch.cpp"> @@ -158,6 +161,9 @@ <ClCompile Include="VTSupport.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Argument.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLICore/Argument.cpp b/src/AppInstallerCLICore/Argument.cpp @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "pch.h" +#include "Argument.h" +#include "Localization.h" + + +namespace AppInstaller::CLI +{ + using namespace AppInstaller::CLI::Execution; + + Argument Argument::ForType(Execution::Args::Type type) + { + constexpr char None = APPINSTALLER_CLI_ARGUMENT_NO_SHORT_VER; + + switch (type) + { + case Args::Type::Query: + return Argument{ "query", 'q', Args::Type::Query, LOCME("The query used to search for an app"), ArgumentType::Positional }; + case Args::Type::Manifest: + return Argument{ "manifest", 'm', Args::Type::Manifest, LOCME("The path to the manifest of the application to install"), ArgumentType::Standard, Visibility::Help }; + case Args::Type::Id: + return Argument{ "id", None, Args::Type::Id, LOCME("Filter results by id"), ArgumentType::Standard, Visibility::Help }; + case Args::Type::Name: + return Argument{ "name", None, Args::Type::Name, LOCME("Filter results by name"), ArgumentType::Standard, Visibility::Help }; + case Args::Type::Moniker: + return Argument{ "moniker", None, Args::Type::Moniker, LOCME("Filter results by app moniker"), ArgumentType::Standard, Visibility::Help }; + case Args::Type::Tag: + return Argument{ "tag", None, Args::Type::Tag, LOCME("Filter results by tag"), ArgumentType::Standard, Visibility::Help }; + case Args::Type::Command: + return Argument{ "command", None, Args::Type::Command, LOCME("Filter results by command"), ArgumentType::Standard, Visibility::Help }; + case Args::Type::Source: + return Argument{ "source", 's', Args::Type::Source, LOCME("Find app using the specified source"), ArgumentType::Standard }; + case Args::Type::Count: + return Argument{ "count", 'n', Args::Type::Count, LOCME("Show no more than specified number of results"), ArgumentType::Standard }; + case Args::Type::Exact: + return Argument{ "exact", 'e', Args::Type::Exact, LOCME("Find app using exact match"), ArgumentType::Flag }; + case Args::Type::Version: + return Argument{ "version", 'v', Args::Type::Version, LOCME("Use the specified version; default is the latest version"), ArgumentType::Standard }; + case Args::Type::Channel: + return Argument{ "channel", 'c', Args::Type::Channel, LOCME("Use the specified channel; default is general audience"), ArgumentType::Standard, Visibility::Hidden }; + case Args::Type::Interactive: + return Argument{ "interactive", 'i', Args::Type::Interactive, LOCME("Request interactive installation; user input may be needed"), ArgumentType::Flag }; + case Args::Type::Silent: + return Argument{ "silent", 'h', Args::Type::Silent, LOCME("Request silent installation"), ArgumentType::Flag }; + case Args::Type::Language: + return Argument{ "lang", 'a', Args::Type::Language, LOCME("Language to install (if supported)"), ArgumentType::Standard, Visibility::Hidden }; + case Args::Type::Log: + return Argument{ "log", 'o', Args::Type::Log, LOCME("Log location (if supported)"), ArgumentType::Standard }; + case Args::Type::Override: + return Argument{ "override", None, Args::Type::Override, LOCME("Override arguments to be passed on to the installer"), ArgumentType::Standard, Visibility::Help }; + case Args::Type::InstallLocation: + return Argument{ "location", 'l', Args::Type::InstallLocation, LOCME("Location to install to (if supported)"), ArgumentType::Standard }; + case Args::Type::HashFile: + return Argument{ "file", 'f', Args::Type::HashFile, LOCME("File to be hashed"), ArgumentType::Positional, true }; + case Args::Type::Msix: + return Argument{ "msix", 'm', Args::Type::Msix, LOCME("Input file will be treated as msix; signature hash will be provided if signed"), ArgumentType::Flag }; + case Args::Type::ListVersions: + return Argument{ "versions", None, Args::Type::ListVersions, LOCME("Show available versions of the app"), ArgumentType::Flag }; + case Args::Type::Help: + return Argument{ "help", APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_CHAR, Args::Type::Help, LOCME("Shows help about the selected command"), ArgumentType::Flag }; + default: + THROW_HR(E_UNEXPECTED); + } + } + + void Argument::GetCommon(std::vector<Argument>& args) + { + args.push_back(ForType(Args::Type::Help)); + } +} diff --git a/src/AppInstallerCLICore/Argument.h b/src/AppInstallerCLICore/Argument.h @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#pragma once +#include "ExecutionContext.h" + +#include <string> +#include <string_view> + + +#define APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR '-' +#define APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_STRING "-" +#define APPINSTALLER_CLI_ARGUMENT_SPLIT_CHAR '=' +#define APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_CHAR '?' +#define APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_STRING "?" +#define APPINSTALLER_CLI_HELP_ARGUMENT APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_STRING APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_STRING + +#define APPINSTALLER_CLI_ARGUMENT_NO_SHORT_VER '\0' + +namespace AppInstaller::CLI +{ + // The type of argument. + enum class ArgumentType + { + // Argument requires specifying the name before the value. + Standard, + // Argument value can be specified alone; position indicates argument name. + Positional, + // Only argument name can be specified and indicates a bool value. + Flag, + }; + + // Controls the visibility of the field. + enum class Visibility + { + // Shown in the example. + Example, + // Shown only in the table below the example. + Help, + // Not shown in help. + Hidden, + }; + + // An argument to a command. + struct Argument + { + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, std::string desc) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, std::string desc, bool required) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_required(required) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, std::string desc, ArgumentType type) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, std::string desc, ArgumentType type, Visibility visibility) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_visibility(visibility) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, std::string desc, ArgumentType type, bool required) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_required(required) {} + + Argument(std::string_view name, char alias, Execution::Args::Type execArgType, std::string desc, ArgumentType type, Visibility visibility, bool required) : + m_name(name), m_alias(alias), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_visibility(visibility), m_required(required) {} + + ~Argument() = default; + + Argument(const Argument&) = default; + Argument& operator=(const Argument&) = default; + + Argument(Argument&&) = default; + Argument& operator=(Argument&&) = default; + + // Gets the argument for the given type. + static Argument ForType(Execution::Args::Type type); + + // Gets the common arguments for all commands. + static void GetCommon(std::vector<Argument>& args); + + std::string_view Name() const { return m_name; } + char Alias() const { return m_alias; } + Execution::Args::Type ExecArgType() const { return m_execArgType; } + const std::string& Description() const { return m_desc; } + bool Required() const { return m_required; } + ArgumentType Type() const { return m_type; } + size_t Limit() const { return m_countLimit; } + Visibility Visibility() const { return m_visibility; } + + Argument& SetRequired(bool required) { m_required = required; return *this; } + + private: + std::string_view m_name; + char m_alias; + Execution::Args::Type m_execArgType; + std::string m_desc; + bool m_required = false; + ArgumentType m_type = ArgumentType::Standard; + ::AppInstaller::CLI::Visibility m_visibility = Visibility::Example; + size_t m_countLimit = 1; + }; +} diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -6,58 +6,240 @@ namespace AppInstaller::CLI { + using namespace std::string_view_literals; + + Command::Command(std::string_view name, std::string_view parent) : + m_name(name) + { + if (!parent.empty()) + { + m_fullName.reserve(parent.length() + 1 + name.length()); + m_fullName = parent; + m_fullName += ParentSplitChar; + m_fullName += name; + } + else + { + m_fullName = name; + } + } + void Command::OutputIntroHeader(Execution::Reporter& reporter) const { - reporter.ShowMsg("AppInstaller Command Line"); - reporter.ShowMsg("Copyright (c) Microsoft Corporation"); + reporter.Info() << + "AppInstaller Command Line v" << Runtime::GetClientVersion() << std::endl << + "Copyright (c) Microsoft Corporation" << std::endl; } void Command::OutputHelp(Execution::Reporter& reporter, const CommandException* exception) const { + // Header OutputIntroHeader(reporter); reporter.EmptyLine(); + // Error if given if (exception) { - reporter.ShowMsg(exception->Message() + " : '" + std::string(exception->Param()) + '\'', Execution::Reporter::Level::Error); - reporter.EmptyLine(); + reporter.Error() << + exception->Message() << " : '" << exception->Param() << '\'' << std::endl << + std::endl; } - for (const auto& line : GetLongDescription()) + // Description + auto infoOut = reporter.Info(); + infoOut << + GetLongDescription() << std::endl << + std::endl; + + // Example usage for this command + std::string commandChain = FullName(); + size_t firstSplit = commandChain.find_first_of(ParentSplitChar); + if (firstSplit == std::string::npos) { - reporter.ShowMsg(line); + commandChain.clear(); } - reporter.EmptyLine(); + else + { + commandChain = commandChain.substr(firstSplit); + for (char& c : commandChain) + { + if (c == ParentSplitChar) + { + c = ' '; + } + } + } + + // Output the command preamble and command chain + infoOut << "usage: <exe>" << commandChain; auto commands = GetCommands(); + auto arguments = GetArguments(); + + bool hasArguments = false; + bool hasOptions = false; + + // Output the command token, made optional if arguments are present. + if (!commands.empty()) + { + infoOut << ' '; + + if (!arguments.empty()) + { + infoOut << '['; + } + + infoOut << "<command>"; + + if (!arguments.empty()) + { + infoOut << ']'; + } + } + + // Arguments are required by a test to have all positionals first. + for (const auto& arg : arguments) + { + if (arg.Type() == ArgumentType::Positional) + { + hasArguments = true; + + infoOut << ' '; + + if (!arg.Required()) + { + infoOut << '['; + } + + infoOut << '['; + + if (arg.Alias() == APPINSTALLER_CLI_ARGUMENT_NO_SHORT_VER) + { + infoOut << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << arg.Name(); + } + else + { + infoOut << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << arg.Alias(); + } + + infoOut << "] <" << arg.Name() << '>'; + + if (!arg.Required()) + { + infoOut << ']'; + } + } + else + { + hasOptions = true; + infoOut << " [<options>]"; + break; + } + } + + infoOut << + std::endl << + std::endl; + if (!commands.empty()) { - reporter.ShowMsg(LOCME("The following commands are available:")); - reporter.EmptyLine(); + if (Name() == FullName()) + { + infoOut << LOCME("The following commands are available:") << std::endl; + } + else + { + infoOut << LOCME("The following sub-commands are available:") << std::endl; + } + + size_t maxCommandNameLength = 0; + for (const auto& command : commands) + { + maxCommandNameLength = std::max(maxCommandNameLength, command->Name().length()); + } for (const auto& command : commands) { - reporter.ShowMsg(" " + std::string(command->Name())); - reporter.ShowMsg(" " + command->ShortDescription()); + size_t fillChars = (maxCommandNameLength - command->Name().length()) + 2; + infoOut << " " << Execution::HelpCommandEmphasis << command->Name() << std::string(fillChars, ' ') << command->ShortDescription() << std::endl; } - reporter.EmptyLine(); - reporter.ShowMsg(std::string(LOCME("For more details on a specific command, pass it the help argument.")) + " [" + APPINSTALLER_CLI_HELP_ARGUMENT + "]"); + infoOut << + std::endl << + LOCME("For more details on a specific command, pass it the help argument.") << " [" << APPINSTALLER_CLI_HELP_ARGUMENT << ']' << std::endl; } - else + + if (!arguments.empty()) { - reporter.ShowMsg(LOCME("The following arguments are available:")); - reporter.EmptyLine(); + if (!commands.empty()) + { + infoOut << std::endl; + } + std::vector<std::string> argNames; + size_t maxArgNameLength = 0; for (const auto& arg : GetArguments()) { - reporter.ShowMsg(" " + std::string(arg.Name())); - reporter.ShowMsg(" " + arg.Description()); + if (arg.Visibility() != Visibility::Hidden) + { + std::ostringstream strstr; + if (arg.Alias() != APPINSTALLER_CLI_ARGUMENT_NO_SHORT_VER) + { + strstr << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << arg.Alias() << ','; + } + strstr << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR << arg.Name(); + + argNames.emplace_back(strstr.str()); + maxArgNameLength = std::max(maxArgNameLength, argNames.back().length()); + } + } + + if (hasArguments) + { + infoOut << LOCME("The following arguments are available:") << std::endl; + + size_t i = 0; + for (const auto& arg : GetArguments()) + { + if (arg.Visibility() != Visibility::Hidden) + { + const std::string& argName = argNames[i++]; + if (arg.Type() == ArgumentType::Positional) + { + size_t fillChars = (maxArgNameLength - argName.length()) + 2; + infoOut << " " << Execution::HelpArgumentEmphasis << argName << std::string(fillChars, ' ') << arg.Description() << std::endl; + } + } + } + } + + if (hasOptions) + { + if (hasArguments) + { + infoOut << std::endl; + } + + infoOut << LOCME("The following options are available:") << std::endl; + + size_t i = 0; + for (const auto& arg : GetArguments()) + { + if (arg.Visibility() != Visibility::Hidden) + { + const std::string& argName = argNames[i++]; + if (arg.Type() != ArgumentType::Positional) + { + size_t fillChars = (maxArgNameLength - argName.length()) + 2; + infoOut << " " << Execution::HelpArgumentEmphasis << argName << std::string(fillChars, ' ') << arg.Description() << std::endl; + } + } + } } } } - std::unique_ptr<Command> Command::FindInvokedCommand(Invocation& inv) const + std::unique_ptr<Command> Command::FindSubCommand(Invocation& inv) const { auto itr = inv.begin(); if (itr == inv.end() || (*itr)[0] == APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR) @@ -75,27 +257,45 @@ namespace AppInstaller::CLI for (auto& command : commands) { - if (*itr == command->Name()) + if (Utility::CaseInsensitiveEquals(*itr, command->Name())) { AICLI_LOG(CLI, Info, << "Found subcommand: " << *itr); inv.consume(itr); - std::unique_ptr<Command> subcommand = command->FindInvokedCommand(inv); - // If we found a subcommand, return it. Otherwise, this is the one. - return (subcommand ? std::move(subcommand) : std::move(command)); + return std::move(command); } } + // TODO: If we get to a large number of commands, do a fuzzy search much like git throw CommandException(LOCME("Unrecognized command"), *itr); } + // Parse arguments as such: + // 1. If argument starts with a single -, only the single character alias is considered. + // a. If the named argument alias (a) needs a VALUE, it can be provided in these ways: + // -a=VALUE + // -a VALUE + // b. If the argument is a flag, additional characters after are treated as if they start + // with a -, repeatedly until the end of the argument is reached. Fails if non-flags hit. + // 2. If the argument starts with a double --, only the full name is considered. + // a. If the named argument (arg) needs a VALUE, it can be provided in these ways: + // --arg=VALUE + // --arg VALUE + // 3. If the argument does not start with any -, it is considered the next positional argument. + // 4. If the argument is only a double --, all further arguments are only considered as positional. void Command::ParseArguments(Invocation& inv, Execution::Args& execArgs) const { auto definedArgs = GetArguments(); + Argument::GetCommon(definedArgs); auto positionalSearchItr = definedArgs.begin(); + // The user can override processing '-blah' as an argument name by passing '--'. + bool onlyPositionalArgsRemain = false; + for (auto incomingArgsItr = inv.begin(); incomingArgsItr != inv.end(); ++incomingArgsItr) { - if ((*incomingArgsItr)[0] != APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR) + const std::string& currArg = *incomingArgsItr; + + if (onlyPositionalArgsRemain || currArg.empty() || currArg[0] != APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR) { // Positional argument, find the next appropriate one if the current itr isn't one or has hit its limit. if (positionalSearchItr != definedArgs.end() && @@ -106,32 +306,118 @@ namespace AppInstaller::CLI if (positionalSearchItr == definedArgs.end()) { - throw CommandException(LOCME("Found a positional argument when none was expected"), *incomingArgsItr); + throw CommandException(LOCME("Found a positional argument when none was expected"), currArg); + } + + execArgs.AddArg(positionalSearchItr->ExecArgType(), currArg); + } + // The currentArg must not be empty, and starts with a - + else if (currArg.length() == 1) + { + throw CommandException(LOCME("Invalid argument specifier"), currArg); + } + // Now it must be at least 2 chars + else if (currArg[1] != APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR) + { + // Parse the single character alias argument + char currChar = currArg[1]; + + auto itr = std::find_if(definedArgs.begin(), definedArgs.end(), [&](const Argument& arg) { return (currChar == arg.Alias()); }); + if (itr == definedArgs.end()) + { + throw CommandException(LOCME("Argument alias was not recognized for the current command"), currArg); } - execArgs.AddArg(positionalSearchItr->ExecArgType(), *incomingArgsItr); + if (itr->Type() == ArgumentType::Flag) + { + execArgs.AddArg(itr->ExecArgType()); + + for (size_t i = 2; i < currArg.length(); ++i) + { + currChar = currArg[i]; + + auto itr2 = std::find_if(definedArgs.begin(), definedArgs.end(), [&](const Argument& arg) { return (currChar == arg.Alias()); }); + if (itr2 == definedArgs.end()) + { + throw CommandException(LOCME("Adjoined flag alias not found"), currArg); + } + else if (itr2->Type() != ArgumentType::Flag) + { + throw CommandException(LOCME("Adjoined alias is not a flag"), currArg); + } + else + { + execArgs.AddArg(itr2->ExecArgType()); + } + } + } + else if (currArg.length() > 2) + { + if (currArg[2] == APPINSTALLER_CLI_ARGUMENT_SPLIT_CHAR) + { + execArgs.AddArg(itr->ExecArgType(), currArg.substr(3)); + } + else + { + throw CommandException(LOCME("Only the single character alias can occur after a single -"), currArg); + } + } + else + { + ++incomingArgsItr; + if (incomingArgsItr == inv.end()) + { + throw CommandException(LOCME("Argument value required, but none found"), currArg); + } + execArgs.AddArg(itr->ExecArgType(), *incomingArgsItr); + } } + // The currentArg is at least 2 chars, both of which are -- + else if (currArg.length() == 2) + { + onlyPositionalArgsRemain = true; + } + // The currentArg is more than 2 chars, both of which are -- else { // This is an arg name, find it and process its value if needed. - // Skip the name identifier char. - std::string argName = incomingArgsItr->substr(1); + // Skip the double arg identifier chars. + std::string argName = currArg.substr(2); bool argFound = false; + bool hasValue = false; + std::string argValue; + size_t splitChar = argName.find_first_of(APPINSTALLER_CLI_ARGUMENT_SPLIT_CHAR); + if (splitChar != std::string::npos) + { + hasValue = true; + argValue = argName.substr(splitChar + 1); + argName.resize(splitChar); + } + for (const auto& arg : definedArgs) { - if (argName == arg.Name()) + if (Utility::CaseInsensitiveEquals(argName, arg.Name())) { if (arg.Type() == ArgumentType::Flag) { + if (hasValue) + { + throw CommandException(LOCME("Flag argument cannot contain adjoined value"), currArg); + } + execArgs.AddArg(arg.ExecArgType()); } + else if (hasValue) + { + execArgs.AddArg(arg.ExecArgType(), std::move(argValue)); + } else { ++incomingArgsItr; if (incomingArgsItr == inv.end()) { - throw CommandException(LOCME("Argument value required, but none found"), *incomingArgsItr); + throw CommandException(LOCME("Argument value required, but none found"), currArg); } execArgs.AddArg(arg.ExecArgType(), *incomingArgsItr); } @@ -140,11 +426,7 @@ namespace AppInstaller::CLI } } - if (argName == APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_STRING) - { - execArgs.AddArg(Execution::Args::Type::Help); - } - else if (!argFound) + if (!argFound) { throw CommandException(LOCME("Argument name was not recognized for the current command"), *incomingArgsItr); } @@ -160,18 +442,7 @@ namespace AppInstaller::CLI return; } - for (const auto& arg : GetArguments()) - { - if (arg.Required() && !execArgs.Contains(arg.ExecArgType())) - { - throw CommandException(LOCME("Required argument not provided"), arg.Name()); - } - - if (arg.Limit() < execArgs.GetCount(arg.ExecArgType())) - { - throw CommandException(LOCME("Argument provided more times than allowed"), arg.Name()); - } - } + ValidateArgumentsInternal(execArgs); } void Command::Execute(Execution::Context& context) const @@ -187,6 +458,22 @@ namespace AppInstaller::CLI } } + void Command::ValidateArgumentsInternal(Execution::Args& execArgs) const + { + for (const auto& arg : GetArguments()) + { + if (arg.Required() && !execArgs.Contains(arg.ExecArgType())) + { + throw CommandException(LOCME("Required argument not provided"), arg.Name()); + } + + if (arg.Limit() < execArgs.GetCount(arg.ExecArgType())) + { + throw CommandException(LOCME("Argument provided more times than allowed"), arg.Name()); + } + } + } + void Command::ExecuteInternal(Execution::Context& context) const { context.Reporter.ShowMsg(LOCME("Oops, we forgot to do this..."), Execution::Reporter::Level::Error); diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h @@ -1,21 +1,18 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #pragma once +#include "Argument.h" +#include "ExecutionContext.h" +#include "Invocation.h" + #include <initializer_list> #include <memory> #include <ostream> #include <string> +#include <string_view> #include <type_traits> #include <vector> -#include "Invocation.h" -#include "ExecutionContext.h" - -#define APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_CHAR '-' -#define APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_STRING "-" -#define APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_CHAR '?' -#define APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_STRING "?" -#define APPINSTALLER_CLI_HELP_ARGUMENT APPINSTALLER_CLI_ARGUMENT_IDENTIFIER_STRING APPINSTALLER_CLI_HELP_ARGUMENT_TEXT_STRING namespace AppInstaller::CLI { @@ -31,57 +28,9 @@ namespace AppInstaller::CLI std::string_view m_param; }; - enum class ArgumentType - { - // Argument requires specifying the name before the value. - Standard, - // Argument value can be specified alone; position indicates argument name. - Positional, - // Only argument name can be specified and indicates a bool value. - Flag, - }; - - struct Argument - { - Argument(std::string_view name, Execution::Args::Type execArgType, std::string desc) : - m_name(name), m_execArgType(execArgType), m_desc(std::move(desc)) {} - - Argument(std::string_view name, Execution::Args::Type execArgType, std::string desc, bool required) : - m_name(name), m_execArgType(execArgType), m_desc(std::move(desc)), m_required(required) {} - - Argument(std::string_view name, Execution::Args::Type execArgType, std::string desc, ArgumentType type) : - m_name(name), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type) {} - - Argument(std::string_view name, Execution::Args::Type execArgType, std::string desc, ArgumentType type, bool required) : - m_name(name), m_execArgType(execArgType), m_desc(std::move(desc)), m_type(type), m_required(required) {} - - ~Argument() = default; - - Argument(const Argument&) = default; - Argument& operator=(const Argument&) = default; - - Argument(Argument&&) = default; - Argument& operator=(Argument&&) = default; - - std::string_view Name() const { return m_name; } - Execution::Args::Type ExecArgType() const { return m_execArgType; } - std::string Description() const { return m_desc; } - bool Required() const { return m_required; } - ArgumentType Type() const { return m_type; } - size_t Limit() const { return m_countLimit; } - - private: - std::string_view m_name; - Execution::Args::Type m_execArgType; - std::string m_desc; - bool m_required = false; - ArgumentType m_type = ArgumentType::Standard; - size_t m_countLimit = 1; - }; - struct Command { - Command(std::string_view name) : m_name(name) {} + Command(std::string_view name, std::string_view parent); virtual ~Command() = default; Command(const Command&) = default; @@ -90,28 +39,34 @@ namespace AppInstaller::CLI Command(Command&&) = default; Command& operator=(Command&&) = default; + // The character used to split between commands and their parents in FullName. + constexpr static char ParentSplitChar = ':'; + std::string_view Name() const { return m_name; } + const std::string& FullName() const { return m_fullName; } virtual std::vector<std::unique_ptr<Command>> GetCommands() const { return {}; } virtual std::vector<Argument> GetArguments() const { return {}; } virtual std::string ShortDescription() const { return {}; } - virtual std::vector<std::string> GetLongDescription() const { return {}; } + virtual std::string GetLongDescription() const { return {}; } virtual void OutputIntroHeader(Execution::Reporter& reporter) const; virtual void OutputHelp(Execution::Reporter& reporter, const CommandException* exception = nullptr) const; - virtual std::unique_ptr<Command> FindInvokedCommand(Invocation& inv) const; + virtual std::unique_ptr<Command> FindSubCommand(Invocation& inv) const; virtual void ParseArguments(Invocation& inv, Execution::Args& execArgs) const; virtual void ValidateArguments(Execution::Args& execArgs) const; virtual void Execute(Execution::Context& context) const; protected: + virtual void ValidateArgumentsInternal(Execution::Args& execArgs) const; virtual void ExecuteInternal(Execution::Context& context) const; private: std::string_view m_name; + std::string m_fullName; }; template <typename Container> diff --git a/src/AppInstallerCLICore/Commands/HashCommand.cpp b/src/AppInstallerCLICore/Commands/HashCommand.cpp @@ -8,14 +8,11 @@ namespace AppInstaller::CLI { using namespace std::string_view_literals; - constexpr std::string_view s_HashCommand_ArgName_File = "file"sv; - constexpr std::string_view s_HashCommand_ArgName_Msix = "msix"sv; - std::vector<Argument> HashCommand::GetArguments() const { return { - Argument{ s_HashCommand_ArgName_File, Execution::Args::Type::HashFile, LOCME("The input file to be hashed."), ArgumentType::Positional, true }, - Argument{ s_HashCommand_ArgName_Msix, Execution::Args::Type::Msix, LOCME("If specified, the input file will be treated as msix. Signature hash will be provided if exists."), ArgumentType::Flag }, + Argument::ForType(Execution::Args::Type::HashFile), + Argument::ForType(Execution::Args::Type::Msix), }; } @@ -24,11 +21,9 @@ namespace AppInstaller::CLI return LOCME("Helper to hash installer files"); } - std::vector<std::string> HashCommand::GetLongDescription() const + std::string HashCommand::GetLongDescription() const { - return { - LOCME("Helper to hash installer files"), - }; + return LOCME("Helper to hash installer files"); } void HashCommand::ExecuteInternal(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/Commands/HashCommand.h b/src/AppInstallerCLICore/Commands/HashCommand.h @@ -7,12 +7,12 @@ namespace AppInstaller::CLI { struct HashCommand final : public Command { - HashCommand() : Command("hash") {} + HashCommand(std::string_view parent) : Command("hash", parent) {} virtual std::vector<Argument> GetArguments() const override; virtual std::string ShortDescription() const override; - virtual std::vector<std::string> GetLongDescription() const override; + virtual std::string GetLongDescription() const override; protected: void ExecuteInternal(Execution::Context& context) const override; diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.cpp b/src/AppInstallerCLICore/Commands/InstallCommand.cpp @@ -13,38 +13,26 @@ namespace AppInstaller::CLI { using namespace std::string_view_literals; - constexpr std::string_view s_InstallCommand_ArgName_Query = "query"sv; - constexpr std::string_view s_InstallCommand_ArgName_Manifest = "manifest"sv; - constexpr std::string_view s_InstallCommand_ArgName_Id = "id"sv; - constexpr std::string_view s_InstallCommand_ArgName_Name = "name"sv; - constexpr std::string_view s_InstallCommand_ArgName_Moniker = "moniker"sv; - constexpr std::string_view s_InstallCommand_ArgName_Version = "version"sv; - constexpr std::string_view s_InstallCommand_ArgName_Channel = "channel"sv; - constexpr std::string_view s_InstallCommand_ArgName_Source = "source"sv; - constexpr std::string_view s_InstallCommand_ArgName_Exact = "exact"sv; - constexpr std::string_view s_InstallCommand_ArgName_Interactive = "interactive"sv; - constexpr std::string_view s_InstallCommand_ArgName_Silent = "silent"sv; - constexpr std::string_view s_InstallCommand_ArgName_Language = "language"sv; - constexpr std::string_view s_InstallCommand_ArgName_Log = "log"sv; - constexpr std::string_view s_InstallCommand_ArgName_Override = "override"sv; + constexpr std::string_view s_InstallCommand_ArgName_QueryOrManifest = "query|manifest"sv; std::vector<Argument> InstallCommand::GetArguments() const { return { - Argument{ s_InstallCommand_ArgName_Query, Execution::Args::Type::Query, LOCME("The name of the application to install"), ArgumentType::Positional, false }, - Argument{ s_InstallCommand_ArgName_Manifest, Execution::Args::Type::Manifest, LOCME("The path to the manifest of the application to install"), ArgumentType::Standard, false }, - Argument{ s_InstallCommand_ArgName_Id, Execution::Args::Type::Id, LOCME("The id of the application to show info"), ArgumentType::Standard }, - Argument{ s_InstallCommand_ArgName_Name, Execution::Args::Type::Name, LOCME("If specified, filter the results by name"), ArgumentType::Standard }, - Argument{ s_InstallCommand_ArgName_Moniker, Execution::Args::Type::Moniker, LOCME("If specified, filter the results by app moniker"), ArgumentType::Standard }, - Argument{ s_InstallCommand_ArgName_Version, Execution::Args::Type::Version, LOCME("If specified, use the specified version. Default is the latest version"), ArgumentType::Standard }, - Argument{ s_InstallCommand_ArgName_Channel, Execution::Args::Type::Channel, LOCME("If specified, use the specified channel. Default is general audience"), ArgumentType::Standard }, - Argument{ s_InstallCommand_ArgName_Source, Execution::Args::Type::Source, LOCME("If specified, find app using the specified source. Default is all source"), ArgumentType::Standard }, - Argument{ s_InstallCommand_ArgName_Exact, Execution::Args::Type::Exact, LOCME("If specified, find app using exact match"), ArgumentType::Flag }, - Argument{ s_InstallCommand_ArgName_Interactive, Execution::Args::Type::Interactive, LOCME("The application installation is interactive. User input is needed."), ArgumentType::Flag, false }, - Argument{ s_InstallCommand_ArgName_Silent, Execution::Args::Type::Silent, LOCME("The application installation is silent."), ArgumentType::Flag, false }, - Argument{ s_InstallCommand_ArgName_Language, Execution::Args::Type::Language, LOCME("Preferred language if application installation supports multiple languages."), ArgumentType::Standard, false }, - Argument{ s_InstallCommand_ArgName_Log, Execution::Args::Type::Log, LOCME("Preferred log location if application installation supports custom log path."), ArgumentType::Standard, false }, - Argument{ s_InstallCommand_ArgName_Override, Execution::Args::Type::Override, LOCME("Override switches to be passed on to application installer."), ArgumentType::Standard, false }, + Argument::ForType(Execution::Args::Type::Query), + Argument::ForType(Execution::Args::Type::Manifest), + Argument::ForType(Execution::Args::Type::Id), + Argument::ForType(Execution::Args::Type::Name), + Argument::ForType(Execution::Args::Type::Moniker), + Argument::ForType(Execution::Args::Type::Version), + Argument::ForType(Execution::Args::Type::Channel), + Argument::ForType(Execution::Args::Type::Source), + Argument::ForType(Execution::Args::Type::Exact), + Argument::ForType(Execution::Args::Type::Interactive), + Argument::ForType(Execution::Args::Type::Silent), + Argument::ForType(Execution::Args::Type::Language), + Argument::ForType(Execution::Args::Type::Log), + Argument::ForType(Execution::Args::Type::Override), + Argument::ForType(Execution::Args::Type::InstallLocation), }; } @@ -53,11 +41,9 @@ namespace AppInstaller::CLI return LOCME("Installs the given application"); } - std::vector<std::string> InstallCommand::GetLongDescription() const + std::string InstallCommand::GetLongDescription() const { - return { - LOCME("Installs the given application"), - }; + return LOCME("Installs the given application"); } void InstallCommand::ExecuteInternal(Execution::Context& context) const @@ -67,18 +53,17 @@ namespace AppInstaller::CLI appInstall.Execute(); } - void InstallCommand::ValidateArguments(Execution::Args& execArgs) const + void InstallCommand::ValidateArgumentsInternal(Execution::Args& execArgs) const { - Command::ValidateArguments(execArgs); - + // TODO: Maybe one day implement argument groups if (!execArgs.Contains(Execution::Args::Type::Query) && !execArgs.Contains(Execution::Args::Type::Manifest)) { - throw CommandException(LOCME("Required argument not provided"), s_InstallCommand_ArgName_Query); + throw CommandException(LOCME("Required argument not provided"), s_InstallCommand_ArgName_QueryOrManifest); } if (execArgs.Contains(Execution::Args::Type::Silent) && execArgs.Contains(Execution::Args::Type::Interactive)) { - throw CommandException(LOCME("More than one install behavior argument provided"), s_InstallCommand_ArgName_Query); + throw CommandException(LOCME("More than one install behavior argument provided"), s_InstallCommand_ArgName_QueryOrManifest); } } } diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.h b/src/AppInstallerCLICore/Commands/InstallCommand.h @@ -7,15 +7,15 @@ namespace AppInstaller::CLI { struct InstallCommand final : public Command { - InstallCommand() : Command("install") {} + InstallCommand(std::string_view parent) : Command("install", parent) {} std::vector<Argument> GetArguments() const override; std::string ShortDescription() const override; - std::vector<std::string> GetLongDescription() const override; + std::string GetLongDescription() const override; protected: + void ValidateArgumentsInternal(Execution::Args& execArgs) const override; void ExecuteInternal(Execution::Context& context) const override; - void ValidateArguments(Execution::Args& execArgs) const override; }; } diff --git a/src/AppInstallerCLICore/Commands/RootCommand.cpp b/src/AppInstallerCLICore/Commands/RootCommand.cpp @@ -15,24 +15,36 @@ namespace AppInstaller::CLI std::vector<std::unique_ptr<Command>> RootCommand::GetCommands() const { return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ - std::make_unique<InstallCommand>(), - std::make_unique<ShowCommand>(), - std::make_unique<SourceCommand>(), - std::make_unique<SearchCommand>(), - std::make_unique<HashCommand>(), + std::make_unique<InstallCommand>(FullName()), + std::make_unique<ShowCommand>(FullName()), + std::make_unique<SourceCommand>(FullName()), + std::make_unique<SearchCommand>(FullName()), + std::make_unique<HashCommand>(FullName()), }); } - std::vector<std::string> RootCommand::GetLongDescription() const + std::vector<Argument> RootCommand::GetArguments() const { - return { - LOCME("AppInstaller command line utility enables installing applications from the"), - LOCME("command line."), + return + { + Argument{ "version", 'v', Execution::Args::Type::ListVersions, LOCME("Display the version of the tool"), ArgumentType::Flag, Visibility::Help }, }; } + std::string RootCommand::GetLongDescription() const + { + return LOCME("AppInstaller command line utility enables installing applications from the command line."); + } + void RootCommand::ExecuteInternal(Execution::Context& context) const { - OutputHelp(context.Reporter); + if (context.Args.Contains(Execution::Args::Type::ListVersions)) + { + context.Reporter.Info() << 'v' << Runtime::GetClientVersion() << std::endl; + } + else + { + OutputHelp(context.Reporter); + } } } diff --git a/src/AppInstallerCLICore/Commands/RootCommand.h b/src/AppInstallerCLICore/Commands/RootCommand.h @@ -7,11 +7,12 @@ namespace AppInstaller::CLI { struct RootCommand final : public Command { - RootCommand() : Command("root") {} + RootCommand() : Command("root", {}) {} - virtual std::vector<std::unique_ptr<Command>> GetCommands() const override; + std::vector<std::unique_ptr<Command>> GetCommands() const override; + std::vector<Argument> GetArguments() const override; - virtual std::vector<std::string> GetLongDescription() const override; + std::string GetLongDescription() const override; protected: virtual void ExecuteInternal(Execution::Context& context) const; diff --git a/src/AppInstallerCLICore/Commands/SearchCommand.cpp b/src/AppInstallerCLICore/Commands/SearchCommand.cpp @@ -7,31 +7,21 @@ namespace AppInstaller::CLI { - using namespace AppInstaller::Workflow; + using namespace AppInstaller::CLI::Execution; using namespace std::string_view_literals; - constexpr std::string_view s_SearchCommand_ArgName_Query = "query"sv; - constexpr std::string_view s_SearchCommand_ArgName_Id = "id"sv; - constexpr std::string_view s_SearchCommand_ArgName_Name = "name"sv; - constexpr std::string_view s_SearchCommand_ArgName_Moniker = "moniker"sv; - constexpr std::string_view s_SearchCommand_ArgName_Tag = "tag"sv; - constexpr std::string_view s_SearchCommand_ArgName_Command = "command"sv; - constexpr std::string_view s_SearchCommand_ArgName_Source = "source"sv; - constexpr std::string_view s_SearchCommand_ArgName_Count = "count"sv; - constexpr std::string_view s_SearchCommand_ArgName_Exact = "exact"sv; - std::vector<Argument> SearchCommand::GetArguments() const { return { - Argument{ s_SearchCommand_ArgName_Query, Execution::Args::Type::Query, LOCME("The query used to search for an app"), ArgumentType::Positional, false }, - Argument{ s_SearchCommand_ArgName_Id, Execution::Args::Type::Id, LOCME("If specified, filter the results by id"), ArgumentType::Standard }, - Argument{ s_SearchCommand_ArgName_Name, Execution::Args::Type::Name, LOCME("If specified, filter the results by name"), ArgumentType::Standard }, - Argument{ s_SearchCommand_ArgName_Moniker, Execution::Args::Type::Moniker, LOCME("If specified, filter the results by app moniker"), ArgumentType::Standard }, - Argument{ s_SearchCommand_ArgName_Tag, Execution::Args::Type::Tag, LOCME("If specified, filter the results by tag"), ArgumentType::Standard }, - Argument{ s_SearchCommand_ArgName_Command, Execution::Args::Type::Command, LOCME("If specified, filter the results by command"), ArgumentType::Standard }, - Argument{ s_SearchCommand_ArgName_Source, Execution::Args::Type::Source, LOCME("If specified, find app using the specified source. Default is all source"), ArgumentType::Standard }, - Argument{ s_SearchCommand_ArgName_Count, Execution::Args::Type::Count, LOCME("If specified, find app and show only up to specified number of results."), ArgumentType::Standard }, - Argument{ s_SearchCommand_ArgName_Exact, Execution::Args::Type::Exact, LOCME("If specified, find app using exact match"), ArgumentType::Flag }, + Argument::ForType(Execution::Args::Type::Query), + Argument::ForType(Execution::Args::Type::Id), + Argument::ForType(Execution::Args::Type::Name), + Argument::ForType(Execution::Args::Type::Moniker), + Argument::ForType(Execution::Args::Type::Tag), + Argument::ForType(Execution::Args::Type::Command), + Argument::ForType(Execution::Args::Type::Source), + Argument::ForType(Execution::Args::Type::Count), + Argument::ForType(Execution::Args::Type::Exact), }; } @@ -40,16 +30,14 @@ namespace AppInstaller::CLI return LOCME("Find and show basic info of apps"); } - std::vector<std::string> SearchCommand::GetLongDescription() const + std::string SearchCommand::GetLongDescription() const { - return { - LOCME("Find and show basic info of apps"), - }; + return LOCME("Find and show basic info of apps"); } - void SearchCommand::ExecuteInternal(Execution::Context& context) const + void SearchCommand::ExecuteInternal(Context& context) const { - SearchFlow appSearch{ context }; + Workflow::SearchFlow appSearch{ context }; appSearch.Execute(); } diff --git a/src/AppInstallerCLICore/Commands/SearchCommand.h b/src/AppInstallerCLICore/Commands/SearchCommand.h @@ -7,12 +7,12 @@ namespace AppInstaller::CLI { struct SearchCommand final : public Command { - SearchCommand() : Command("search") {} + SearchCommand(std::string_view parent) : Command("search", parent) {} virtual std::vector<Argument> GetArguments() const override; virtual std::string ShortDescription() const override; - virtual std::vector<std::string> GetLongDescription() const override; + virtual std::string GetLongDescription() const override; protected: void ExecuteInternal(Execution::Context& context) const override; diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.cpp b/src/AppInstallerCLICore/Commands/ShowCommand.cpp @@ -10,28 +10,18 @@ namespace AppInstaller::CLI using namespace AppInstaller::Workflow; using namespace std::string_view_literals; - constexpr std::string_view s_ShowCommand_ArgName_Query = "query"sv; - constexpr std::string_view s_ShowCommand_ArgName_Id = "id"sv; - constexpr std::string_view s_ShowCommand_ArgName_Name = "name"sv; - constexpr std::string_view s_ShowCommand_ArgName_Moniker = "moniker"sv; - constexpr std::string_view s_ShowCommand_ArgName_Version = "version"sv; - constexpr std::string_view s_ShowCommand_ArgName_Channel = "channel"sv; - constexpr std::string_view s_ShowCommand_ArgName_Source = "source"sv; - constexpr std::string_view s_ShowCommand_ArgName_Exact = "exact"sv; - constexpr std::string_view s_ShowCommand_ArgName_ListVersions = "listversions"sv; - std::vector<Argument> ShowCommand::GetArguments() const { return { - Argument{ s_ShowCommand_ArgName_Query, Execution::Args::Type::Query, LOCME("The query used to search for an app"), ArgumentType::Positional, true }, - Argument{ s_ShowCommand_ArgName_Id, Execution::Args::Type::Id, LOCME("The id of the application to show info"), ArgumentType::Standard }, - Argument{ s_ShowCommand_ArgName_Name, Execution::Args::Type::Name, LOCME("If specified, filter the results by name"), ArgumentType::Standard }, - Argument{ s_ShowCommand_ArgName_Moniker, Execution::Args::Type::Moniker, LOCME("If specified, filter the results by app moniker"), ArgumentType::Standard }, - Argument{ s_ShowCommand_ArgName_Version, Execution::Args::Type::Version, LOCME("If specified, use the specified version. Default is the latest version"), ArgumentType::Standard }, - Argument{ s_ShowCommand_ArgName_Channel, Execution::Args::Type::Channel, LOCME("If specified, use the specified channel. Default is general audience"), ArgumentType::Standard }, - Argument{ s_ShowCommand_ArgName_Source, Execution::Args::Type::Source, LOCME("If specified, find app using the specified source. Default is all source"), ArgumentType::Standard }, - Argument{ s_ShowCommand_ArgName_Exact, Execution::Args::Type::Exact, LOCME("If specified, find app using exact match"), ArgumentType::Flag }, - Argument{ s_ShowCommand_ArgName_ListVersions, Execution::Args::Type::ListVersions, LOCME("If specified, only show available versions of the app"), ArgumentType::Flag }, + Argument::ForType(Execution::Args::Type::Query), + Argument::ForType(Execution::Args::Type::Id), + Argument::ForType(Execution::Args::Type::Name), + Argument::ForType(Execution::Args::Type::Moniker), + Argument::ForType(Execution::Args::Type::Version), + Argument::ForType(Execution::Args::Type::Channel), + Argument::ForType(Execution::Args::Type::Source), + Argument::ForType(Execution::Args::Type::Exact), + Argument::ForType(Execution::Args::Type::ListVersions), }; } @@ -40,11 +30,9 @@ namespace AppInstaller::CLI return LOCME("Shows info of the given application"); } - std::vector<std::string> ShowCommand::GetLongDescription() const + std::string ShowCommand::GetLongDescription() const { - return { - LOCME("Shows info of the given application"), - }; + return LOCME("Shows info of the given application"); } void ShowCommand::ExecuteInternal(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.h b/src/AppInstallerCLICore/Commands/ShowCommand.h @@ -7,12 +7,12 @@ namespace AppInstaller::CLI { struct ShowCommand final : public Command { - ShowCommand() : Command("show") {} + ShowCommand(std::string_view parent) : Command("show", parent) {} virtual std::vector<Argument> GetArguments() const override; virtual std::string ShortDescription() const override; - virtual std::vector<std::string> GetLongDescription() const override; + virtual std::string GetLongDescription() const override; protected: void ExecuteInternal(AppInstaller::CLI::Execution::Context& context) const override; diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.cpp b/src/AppInstallerCLICore/Commands/SourceCommand.cpp @@ -6,19 +6,23 @@ namespace AppInstaller::CLI { + using namespace AppInstaller::CLI::Execution; using namespace std::string_view_literals; constexpr std::string_view s_SourceCommand_ArgName_Name = "name"sv; + constexpr char s_SourceCommand_ArgAlias_Name = 'n'; constexpr std::string_view s_SourceCommand_ArgName_Type = "type"sv; + constexpr char s_SourceCommand_ArgAlias_Type = 't'; constexpr std::string_view s_SourceCommand_ArgName_Arg = "arg"sv; + constexpr char s_SourceCommand_ArgAlias_Arg = 'a'; std::vector<std::unique_ptr<Command>> SourceCommand::GetCommands() const { return InitializeFromMoveOnly<std::vector<std::unique_ptr<Command>>>({ - std::make_unique<SourceAddCommand>(), - std::make_unique<SourceListCommand>(), - std::make_unique<SourceUpdateCommand>(), - std::make_unique<SourceRemoveCommand>(), + std::make_unique<SourceAddCommand>(FullName()), + std::make_unique<SourceListCommand>(FullName()), + std::make_unique<SourceUpdateCommand>(FullName()), + std::make_unique<SourceRemoveCommand>(FullName()), }); } @@ -27,11 +31,9 @@ namespace AppInstaller::CLI return LOCME("Manage sources of applications"); } - std::vector<std::string> SourceCommand::GetLongDescription() const + std::string SourceCommand::GetLongDescription() const { - return { - LOCME("Manage sources of applications"), - }; + return LOCME("Manage sources of applications"); } void SourceCommand::ExecuteInternal(Execution::Context& context) const @@ -42,9 +44,9 @@ namespace AppInstaller::CLI std::vector<Argument> SourceAddCommand::GetArguments() const { return { - Argument{ s_SourceCommand_ArgName_Name, Execution::Args::Type::SourceName, LOCME("Name of the source for future reference"), ArgumentType::Positional, true }, - Argument{ s_SourceCommand_ArgName_Arg, Execution::Args::Type::SourceArg, LOCME("Argument given to the source"), ArgumentType::Positional, true }, - Argument{ s_SourceCommand_ArgName_Type, Execution::Args::Type::SourceType, LOCME("Type of the source"), ArgumentType::Positional, false }, + Argument{ s_SourceCommand_ArgName_Name, s_SourceCommand_ArgAlias_Name, Args::Type::SourceName, LOCME("Name of the source for future reference"), ArgumentType::Positional, true }, + Argument{ s_SourceCommand_ArgName_Arg, s_SourceCommand_ArgAlias_Arg, Args::Type::SourceArg, LOCME("Argument given to the source"), ArgumentType::Positional, true }, + Argument{ s_SourceCommand_ArgName_Type, s_SourceCommand_ArgAlias_Type, Args::Type::SourceType, LOCME("Type of the source"), ArgumentType::Positional }, }; } @@ -53,11 +55,9 @@ namespace AppInstaller::CLI return LOCME("Add a new source"); } - std::vector<std::string> SourceAddCommand::GetLongDescription() const + std::string SourceAddCommand::GetLongDescription() const { - return { - LOCME("Add a new source"), - }; + return LOCME("Add a new source"); } void SourceAddCommand::ExecuteInternal(Execution::Context& context) const @@ -86,7 +86,7 @@ namespace AppInstaller::CLI std::vector<Argument> SourceListCommand::GetArguments() const { return { - Argument{ s_SourceCommand_ArgName_Name, Execution::Args::Type::SourceName, LOCME("Name of the source to list full details for"), ArgumentType::Positional, false }, + Argument{ s_SourceCommand_ArgName_Name, s_SourceCommand_ArgAlias_Name, Args::Type::SourceName, LOCME("Name of the source to list full details for"), ArgumentType::Positional }, }; } @@ -95,11 +95,9 @@ namespace AppInstaller::CLI return LOCME("List current sources"); } - std::vector<std::string> SourceListCommand::GetLongDescription() const + std::string SourceListCommand::GetLongDescription() const { - return { - LOCME("List current sources"), - }; + return LOCME("List current sources"); } void SourceListCommand::ExecuteInternal(Execution::Context& context) const @@ -154,7 +152,7 @@ namespace AppInstaller::CLI std::vector<Argument> SourceUpdateCommand::GetArguments() const { return { - Argument{ s_SourceCommand_ArgName_Name, Execution::Args::Type::SourceName, LOCME("Name of the source to update"), ArgumentType::Positional, false }, + Argument{ s_SourceCommand_ArgName_Name, s_SourceCommand_ArgAlias_Name, Args::Type::SourceName, LOCME("Name of the source to update"), ArgumentType::Positional }, }; } @@ -163,11 +161,9 @@ namespace AppInstaller::CLI return LOCME("Update current sources"); } - std::vector<std::string> SourceUpdateCommand::GetLongDescription() const + std::string SourceUpdateCommand::GetLongDescription() const { - return { - LOCME("Update current sources"), - }; + return LOCME("Update current sources"); } void SourceUpdateCommand::ExecuteInternal(Execution::Context& context) const @@ -203,7 +199,7 @@ namespace AppInstaller::CLI std::vector<Argument> SourceRemoveCommand::GetArguments() const { return { - Argument{ s_SourceCommand_ArgName_Name, Execution::Args::Type::SourceName, LOCME("Name of the source to remove"), ArgumentType::Positional, true }, + Argument{ s_SourceCommand_ArgName_Name, s_SourceCommand_ArgAlias_Name, Args::Type::SourceName, LOCME("Name of the source to remove"), ArgumentType::Positional, true }, }; } @@ -212,11 +208,9 @@ namespace AppInstaller::CLI return LOCME("Remove current sources"); } - std::vector<std::string> SourceRemoveCommand::GetLongDescription() const + std::string SourceRemoveCommand::GetLongDescription() const { - return { - LOCME("Remove current sources"), - }; + return LOCME("Remove current sources"); } void SourceRemoveCommand::ExecuteInternal(Execution::Context& context) const diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.h b/src/AppInstallerCLICore/Commands/SourceCommand.h @@ -7,12 +7,12 @@ namespace AppInstaller::CLI { struct SourceCommand final : public Command { - SourceCommand() : Command("source") {} + SourceCommand(std::string_view parent) : Command("source", parent) {} virtual std::vector<std::unique_ptr<Command>> GetCommands() const override; virtual std::string ShortDescription() const override; - virtual std::vector<std::string> GetLongDescription() const override; + virtual std::string GetLongDescription() const override; protected: virtual void ExecuteInternal(Execution::Context& context) const; @@ -20,12 +20,12 @@ namespace AppInstaller::CLI struct SourceAddCommand final : public Command { - SourceAddCommand() : Command("add") {} + SourceAddCommand(std::string_view parent) : Command("add", parent) {} virtual std::vector<Argument> GetArguments() const override; virtual std::string ShortDescription() const override; - virtual std::vector<std::string> GetLongDescription() const override; + virtual std::string GetLongDescription() const override; protected: virtual void ExecuteInternal(Execution::Context& context) const override; @@ -33,12 +33,12 @@ namespace AppInstaller::CLI struct SourceListCommand final : public Command { - SourceListCommand() : Command("list") {} + SourceListCommand(std::string_view parent) : Command("list", parent) {} virtual std::vector<Argument> GetArguments() const override; virtual std::string ShortDescription() const override; - virtual std::vector<std::string> GetLongDescription() const override; + virtual std::string GetLongDescription() const override; protected: virtual void ExecuteInternal(Execution::Context& context) const override; @@ -46,12 +46,12 @@ namespace AppInstaller::CLI struct SourceUpdateCommand final : public Command { - SourceUpdateCommand() : Command("update") {} + SourceUpdateCommand(std::string_view parent) : Command("update", parent) {} virtual std::vector<Argument> GetArguments() const override; virtual std::string ShortDescription() const override; - virtual std::vector<std::string> GetLongDescription() const override; + virtual std::string GetLongDescription() const override; protected: virtual void ExecuteInternal(Execution::Context& context) const override; @@ -59,12 +59,12 @@ namespace AppInstaller::CLI struct SourceRemoveCommand final : public Command { - SourceRemoveCommand() : Command("remove") {} + SourceRemoveCommand(std::string_view parent) : Command("remove", parent) {} virtual std::vector<Argument> GetArguments() const override; virtual std::string ShortDescription() const override; - virtual std::vector<std::string> GetLongDescription() const override; + virtual std::string GetLongDescription() const override; protected: virtual void ExecuteInternal(Execution::Context& context) const override; diff --git a/src/AppInstallerCLICore/Core.cpp b/src/AppInstallerCLICore/Core.cpp @@ -72,38 +72,35 @@ namespace AppInstaller::CLI return strstr.str(); }()); - RootCommand root; Invocation invocation{ std::move(utf8Args) }; // The root command is our fallback in the event of very bad or very little input - Command* commandToExecute = &root; - std::unique_ptr<Command> foundCommand; + std::unique_ptr<Command> command = std::make_unique<RootCommand>(); try { - foundCommand = root.FindInvokedCommand(invocation); - if (foundCommand) + std::unique_ptr<Command> subCommand = command->FindSubCommand(invocation); + while (subCommand) { - commandToExecute = foundCommand.get(); + command = std::move(subCommand); + subCommand = command->FindSubCommand(invocation); } + Logging::Telemetry().LogCommand(command->FullName()); - // TODO: Log full command (so source::add) rather than just leaf command - Logging::Telemetry().LogCommand(commandToExecute->Name()); - - commandToExecute->ParseArguments(invocation, context.Args); - commandToExecute->ValidateArguments(context.Args); + command->ParseArguments(invocation, context.Args); + command->ValidateArguments(context.Args); } // Exceptions specific to parsing the arguments of a command catch (const CommandException& ce) { - commandToExecute->OutputHelp(context.Reporter, &ce); + command->OutputHelp(context.Reporter, &ce); AICLI_LOG(CLI, Error, << "Error encountered parsing command line: " << ce.Message()); return APPINSTALLER_CLI_ERROR_INVALID_CL_ARGUMENTS; } try { - commandToExecute->Execute(context); + command->Execute(context); } // Exceptions that may occur in the process of executing an arbitrary command catch (const winrt::hresult_error& hre) @@ -122,7 +119,7 @@ namespace AppInstaller::CLI return APPINSTALLER_CLI_ERROR_COMMAND_FAILED; } - Logging::Telemetry().LogCommandSuccess(commandToExecute->Name()); + Logging::Telemetry().LogCommandSuccess(command->FullName()); return 0; } // End of the line exceptions that are not ever expected. diff --git a/src/AppInstallerCLICore/ExecutionReporter.cpp b/src/AppInstallerCLICore/ExecutionReporter.cpp @@ -6,6 +6,9 @@ namespace AppInstaller::CLI::Execution { + VirtualTerminal::Sequence HelpCommandEmphasis = VirtualTerminal::TextFormat::Foreground::BrightWhite; + VirtualTerminal::Sequence HelpArgumentEmphasis = VirtualTerminal::TextFormat::Foreground::BrightWhite; + namespace { // The reporter that will receive CTRL signals diff --git a/src/AppInstallerCLICore/ExecutionReporter.h b/src/AppInstallerCLICore/ExecutionReporter.h @@ -76,10 +76,10 @@ namespace AppInstaller::CLI::Execution void AddFormat(const VirtualTerminal::Sequence& sequence); template <typename T> - OutputStream& operator<<(T&& t) + OutputStream& operator<<(const T& t) { ApplyFormat(); - m_out << std::forward<T>(t); + m_out << t; return *this; } @@ -166,4 +166,8 @@ namespace AppInstaller::CLI::Execution wil::srwlock m_progressCallbackLock; std::atomic<ProgressCallback*> m_progressCallback; }; + + // Indirection to enable change without tracking down every place + extern VirtualTerminal::Sequence HelpCommandEmphasis; + extern VirtualTerminal::Sequence HelpArgumentEmphasis; } diff --git a/src/AppInstallerCLICore/Invocation.h b/src/AppInstallerCLICore/Invocation.h @@ -6,6 +6,7 @@ namespace AppInstaller::CLI { + // Contains the raw command line arguments and functionality to iterate and consume them. struct Invocation { Invocation(std::vector<std::string>&& args) : m_args(std::move(args)) {} diff --git a/src/AppInstallerCLICore/VTSupport.cpp b/src/AppInstallerCLICore/VTSupport.cpp @@ -78,6 +78,7 @@ namespace AppInstaller::CLI::VirtualTerminal { Sequence BrightRed = AICLI_VT_TEXTFORMAT(91); Sequence BrightYellow = AICLI_VT_TEXTFORMAT(93); + Sequence BrightWhite = AICLI_VT_TEXTFORMAT(97); } namespace Background diff --git a/src/AppInstallerCLICore/VTSupport.h b/src/AppInstallerCLICore/VTSupport.h @@ -66,6 +66,7 @@ namespace AppInstaller::CLI::VirtualTerminal { extern Sequence BrightRed; extern Sequence BrightYellow; + extern Sequence BrightWhite; } namespace Background diff --git a/src/AppInstallerCLICore/pch.h b/src/AppInstallerCLICore/pch.h @@ -2,6 +2,7 @@ // Licensed under the MIT License. #pragma once +#define NOMINMAX #include <windows.h> #include <WinInet.h> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj @@ -153,6 +153,7 @@ <ClInclude Include="TestHooks.h" /> </ItemGroup> <ItemGroup> + <ClCompile Include="Command.cpp" /> <ClCompile Include="Downloader.cpp" /> <ClCompile Include="HashCommand.cpp" /> <ClCompile Include="MsixInfo.cpp" /> diff --git a/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters b/src/AppInstallerCLITests/AppInstallerCLITests.vcxproj.filters @@ -83,6 +83,9 @@ <ClCompile Include="Strings.cpp"> <Filter>Source Files</Filter> </ClCompile> + <ClCompile Include="Command.cpp"> + <Filter>Source Files</Filter> + </ClCompile> </ItemGroup> <ItemGroup> <None Include="PropertySheet.props" /> diff --git a/src/AppInstallerCLITests/Command.cpp b/src/AppInstallerCLITests/Command.cpp @@ -0,0 +1,445 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. +#include "pch.h" +#include "TestCommon.h" +#include <Command.h> +#include <AppInstallerStrings.h> +#include <Commands/RootCommand.h> + +using namespace std::string_literals; +using namespace TestCommon; +using namespace AppInstaller; +using namespace AppInstaller::CLI; +using namespace AppInstaller::CLI::Execution; + +std::string GetCommandName(const std::unique_ptr<Command>& command) +{ + return std::string{ command->Name() }; +} + +std::string GetArgumentName(const Argument& arg) +{ + return std::string{ arg.Name() }; +} + +std::string GetArgumentAlias(const Argument& arg) +{ + if (arg.Alias() == APPINSTALLER_CLI_ARGUMENT_NO_SHORT_VER) + { + return {}; + } + else + { + return std::string(1, arg.Alias()); + } +} + +template <typename Enumerable, typename Op> +void EnsureStringsAreLowercaseAndNoCollisions(const std::string& info, const Enumerable& e, Op& op, bool requireLower = true) +{ + INFO(info); + std::unordered_set<std::string> values; + + for (const auto& val : e) + { + std::string valString = op(val); + if (valString.empty()) + { + continue; + } + INFO(valString); + + if (requireLower) + { + std::string lowerVal = Utility::ToLower(valString); + REQUIRE(valString == lowerVal); + } + + REQUIRE(values.find(valString) == values.end()); + + values.emplace(std::move(valString)); + } +} + +void EnsureCommandConsistency(const Command& command) +{ + EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " commands", command.GetCommands(), GetCommandName); + + auto args = command.GetArguments(); + Argument::GetCommon(args); + EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " argument names", args, GetArgumentName); + EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " argument alias", args, GetArgumentAlias, false); + + // No : allowed in commands + for (const auto& comm : command.GetCommands()) + { + INFO(command.FullName()); + INFO(comm->Name()); + + REQUIRE(comm->Name().find_first_of(Command::ParentSplitChar) == std::string_view::npos); + } + + // No = allowed in arguments + // All positional args should be listed first + bool foundNonPositional = false; + for (const auto& arg : command.GetArguments()) + { + INFO(command.FullName()); + INFO(arg.Name()); + + REQUIRE(arg.Name().find_first_of(APPINSTALLER_CLI_ARGUMENT_SPLIT_CHAR) == std::string_view::npos); + + if (arg.Type() == ArgumentType::Positional) + { + REQUIRE(!foundNonPositional); + } + else + { + foundNonPositional = true; + } + } + + // Recurse for all subcommands + for (const auto& sub : command.GetCommands()) + { + EnsureCommandConsistency(*sub.get()); + } +} + +// This test ensure that the command tree we expose does not have any incosistencies. +// 1. No command name collisions +// 2. All command names are lower cased +// 3. No argument name collisions +// 4. All arguments are lower cased +// 5. No argument alias collisions +// 6. All argument alias are lower cased +// 7. No argument names contain '=' +// 8. All positional arguments are first in the list +TEST_CASE("EnsureCommandTreeConsistency", "[command]") +{ + RootCommand root; + EnsureCommandConsistency(root); +} + +struct TestCommand : public Command +{ + TestCommand(std::vector<Argument> args) : Command("test", ""), m_args(std::move(args)) {} + + std::vector<Argument> GetArguments() const override + { + return m_args; + } + + std::vector<Argument> m_args; +}; + +// Matcher that lets us verify CommandExceptions. +struct CommandExceptionMatcher : public Catch::MatcherBase<CommandException> +{ + CommandExceptionMatcher(const std::string &arg) : m_expectedArg(arg) {} + + bool match(const CommandException& ce) const override + { + return ce.Param() == m_expectedArg; + } + + std::string describe() const override + { + std::ostringstream result; + result << "has param == " << m_expectedArg; + return result.str(); + } + +private: + std::string m_expectedArg; +}; + +#define REQUIRE_COMMAND_EXCEPTION(_expr_, _arg_) REQUIRE_THROWS_MATCHES(_expr_, CommandException, CommandExceptionMatcher(_arg_)) + +void RequireValueParsedToArg(const std::string& value, const Argument& arg, const Args& args) +{ + REQUIRE(args.Contains(arg.ExecArgType())); + REQUIRE(value == args.GetArg(arg.ExecArgType())); +} + +TEST_CASE("ParseArguments_MultiplePositional", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + }); + + std::vector<std::string> values{ "val1", "val2" }; + Invocation inv{ std::vector<std::string>(values) }; + + command.ParseArguments(inv, args); + + RequireValueParsedToArg(values[0], command.m_args[0], args); + RequireValueParsedToArg(values[1], command.m_args[2], args); +} + +TEST_CASE("ParseArguments_ForcePositional", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + }); + + std::vector<std::string> values{ "val1", "--", "-std1" }; + Invocation inv{ std::vector<std::string>(values) }; + + command.ParseArguments(inv, args); + + RequireValueParsedToArg(values[0], command.m_args[0], args); + RequireValueParsedToArg(values[2], command.m_args[2], args); +} + +TEST_CASE("ParseArguments_TooManyPositional", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + }); + + std::vector<std::string> values{ "val1", "--", "-std1" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[2]); +} + +TEST_CASE("ParseArguments_InvalidChar", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + }); + + std::vector<std::string> values{ "val1", "-", "-std1" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); +} + +TEST_CASE("ParseArguments_InvalidAlias", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + }); + + std::vector<std::string> values{ "val1", "-b", "-std1" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); +} + +TEST_CASE("ParseArguments_MultiFlag", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "flag1", 's', Args::Type::Command, "", ArgumentType::Flag }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + Argument{ "flag2", 't', Args::Type::Exact, "", ArgumentType::Flag }, + }); + + std::vector<std::string> values{ "val1", "-st", "val2" }; + Invocation inv{ std::vector<std::string>(values) }; + + command.ParseArguments(inv, args); + + REQUIRE(args.Contains(command.m_args[1].ExecArgType())); + REQUIRE(args.Contains(command.m_args[3].ExecArgType())); +} + +TEST_CASE("ParseArguments_FlagThenUnknown", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "flag1", 's', Args::Type::Command, "", ArgumentType::Flag }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + Argument{ "flag2", 't', Args::Type::Exact, "", ArgumentType::Flag }, + }); + + std::vector<std::string> values{ "val1", "-sr", "val2" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); +} + +TEST_CASE("ParseArguments_FlagThenNonFlag", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "flag1", 's', Args::Type::Command, "", ArgumentType::Flag }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + Argument{ "flag2", 't', Args::Type::Exact, "", ArgumentType::Flag }, + }); + + std::vector<std::string> values{ "val1", "-sp", "val2" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); +} + +TEST_CASE("ParseArguments_NameUsingAliasSpecifier", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + Argument{ "flag1", 'f', Args::Type::Exact, "", ArgumentType::Flag }, + }); + + std::vector<std::string> values{ "another", "-flag1" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); +} + +TEST_CASE("ParseArguments_AliasWithAdjoinedValue", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + }); + + std::vector<std::string> values{ "-s=Val1" }; + Invocation inv{ std::vector<std::string>(values) }; + + command.ParseArguments(inv, args); + + RequireValueParsedToArg(values[0].substr(3), command.m_args[1], args); +} + +TEST_CASE("ParseArguments_AliasWithSeparatedValue", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + }); + + std::vector<std::string> values{ "-s", "Val1" }; + Invocation inv{ std::vector<std::string>(values) }; + + command.ParseArguments(inv, args); + + RequireValueParsedToArg(values[1], command.m_args[1], args); +} + +TEST_CASE("ParseArguments_AliasWithSeparatedValueMissing", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + }); + + std::vector<std::string> values{ "-s" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[0]); +} + +TEST_CASE("ParseArguments_NameWithAdjoinedValue", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + }); + + std::vector<std::string> values{ "--pos1=Val1" }; + Invocation inv{ std::vector<std::string>(values) }; + + command.ParseArguments(inv, args); + + RequireValueParsedToArg(values[0].substr(7), command.m_args[0], args); +} + +TEST_CASE("ParseArguments_NameFlag", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + Argument{ "flag1", 'f', Args::Type::Exact, "", ArgumentType::Flag }, + }); + + std::vector<std::string> values{ "--flag1", "arbitrary" }; + Invocation inv{ std::vector<std::string>(values) }; + + command.ParseArguments(inv, args); + + RequireValueParsedToArg(values[1], command.m_args[0], args); + REQUIRE(args.Contains(command.m_args[3].ExecArgType())); +} + +TEST_CASE("ParseArguments_NameFlagWithAdjoinedValue", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + Argument{ "flag1", 'f', Args::Type::Exact, "", ArgumentType::Flag }, + }); + + std::vector<std::string> values{ "another", "--flag1=arbitrary" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); +} + +TEST_CASE("ParseArguments_NameWithSeparatedValue", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + Argument{ "flag1", 'f', Args::Type::Exact, "", ArgumentType::Flag }, + }); + + std::vector<std::string> values{ "--pos2", "arbitrary" }; + Invocation inv{ std::vector<std::string>(values) }; + + command.ParseArguments(inv, args); + + RequireValueParsedToArg(values[1], command.m_args[2], args); +} + +TEST_CASE("ParseArguments_UnknownName", "[command]") +{ + Args args; + TestCommand command({ + Argument{ "pos1", 'p', Args::Type::Channel, "", ArgumentType::Positional }, + Argument{ "std1", 's', Args::Type::Command, "", ArgumentType::Standard }, + Argument{ "pos2", 'q', Args::Type::Count, "", ArgumentType::Positional }, + Argument{ "flag1", 'f', Args::Type::Exact, "", ArgumentType::Flag }, + }); + + std::vector<std::string> values{ "another", "--nope" }; + Invocation inv{ std::vector<std::string>(values) }; + + REQUIRE_COMMAND_EXCEPTION(command.ParseArguments(inv, args), values[1]); +} diff --git a/src/AppInstallerCLITests/HashCommand.cpp b/src/AppInstallerCLITests/HashCommand.cpp @@ -14,7 +14,7 @@ TEST_CASE("HashCommandWithTestMsix", "[Sha256Hash]") Execution::Context context{ hashOutput, std::cin }; context.Args.AddArg(Execution::Args::Type::HashFile, TestDataFile("TestSignedApp.msix").GetPath().u8string()); context.Args.AddArg(Execution::Args::Type::Msix); - HashCommand hashCommand; + HashCommand hashCommand({}); hashCommand.Execute(context); diff --git a/src/AppInstallerCLITests/pch.h b/src/AppInstallerCLITests/pch.h @@ -23,8 +23,10 @@ #include <iostream> #include <memory> #include <sstream> +#include <string> +#include <string_view> +#include <unordered_set> #include <utility> #include <vector> -#include <string> #include <yaml-cpp/yaml.h>