3

Is there a clean way to append SEP=, to the start of a CSV file created by Export-CSV?

NB: This is an XY Problem; see Why's this needed below for info on my root issue. I'm asking for the SEP piece since this seems like the best fix given I need to continue using UTF8 CSVs and don't want users to have to change how they work to avoid issues.

Amend File After Creation

The simple option is this; however it feels messy (i.e. we release the lock on the file then have to go back and update it).

function Repair-Csv {
    [CmdletBinding()]
    Param (
        [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
        [string]$Path
    )
    Begin {
        $sep = "SEP=,`r`n" 
    }
    Process {
        $sep + (Get-Content -Path $Path -Raw) | Set-Content -Path $Path
    }
}

Proxy Function Solution Attempt

I tried creating a proxy function (code below), but found that the wrapped Export-CSV command doesn't append its output to my tweaked file, but rather reads in the content I've already written and attempts to use SEP= as a column heading; so I end up with no data (unless one of my exported object's properties happens to be called SEP=.

# $MetaData = New-Object System.Management.Automation.CommandMetaData (Get-Command 'Export-CSV')
# [System.Management.Automation.ProxyCommand]::Create($MetaData)
function Export-CsvAdvanced {
    [CmdletBinding(DefaultParameterSetName='Delimiter', SupportsShouldProcess=$true, ConfirmImpact='Medium', HelpUri='http://go.microsoft.com/fwlink/?LinkID=113299')]
    param (
        [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
        [psobject]
        ${InputObject}
        ,
        [Parameter(Position=0)]
        [ValidateNotNullOrEmpty()]
        [string]
        ${Path}
        ,
        [Alias('PSPath')]
        [ValidateNotNullOrEmpty()]
        [string]
        ${LiteralPath}
        ,
        [switch]
        ${Force}
        ,
        [Alias('NoOverwrite')]
        [switch]
        ${NoClobber}
        ,
        [ValidateSet('Unicode','UTF7','UTF8','ASCII','UTF32','BigEndianUnicode','Default','OEM')]
        [string]
        ${Encoding}
        ,
        [switch]
        ${Append}
        ,
        [Parameter(ParameterSetName='Delimiter', Position=1)]
        [ValidateNotNull()]
        [char]
        ${Delimiter}
        ,
        [Parameter(ParameterSetName='UseCulture')]
        [switch]
        ${UseCulture}
        ,
        [Alias('NTI')]
        [switch]
        ${NoTypeInformation}
        ,
        [Alias('SEP')]
        [switch]
        ${PrefixWithSep}
    )
    begin {
        try {
            $outBuffer = $null
            if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
            {
                $PSBoundParameters['OutBuffer'] = 1
            }
            if ($PrefixWithSep.IsPresent) {
                if (!$Delimiter) {
                    if ($UseCulture.IsPresent) {
                        $Delimiter = [System.Globalization.CultureInfo]::CurrentCulture.TextInfo.ListSeparator
                    } else {
                        $Delimiter = ','
                    }
                }
                $splat = @{}
                if ($Encoding) { $splat.Add('Encoding', $Encoding) }
                if ($Force) { $splat.Add('Force', $Force) }
                if ($Append) { $splat.Add('Append', $Append) }
                if ($NoClobber) { $splat.Add('NoClobber', $NoClobber) }
                if ($PSCmdlet.ParameterSetName -eq 'PSPath') {
                    $splat.Add('LiteralPath', $LiteralPath)
                } else {
                    $splat.Add('FilePath', $Path)
                }
                ("SEP={0}`r`n" -f $Delimiter) | Out-File @splat
                $PSBoundParameters.Remove('PrefixWithSep') #Export-CSV won't understand our custom parameter, so remove it (NB: if this is not the first call this will already have been removed)
                $PSBoundParameters['Append'] = $true #we don't want to delete the file after adding a header
                $PSBoundParameters['Force'] = $true #Don't treat `SEP=,` as a heading when appending to the file
            }

            $wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Export-Csv', [System.Management.Automation.CommandTypes]::Cmdlet)
            $scriptCmd = {& $wrappedCmd @PSBoundParameters }
            $steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
            $steppablePipeline.Begin($PSCmdlet)
        } catch {
            throw
        }
    }
    process {
        try {
            $steppablePipeline.Process($_)
        } catch {
            throw
        }
    }
    end {
        try {
            $steppablePipeline.End()
        } catch {
            throw
        }
    }
    <#
    .ForwardHelpTargetName Microsoft.PowerShell.Utility\Export-Csv
    .ForwardHelpCategory Cmdlet
    #>
}

#test populated
Get-Process | Export-CsvAdvanced -Path 'c:\temp\processSep.csv' -NoTypeInformation -PrefixWithSep
Get-Process | Export-CsvAdvanced -Path 'c:\temp\process.csv' -NoTypeInformation
#test blank
$blankArray = @()
$blankArray | Export-CsvAdvanced -Path 'c:\temp\emptySep.csv' -NoTypeInformation -PrefixWithSep
$blankArray | Export-CsvAdvanced -Path 'c:\temp\empty.csv' -NoTypeInformation

I could always create a proxy function over ConvertTo-CSV adding streams to output the results to file; but that doesn't sit comfortably (i.e. if there's an exception it gets complicated trying to correctly dispose of the streams).

Why's this needed?

I don't strictly need the SEP prefix; it would be useful; but the truth is I'm trying to solve a different problem (so this is an XY problem).

I have some functions which output data to CSV. In some cases, the exported files are blank (i.e. as there were no results). In such cases I still want a file to be created (i.e. so it's clear the code has run and there were no results, vs the code simply not running). However, my CSVs are UTF8, which when blank show as a BOM in Excel (i.e. cell A1 contains ).

Since I have to continue using CSVs and ideally wish to continue using UTF8 it seems the best solution is to add some content; one suggestion is to prefix the SEP=, instruction (which is also useful when sharing the CSV with users in other countries, where a different default delimiter is used).

JohnLBevan
  • 18,988
  • 5
  • 75
  • 151
  • 1
    I don't see a clear and concise question. What's wrong with your first solution? – Bill_Stewart Apr 24 '18 at 15:26
  • The first workaround does work; however it gets applied after the file's created (so we release the lock on the file then take it again), it means rewriting the entire file (expensive for large files), and it means finding everywhere Export-CSV is used and ensuring those files are then corrected; rather than having a single function do everything. It's all very hacky / I want to find a cleaner solution. – JohnLBevan Apr 24 '18 at 15:31
  • My question is the first sentence; the rest of the post is just there to show what I've tried / why those options don't work for me / don't sit well with me. – JohnLBevan Apr 24 '18 at 15:33
  • Perhaps a different approach? `Import-Csv file.csv | Measure-Object` has `Count` property of `0` if there are no records (this is true even for an empty file that contains only a UTF8 BOM). – Bill_Stewart Apr 24 '18 at 15:41

0 Answers0