diff --git a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CSVCommands.cs b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CSVCommands.cs
index 0c40089205..5a2cd9e9ff 100644
--- a/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CSVCommands.cs
+++ b/src/Microsoft.PowerShell.Commands.Utility/commands/utility/CSVCommands.cs
@@ -59,8 +59,36 @@ namespace Microsoft.PowerShell.Commands
[Alias("NTI")]
public SwitchParameter NoTypeInformation { get; set; } = true;
+ ///
+ /// Gets or sets option to use or suppress quotes in output.
+ ///
+ [Parameter]
+ [Alias("UQ")]
+ public QuoteKind UseQuotes { get; set; } = QuoteKind.Always;
+
#endregion Command Line Parameters
+ ///
+ /// Kind of output quoting.
+ ///
+ public enum QuoteKind
+ {
+ ///
+ /// Never quote output.
+ ///
+ Never,
+
+ ///
+ /// Always quote output.
+ ///
+ Always,
+
+ ///
+ /// Quote output as needed (a field contains used delimiter).
+ ///
+ AsNeeded
+ }
+
///
/// Write the string to a file or pipeline.
///
@@ -217,7 +245,7 @@ namespace Microsoft.PowerShell.Commands
CreateFileStream();
- _helper = new ExportCsvHelper(this, base.Delimiter);
+ _helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes);
}
///
@@ -644,7 +672,7 @@ namespace Microsoft.PowerShell.Commands
protected override void BeginProcessing()
{
base.BeginProcessing();
- _helper = new ExportCsvHelper(this, base.Delimiter);
+ _helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes);
}
///
@@ -806,28 +834,24 @@ namespace Microsoft.PowerShell.Commands
#region ExportHelperConversion
///
- /// Helper class for Export-Csvlper.
+ /// Helper class for Export-Csv and ConvertTo-Csv.
///
internal class ExportCsvHelper : IDisposable
{
- private PSCmdlet _cmdlet;
-
private char _delimiter;
+ readonly private BaseCsvWritingCommand.QuoteKind _quoteKind;
+ readonly private StringBuilder _outputString;
///
+ /// Create ExportCsvHelper instance.
///
- ///
- ///
- /// Throw if cmdlet is null.
- internal ExportCsvHelper(PSCmdlet cmdlet, char delimiter)
+ /// Delimiter char.
+ /// Kind of quoting.
+ internal ExportCsvHelper(char delimiter, BaseCsvWritingCommand.QuoteKind quoteKind)
{
- if (cmdlet == null)
- {
- throw new ArgumentNullException("cmdlet");
- }
-
- _cmdlet = cmdlet;
_delimiter = delimiter;
+ _quoteKind = quoteKind;
+ _outputString = new StringBuilder(128);
}
// Name of properties to be written in CSV format
@@ -865,11 +889,12 @@ namespace Microsoft.PowerShell.Commands
{
if (propertyNames == null)
{
- throw new ArgumentNullException("propertyNames");
+ throw new ArgumentNullException(nameof(propertyNames));
}
- StringBuilder dest = new StringBuilder();
+ _outputString.Clear();
bool first = true;
+
foreach (string propertyName in propertyNames)
{
if (first)
@@ -878,14 +903,32 @@ namespace Microsoft.PowerShell.Commands
}
else
{
- // changed to delimiter
- dest.Append(_delimiter);
+ _outputString.Append(_delimiter);
}
- EscapeAndAppendString(dest, propertyName);
+ switch (_quoteKind)
+ {
+ case BaseCsvWritingCommand.QuoteKind.Always:
+ AppendStringWithEscapeAlways(_outputString, 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;
+ }
}
- return dest.ToString();
+ return _outputString.ToString();
}
///
@@ -898,10 +941,10 @@ namespace Microsoft.PowerShell.Commands
{
if (propertyNames == null)
{
- throw new ArgumentNullException("propertyNames");
+ throw new ArgumentNullException(nameof(propertyNames));
}
- StringBuilder dest = new StringBuilder();
+ _outputString.Clear();
bool first = true;
foreach (string propertyName in propertyNames)
@@ -912,21 +955,41 @@ namespace Microsoft.PowerShell.Commands
}
else
{
- dest.Append(_delimiter);
+ _outputString.Append(_delimiter);
}
- PSPropertyInfo property = mshObject.Properties[propertyName] as PSPropertyInfo;
- string value = null;
- // If property is not present, assume value is null
- if (property != null)
+ // If property is not present, assume value is null and skip it.
+ if (mshObject.Properties[propertyName] is PSPropertyInfo property)
{
- value = GetToStringValueForProperty(property);
- }
+ var value = GetToStringValueForProperty(property);
- EscapeAndAppendString(dest, value);
+ switch (_quoteKind)
+ {
+ case BaseCsvWritingCommand.QuoteKind.Always:
+ AppendStringWithEscapeAlways(_outputString, 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;
+ }
+ }
}
- return dest.ToString();
+ return _outputString.ToString();
}
///
@@ -938,7 +1001,7 @@ namespace Microsoft.PowerShell.Commands
{
if (property == null)
{
- throw new ArgumentNullException("property");
+ throw new ArgumentNullException(nameof(property));
}
string value = null;
@@ -999,12 +1062,13 @@ namespace Microsoft.PowerShell.Commands
/// Escapes the " in string if necessary.
/// Encloses the string in double quotes if necessary.
///
- internal static void EscapeAndAppendString(StringBuilder dest, string source)
+ internal static void AppendStringWithEscapeAlways(StringBuilder dest, string source)
{
if (source == null)
{
return;
}
+
// Adding Double quote to all strings
dest.Append('"');
for (int i = 0; i < source.Length; i++)
@@ -1102,12 +1166,12 @@ namespace Microsoft.PowerShell.Commands
{
if (cmdlet == null)
{
- throw new ArgumentNullException("cmdlet");
+ throw new ArgumentNullException(nameof(cmdlet));
}
if (streamReader == null)
{
- throw new ArgumentNullException("streamReader");
+ throw new ArgumentNullException(nameof(streamReader));
}
_cmdlet = cmdlet;
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 f5f8c18ee6..edb67e1883 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/ConvertTo-Csv.Tests.ps1
@@ -19,16 +19,15 @@ Describe "ConvertTo-Csv DRT Unit Tests" -Tags "CI" {
}
It "Test convertto-csv with a useculture flag" {
- #The default value is ','
+ $delimiter = [CultureInfo]::CurrentCulture.TextInfo.ListSeparator
$returnObject = $inputObject | ConvertTo-Csv -UseCulture -IncludeTypeInformation
$returnObject.Count | Should -Be 3
$returnObject[0] | Should -BeExactly "#TYPE System.Management.Automation.PSCustomObject"
- $returnObject[1] | Should -BeExactly "`"First`",`"Second`""
- $returnObject[2] | Should -BeExactly "`"1`",`"2`""
+ $returnObject[1] | Should -BeExactly "`"First`"$($delimiter)`"Second`""
+ $returnObject[2] | Should -BeExactly "`"1`"$($delimiter)`"2`""
}
It "Test convertto-csv with Delimiter" {
- #The default value is ','
$returnObject = $inputObject | ConvertTo-Csv -Delimiter ";" -IncludeTypeInformation
$returnObject.Count | Should -Be 3
$returnObject[0] | Should -BeExactly "#TYPE System.Management.Automation.PSCustomObject"
@@ -38,11 +37,13 @@ Describe "ConvertTo-Csv DRT Unit Tests" -Tags "CI" {
}
Describe "ConvertTo-Csv" -Tags "CI" {
- $Name = "Hello"; $Data = "World";
- $testObject = New-Object psobject -Property @{ FirstColumn = $Name; SecondColumn = $Data }
+ BeforeAll {
+ $Name = "Hello"; $Data = "World";
+ $testObject = [pscustomobject]@{ FirstColumn = $Name; SecondColumn = $Data }
+ }
It "Should Be able to be called without error" {
- { $testObject | ConvertTo-Csv } | Should -Not -Throw
+ { $testObject | ConvertTo-Csv } | Should -Not -Throw
}
It "Should output an array of objects" {
@@ -51,22 +52,22 @@ Describe "ConvertTo-Csv" -Tags "CI" {
}
It "Should return the type of data in the first element of the output array" {
- $result = $testObject | ConvertTo-Csv -IncludeTypeInformation
+ $result = $testObject | ConvertTo-Csv -IncludeTypeInformation
- $result[0] | Should -BeExactly "#TYPE System.Management.Automation.PSCustomObject"
+ $result[0] | Should -BeExactly "#TYPE System.Management.Automation.PSCustomObject"
}
It "Should return the column info in the second element of the output array" {
- $result = $testObject | ConvertTo-Csv -IncludeTypeInformation
+ $result = $testObject | ConvertTo-Csv -IncludeTypeInformation
- $result[1] | Should -Match "`"FirstColumn`""
- $result[1] | Should -Match "`"SecondColumn`""
+ $result[1] | Should -Match "`"FirstColumn`""
+ $result[1] | Should -Match "`"SecondColumn`""
}
It "Should return the data as a comma-separated list in the third element of the output array" {
- $result = $testObject | ConvertTo-Csv -IncludeTypeInformation
- $result[2] | Should -Match "`"Hello`""
- $result[2] | Should -Match "`"World`""
+ $result = $testObject | ConvertTo-Csv -IncludeTypeInformation
+ $result[2] | Should -Match "`"Hello`""
+ $result[2] | Should -Match "`"World`""
}
It "Includes type information when -IncludeTypeInformation is supplied" {
@@ -76,7 +77,7 @@ Describe "ConvertTo-Csv" -Tags "CI" {
}
It "Does not include type information by default" {
- $result = $testObject | ConvertTo-Csv
+ $result = $testObject | ConvertTo-Csv
$result | Should -Not -Match ([regex]::Escape('System.Management.Automation.PSCustomObject'))
$result | Should -Not -Match ([regex]::Escape('#TYPE'))
@@ -90,8 +91,37 @@ Describe "ConvertTo-Csv" -Tags "CI" {
}
It "Does not support -IncludeTypeInformation and -NoTypeInformation at the same time" {
- { $testObject | ConvertTo-Csv -IncludeTypeInformation -NoTypeInformation } |
+ { $testObject | ConvertTo-Csv -IncludeTypeInformation -NoTypeInformation } |
Should -Throw -ErrorId "CannotSpecifyIncludeTypeInformationAndNoTypeInformation,Microsoft.PowerShell.Commands.ConvertToCsvCommand"
}
+ Context "UseQuotes parameter" {
+ It "UseQuotes Always" {
+ $result = $testObject | ConvertTo-Csv -UseQuotes Always -Delimiter ','
+
+ $result[0] | Should -BeExactly "`"FirstColumn`",`"SecondColumn`""
+ $result[1] | Should -BeExactly "`"Hello`",`"World`""
+ }
+
+ It "UseQuotes Always is default" {
+ $result = $testObject | ConvertTo-Csv -UseQuotes Always -Delimiter ','
+ $result2 = $testObject | ConvertTo-Csv -Delimiter ','
+
+ $result | Should -BeExactly $result2
+ }
+
+ It "UseQuotes Never" {
+ $result = $testObject | ConvertTo-Csv -UseQuotes Never -Delimiter ','
+
+ $result[0] | Should -BeExactly "FirstColumn,SecondColumn"
+ $result[1] | Should -BeExactly "Hello,World"
+ }
+
+ It "UseQuotes AsNeeded" {
+ $result = $testObject | ConvertTo-Csv -UseQuotes AsNeeded -Delimiter 'r'
+
+ $result[0] | Should -BeExactly "`"FirstColumn`"rSecondColumn"
+ $result[1] | Should -BeExactly "Hellor`"World`""
+ }
+ }
}
diff --git a/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1
index f5e93a4444..e54544baac 100644
--- a/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1
+++ b/test/powershell/Modules/Microsoft.PowerShell.Utility/Export-Csv.Tests.ps1
@@ -233,4 +233,33 @@ Describe "Export-Csv" -Tags "CI" {
$contents[0].Contains($delimiter) | Should -BeTrue
$contents[1].Contains($delimiter) | Should -BeTrue
}
+
+ Context "UseQuotes parameter" {
+
+ # A minimum of tests. The rest are in ConvertTo-Csv.Tests.ps1
+
+ BeforeAll {
+ $Name = "Hello"; $Data = "World";
+ $testOutputObject = [pscustomobject]@{ FirstColumn = $Name; SecondColumn = $Data }
+ $testFile = Join-Path -Path $TestDrive -ChildPath "output.csv"
+ $testFile2 = Join-Path -Path $TestDrive -ChildPath "output2.csv"
+ }
+
+ It "UseQuotes Always" {
+ $testOutputObject | Export-Csv -Path $testFile -UseQuotes Always -Delimiter ','
+ $result = Get-Content -Path $testFile
+
+ $result[0] | Should -BeExactly "`"FirstColumn`",`"SecondColumn`""
+ $result[1] | Should -BeExactly "`"Hello`",`"World`""
+ }
+
+ It "UseQuotes Always is default" {
+ $testOutputObject | Export-Csv -Path $testFile -UseQuotes Always -Delimiter ','
+ $result = Get-Content -Raw -Path $testFile
+ $testOutputObject | Export-Csv -Path $testFile2 -Delimiter ','
+ $result2 = Get-Content -Raw -Path $testFile2
+
+ $result | Should -BeExactly $result2
+ }
+ }
}