Search code examples
arrayspowershellpipeline

How to pass a string array to a pipeline function in Powershell?


My function builds a representation of a table as a PSCustomObject. It has a title, an array of column headers, and a [System.Collection.Generic.List][string] of rows:

function Create-ConfluenceTable{
    param(
        [Parameter(mandatory=$true)][string]$Title,
        [Parameter(mandatory=$true)][string[]]$Columns
    )

    $rows = [System.Collections.Generic.List[string]]::new()

    $t = [PSCustomObject]@{
        Title = $Title;
        Columns = $Columns;
        Rows = $rows
    }

    return $t
}

I then have a pipeline-aware function for adding rows to the table.

function AddRow {
    param(
        [Parameter(mandatory,ValueFromPipeline)]$ConfluenceTable,
        [Parameter(mandatory=$true)][string[]]$RowValues
    )

    process
    {
        foreach($rv in $RowValues) {
            $ConfluenceTable.Rows.Add($rv)
        }
    }
}

When I create the table and then try to pass values to it, I can't get any string variables to correctly resolve. When I run this code...

$bar="https://example.com"
$ct = Create-ConfluenceTable -title "URLs" -Columns {"Type","Url"}
$ct | AddRow -RowValues {"Foo",$bar}
$ct

...my output is:

Title Columns        Rows        
----- -------        ----        
URLs  {"Type","Url"} {"Foo",$bar}

What I would like to see is:

Title Columns        Rows        
----- -------        ----        
URLs  {"Type","Url"} {"Foo","https://example.com"}

Solution

  • The issue with your code is that you're passing scriptblocks {...} as argument of your functions instead of strings, by simply removing the curly braces the issue will be gone:

    $bar = "https://example.com"
    $ct = Create-ConfluenceTable -title "URLs" -Columns "Type", "Url"
    $ct | AddRow -RowValues "Foo", $bar
    $ct
    

    Opinionated answer here but it seems you could benefit from a class instead of using separated functions, here is how it would look:

    using namespace System.Collections.Generic
    using namespace System.Collections.ObjectModel
    
    class ConfluenceTable {
        [string] $Title
        [string[]] $Columns
        hidden [List[string]] $_rows
    
        static ConfluenceTable() {
            # add a getter for Rows
            $updateTypeDataSplat = @{
                TypeName   = 'ConfluenceTable'
                MemberType = 'CodeProperty'
                MemberName = 'Rows'
                Value      = [ConfluenceTable].GetMethod('get_Rows')
            }
            Update-TypeData @updateTypeDataSplat
    
            # unfortunate but needed to keep property order :(
            $updateTypeDataSplat = @{
                TypeName                  = 'ConfluenceTable'
                DefaultDisplayPropertySet = 'Title', 'Columns', 'Rows'
            }
            Update-TypeData @updateTypeDataSplat
        }
    
        hidden static [ReadOnlyCollection[string]] get_Rows([psobject] $instance) {
            return [ReadOnlyCollection[string]]::new($instance._rows)
        }
    
        ConfluenceTable([string] $title, [string[]] $columns) {
            $this.Title, $this.Columns = $title, $columns
            $this._rows = [List[string]]::new()
        }
    
        [void] AddRow([string] $row) {
            $this._rows.Add($row)
        }
    
        [void] AddRowRange([string[]] $rows) {
            $this._rows.AddRange($rows)
        }
    }
    
    $bar = 'https://example.com'
    $table = [ConfluenceTable]::new('URLs', ('Type', 'Url'))
    $table.AddRowRange(('Foo', $bar))
    $table