How to loop through arrays in hash table - passing parameters based on values read from a CSV file - powershell

Curious about how to loop through a hash table where each value is an array. Example:
$test = #{
a = "a","1";
b = "b","2";
c = "c","3";
}
Then I would like to do something like:
foreach ($T in $test) {
write-output $T
}
Expected result would be something like:
name value
a a
b b
c c
a 1
b 2
c 3
That's not what currently happens and my use case is to basically pass a hash of parameters to a function in a loop. My approach might be all wrong, but figured I would ask and see if anyone's tried to do this?
Edit**
A bit more clarification. What I'm basically trying to do is pass a lot of array values into a function and loop through those in the hash table prior to passing to a nested function. Example:
First something like:
$parameters = import-csv .\NewComputers.csv
Then something like
$parameters | New-LabVM
Lab VM Code below:
function New-LabVM
{
[CmdletBinding()]
Param (
# Param1 help description
[Parameter(Mandatory=$true,
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true)]
[Alias("p1")]
[string[]]$ServerName,
# Param2 help description
[Parameter(Position = 1)]
[int[]]$RAM = 2GB,
# Param3 help description
[Parameter(Position=2)]
[int[]]$ServerHardDriveSize = 40gb,
# Parameter help description
[Parameter(Position=3)]
[int[]]$VMRootPath = "D:\VirtualMachines",
[Parameter(Position=4)]
[int[]]$NetworkSwitch = "VM Switch 1",
[Parameter(Position=4)]
[int[]]$ISO = "D:\ISO\Win2k12.ISO"
)
process
{
New-Item -Path $VMRootPath\$ServerName -ItemType Directory
$Arguments = #{
Name = $ServerName;
MemoryStartupBytes = $RAM;
NewVHDPath = "$VMRootPath\$ServerName\$ServerName.vhdx";
NewVHDSizeBytes = $ServerHardDriveSize
SwitchName = $NetworkSwitch;}
foreach ($Argument in $Arguments){
# Create Virtual Machines
New-VM #Arguments
# Configure Virtual Machines
Set-VMDvdDrive -VMName $ServerName -Path $ISO
Start-VM $ServerName
}
# Create Virtual Machines
New-VM #Arguments
}
}

What you're looking for is parameter splatting.
The most robust way to do that is via hashtables, so you must convert the custom-object instances output by Import-Csv to hashtables:
Import-Csv .\NewComputers.csv | ForEach-Object {
# Convert the custom object at hand to a hashtable.
$htParams = #{}
$_.psobject.properties | ForEach-Object { $htParams[$_.Name] = $_.Value }
# Pass the hashtable via splatting (#) to the target function.
New-LabVM #htParams
}
Note that since parameter binding via splatting is key-based (the hashtable keys are matched against the parameter names), it is fine to use a regular hashtable with its unpredictable key ordering (no need for an ordered hashtable ([ordered] #{ ... }) in this case).

Try this:
for($i=0;$i -lt $test.Count; $i++)
{$test.keys | %{write-host $test.$_[$i]}}
Weirdly, it outputs everything in the wrong order (because $test.keys outputs it backwards).
EDIT: Here's your solution.
Using the [System.Collections.Specialized.OrderedDictionary] type, you guarantee that the output will come out the same order as you entered it.
$test = [ordered] #{
a = "a","1";
b = "b","2";
c = "c","3";
}
After running the same solution code as before, you get exactly the output you wanted.

Related

Looping Register-ArgumentCompleter produces incorrect parameter completions

I have a module with a hashtable of dynamically derived enum values, that I thought would be slick to incorporate into Register-ArgumentCompleter for tab completion.
The motivation here is that I can't directly set the module function's input parameters to autoconvert into the enum type (which would properly enable tab completion), because I wish to dynamically derive the enums to save users from manually managing the enum values, as well as due to limitations with the .NET implementation of enums -- I need to allow for strings with dashes or starting with numbers, and potentially null values, all of which enums sadly don't allow. My idea is to do a workaround by adding tab-completed parameter values via Register-ArgumentCompleter.
Problem: I build this workaround as a script that's loaded in the first position of the ScriptsToProcess member of the module manifest, whereupon I discovered that incorrect values are being set when I loop over the hashtable keys and run Register-ArgumentCompleter.
Sample code to reproduce:
function test {param($a, $b, $c, $d )}
$ht = #{
'1' = #('a', #('a1','a2'))
'2' = #('b', #('b1','b2'))
'3' = #('c', #('c1','c2'))
'4' = #('d', #('d1','d2'))
}
Foreach ($enum in $ht.Keys){
$paramName = $ht.$enum[0]
$paramValue = $ht.$enum[1]
write-host $paramName
write-host $paramValue
Register-ArgumentCompleter -CommandName test2 -ParameterName $paramName -ScriptBlock {$paramValue}
}
PS> test -a <tab>
b1 b2
This is PS 7.2.5. In Windows PowerShell 5.1.19041 I get c1 c2 as suggested values. You can see from the host writes that it's down to whichever key is parsed last in the ht loop.
I also tried $ht.["$enum"][0|1] to cast the key type explicitly to a string, to no avail. When I write-host in the loop, all the values seem correct.
Does this seem like an error from me or a bug?
By the time the loop completes, $enum will have a value of whatever the last key in its sort order is.
Use ScriptBlock.GetNewClosure() to close over the value of $ht and $enum by the time GetNewClosure() is called, making the scriptblock retain the original values of $ht and $enum:
function test {param($a, $b, $c, $d )}
$ht = #{
'1' = #('a', #('a1','a2'))
'2' = #('b', #('b1','b2'))
'3' = #('c', #('c1','c2'))
'4' = #('d', #('d1','d2'))
}
Foreach ($enum in $ht.Keys){
Register-ArgumentCompleter -CommandName test -ParameterName $ht.$enum[0] -ScriptBlock { $ht.$enum[1] }.GetNewClosure()
}
FWIW you can simplify the $ht table significantly:
$ht = #{
'a' = #('a1','a2')
'b' = #('b1','b2')
'c' = #('c1','c2')
'd' = #('d1','d2')
}
Foreach ($enum in $ht.Keys){
Register-ArgumentCompleter -CommandName test -ParameterName $enum -ScriptBlock { $ht[$enum] }.GetNewClosure()
}

Powershell - pass a value to parameter

How to pass value along with parameter? Something like ./test.ps1 -controllers 01. I want the script to use hyphen and also a value is passed along for the parameter.
Here is the part of the script I wrote. But if I call the script with hyphen (.\test.ps1 -Controllers) it says A parameter cannot be found that matches parameter name 'Controllers'.
param(
# [Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
Also I need to pass a value to it which is then used for a property.
if ($options -eq "controllers")
{
$callsomething.$arg1 | where {$_ -eq "$arg2" }
}
Lets talk about why it does not work
function Test()
param(
[Parameter(Mandatory=$false, Position=0)]
[ValidateSet('Controllers','test2','test3')]
[String]$options
)
}
Parameters are Variables that are created and filled out at the start of the script
ValidateSet will only allow the script to run if $Options equals one of the three choices 'Controllers','test2','test3'
Lets talk about what exactly all the [] are doing
Mandatory=$false means that $options doesnt have to be anything in order for the script to run.
Position=0 means that if you entered the script without using the -options then the very first thing you put would still be options
Example
#If Position=0 then this would work
Test "Controllers"
#Also this would work
Test -options Controllers
[ValidateSet('Controllers','test2','test3')] means that if Option is used or is Mandatory then it has to equal 'Controllers','test2','test3'
It sounds like you are trying to create parameters at runtime. Well that is possible using DynamicParam.
function Test{
[CmdletBinding()]
param()
DynamicParam {
$Parameters = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
'Controllers','test2','test3' | Foreach-object{
$Param = New-Object System.Management.Automation.ParameterAttribute
$Param.Mandatory = $false
$AttribColl = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$AttribColl.Add($Param)
$RuntimeParam = New-Object System.Management.Automation.RuntimeDefinedParameter("$_", [string], $AttribColl)
$Parameters.Add("$_", $RuntimeParam)
}
return $Parameters
}
begin{
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
}
process {
"$Controllers $Test2 $Test3"
}
}
DynamicParam allows you to create parameters in code.
The example above turns the array 'Controllers','test2','test3' into 3 separate parameters.
Test -Controllers "Hello" -test2 "Hey" -test3 "Awesome"
returns
Hello Hey Awesome
But you said you wanted to keep the hypen and the parameter
So the line
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value $_.Value
}
allows you to define each parameter value. a slight change like :
$PSBoundParameters.GetEnumerator() | ForEach-Object{
Set-Variable $_.Key -Value "-$($_.Key) $($_.Value)"
}
Would return
-Controllers Hello -test2 Hey -test3 Awesome

Powershell script to table doesn't output anything [duplicate]

I have a problem in a PowerShell script:
When I want to pass a Hashtable to a function, this hashtable is not recognized as a hashtable.
function getLength(){
param(
[hashtable]$input
)
$input.Length | Write-Output
}
$table = #{};
$obj = New-Object PSObject;$obj | Add-Member NoteProperty Size 2895 | Add-Member NoteProperty Count 5124587
$table["Test"] = $obj
$table.GetType() | Write-Output ` Hashtable
$tx_table = getLength $table `Unable to convert System.Collections.ArrayList+ArrayListEnumeratorSimple in System.Collections.Hashtable
Why?
$Input is an automatic variable that enumerates the input given.
Chose any other variable name and it'll work - although not necessarily as you might expect - to get the number of entries in a hashtable you need to inspect the Count property:
function Get-Length {
param(
[hashtable]$Table
)
$Table.Count
}
Write-Output is implied when you just leave the $Table.Count as is.
Also, the () suffix in the function name is unnecessary syntactic sugar with zero meaning when you declare your parameters inline with Param() - drop it
I'm not really sure what to comment here, it seems self-explanatory. If not, leave a comment and I'll clarify.
$ExampleHashTable = #{
"one" = "the loneliest number"
"two" = "just as bad as one"
}
Function PassingAHashtableToAFunctionTest {
param(
[hashtable] $PassedHashTable,
[string] $AHashTableElement
)
Write-Host "One is ... "
Write-Host $PassedHashTable["one"]
Write-Host "Two is ... "
Write-Host $AHashTableElement
}
PassingAHashtableToAFunctionTest -PassedHashTable $ExampleHashTable `
-AHashTableElement $ExampleHashTable["two"]
Output:
One is ...
the loneliest number
Two is ...
just as bad as one

How to Pass Multiple Objects via the Pipeline Between Two Functions in Powershell

I am attempting to pass a list of objects from one function to another, one by one.
First function: generate a list of users (objects) near expiry;
Second function: send an email to each user (object)
The first function works fine and outputs a group of objects (or so it would seem) and the second function will accept input and email a single user without issue.
Issues arise only when multiple objects are passed from the first function to the second.
Relevant code snippets are below:
The First function creates a custom object for each located user and adds it to an array, which is then outputted in the end block. Below is an extremely simplified snippet of the code with the essential object creation step:
Function 01
{
#param block goes here etc...
Foreach ($user in $users)
{
$userOutput = #()
$userTable = New-Object PSObject -Property #{
name = $User.Name
SamAccountName = $User.SamAccountName
emailAddress = $User.EmailAddress
expired = $user.PasswordExpired
expiryDate = $ExpiryDate.ToShortDateString()
daysTillExpiry = $daysTillExpiry
smtpRecipientAddress = $User.EmailAddress
smtpRecipientName = $User.Name
}
$userOutput += $userTable
}
Write-Output $userOutput
}
I have also tried writing each custom object ($userTable) straight to the console within each iteration of the Foreach (users) loop.
The Second function accepts pipeline input for a number of matching parameters from the first function, e.g:
[Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)][string]$smtpRecipientName
The second function also calls a third function designed specifically to send smtp mail and contains no loops, it just takes the current object from the pipeline and deals with it.
I haven't included the full code for either mail function because it is largely irrelevant. I just want to know whether the objects outputted from the first function can be dealt with one-by-one by the second.
At present, the mail function deals with the first object passed to it, and no others.
Update:
This is what I have in mind (but the second function only deals with the last object that was piped in:
Function Test-UserExp
{
$iteration = 0
For ($i=0;$i -le 9;$i++)
{
$iteration ++
$userTable = New-Object PSObject -Property #{
expiryDate = "TestExpDate_$iteration"
daysTillExpiry = "TestDaysTillExpiry_$iteration"
smtpRecipientAddress = "TestSMTPRecipientAddress_$iteration"
smtpRecipientName = "TestSMTPRecipientName_$iteration"
}
$userTable
}
}
Function Test-MailSend
{
Param
(
[Parameter(ValueFromPipelineByPropertyName=$true)][string]$expiryDate,
[Parameter(ValueFromPipelineByPropertyName=$true)][string]$daysTillExpiry,
[Parameter(ValueFromPipelineByPropertyName=$true)][string]$smtpRecipientAddress,
[Parameter(ValueFromPipelineByPropertyName=$true)][string]$smtpRecipientName
)
Write-Host 'Output from Test-MailSend:'
$expiryDate
$daysTillExpiry
$smtpRecipientAddress
$smtpRecipientName
}
First of all: if you want to process objects in a pipeline, one at the time, do not kill experience by collecting all the objects - that's only necessary if you intend to do something about whole collection at some point. If not than just output objects as soon as you get them:
foreach ($user in $users) {
New-Object PSObject -Property #{
name = $User.Name
SamAccountName = $User.SamAccountName
emailAddress = $User.EmailAddress
# ...
}
}
In your case you output whole collection at the end. That's hardly a pipeline experience if you would ask me.
For the second command: if you intend to create parameter for each property, just leave the part 'ValueFromPipeline' out. Otherwise you may end up with whole object converted to string... If you want to take an object as a whole, leave out 'ValueFromPipelineByPropertyName' and specify correct type. And make sure you have process {} wrapped around the code that uses parameters taken from pipeline.
And finally: why would you write a function to send mails? You have Send-MailMessage, so unless you do something this cmdlet doesn't cover, you probably don't need hand-crafted replacement...
In function 1 you want to create the array before the ForEach loop, so you aren't re-creating the array every iteration.
In the param block for the second function, you want to declare the parameter as an array of strings, not just a string.
Finally, when accepting pipeline input for the second function you will need to use the Begin, Process, and End blocks. The part of the function that repeats for each item should be in the Process block.
Here is a short working sample below:
Function fun1{
$users = #(1,2,3)
$userOutput = #()
Foreach ($user in $users){
$userTable = New-Object PSObject -Property #{
emailAddress = "$user#blah.com"
}
$userOutput += $userTable
}
$userOutput
}
Function fun2{
param(
[parameter(ValueFromPipeLine=$true)]
[String[]]$Recipients
)
begin{}
process{
ForEach ($Recipient in $Recipients){
$_
}
}
end{}
}
fun1 | Select emailAddress | fun2
This will give you the output below:
emailAddress
------------
1#blah.com
2#blah.com
3#blah.com
Here is a great breakdown of how the Begin/Process/End blocks work in PowerShell http://technet.microsoft.com/en-us/magazine/hh413265.aspx
function Set-UserExpiry {
1..10 | foreach {
[PSCustomObject]#{
ExpiryDate = "TestExpDate_$_"
DaysTillExpiry = "TestDaysTillExpiry_$_"
SmtpRecipientAddress = "TestSMTPRecipientAddress_$_"
SmtpRecipientName = "TestSMTPRecipientName_$_"
}
}
}
function Test-UserExpiry {
param
(
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$ExpiryDate,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$DaysTillExpiry,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$SmtpRecipientAddress,
[Parameter(ValueFromPipelineByPropertyName = $true)]
[string]$SmtpRecipientName
)
process {
Write-Output 'Output from Test-MailSend:'
$expiryDate
$daysTillExpiry
$smtpRecipientAddress
$smtpRecipientName
Write-Output ''
}
}
Set-UserExpiry | Test-UserExpiry

Pass an unspecified set of parameters into a function and thru to a cmdlet

Let's say I want to write a helper function that wraps Read-Host. This function will enhance Read-Host by changing the prompt color, calling Read-Host, then changing the color back (simple example for illustrative purposes - not actually trying to solve for this).
Since this is a wrapper around Read-Host, I don't want to repeat the all of the parameters of Read-Host (i.e. Prompt and AsSecureString) in the function header. Is there a way for a function to take an unspecified set of parameters and then pass those parameters directly into a cmdlet call within the function? I'm not sure if Powershell has such a facility.
for example...
function MyFunc( [string] $MyFuncParam1, [int] $MyFuncParam2 , Some Thing Here For Cmdlet Params that I want to pass to Cmdlet )
{
# ...Do some work...
Read-Host Passthru Parameters Here
# ...Do some work...
}
It sounds like you're interested in the 'ValueFromRemainingArguments' parameter attribute. To use it, you'll need to create an advanced function. See the about_Functions_Advanced and about_Functions_Advanced_Parameters help topics for more info.
When you use that attribute, any extra unbound parameters will be assigned to that parameter. I don't think they're usable as-is, though, so I made a little function that will parse them (see below). After parsing them, two variables are returned: one for any unnamed, positional parameters, and one for named parameters. Those two variables can then be splatted to the command you want to run. Here's the helper function that can parse the parameters:
function ParseExtraParameters {
[CmdletBinding()]
param(
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$ParamHashTable = #{}
$UnnamedParams = #()
$CurrentParamName = $null
$ExtraParameters | ForEach-Object -Process {
if ($_ -match "^-") {
# Parameter names start with '-'
if ($CurrentParamName) {
# Have a param name w/o a value; assume it's a switch
# If a value had been found, $CurrentParamName would have
# been nulled out again
$ParamHashTable.$CurrentParamName = $true
}
$CurrentParamName = $_ -replace "^-|:$"
}
else {
# Parameter value
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName += $_
$CurrentParamName = $null
}
else {
$UnnamedParams += $_
}
}
} -End {
if ($CurrentParamName) {
$ParamHashTable.$CurrentParamName = $true
}
}
,$UnnamedParams
$ParamHashTable
}
You could use it like this:
PS C:\> ParseExtraParameters -NamedParam1 1,2,3 -switchparam -switchparam2:$false UnnamedParam1
UnnamedParam1
Name Value
---- -----
switchparam True
switchparam2 False
NamedParam1 {1, 2, 3}
Here are two functions that can use the helper function (one is your example):
function MyFunc {
[CmdletBinding()]
param(
[string] $MyFuncParam1,
[int] $MyFuncParam2,
[Parameter(Position=0, ValueFromRemainingArguments=$true)]
$ExtraParameters
)
# ...Do some work...
$UnnamedParams, $NamedParams = ParseExtraParameters #ExtraParameters
Read-Host #UnnamedParams #NamedParams
# ...Do some work...
}
function Invoke-Something {
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, Position=0)]
[string] $CommandName,
[Parameter(ValueFromRemainingArguments=$true)]
$ExtraParameters
)
$UnnamedParameters, $NamedParameters = ParseExtraParameters #ExtraParameters
&$CommandName #UnnamedParameters #NamedParameters
}
After importing all three functions, try these commands:
MyFunc -MyFuncParam1 Param1Here "PromptText" -assecure
Invoke-Something -CommandName Write-Host -Fore Green "Some text" -Back Red
One word: splatting.
Few more words: you can use combination of $PSBoundParameters and splatting to pass parameters from external command, to internal command (assuming names match). You would need to remove any parameter that you don't want to use though from $PSBoundParameters first:
$PSBoundParameters.Remove('MyFuncParam1')
$PSBoundParameters.Remove('MyFuncParam2')
Read-Host #PSBoundParameters
EDIT
Sample function body:
function Read-Data {
param (
[string]$First,
[string]$Second,
[string]$Prompt,
[switch]$AsSecureString
)
$PSBoundParameters.Remove('First') | Out-Null
$PSBoundParameters.Remove('Second') | Out-Null
$Result = Read-Host #PSBoundParameters
"First: $First Second: $Second Result: $Result"
}
Read-Data -First Test -Prompt This-is-my-prompt-for-read-host