Kain
Kain

Reputation: 33

Powershell: Issue with foreach loop. "Cannot convert the value of type "System.String" to type "System.Management.Automation.SwitchParameter"

I have a report that I have automated. the report is a CSV file, and it is split every 80,000 lines, so I regularly end up with reports in multiple parts. I have a large function that processes the report perfectly fine when I have a single file, but I am having issues with ingesting multiple files at the same time. The report filenames are formatted as such:

Part_1.csv
Part_2.csv
Part_3.csv

What I'm doing is getting the last downloaded file, and checking to see how many parts it had, like this:

$FileName = Get-ChildItem $env:USERPROFILE\Downloads | where Extension -like .csv | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1
$FileCount = [int]$FileName.basename.substring($FileName.Basename.Length-1)

If the I get isn't Part_1.csv I know I need to load multiple files, and I'm doing that with this:

 if ($FileCount -ne "1")
        {
        $List = Get-ChildItem $env:USERPROFILE\Downloads | where Extension -like .csv | Sort-Object -Property LastWriteTime -Descending | Select-Object -First $FileCount | Sort-object       

After this step, $List contains the following:

PS C:\> $List

    Directory: C:\Users\username\Downloads

Mode                LastWriteTime         Length Name                                                                                                                                  
----                -------------         ------ ----                                                                                                                                  
-a----        2/18/2025   3:25 PM       16930338 Report_Part_1.csv
-a----        2/18/2025   3:25 PM       16930338 Report_Part_2.csv                                                                                                
-a----        2/18/2025   3:26 PM        1448835 Report_Part_3.csv                                                                                                  

PS C:\> 

I then load each different part of the report with a foreach loop:

Foreach($File in $List.FullName)
    {
    $Object += Import-Csv -Path $File
    }

Here's this entire part of the script in one block for you.

$Object = @()

$FileName = Get-ChildItem $env:USERPROFILE\Downloads | where Extension -like .csv | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1

$FileCount = [int]$FileName.basename.substring($FileName.Basename.Length-1)

if ($FileCount -ne "1")
    {
    $List = Get-ChildItem $env:USERPROFILE\Downloads | where Extension -like .csv | Sort-Object -Property LastWriteTime -Descending | Select-Object -First $FileCount | Sort-object
    Foreach($File in $List.FullName)
        {
        $Object += Import-Csv -Path $File
        }
    }
Else
    {
    $Object = Import-Csv -Path $FileName.FullName
    }

Here's the kicker, if I run this isolated, all by itself, it works exactly how I expected it to. $Object contains all of the data from each part. but as soon as I put this at the top of my function that actually processes the report:

Function Read-Report
    {
    $Object = @()

    $FileName = Get-ChildItem $env:USERPROFILE\Downloads | where Extension -like .csv | Sort-Object -Property LastWriteTime -Descending | Select-Object -First 1

    $FileCount = [int]$FileName.basename.substring($FileName.Basename.Length-1)

    if ($FileCount -ne "1")
        {
        $List = Get-ChildItem $env:USERPROFILE\Downloads | where Extension -like .csv | Sort-Object -Property LastWriteTime -Descending | Select-Object -First $FileCount | Sort-object #([int]::Parse($filename.basename.substring($Filename.Basename.Length-1))) | Sort-Object
        Foreach($File in $List.FullName)
            {
            $Object += Import-Csv -Path $File
            }
        }
    Else
        {
        $Object = Import-Csv -Path $FileName.FullName
        }

#Working report processing script continues below

I get an error on the line that contains Foreach($File in $List.FullName) stating:

Cannot convert the "C:\Users\username\Downloads\Report_Part_1.csv" value of type "System.String" to type "System.Management.Automation.SwitchParameter".
At C:\Users\username\Documents\Scripting\Powershell\ReportsFunctions.ps1:43 char:17
+         Foreach($File in $List.FullName)
+                 ~~~~~
    + CategoryInfo          : InvalidArgument: (:) [], RuntimeException
    + FullyQualifiedErrorId : ConvertToFinalInvalidCastException

I would think that its just choking on the file path, except that it works when its not inside my function.

Also, changing the foreach loop to the following has the exact same error

Foreach($File in $List)
    {
    $Object += Import-Csv -Path $File.FullName
    }

Upvotes: 3

Views: 88

Answers (2)

mclayton
mclayton

Reputation: 10075

I can reproduce your error with the following:

function Read-Report
{
    param( [switch] $File )

    foreach( $File in @( "aaa" ) )
    {
        write-host $File
    }
}

Read-Report

What's happening is the function declares a type constrained variable with [switch] $File so that PowerShell requires $File to always contain a value of type [switch], but you're then using the same variable in a foreach which tries to iterate over an array of [string]s.

Powershell's implicit type conversion tries to turn the [string] "aaa" into a [switch] but there's no implicit conversion that can do this so you get the error:

 Cannot convert the "aaa" value of type "System.String" to type "System.Management.Automation.SwitchParameter".

(Note that [switch] is a Type Accelerator for [System.Management.Automation.SwitchParameter])

Your code doesn't show where you're declaring $File so I can't suggest a direct fix, but a workaround would be to use a different variable in the foreach iterator so it doesn't have the same [switch] type constraint and then PowerShell won't try to convert the [string] "aaa" value into another type:

function Read-Report
{
    param( [switch] $File )

    foreach( $MyFile in @( "aaa" ) )
    {
        write-host $MyFile
    }
}

Read-Report

Upvotes: 4

sirtao
sirtao

Reputation: 2880

To start, a few pointers:

  1. if you are using this code in a function, it's always better to pass any parameter that is not native to the function.
    Which includes the value of $env:USERPROFILE\Downloads.
  2. Treat all parameter variables as read-only. As others said, this might be the source of your issue.
  3. given you are filtering simply for the extension, you can skip Where-Object and just add -Filter '*.csv' to Get-ChildItem.
  4. by using Select-Object after Sort-Object you are still loading the full extent of the results from Get-ChildItem. Which means you don't have to repeat the same request later.
  5. Direct Assignment from Loops $Array=foreach($item in $collection){ $value } is the way.
  6. what is the point of typecasting $Filecount as a INT when you compare it to a String?
  7. in fact, why uusing .Substring()? .EndsWith() is available even in 5.1
  8. Not like it'd matter: both would match $true to a filename ending in 11, for example.
  9. don't use aliases in scripts. there is a non-zero risk of collision with other cmdlets\programs. Aliases are cool for on-terminal fast writing tho'.

last: I have been unable to reproduce the issue on my system, on 5.1 nor 7.5 In fact, I haven't been able to make it work at all. Are you using Powershell ISE to run this, by chance?

have an alternative that works on 5.1 and 7.5

Function Read-Report {
    # add checks as necessary
    param([string]$Path)

    # Let us get all CSV files in the $Path that also contain Part in their names.   
    # adapt as necessary, of course.  
    # evaluate having the filter be a parameter itself.   
    $FileList = Get-ChildItem -LiteralPath $Path -Filter '*part*.csv' | 
        # sort by the most recently written first
        Sort-Object -Property LastWriteTime -Descending  


    # we need now to check the last written file ends with 1.  
    # Get-ChildItem always returns a collection even when it's only 1 item, 
    # so we can just use [0] to get the first item no worries.
    # Using regex matching "end with 1, preceded by a Non-Digit"
    $ImportList = if ($FileList[0].BaseName -match '\D1$') {
        # we already have the full file list in $FileList, no need to request it again.  
        # the latest file is the first file, so we only need it
        $FileList[0]
    }
    else {
        # the latest file is not the first file, so we need the whole list
        $FileList
    }

    # without comments you can one-line this as 
    # $ImportList = if ($FileList[0].BaseName -match '\D1$') { $FileList[0] } else { $FileList }



    # Direct assignment FTW
    # Work even with just one element! Much easier to maintain!   
    ## NOTE WELL: this is if you *DO NOT* plan to add\remove items from $ObjectArray.   
    ## If you do, typecast it as a [System.Collections.Generic.List[object]].   
    $ObjectArray = Foreach ($File in $ImportList.FullName) {
        Import-Csv -LiteralPath $File
    }


    # returning the object to show the homework, obviously do whatever else you need otherwhise.
    $ObjectArray
}

Read-Report -Path $PWD

Upvotes: 2

Related Questions