commit a5e202c7ceff44515489b077aefc122f0ac1cec0 parent 1b6f681b4ba12e04d5355cbc71100f30731439a3 Author: Kaleb Luedtke <jluedtk@jci.com> Date: Tue, 2 Aug 2022 13:25:55 -0500 Add command aliases (#2390) Diffstat:
18 files changed, 123 insertions(+), 28 deletions(-)
diff --git a/src/AppInstallerCLICore/Command.cpp b/src/AppInstallerCLICore/Command.cpp @@ -26,11 +26,12 @@ namespace AppInstaller::CLI Command::Command( std::string_view name, + std::vector<std::string_view> aliases, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature, Settings::TogglePolicy::Policy groupPolicy) : - m_name(name), m_visibility(visibility), m_feature(feature), m_groupPolicy(groupPolicy) + m_name(name), m_aliases(std::move(aliases)), m_visibility(visibility), m_feature(feature), m_groupPolicy(groupPolicy) { if (!parent.empty()) { @@ -115,6 +116,7 @@ namespace AppInstaller::CLI // Output the command preamble and command chain infoOut << Resource::String::Usage << ": winget"_liv << Utility::LocIndView{ commandChain }; + auto commandAliases = Aliases(); auto commands = GetVisibleCommands(); auto arguments = GetVisibleArguments(); @@ -183,6 +185,17 @@ namespace AppInstaller::CLI std::endl << std::endl; + if (!commandAliases.empty()) + { + infoOut << Resource::String::AvailableCommandAliases << std::endl; + + for (const auto& commandAlias : commandAliases) + { + infoOut << " "_liv << Execution::HelpCommandEmphasis << commandAlias << std::endl; + } + infoOut << std::endl; + } + if (!commands.empty()) { if (Name() == FullName()) @@ -291,7 +304,10 @@ namespace AppInstaller::CLI for (auto& command : commands) { - if (Utility::CaseInsensitiveEquals(*itr, command->Name())) + if ( + Utility::CaseInsensitiveEquals(*itr, command->Name()) || + Utility::CaseInsensitiveContains(command->Aliases(), *itr) + ) { if (!ExperimentalFeature::IsEnabled(command->Feature())) { @@ -709,6 +725,17 @@ namespace AppInstaller::CLI { context.Reporter.Completion() << command->Name() << std::endl; } + // Allow for command aliases to be auto-completed + if (!(command->Aliases()).empty() && !word.empty()) + { + for (const auto& commandAlias : command->Aliases()) + { + if (Utility::CaseInsensitiveStartsWith(commandAlias, word)) + { + context.Reporter.Completion() << commandAlias << std::endl; + } + } + } } } diff --git a/src/AppInstallerCLICore/Command.h b/src/AppInstallerCLICore/Command.h @@ -56,16 +56,18 @@ namespace AppInstaller::CLI }; Command(std::string_view name, std::string_view parent) : - Command(name, parent, Settings::ExperimentalFeature::Feature::None) {} - Command(std::string_view name, std::string_view parent, Command::Visibility visibility) : - Command(name, parent, visibility, Settings::ExperimentalFeature::Feature::None) {} - Command(std::string_view name, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : - Command(name, parent, Command::Visibility::Show, feature) {} - Command(std::string_view name, std::string_view parent, Settings::TogglePolicy::Policy groupPolicy) : - Command(name, parent, Command::Visibility::Show, Settings::ExperimentalFeature::Feature::None, groupPolicy) {} - Command(std::string_view name, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature) : - Command(name, parent, visibility, feature, Settings::TogglePolicy::Policy::None) {} - Command(std::string_view name, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature, Settings::TogglePolicy::Policy groupPolicy); + Command(name, {}, parent) {} + Command(std::string_view name,std::vector<std::string_view> aliases, std::string_view parent) : + Command(name, aliases, parent, Settings::ExperimentalFeature::Feature::None) {} + Command(std::string_view name,std::vector<std::string_view> aliases, std::string_view parent, Command::Visibility visibility) : + Command(name, aliases, parent, visibility, Settings::ExperimentalFeature::Feature::None) {} + Command(std::string_view name,std::vector<std::string_view> aliases, std::string_view parent, Settings::ExperimentalFeature::Feature feature) : + Command(name, aliases, parent, Command::Visibility::Show, feature) {} + Command(std::string_view name,std::vector<std::string_view> aliases, std::string_view parent, Settings::TogglePolicy::Policy groupPolicy) : + Command(name, aliases, parent, Command::Visibility::Show, Settings::ExperimentalFeature::Feature::None, groupPolicy) {} + Command(std::string_view name,std::vector<std::string_view> aliases, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature) : + Command(name, aliases, parent, visibility, feature, Settings::TogglePolicy::Policy::None) {} + Command(std::string_view name,std::vector<std::string_view> aliases, std::string_view parent, Command::Visibility visibility, Settings::ExperimentalFeature::Feature feature, Settings::TogglePolicy::Policy groupPolicy); virtual ~Command() = default; Command(const Command&) = default; @@ -78,6 +80,7 @@ namespace AppInstaller::CLI constexpr static char ParentSplitChar = ':'; std::string_view Name() const { return m_name; } + const std::vector<std::string_view>& Aliases() const& { return m_aliases; } const std::string& FullName() const { return m_fullName; } Command::Visibility GetVisibility() const; Settings::ExperimentalFeature::Feature Feature() const { return m_feature; } @@ -111,6 +114,7 @@ namespace AppInstaller::CLI private: std::string_view m_name; std::string m_fullName; + std::vector<std::string_view> m_aliases; Command::Visibility m_visibility; Settings::ExperimentalFeature::Feature m_feature; Settings::TogglePolicy::Policy m_groupPolicy; diff --git a/src/AppInstallerCLICore/Commands/CompleteCommand.h b/src/AppInstallerCLICore/Commands/CompleteCommand.h @@ -9,7 +9,7 @@ namespace AppInstaller::CLI // be context sensitive in their data output. struct CompleteCommand final : public Command { - CompleteCommand(std::string_view parent) : Command("complete", parent, Visibility::Hidden) {} + CompleteCommand(std::string_view parent) : Command("complete", {}, parent, Visibility::Hidden) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/ExperimentalCommand.h b/src/AppInstallerCLICore/Commands/ExperimentalCommand.h @@ -10,7 +10,7 @@ namespace AppInstaller::CLI { // This command is used as an example on how experimental features can be used. // To enable this command set ExperimentalCmd = true in the settings file. - ExperimentalCommand(std::string_view parent) : Command("experimental", parent, Settings::ExperimentalFeature::Feature::ExperimentalCmd) {} + ExperimentalCommand(std::string_view parent) : Command("experimental", {}, parent, Settings::ExperimentalFeature::Feature::ExperimentalCmd) {} virtual std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/InstallCommand.h b/src/AppInstallerCLICore/Commands/InstallCommand.h @@ -7,7 +7,7 @@ namespace AppInstaller::CLI { struct InstallCommand final : public Command { - InstallCommand(std::string_view parent) : Command("install", parent) {} + InstallCommand(std::string_view parent) : Command("install", { "add" }, parent) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/ListCommand.h b/src/AppInstallerCLICore/Commands/ListCommand.h @@ -8,7 +8,7 @@ namespace AppInstaller::CLI // Command to get the set of installed packages on the system. struct ListCommand final : public Command { - ListCommand(std::string_view parent) : Command("list", parent) {} + ListCommand(std::string_view parent) : Command("list", { "ls" }, parent) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/SearchCommand.h b/src/AppInstallerCLICore/Commands/SearchCommand.h @@ -5,9 +5,10 @@ namespace AppInstaller::CLI { + struct SearchCommand final : public Command { - SearchCommand(std::string_view parent) : Command("search", parent) {} + SearchCommand(std::string_view parent) : Command("search", { "find" }, parent) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/SettingsCommand.h b/src/AppInstallerCLICore/Commands/SettingsCommand.h @@ -7,7 +7,7 @@ namespace AppInstaller::CLI { struct SettingsCommand final : public Command { - SettingsCommand(std::string_view parent) : Command("settings", parent, Settings::TogglePolicy::Policy::Settings) {} + SettingsCommand(std::string_view parent) : Command("settings", { "config" }, parent, Settings::TogglePolicy::Policy::Settings) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/ShowCommand.h b/src/AppInstallerCLICore/Commands/ShowCommand.h @@ -7,7 +7,7 @@ namespace AppInstaller::CLI { struct ShowCommand final : public Command { - ShowCommand(std::string_view parent) : Command("show", parent) {} + ShowCommand(std::string_view parent) : Command("show", { "view" }, parent) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/SourceCommand.h b/src/AppInstallerCLICore/Commands/SourceCommand.h @@ -22,7 +22,7 @@ namespace AppInstaller::CLI struct SourceAddCommand final : public Command { - SourceAddCommand(std::string_view parent) : Command("add", parent, Settings::TogglePolicy::Policy::AllowedSources) {} + SourceAddCommand(std::string_view parent) : Command("add", {}, parent, Settings::TogglePolicy::Policy::AllowedSources) {} std::vector<Argument> GetArguments() const override; @@ -54,7 +54,7 @@ namespace AppInstaller::CLI struct SourceUpdateCommand final : public Command { - SourceUpdateCommand(std::string_view parent) : Command("update", parent) {} + SourceUpdateCommand(std::string_view parent) : Command("update", { "refresh" }, parent) {} std::vector<Argument> GetArguments() const override; @@ -72,7 +72,7 @@ namespace AppInstaller::CLI struct SourceRemoveCommand final : public Command { // We can remove user or default sources, so this is not gated by any single policy. - SourceRemoveCommand(std::string_view parent) : Command("remove", parent) {} + SourceRemoveCommand(std::string_view parent) : Command("remove", { "rm" }, parent) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/UninstallCommand.h b/src/AppInstallerCLICore/Commands/UninstallCommand.h @@ -7,7 +7,7 @@ namespace AppInstaller::CLI { struct UninstallCommand final : public Command { - UninstallCommand(std::string_view parent) : Command("uninstall", parent) {} + UninstallCommand(std::string_view parent) : Command("uninstall", { "remove", "rm" }, parent) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Commands/UpgradeCommand.h b/src/AppInstallerCLICore/Commands/UpgradeCommand.h @@ -7,7 +7,7 @@ namespace AppInstaller::CLI { struct UpgradeCommand final : public Command { - UpgradeCommand(std::string_view parent) : Command("upgrade", parent) {} + UpgradeCommand(std::string_view parent) : Command("upgrade", { "update" }, parent) {} std::vector<Argument> GetArguments() const override; diff --git a/src/AppInstallerCLICore/Resources.h b/src/AppInstallerCLICore/Resources.h @@ -28,6 +28,7 @@ namespace AppInstaller::CLI::Resource WINGET_DEFINE_RESOURCE_STRINGID(AdminSettingEnabled); WINGET_DEFINE_RESOURCE_STRINGID(AdminSettingEnableDescription); WINGET_DEFINE_RESOURCE_STRINGID(AvailableArguments); + WINGET_DEFINE_RESOURCE_STRINGID(AvailableCommandAliases); WINGET_DEFINE_RESOURCE_STRINGID(AvailableCommands); WINGET_DEFINE_RESOURCE_STRINGID(AvailableHeader); WINGET_DEFINE_RESOURCE_STRINGID(AvailableOptions); diff --git a/src/AppInstallerCLIE2ETests/SearchCommand.cs b/src/AppInstallerCLIE2ETests/SearchCommand.cs @@ -23,6 +23,14 @@ namespace AppInstallerCLIE2ETests Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); } + public void SearchUsingAlias() + { + var result = TestCommon.RunAICLICommand("find", "TestExampleInstaller"); + Assert.AreEqual(Constants.ErrorCode.S_OK, result.ExitCode); + Assert.True(result.StdOut.Contains("TestExampleInstaller")); + Assert.True(result.StdOut.Contains("AppInstallerTest.TestExampleInstaller")); + } + [Test] public void SearchWithName() { diff --git a/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw b/src/AppInstallerCLIPackage/Shared/Strings/en-us/winget.resw @@ -126,6 +126,9 @@ <data name="AvailableArguments" xml:space="preserve"> <value>The following arguments are available:</value> </data> + <data name="AvailableCommandAliases" xml:space="preserve"> + <value>The following command aliases are available:</value> + </data> <data name="AvailableCommands" xml:space="preserve"> <value>The following commands are available:</value> <comment>Commands the tool supports</comment> diff --git a/src/AppInstallerCLITests/Command.cpp b/src/AppInstallerCLITests/Command.cpp @@ -15,7 +15,12 @@ using namespace AppInstaller::CLI::Execution; std::string GetCommandName(const std::unique_ptr<Command>& command) { - return std::string{ command->Name() }; + return std::string{ command->FullName() }; +} + +std::vector<std::string_view> GetCommandAliases(const std::unique_ptr<Command>& command) +{ + return std::vector<std::string_view> { command->Aliases() }; } std::string GetArgumentName(const Argument& arg) @@ -47,6 +52,11 @@ std::string GetArgumentAlias(const Argument& arg) } } +bool StringIsLowercase(const std::string& s) +{ + return Utility::ToLower(s) == s; +} + template <typename Enumerable, typename Op> void EnsureStringsAreLowercaseAndNoCollisions(const std::string& info, const Enumerable& e, Op& op, std::unordered_set<std::string>& values, bool requireLower = true) { @@ -63,8 +73,7 @@ void EnsureStringsAreLowercaseAndNoCollisions(const std::string& info, const Enu if (requireLower) { - std::string lowerVal = Utility::ToLower(valString); - REQUIRE(valString == lowerVal); + REQUIRE(StringIsLowercase(valString)); } REQUIRE(values.find(valString) == values.end()); @@ -80,9 +89,42 @@ void EnsureStringsAreLowercaseAndNoCollisions(const std::string& info, const Enu EnsureStringsAreLowercaseAndNoCollisions(info, e, op, values, requireLower); } +template <typename Enumerable, typename Op> +void EnsureVectorStringViewsAreLowercaseAndNoCollisions(const std::string& info, const Enumerable& e, Op& op, std::unordered_set<std::string>& values, bool requireLower = true) +{ + INFO(info); + + for (const auto& val : e) + { + std::vector<std::string_view> aliasVector = op(val); + std::vector<std::string> valVector(aliasVector.begin(), aliasVector.end()); + if (valVector.empty()) + { + continue; + } + // When op returns a vector, we need to ensure every value in the vector does not cause a collision + for (auto& valString : valVector) + { + INFO(valString); + + if (requireLower) + { + REQUIRE(StringIsLowercase(valString)); + } + + REQUIRE(values.find(valString) == values.end()); + values.emplace(std::move(valString)); + } + } +} + void EnsureCommandConsistency(const Command& command) { - EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " commands", command.GetCommands(), GetCommandName); + // Command names and aliases exist in the same space, so both need to be checked as a set + // However, collisions do not occur between levels, so the full name must be used to check for collision + std::unordered_set<std::string> allCommandAliasNames; + EnsureStringsAreLowercaseAndNoCollisions(command.FullName() + " commands", command.GetCommands(), GetCommandName, allCommandAliasNames); + EnsureVectorStringViewsAreLowercaseAndNoCollisions(command.FullName() + " aliases", command.GetCommands(), GetCommandAliases, allCommandAliasNames); auto args = command.GetArguments(); Argument::GetCommon(args); diff --git a/src/AppInstallerCommonCore/AppInstallerStrings.cpp b/src/AppInstallerCommonCore/AppInstallerStrings.cpp @@ -105,6 +105,12 @@ namespace AppInstaller::Utility return ToLower(a) == ToLower(b); } + bool CaseInsensitiveContains(const std::vector<std::string_view>& a, std::string_view b) + { + auto B = ToLower(b); + return std::any_of(a.begin(), a.end(), [&](const std::string_view& s) { return ToLower(s) == B; }); + } + bool CaseInsensitiveStartsWith(std::string_view a, std::string_view b) { return a.length() >= b.length() && CaseInsensitiveEquals(a.substr(0, b.length()), b); diff --git a/src/AppInstallerCommonCore/Public/AppInstallerStrings.h b/src/AppInstallerCommonCore/Public/AppInstallerStrings.h @@ -104,6 +104,9 @@ namespace AppInstaller::Utility // Use this if one of the values is a known value, and thus ToLower is sufficient. bool CaseInsensitiveEquals(std::string_view a, std::string_view b); + // Returns if a UTF8 string is contained within a vector in a case insensitive manner. + bool CaseInsensitiveContains(const std::vector<std::string_view>& a, std::string_view b); + // Determines if string a starts with string b. // Use this if one of the values is a known value, and thus ToLower is sufficient. bool CaseInsensitiveStartsWith(std::string_view a, std::string_view b);