From 0a947b335a963deae30f0b9533d0b6793f3f9278 Mon Sep 17 00:00:00 2001 From: Cory Knox Date: Thu, 13 Oct 2022 15:18:29 -0700 Subject: [PATCH] (#2591) Add headers to limit output commands When a user asks for limited output from Chocolatey, it is not uncommon to pipe that output to `ConvertFrom-String` or `ConvertFrom-Csv` and manually add headers to get back an object. This allows for getting a header row back so that the end user doesn't need to add their own headers and discern what they are. This also adds a StringResources static class that allows us to store constant strings in and use them across the code to reduce duplication. --- src/chocolatey/StringResources.cs | 12 +- .../ApplicationParameters.cs | 1 + .../builders/ConfigurationBuilder.cs | 3 +- .../commands/ChocolateyApiKeyCommand.cs | 8 ++ .../commands/ChocolateyConfigCommand.cs | 4 + .../commands/ChocolateyFeatureCommand.cs | 3 + .../commands/ChocolateyInfoCommand.cs | 3 + .../commands/ChocolateyListCommand.cs | 5 +- .../commands/ChocolateyOutdatedCommand.cs | 3 + .../commands/ChocolateyPinCommand.cs | 8 ++ .../commands/ChocolateySourceCommand.cs | 3 + .../commands/ChocolateyTemplateCommand.cs | 5 +- .../configuration/ChocolateyConfiguration.cs | 2 + .../ChocolateyConfigSettingsService.cs | 14 ++ .../services/ChocolateyPackageService.cs | 25 +++- .../services/TemplateService.cs | 9 +- tests/pester-tests/features/Headers.Tests.ps1 | 133 ++++++++++++++++++ 17 files changed, 233 insertions(+), 8 deletions(-) create mode 100644 tests/pester-tests/features/Headers.Tests.ps1 diff --git a/src/chocolatey/StringResources.cs b/src/chocolatey/StringResources.cs index 4bbf0f6c99..5ca47b397f 100644 --- a/src/chocolatey/StringResources.cs +++ b/src/chocolatey/StringResources.cs @@ -56,5 +56,15 @@ public static class EnvironmentVariables [Browsable(false)] internal const string PackageNuspecVersion = "packageNuspecVersion"; } + + public static class OptionDescriptions + { + public const string DISPLAY_HEADERS = "Display headers - Display headers when limit-output is used. Requires 2.3.0"; + } + + public static class Options + { + public const string DISPLAY_HEADERS = "headers"; // TODO: This option name needs to be decided and agreed upon. + } } -} \ No newline at end of file +} diff --git a/src/chocolatey/infrastructure.app/ApplicationParameters.cs b/src/chocolatey/infrastructure.app/ApplicationParameters.cs index 4f28197ce0..430d03624f 100644 --- a/src/chocolatey/infrastructure.app/ApplicationParameters.cs +++ b/src/chocolatey/infrastructure.app/ApplicationParameters.cs @@ -226,6 +226,7 @@ public static class Features public static readonly string LogValidationResultsOnWarnings = "logValidationResultsOnWarnings"; public static readonly string UsePackageRepositoryOptimizations = "usePackageRepositoryOptimizations"; public static readonly string DisableCompatibilityChecks = "disableCompatibilityChecks"; + public static readonly string AlwaysDisplayHeaders = "alwaysDisplayHeaders"; } public static class Messages diff --git a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs index 2f0055b3af..1e8f0f2195 100644 --- a/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs +++ b/src/chocolatey/infrastructure.app/builders/ConfigurationBuilder.cs @@ -334,7 +334,8 @@ private static void SetAllFeatureFlags(ChocolateyConfiguration config, ConfigFil config.Features.LogValidationResultsOnWarnings = SetFeatureFlag(ApplicationParameters.Features.LogValidationResultsOnWarnings, configFileSettings, defaultEnabled: true, description: "Log validation results on warnings - Should the validation results be logged if there are warnings?"); config.Features.UsePackageRepositoryOptimizations = SetFeatureFlag(ApplicationParameters.Features.UsePackageRepositoryOptimizations, configFileSettings, defaultEnabled: true, description: "Use Package Repository Optimizations - Turn on optimizations for reducing bandwidth with repository queries during package install/upgrade/outdated operations. Should generally be left enabled, unless a repository needs to support older methods of query. When disabled, this makes queries similar to the way they were done in earlier versions of Chocolatey."); config.PromptForConfirmation = !SetFeatureFlag(ApplicationParameters.Features.AllowGlobalConfirmation, configFileSettings, defaultEnabled: false, description: "Prompt for confirmation in scripts or bypass."); - config.DisableCompatibilityChecks = SetFeatureFlag(ApplicationParameters.Features.DisableCompatibilityChecks, configFileSettings, defaultEnabled: false, description: "Disable Compatibility Checks - Disable showing a warning when there is an incompatibility between Chocolatey CLI and Chocolatey Licensed Extension. Available in 1.1.0+"); + config.DisableCompatibilityChecks = SetFeatureFlag(ApplicationParameters.Features.DisableCompatibilityChecks, configFileSettings, defaultEnabled: false, description: "Disable Compatibility Checks - Disable showing a warning when there is an incompatibility between Chocolatey CLI and Chocolatey Licensed Extension. Available in 1.1.0+"), + config.DisplayHeaders = SetFeatureFlag(ApplicationParameters.Features.AlwaysDisplayHeaders, configFileSettings, defaultEnabled: false, description: StringResources.OptionDescriptions.DISPLAY_HEADERS); } private static bool SetFeatureFlag(string featureName, ConfigFileSettings configFileSettings, bool defaultEnabled, string description) diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyApiKeyCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyApiKeyCommand.cs index 5840d1db41..0887c5c8c8 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyApiKeyCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyApiKeyCommand.cs @@ -50,6 +50,9 @@ public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfi .Add("k=|key=|apikey=|api-key=", "ApiKey - The API key for the source. This is the authentication that identifies you and allows you to push to a source. With some sources this is either a key or it could be a user name and password specified as 'user:password'.", option => configuration.ApiKeyCommand.Key = option.UnquoteSafe()) + .Add(StringResources.Options.DISPLAY_HEADERS, + StringResources.OptionDescriptions.DISPLAY_HEADERS, + option => configuration.DisplayHeaders = true) ; } @@ -192,6 +195,11 @@ public virtual void Run(ChocolateyConfiguration configuration) _configSettingsService.SetApiKey(configuration); break; default: + if (!configuration.RegularOutput && configuration.DisplayHeaders) + { + this.Log().Info("Source|Key"); + } + _configSettingsService.GetApiKey(configuration, (key) => { string authenticatedString = string.IsNullOrWhiteSpace(key.Key) ? string.Empty : "(Authenticated)"; diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyConfigCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyConfigCommand.cs index e878133fb8..00362ff762 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyConfigCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyConfigCommand.cs @@ -51,6 +51,10 @@ public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfi "value=", "Value - the value of the config setting. Required with some actions. Defaults to empty.", option => configuration.ConfigCommand.ConfigValue = option.UnquoteSafe()) + .Add( + StringResources.Options.DISPLAY_HEADERS, + StringResources.OptionDescriptions.DISPLAY_HEADERS, + option => configuration.DisplayHeaders = true) ; } diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyFeatureCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyFeatureCommand.cs index 968e161661..c7e5f46f33 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyFeatureCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyFeatureCommand.cs @@ -47,6 +47,9 @@ public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfi .Add("n=|name=", "Name - the name of the source. Required with actions other than list. Defaults to empty.", option => configuration.FeatureCommand.Name = option.UnquoteSafe()) + .Add(StringResources.Options.DISPLAY_HEADERS, + StringResources.OptionDescriptions.DISPLAY_HEADERS, + option => configuration.DisplayHeaders = true) ; } diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyInfoCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyInfoCommand.cs index cc32c98d74..b3c8213ef1 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyInfoCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyInfoCommand.cs @@ -74,6 +74,9 @@ public override void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConf configuration.Features.UsePackageRepositoryOptimizations = false; } }) + .Add(StringResources.Options.DISPLAY_HEADERS, + StringResources.OptionDescriptions.DISPLAY_HEADERS, + option => configuration.DisplayHeaders = true) ; } diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyListCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyListCommand.cs index 9e37dd568b..aef197eb3a 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyListCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyListCommand.cs @@ -134,7 +134,10 @@ public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfi option => configuration.ListCommand.IdStartsWith = option != null) .Add("detail|detailed", "Detailed - Alias for verbose.", - option => configuration.Verbose = option != null); + option => configuration.Verbose = option != null), + .Add(StringResources.Options.DISPLAY_HEADER, + StringResources.OptionDescriptions.DISPLAY_HEADER, + option => configuration.DisplayHeaders = true); } public virtual int Count(ChocolateyConfiguration config) diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyOutdatedCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyOutdatedCommand.cs index af63ffed56..798105a095 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyOutdatedCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyOutdatedCommand.cs @@ -72,6 +72,9 @@ public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfi configuration.Features.UsePackageRepositoryOptimizations = false; } }) + .Add(StringResources.Options.DISPLAY_HEADERS, + StringResources.OptionDescriptions.DISPLAY_HEADERS, + option => configuration.DisplayHeaders = true) ; } diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyPinCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyPinCommand.cs index ef4ebe3d36..57132708ef 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyPinCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyPinCommand.cs @@ -56,6 +56,9 @@ public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfi .Add("version=", "Version - Used when multiple versions of a package are installed. Defaults to empty.", option => configuration.Version = option.UnquoteSafe()) + .Add(StringResources.Options.DISPLAY_HEADERS, + StringResources.OptionDescriptions.DISPLAY_HEADERS, + option => configuration.DisplayHeaders = true) ; } @@ -169,6 +172,11 @@ public virtual void ListPins(ChocolateyConfiguration config) config.QuietOutput = quiet; config.Input = input; + if (!config.RegularOutput && config.DisplayHeaders) + { + this.Log().Info("PackageId|Version"); + } + foreach (var pkg in packages.OrEmpty()) { var pkgInfo = _packageInfoService.Get(pkg.PackageMetadata); diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateySourceCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateySourceCommand.cs index bf78c4ca26..fb7bba9bb3 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateySourceCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateySourceCommand.cs @@ -75,6 +75,9 @@ public virtual void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfi .Add("adminonly|admin-only", "Visible to Administrators Only - Should this source be visible to non-administrators? Requires business edition (v1.12.2+). Defaults to false.", option => configuration.SourceCommand.VisibleToAdminsOnly = option != null) + .Add(StringResources.Options.DISPLAY_HEADERS, + StringResources.OptionDescriptions.DISPLAY_HEADERS, + option => configuration.DisplayHeaders = true) ; } diff --git a/src/chocolatey/infrastructure.app/commands/ChocolateyTemplateCommand.cs b/src/chocolatey/infrastructure.app/commands/ChocolateyTemplateCommand.cs index ee8fa0aac1..8900df8891 100644 --- a/src/chocolatey/infrastructure.app/commands/ChocolateyTemplateCommand.cs +++ b/src/chocolatey/infrastructure.app/commands/ChocolateyTemplateCommand.cs @@ -45,7 +45,10 @@ public void ConfigureArgumentParser(OptionSet optionSet, ChocolateyConfiguration optionSet .Add("n=|name=", "The name of the template to get information about.", - option => configuration.TemplateCommand.Name = option.UnquoteSafe().ToLower()); + option => configuration.TemplateCommand.Name = option.UnquoteSafe().ToLower()), + .Add(StringResources.Options.DISPLAY_HEADERS, + StringResources.OptionDescriptions.DISPLAY_HEADERS, + option => configuration.DisplayHeaders = true); // todo: #2570 Allow for templates from an external path? Requires #1477 } diff --git a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs index 5ec4816f43..ed7b8c184f 100644 --- a/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs +++ b/src/chocolatey/infrastructure.app/configuration/ChocolateyConfiguration.cs @@ -58,6 +58,7 @@ public ChocolateyConfiguration() ExportCommand = new ExportCommandConfiguration(); TemplateCommand = new TemplateCommandConfiguration(); CacheCommand = new CacheCommandConfiguration(); + DisplayHeaders = false; #if DEBUG AllowUnofficialBuild = true; #endif @@ -358,6 +359,7 @@ private void AppendOutput(StringBuilder propertyValues, string append) public string DownloadChecksumType { get; set; } public string DownloadChecksumType64 { get; set; } public bool PinPackage { get; set; } + public bool DisplayHeaders { get; set; } /// /// Configuration values provided by choco. diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyConfigSettingsService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyConfigSettingsService.cs index 3cc63103d1..0533e202dc 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyConfigSettingsService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyConfigSettingsService.cs @@ -62,6 +62,10 @@ public virtual bool SkipSource(ConfigFileSourceSetting source, ChocolateyConfigu public virtual IEnumerable ListSources(ChocolateyConfiguration configuration) { + if (!configuration.RegularOutput && configuration.DisplayHeaders) + { + this.Log().Info("SourceId|Location|Disabled|UserName|Certificate|Priority|BypassProxy|AllowSelfService|AdminOnly"); + } var list = new List(); foreach (var source in ConfigFileSettings.Sources.OrEmpty().OrderBy(s => s.Id)) { @@ -218,6 +222,11 @@ public void EnableSource(ChocolateyConfiguration configuration) public void ListFeatures(ChocolateyConfiguration configuration) { + if (!configuration.RegularOutput && configuration.DisplayHeaders) + { + this.Log().Info("FeatureName|Enabled|Description"); + } + foreach (var feature in ConfigFileSettings.Features.OrEmpty().OrderBy(f => f.Name)) { if (configuration.RegularOutput) @@ -387,6 +396,11 @@ public void RemoveApiKey(ChocolateyConfiguration configuration) public void ListConfig(ChocolateyConfiguration configuration) { + if (!configuration.RegularOutput && configuration.DisplayHeaders) + { + this.Log().Info("Name|Value|Description"); + } + foreach (var config in ConfigFileSettings.ConfigSettings.OrEmpty().OrderBy(c => c.Key)) { if (configuration.RegularOutput) diff --git a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs index e79199659d..8af3f152b6 100644 --- a/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs +++ b/src/chocolatey/infrastructure.app/services/ChocolateyPackageService.cs @@ -255,7 +255,18 @@ public virtual IEnumerable List(ChocolateyConfiguration config) yield break; } - if (config.RegularOutput) this.Log().Debug(() => "Searching for package information"); + if (config.RegularOutput) + { + // This doesn't make sense as a Debug message to me... Debug messages don't really show up when you're running normally... + this.Log().Debug(() => "Searching for package information"); + } + else + { + if (config.DisplayHeaders) + { + this.Log().Info("PackageID|Version"); + } + } var packages = new List(); @@ -758,9 +769,19 @@ public virtual void Outdated(ChocolateyConfiguration config) return; } - if (config.RegularOutput) this.Log().Info(ChocolateyLoggers.Important, @"Outdated Packages + if (config.RegularOutput) + { + this.Log().Info(ChocolateyLoggers.Important, @"Outdated Packages Output is package name | current version | available version | pinned? "); + } + else + { + if (config.DisplayHeaders) + { + this.Log().Info("PackageName|CurrentVersion|AvailableVersion|Pinned"); + } + } config.PackageNames = ApplicationParameters.AllPackages; config.UpgradeCommand.NotifyOnlyAvailableUpgrades = true; diff --git a/src/chocolatey/infrastructure.app/services/TemplateService.cs b/src/chocolatey/infrastructure.app/services/TemplateService.cs index f1f103a276..359b56a69a 100644 --- a/src/chocolatey/infrastructure.app/services/TemplateService.cs +++ b/src/chocolatey/infrastructure.app/services/TemplateService.cs @@ -227,6 +227,11 @@ public void List(ChocolateyConfiguration configuration) if (string.IsNullOrWhiteSpace(configuration.TemplateCommand.Name)) { + if (!configuration.RegularOutput && configuration.DisplayHeaders) + { + this.Log().Info("TemplateName|Version"); + } + if (templateDirList.Any()) { foreach (var templateDir in templateDirList) @@ -235,11 +240,11 @@ public void List(ChocolateyConfiguration configuration) ListCustomTemplateInformation(configuration); } - this.Log().Info(configuration.RegularOutput ? "{0} Custom templates found at {1}{2}".FormatWith(templateDirList.Count(), ApplicationParameters.TemplatesLocation, Environment.NewLine) : string.Empty); + this.Log().Info(configuration.RegularOutput ? ChocolateyLoggers.Normal : ChocolateyLoggers.LogFileOnly, "{0} Custom templates found at {1}{2}".FormatWith(templateDirList.Count(), ApplicationParameters.TemplatesLocation, Environment.NewLine)); } else { - this.Log().Info(configuration.RegularOutput ? "No custom templates installed in {0}{1}".FormatWith(ApplicationParameters.TemplatesLocation, Environment.NewLine) : string.Empty); + this.Log().Info(configuration.RegularOutput ? ChocolateyLoggers.Normal : ChocolateyLoggers.LogFileOnly, "No custom templates installed in {0}{1}".FormatWith(ApplicationParameters.TemplatesLocation, Environment.NewLine)); } ListBuiltinTemplateInformation(configuration, isBuiltInTemplateOverridden, isBuiltInOrDefaultTemplateDefault); diff --git a/tests/pester-tests/features/Headers.Tests.ps1 b/tests/pester-tests/features/Headers.Tests.ps1 new file mode 100644 index 0000000000..f2ff49bf79 --- /dev/null +++ b/tests/pester-tests/features/Headers.Tests.ps1 @@ -0,0 +1,133 @@ +Import-Module helpers/common-helpers + +Describe "choco headers tests" -Tag Chocolatey, HeadersFeature { + BeforeDiscovery { + $Commands = @( + @{ + Command = 'list' + CommandLine = '--local-only' + ExpectedHeaders = 'PackageID|Version' + } + @{ + # + Command = 'info' + CommandLine = 'chocolatey --local-only' + ExpectedHeaders = 'PackageID|Version' + } + @{ + # Needs to pin something... + Command = 'pin' + ExpectedHeaders = 'PackageID|Version' + } + @{ + Command = 'outdated' + ExpectedHeaders = 'PackageName|CurrentVersion|AvailableVersion|Pinned' + } + @{ + Command = 'source' + ExpectedHeaders = 'SourceId|Location|Disabled|UserName|Certificate|Priority|BypassProxy|AllowSelfService|AdminOnly' + } + @{ + Command = 'config' + ExpectedHeaders = 'Name|Value|Description' + } + @{ + Command = 'feature' + ExpectedHeaders = 'FeatureName|Enabled|Description' + } + @{ + Command = 'apikey' + ExpectedHeaders = 'Source|Key' + } + @{ + Command = 'template' + ExpectedHeaders = 'TemplateName|Version' + } + ) + } + BeforeAll { + Initialize-ChocolateyTestInstall + + New-ChocolateyInstallSnapshot + } + + AfterAll { + Remove-ChocolateyTestInstall + } + + Context "Outputs headers for when '--display-headers' provided configured" -ForEach $Commands { + BeforeAll { + $Output = Invoke-Choco $Command $CommandLine --limit-output --display-headers + } + + It 'Exits success (0)' { + $Output.ExitCode | Should -Be 0 -Because $Output.String + } + + It "Displays appropriate header" { + # Some commands won't output anything but the header. In that case we want just the lines instead of indexing into it. + $ActualOutput = if ($Output.Lines.Count -gt 1) { + $Output.Lines[0] + } + else { + $Output.Lines + } + + $ActualOutput | Should -Be $ExpectedHeaders + } + + } + + Context "Does not output headers for by default" -ForEach $Commands { + BeforeAll { + $Output = Invoke-Choco $Command $CommandLine --limit-output + } + + It 'Exits success (0)' { + $Output.ExitCode | Should -Be 0 -Because $Output.String + } + + It "Does not display header" { + # Some commands won't output anything but the header. In that case we want just the lines instead of indexing into it. + $ActualOutput = if ($Output.Lines.Count -gt 1) { + $Output.Lines[0] + } + else { + $Output.Lines + } + + $ActualOutput | Should -Not -Be $ExpectedHeaders + } + + } + + Context "Outputs headers when feature enabled" { + BeforeAll { + Restore-ChocolateyInstallSnapshot + + Enable-ChocolateyFeature -Name AlwaysDisplayHeaders + } + + Context "Outputs headers for " -ForEach $Commands { + BeforeAll { + $Output = Invoke-Choco $Command $CommandLine --limit-output + } + + It 'Exits success (0)' { + $Output.ExitCode | Should -Be 0 -Because $Output.String + } + + It "Displays appropriate header" { + # Some commands won't output anything but the header. In that case we want just the lines instead of indexing into it. + $ActualOutput = if ($Output.Lines.Count -gt 1) { + $Output.Lines[0] + } + else { + $Output.Lines + } + + $ActualOutput | Should -Be $ExpectedHeaders + } + } + } +}