Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -59,8 +59,36 @@ public abstract class BaseCsvWritingCommand : PSCmdlet
[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>
Expand Down Expand Up @@ -217,7 +245,7 @@ protected override void BeginProcessing()

CreateFileStream();

_helper = new ExportCsvHelper(this, base.Delimiter);
_helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes);
}

/// <summary>
Expand Down Expand Up @@ -644,7 +672,7 @@ public sealed class ConvertToCsvCommand : BaseCsvWritingCommand
protected override void BeginProcessing()
{
base.BeginProcessing();
_helper = new ExportCsvHelper(this, base.Delimiter);
_helper = new ExportCsvHelper(base.Delimiter, base.UseQuotes);
}

/// <summary>
Expand Down Expand Up @@ -806,28 +834,24 @@ protected override void ProcessRecord()
#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
Expand Down Expand Up @@ -865,11 +889,12 @@ internal string ConvertPropertyNamesCSV(IList<string> propertyNames)
{
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)
Expand All @@ -878,14 +903,32 @@ internal string ConvertPropertyNamesCSV(IList<string> propertyNames)
}
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>
Expand All @@ -898,10 +941,10 @@ internal string ConvertPSObjectToCSV(PSObject mshObject, IList<string> propertyN
{
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)
Expand All @@ -912,21 +955,41 @@ internal string ConvertPSObjectToCSV(PSObject mshObject, IList<string> propertyN
}
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>
Expand All @@ -938,7 +1001,7 @@ internal static string GetToStringValueForProperty(PSPropertyInfo property)
{
if (property == null)
{
throw new ArgumentNullException("property");
throw new ArgumentNullException(nameof(property));
}

string value = null;
Expand Down Expand Up @@ -999,12 +1062,13 @@ internal static string GetTypeString(PSObject source)
/// 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++)
Expand Down Expand Up @@ -1102,12 +1166,12 @@ internal ImportCsvHelper(PSCmdlet cmdlet, char delimiter, IList<string> header,
{
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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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" {
Expand All @@ -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" {
Expand All @@ -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'))
Expand All @@ -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`""
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
}