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 ','