Gordon
Gordon

Reputation: 6863

Powershell splatting a nested hash table

I have a function that returns a complex nested hash table data structure, where part of it forms the arguments for a further function call, and part of it is strings for reporting errors that result in the arguments not being populated. Ideally I would like to splat just the arguments hash table, but I am starting to think that can't be done. So, an example of the basic problem looks like this...

function Test {
    param (
        [String]$A,
        [String]$B,
        [String]$C
    )
    Write-Host "A: $A"
    Write-Host "B: $B"
    Write-Host "C: $C"
}

$data = @{
    arguments = @{
        A = 'A string'
        B = 'Another string'
        C = 'The last string'
    }
    kruft = 'A string that doesn not need to get passed'
}

Ideally I want to splat $data.arguments, so something like this... Test @data.arguments But that doesn't work, resulting in the error

The splatting operator '@' cannot be used to reference variables in an expression. '@data' can be used only as an argument to a command. To 
reference variables in an expression use '$data'.

So I tried... Test @(data.arguments) Which results in the error

data.arguments : The term 'data.arguments' is not recognized as the name of a cmdlet, function, script file, or operable program. Check the 
spelling of the name, or if a path was included, verify that the path is correct and try again.

I also tried... Test @($data.arguments) Which results in the whole hash table being passed as a single argument and the output is

A: System.Collections.Hashtable
B: 
C:

What DOES work is...

$arguments = $data.arguments
Test @arguments

Which has me thinking you really cannot splat anything but a simple variable that is an appropriate hash table. But, I am hoping someone can verify that is indeed true, or point out the solution I haven't come up with yet. The actual code requires 5 arguments, with somewhat verbose names because I prefer descriptive names, so splatting is very much an appropriate solution. Needing to make a new variable with just the hash table to be passed isn't an issue, just wondering if it really is the only option.

Upvotes: 0

Views: 1916

Answers (4)

catgrep
catgrep

Reputation: 1

It is possible by passing the object property to the ForEach-Object cmdlet:

New-Variable -Name objTask -Value (New-Object PSCustomObject -ArgumentList @{
    TaskName = "Task Name"
    TaskPath = '\Task\Path\'
    Description = "Describe the function of this task"
    Parameters = @{
        Principal = @{
            UserId    = "S-1-5-18"
            LogonType = "ServiceAccount"
            RunLevel  = "Highest"
        }
        Triggers = @(
            @{
                Daily        = $true
                At           = ([datetime]"01/01/2015 10:00:00")
                DaysInterval = 1
            },
            @{
                Daily        = $true
                At           = ([datetime]"01/01/2015 14:00:00")
                DaysInterval = 1
            }
        )
        Actions = @{
            Execute = "powershell.exe"
            Argument = "-ep bypass -c `"myscript.ps1`""
        }
        Settings = @{
            MultipleInstances = "IgnoreNew"
        }
    }
}
$objTask | ForEach-Object {
    $stPrincipal = $_.Parameters.Principal | ForEach-Object {
        New-ScheduledTaskPrincipal @_
    }
    $stTriggers = $_.Parameters.Triggers | ForEach-Object {
        New-ScheduledTaskTrigger @_
    }
    $stActions = $_.Parameters.Actions | ForEach-Object {
        New-ScheduledTaskAction @_
    }
    $stSettings = $_.Parameters.Settings | ForEach-Object {
        New-ScheduledTaskSettingsSet @_
    }
    $stTask = New-ScheduledTask `
        -Principal $stPrincipal `
        -Trigger $stTriggers `
        -Action $stActions `
        -Settings $stSettings `
        -Description $_.Description
    $myTask = $stTask | Register-ScheduledTask `
        -TaskName $_.TaskName `
        -TaskPath $_.TaskPath `
        -Force `
        -ErrorAction SilentlyContinue
}

Upvotes: 0

Mark Schultheiss
Mark Schultheiss

Reputation: 34168

This is a totally artificial way (more of a hack) of doing this but you could pass it as a hash table then self-call with the arguments splatted

function Test {
    param (
    [Parameter()]
        [hashtable]$ht,
        [String]$A,
        [String]$B,
        [String]$C
    )
    if($null -eq $ht){
        Write-Host "A: $a" 
        Write-Host "B: $B" 
        Write-Host "C: $C" 
    }
    else
    {
        Write-Host "ht.A: $($ht.a)" 
        Test -a $($ht.A+ " new blob")  -b $ht.B -c $ht.C
        Test @ht
    }
}

$tdata = @{
    arguments = @{
        A = 'A string'
        B = 'Another string'
        C = 'The last string'
    }
    kruft = 'A string that doesn not need to get passed'
}

Write-Host 'Test ht' -ForegroundColor Yellow
Test -ht $tdata.arguments

Upvotes: 0

swbbl
swbbl

Reputation: 864

That's not possible.

Any variable (and only variables) provided with "@" instead of "$" for a function parameter is declared as TokenKind "SplattedVariable".

See: https://learn.microsoft.com/en-us/dotnet/api/system.management.automation.language.tokenkind?view=powershellsdk-7.0.0

PS: Some quick tests from my side which could have succeed (apart from PS design):

Write-Host 'Test 1' -ForegroundColor Yellow
Test @$data.arguments

Write-Host 'Test 2' -ForegroundColor Yellow
Test @$($bla = $data.arguments)

Write-Host 'Test 3' -ForegroundColor Yellow
Test @$bla = $data.arguments

Write-Host 'Test 4' -ForegroundColor Yellow
Test @$bla = $data.arguments.GetEnumerator()

Write-Host 'Test 5' -ForegroundColor Yellow
Test @$($data.arguments.GetEnumerator())

... but they didn't.

Upvotes: 1

arco444
arco444

Reputation: 22821

I cannot claim to fully understand what you're attempting, but here are a couple of solutions that might help.

Your Test function is expecting three strings, but I don't see anything in your example that satisfies that. I would either change it to accept a Hashtable (which is the data type in question) or have it accept a string array and pass $data.arguments.values

Using a Hashtable:

function Test {
  param (
    [Hashtable]$hash
  )

  write-host $hash.A
  write-host $hash.B
  write-host $hash.C
}

Test $data.arguments

Using a String array:

function Test {
  param (
    [String[]]$a
  )

  $a | % { write-host $_ }
}

Test $data.arguments.values

Upvotes: 1

Related Questions