diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CSVCommands.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CSVCommands.cs index 5a2cd9e9ff2..46fc91a5ef8 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CSVCommands.cs +++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CSVCommands.cs @@ -59,6 +59,13 @@ public abstract class BaseCsvWritingCommand : PSCmdlet [Alias("NTI")] public SwitchParameter NoTypeInformation { get; set; } = true; + /// + /// Gets or sets list of fields to quote in output. + /// + [Parameter] + [Alias("QF")] + public string[] QuoteFields { get; set; } + /// /// Gets or sets option to use or suppress quotes in output. /// @@ -101,6 +108,13 @@ public virtual void WriteCsvLine(string line) /// protected override void BeginProcessing() { + if (this.MyInvocation.BoundParameters.ContainsKey(nameof(QuoteFields)) && this.MyInvocation.BoundParameters.ContainsKey(nameof(UseQuotes))) + { + InvalidOperationException exception = new InvalidOperationException(CsvCommandStrings.CannotSpecifyQuoteFieldsAndUseQuotes); + ErrorRecord errorRecord = new ErrorRecord(exception, "CannotSpecifyQuoteFieldsAndUseQuotes", ErrorCategory.InvalidData, null); + this.ThrowTerminatingError(errorRecord); + } + if (this.MyInvocation.BoundParameters.ContainsKey(nameof(IncludeTypeInformation)) && this.MyInvocation.BoundParameters.ContainsKey(nameof(NoTypeInformation))) { InvalidOperationException exception = new InvalidOperationException(CsvCommandStrings.CannotSpecifyIncludeTypeInformationAndNoTypeInformation); @@ -245,7 +259,7 @@ protected override void BeginProcessing() CreateFileStream(); - _helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes); + _helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes, base.QuoteFields); } /// @@ -672,7 +686,7 @@ public sealed class ConvertToCsvCommand : BaseCsvWritingCommand protected override void BeginProcessing() { base.BeginProcessing(); - _helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes); + _helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes, base.QuoteFields); } /// @@ -840,6 +854,7 @@ internal class ExportCsvHelper : IDisposable { private char _delimiter; readonly private BaseCsvWritingCommand.QuoteKind _quoteKind; + readonly private HashSet _quoteFields; readonly private StringBuilder _outputString; /// @@ -847,10 +862,12 @@ internal class ExportCsvHelper : IDisposable /// /// Delimiter char. /// Kind of quoting. - internal ExportCsvHelper(char delimiter, BaseCsvWritingCommand.QuoteKind quoteKind) + /// List of fields to quote. + internal ExportCsvHelper(char delimiter, BaseCsvWritingCommand.QuoteKind quoteKind, string[] quoteFields) { _delimiter = delimiter; _quoteKind = quoteKind; + _quoteFields = quoteFields == null ? null : new HashSet(quoteFields, StringComparer.OrdinalIgnoreCase); _outputString = new StringBuilder(128); } @@ -906,25 +923,39 @@ internal string ConvertPropertyNamesCSV(IList propertyNames) _outputString.Append(_delimiter); } - switch (_quoteKind) + if (_quoteFields != null) { - case BaseCsvWritingCommand.QuoteKind.Always: + if (_quoteFields.TryGetValue(propertyName, out _)) + { AppendStringWithEscapeAlways(_outputString, propertyName); - break; - case BaseCsvWritingCommand.QuoteKind.AsNeeded: - if (propertyName.Contains(_delimiter)) - { + } + else + { + _outputString.Append(propertyName); + } + } + else + { + switch (_quoteKind) + { + case BaseCsvWritingCommand.QuoteKind.Always: AppendStringWithEscapeAlways(_outputString, propertyName); - } - else - { - _outputString.Append(propertyName); - } + break; + case BaseCsvWritingCommand.QuoteKind.AsNeeded: + if (propertyName.Contains(_delimiter)) + { + AppendStringWithEscapeAlways(_outputString, propertyName); + } + else + { + _outputString.Append(propertyName); + } - break; - case BaseCsvWritingCommand.QuoteKind.Never: - _outputString.Append(propertyName); - break; + break; + case BaseCsvWritingCommand.QuoteKind.Never: + _outputString.Append(propertyName); + break; + } } } @@ -963,28 +994,42 @@ internal string ConvertPSObjectToCSV(PSObject mshObject, IList propertyN { var value = GetToStringValueForProperty(property); - switch (_quoteKind) + if (_quoteFields != null) { - case BaseCsvWritingCommand.QuoteKind.Always: + if (_quoteFields.TryGetValue(propertyName, out _)) + { AppendStringWithEscapeAlways(_outputString, value); - break; - case BaseCsvWritingCommand.QuoteKind.AsNeeded: - if (value.Contains(_delimiter)) - { + } + else + { + _outputString.Append(value); + } + } + else + { + switch (_quoteKind) + { + case BaseCsvWritingCommand.QuoteKind.Always: AppendStringWithEscapeAlways(_outputString, value); - } - else - { - _outputString.Append(value); - } + break; + case BaseCsvWritingCommand.QuoteKind.AsNeeded: + if (value.Contains(_delimiter)) + { + AppendStringWithEscapeAlways(_outputString, value); + } + else + { + _outputString.Append(value); + } - break; - case BaseCsvWritingCommand.QuoteKind.Never: - _outputString.Append(value); - break; - default: - Diagnostics.Assert(false, "BaseCsvWritingCommand.QuoteKind has new item."); - break; + break; + case BaseCsvWritingCommand.QuoteKind.Never: + _outputString.Append(value); + break; + default: + Diagnostics.Assert(false, "BaseCsvWritingCommand.QuoteKind has new item."); + break; + } } } } diff --git a/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx b/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx index d2250fb572a..0eff0d6f84f 100644 --- a/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx +++ b/src/Microsoft.PowerShell.Commands.Utility/resources/CsvCommandStrings.resx @@ -127,6 +127,9 @@ Reviewed by TArcher on 2010-06-29. + + You must specify either the -UseQuotes or -QuoteFields parameters, but not both. + You must specify either the -IncludeTypeInformation or -NoTypeInformation parameters, but not both. diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1 index edb67e18838..7c4193c221a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1 @@ -90,11 +90,31 @@ Describe "ConvertTo-Csv" -Tags "CI" { $result | Should -Not -Match ([regex]::Escape('#TYPE')) } + It "Does not support -UseQuotes and -QuoteFields at the same time" { + { $testObject | ConvertTo-Csv -UseQuotes Always -QuoteFields "TestFieldName" } | + Should -Throw -ErrorId "CannotSpecifyQuoteFieldsAndUseQuotes,Microsoft.PowerShell.Commands.ConvertToCsvCommand" + } + It "Does not support -IncludeTypeInformation and -NoTypeInformation at the same time" { { $testObject | ConvertTo-Csv -IncludeTypeInformation -NoTypeInformation } | Should -Throw -ErrorId "CannotSpecifyIncludeTypeInformationAndNoTypeInformation,Microsoft.PowerShell.Commands.ConvertToCsvCommand" } + Context "QuoteFields parameter" { + It "QuoteFields" { + # Use 'FiRstCoLumn' to test case insensitivity + $result = $testObject | ConvertTo-Csv -QuoteFields FiRstCoLumn -Delimiter ',' + + $result[0] | Should -BeExactly "`"FirstColumn`",SecondColumn" + $result[1] | Should -BeExactly "`"Hello`",World" + + $result = $testObject | ConvertTo-Csv -QuoteFields FiRstCoLumn,SeCondCoLumn -Delimiter ',' + + $result[0] | Should -BeExactly "`"FirstColumn`",`"SecondColumn`"" + $result[1] | Should -BeExactly "`"Hello`",`"World`"" + } + } + Context "UseQuotes parameter" { It "UseQuotes Always" { $result = $testObject | ConvertTo-Csv -UseQuotes Always -Delimiter ','