Add UseQuotes parameter (#8951)

This commit is contained in:
Ilya
2019-03-06 11:59:15 -08:00
committed by Andrew
parent 45aba2acb5
commit 2953959dc5
3 changed files with 176 additions and 53 deletions
@@ -59,8 +59,36 @@ namespace Microsoft.PowerShell.Commands
[Alias("NTI")]
public SwitchParameter NoTypeInformation { get; set; } = true;
/// <summary>
/// Gets or sets option to use or suppress quotes in output.
/// </summary>
[Parameter]
[Alias("UQ")]
public QuoteKind UseQuotes { get; set; } = QuoteKind.Always;
#endregion Command Line Parameters
/// <summary>
/// Kind of output quoting.
/// </summary>
public enum QuoteKind
{
/// <summary>
/// Never quote output.
/// </summary>
Never,
/// <summary>
/// Always quote output.
/// </summary>
Always,
/// <summary>
/// Quote output as needed (a field contains used delimiter).
/// </summary>
AsNeeded
}
/// <summary>
/// Write the string to a file or pipeline.
/// </summary>
@@ -217,7 +245,7 @@ namespace Microsoft.PowerShell.Commands
CreateFileStream();
_helper = new ExportCsvHelper(this, base.Delimiter);
_helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes);
}
/// <summary>
@@ -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);
}
/// <summary>
@@ -806,28 +834,24 @@ namespace Microsoft.PowerShell.Commands
#region ExportHelperConversion
/// <summary>
/// Helper class for Export-Csvlper.
/// Helper class for Export-Csv and ConvertTo-Csv.
/// </summary>
internal class ExportCsvHelper : IDisposable
{
private PSCmdlet _cmdlet;
private char _delimiter;
readonly private BaseCsvWritingCommand.QuoteKind _quoteKind;
readonly private StringBuilder _outputString;
/// <summary>
/// Create ExportCsvHelper instance.
/// </summary>
/// <param name="cmdlet"></param>
/// <param name="delimiter"></param>
/// <exception cref="ArgumentNullException">Throw if cmdlet is null.</exception>
internal ExportCsvHelper(PSCmdlet cmdlet, char delimiter)
/// <param name="delimiter">Delimiter char.</param>
/// <param name="quoteKind">Kind of quoting.</param>
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();
}
/// <summary>
@@ -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();
}
/// <summary>
@@ -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.
/// </summary>
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;
@@ -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`""
}
}
}
@@ -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
}
}
}