We're trying to write an array of ordered hashtables to a file. While doing this we would like to to be aligned properly in a table format. A bit similar to Format-Table but differently presented.
The code below works fine for the first case but not for the second one. In the second one we have a property that is of type array and is incorrectly assessed. It feels a bit like we're reinventing the wheel here, is there a better solution?
Usually the values will be of type string, string[] or boolean, but what if another type is passed? Thanks for your help.
Desired goals
Describe 'ConvertTo-TextTableHC' {
It 'should convert a collection of hashtable to a text table' {
$testParameters = #(
[ordered]#{
Name = 'ScriptName'
Value = 'Get printers'
Type = 'String'
Mandatory = $true
}
[ordered]#{
Name = 'PrinterName'
Value = ''
Type = 'String[]'
Mandatory = $false
}
)
$actual = ConvertTo-TextTableHC -Name $testParameters
$expected = #"
Name Value Type Mandatory
ScriptName Get printers String True
PrinterName String[] False
"#
$actual | Should -Be $expected
}
It 'should convert a collection of hashtable to a text table' {
$testParameters = #(
[ordered]#{
Mandatory = $true
Name = 'ScriptName'
Value = 'Get printers'
Type = 'String'
}
[ordered]#{
Mandatory = $false
Name = 'OU'
Value = #('ou1', 'ou2')
Type = 'String[]'
}
)
$actual = ConvertTo-TextTableHC -Name $testParameters
$expected = #"
Mandatory Name Value Type
True ScriptName Get printers String
False OU ou1, ou2 String[]
"#
$actual | Should -Be $expected
} -Tag test
}
This is the function:
Function ConvertTo-TextTableHC {
[CmdletBinding()]
Param (
[Parameter(Mandatory)]
[System.Collections.Specialized.OrderedDictionary[]]$Name,
[int]$Spacing = 1
)
$result = [String[]]::new($Name.Count + 1)
foreach ($prop in $($name[0].Keys)) {
$rows = , $prop + $Name.$prop
$maxChars = ($rows | Measure-Object -Maximum -Property Length).Maximum + $Spacing
for ($i = 0; $i -lt $rows.Count; $i++) {
$spaces = ' ' * (
$maxChars -
($rows[$i] | Measure-Object -Character).Characters
# fails on boolean values:
# ($rows[$i] | Measure-Object -Maximum -Property Length).Maximum
)
$result[$i] += '{0}{1}' -f $rows[$i], $spaces
}
}
# remove spaces from last column
$result = $result.ForEach({$_.Trim()})
$result -join "`r`n"
}
I have the following code
function Get($url) {
$x = Invoke-RestMethod $url
# ....
$result # return a json which some values (id, name, color) and a list
}
Get "http://...." |
% {
$id = $_.id
$url = "${$_.url}/xxx"
Get $ur |
% {
$name = $_.name
$url = $_.url
Get $url |
% {
$color = $_.color
$url = $_.url
Get $url | % {
# other layer omitted
}
}
}
}
The code looks bad. It seems the embedded layers should be resolved by monad. Is there a way to do it in PowerShell?
The following shows a pseudo F# code.
myMonad {
let! (id1, _, _, urls1, _ ) = Get ["http://..."]
let! (_, name2, _, urls2, _ ) = Get urls1
let! (_, _, color3, urls3, names) = Get urls2
// ....
printfn "%d %s %s %A" id1, name2, color3, names
}
PowerShell is clearly not a function language, though it has some functional traits and its many constructs and dynamic nature allow you to emulate functional features:
The code below defines a recurse-and-aggregate function named recurse that can invoke your Get function as follows in order to aggregate properties Id, Name, and Color from recursive calls to Get via each returned object's .URL property:
recurse -Function Get <# target function #> `
-Argument http://example.org <# input URL #> `
-RecurseOn URL <# what result property to recurse on #> `
-PropertyNames Id, Name, Color <# what properties to aggregate successively,
in each iteration #>
recurse source code with sample code:
# A fairly generic recurse-and-aggregate function that aggregates properties from
# result objects from recursive calls to a given function based on a single result property.
function recurse($Function, $Arguments, $RecurseOn, $PropertyNames, $outProperties = [ordered] #{ }) {
# Call the target function with the specified arguments.
$result = & $Function $Arguments
# Split into the names of the current and the remaining properties to aggregate.
$propName, $remainingPropNames = $PropertyNames
# Add the value of the current property of interest to the output hashtable, if present.
if ($null -ne $result.$propName) {
if ($outProperties.Contains($propName)) { # not the first value
if ($outProperties.$propName -is [array]) { # already an array -> "extend" the array.
$outProperties.$propName += $result.$propName
}
else { # not an array yet -> convert to array, with the previous value and the new one as the elements.
$outProperties.$propName = $outProperties.$propName, $result.$propName
}
}
else { # first value -> assign as-is
$outProperties.$propName = $result.$propName
}
}
if ($remainingPropNames) {
# Recurse on the value(s) of the property specfied with -RecurseOn
foreach ($newArgument in $result.$RecurseOn) {
$null = recurse -Function $function -Argument $newArgument -RecurseOn $RecurseOn -PropertyNames $remainingPropNames -outProperties $outProperties
}
}
# Return the aggregated properties.
$outProperties
}
# Sample input function:
# It makes a REST call to the given URL and returns the
# result object.
function Get($url) {
Write-Host "Get $url"
$id = 1
$name = ''
$color = ''
if ($url -eq 'http://example.org') {
$urls = 'http://example.org/1', 'http://example.org/2'
$id = 1
}
elseif ($url -match 'http://example.org/\d$') {
$urls = "$url/a", "$url/b"
$name = 'test'
}
elseif ($url -match 'http://example.org/\d/a') {
$urls = '...'
$name = 'test'
$color = "[color of $url] #1", "[color of $url] #2"
}
elseif ($url -match 'http://example.org/\d/b') {
$urls = '...'
$name = 'test'
$color = "[color of $url] #1", "[color of $url] #2"
}
[pscustomobject] #{
URL = $urls
Id = $id
Name = $name
Color = $color
}
}
# Call the recurse-and-aggregate function, passing it the name of the Get()
# function, an input URL, what result property to recurse on, and a list of properties to aggregate.
# The output is an ordered hashtable containing the aggregated property values.
recurse -Function Get <# target function #> `
-Argument http://example.org <# input URL #> `
-RecurseOn URL <# what result property to recurse on #> `
-PropertyNames Id, Name, Color <# what properties to aggregate successively,
in each iteration #>
The above yields:
Name Value
---- -----
Id 1
Name {test, test}
Color {[color of http://example.org/1/a] #1, [color of http://example.org/1/a] #2, [color of http://example.org/1/b] #1, [color of htt…
Well, you could insert F# code into Powershell with Add-Type. See Example 7: https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.utility/add-type?view=powershell-5.1
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
teaching myself about functions - I have written the below and problem I have at the moment is this:
1.The output is displayed in a hash table like format - ie #{FileVersion=6.1.7601.18606; IsReadOnly?=False; Directory=C:\windows\system32}. Is there a way of not displaying it in this format?
2.For the IF statment how do I get the code to just look at the ProductVersion bit of the output and not the whole function...or is that the point of a function, should I just have a function that just gets the file version separatly?
clear
function fileversion {
param ([string]$fileToCheck)
$fileInput = Get-Item $fileToCheck
$versionCheck = $fileInput.VersionInfo.ProductVersion
$DirectoryOfFile = $fileInput.DirectoryName
$IsReadOnly = $fileInput.IsReadOnly
$obj = New-Object psobject
$obj | Add-Member NoteProperty FileVersion $versionCheck
$obj | Add-Member NoteProperty IsReadOnly? $IsReadOnly
$obj | Add-Member NoteProperty Directory $DirectoryOfFile
Write-host $obj
}
$firstFile = fileversion C:\windows\system32\lsasrv.dll
If ($versionCheck -eq '6.1.7601.18606')
{ Write-host "File for is $TRUE" }
Else
{ Write-Host $False }
Don't know why you need a function since in the end you are just using one property of the object returned. Still, if you need the function you can just simplify it.
function fileversion{
param ([string]$fileToCheck)
Get-Item $fileToCheck | Select-Object #{Label="FileVersion";Expression={$_.VersionInfo.ProductVersion}},
#{Label="IsReadOnly?";Expression={$_.IsReadOnly}},
#{Label="Directory";Expression={$_.DirectoryName}}
}
No need to create a new object when the current one already has what you need. We use calculated properties to create the property names you wanted.
By default all output from functions is returned to the output stream. By sending the $obj to Write-Host you were removing that data from the stream. The output you say was Write-host casting the entire object as a [string]
Simple If
You could have just done this as well depending on your needs. This way you dont need a custom function
$version = (Get-Item C:\windows\system32\lsasrv.dll).VersionInfo.ProductVersion
If($version -eq '6.1.7601.18606'){
write-host "Version match"
} Else {
write-host "$version does not match"
}
You could always change the file path to a variable. Again, this was just suggestion. Please do whatever makes your scripting life comfortable.
use Write-output instead of write-host and check the version using the right property name (FileVersion) on the returned object:
If ($firstFile.FileVersion -eq '6.1.7601.18606')
{ Write-host "File for is $TRUE" }
Else
{ Write-Host $False }
** Revised **
1.) 'IF' condition statement should be applied to the property before $obj is created.
2.) To make the function more flexible and reusable, you should expand on the parameter criteria to allow for values from pipeline and and array of values as input.
function fileversion {
param(
[Parameter(Mandatory=$True,
ValueFromPipeline=$True,
ValueFromPipelineByPropertyName=$True)]
[Alias('files')]
[string[]]$filesToCheck
)
$fileInput = Get-Item $filesToCheck
foreach($f in $fileInput) {
$properties = #{
'File'=Split-Path $f -Leaf;
'FileVersion'=$f.VersionInfo.ProductVersion;
'CorrectVersion'=IF($versionCheck -eq '6.1.7601.18606') {$TRUE } ELSE {$False };
'IsReadyOnly'=$f.IsReadOnly;
'DirectoryOfFile'=$f.DirectoryName
}
$obj = New-Object psobject -Property $properties
Write-Output $obj
}
}
I'm trying to find a way to get all parameter information from a powershell script. Ex script:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach ($key in $MyInvocation.BoundParameters.keys)
{
write-host "Parameter: $($key) -> $($MyInvocation.BoundParameters[$key])"
}
}
test -foo "foo!"
I'd like to get the values of $bar and $baz in a dynamic way without knowing the names of the parameters ahead of time.
I've looked through $MyInvocation properties and methods but I don't see anything besides parameters that are set/passed.
Update 1:
I'm close to getting it with:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach($var in (get-variable -scope private))
{
write-host "$($var.name) -> $($var.value)"
}
}
test -foo "foo!"
If i could filter out the script parameters vs the default parameters I would be good to go.
Update 2:
The final working solution looks like this:
function test {
param (
[string] $Bar = 'test'
, [string] $Baz
, [string] $Asdf
)
$ParameterList = (Get-Command -Name $MyInvocation.InvocationName).Parameters;
foreach ($key in $ParameterList.keys)
{
$var = Get-Variable -Name $key -ErrorAction SilentlyContinue;
if($var)
{
write-host "$($var.name) > $($var.value)"
}
}
}
test -asdf blah;
Check this solution out. This uses the CmdletBinding() attribute, which provides some additional metadata through the use of the $PSCmdlet built-in variable. You can:
Dynamically retrieve the command's name, using $PSCmdlet
Get a list of the parameter for the command, using Get-Command
Examine the value of each parameter, using the Get-Variable cmdlet
Code:
function test {
[CmdletBinding()]
param (
[string] $Bar = 'test'
, [string] $Baz
, [string] $Asdf
)
# Get the command name
$CommandName = $PSCmdlet.MyInvocation.InvocationName;
# Get the list of parameters for the command
$ParameterList = (Get-Command -Name $CommandName).Parameters;
# Grab each parameter value, using Get-Variable
foreach ($Parameter in $ParameterList) {
Get-Variable -Name $Parameter.Values.Name -ErrorAction SilentlyContinue;
#Get-Variable -Name $ParameterList;
}
}
test -asdf blah;
Output
The output from the command looks like this:
Name Value
---- -----
Bar test
Baz
Asdf blah
To read the value dynamically use the get-variable function / cmdlet
write-host (get-variable "foo")
To print out all of the parameters do the following
foreach ($key in $MyInvocation.BoundParameters.keys)
{
$value = (get-variable $key).Value
write-host "$key -> $value"
}
Hopefully, some may find this one-liner useful:
function test()
{
Param(
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
$MyInvocation.MyCommand.Parameters | Format-Table -AutoSize #{ Label = "Key"; Expression={$_.Key}; }, #{ Label = "Value"; Expression={(Get-Variable -Name $_.Key -EA SilentlyContinue).Value}; }
}
test -foo "foo!"
Result
Keys Value
---- -----
foo foo!
bar
baz baz
I found this most useful for PS4 (Windows 2012 R2) - it includes default values / optional parameters:
$cmdName = $MyInvocation.InvocationName
$paramList = (Get-Command -Name $cmdName).Parameters
foreach ( $key in $paramList.Keys ) {
$value = (Get-Variable $key -ErrorAction SilentlyContinue).Value
if ( $value -or $value -eq 0 ) {
Write-Host "$key -> $value"
}
}
For those of you who do not want to use cmdletbinding() here's a variation on the one liner I found above:
(Get-Command -Name $PSCommandPath).Parameters | Format-Table -AutoSize #{ Label = "Key"; Expression={$_.Key}; }, #{ Label = "Value"; Expression={(Get-Variable -Name $_.Key -EA SilentlyContinue).Value}; }
$PSCommandPath is always available
I played with the 2 solutions i liked in this thread, they both work.
however I needed to produce an error out on missing parameter for a build script
$cmdName = $MyInvocation.InvocationName
$paramList = (Get-Command -Name $cmdName).Parameters
foreach ( $key in $paramList.Keys ) {
$value = (Get-Variable $key -ErrorAction Stop)
#Write-Host $value.Value #remove comment for error checking
if ([string]::IsNullOrEmpty($value.Value)){
$(throw ("$key is a mandatory value please declare with -$key <Required value> " ))
}
}
What if I don't know what arguments or how many will be passed? For example, the function could be called with:
test -foo "value1" -bar "value2" -baz "value3"
and someone else might call it with:
test -variable1 "somevalue" -variable2 "somevalue2" -variable3 "somevalue3" -variable4 "value4"
I looked at $MyInvocation and the arguments come across as UnboundArguments, so theoretically I could "pair up" argument 0 with argument 1, 2 with 3, etc and error out if there's an odd number of UnboundArguments.
Stumbled upon this trying to do something similar and figured out my preferred option.
Function ParamTest {
Param (
$t1 = '1234',
$t2,
[switch]$3
)
$MyInvocation |Add-Member -Name:'Param' -MemberType:'NoteProperty' -Value:(
(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys)|
ForEach-Object -begin:{$h=#{}} -process:{$h.add($_.Name,$_.Value)} -end:{$h}
))
$MyInvocation.Param
}
Result
Name Value
---- -----
t1 1234
3 False
t2
PSVersionTable
Name Value
---- -----
PSVersion 5.1.19041.1320
PSEdition Desktop
PSCompatibleVersions {1.0, 2.0, 3.0, 4.0...}
BuildVersion 10.0.19041.1320
CLRVersion 4.0.30319.42000
WSManStackVersion 3.0
PSRemotingProtocolVersion 2.3
SerializationVersion 1.1.0.1
A streamlined solution that:
builds on your own approach now shown at the bottom of the question,
while also including an additional piece of information, namely whether each parameter was bound on invocation, i.e. whether an argument was passed or not, based on its presence in the automatic $PSBoundParameter variable, which is a dictionary containing the bound parameters and their values (arguments).
Note: $PSBoundParameters does not include parameters bound by a default value, only those parameters to which an argument was explicitly passed; for your use case, that is arguably desirable in order to distinguish between a default value and an explicit one, but in scenarios where arguments should be passed through it may be desirable to have default-value parameters included - see GitHub issue #3285.
function test {
param (
[string]$foo,
[string]$bar,
[string]$baz = "baz"
)
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
$bound = $PSBoundParameters.ContainsKey($paramName)
[pscustomobject] #{
ParameterName = $paramName
ParameterValue = if ($bound) { $PSBoundParameters[$paramName] }
else { Get-Variable -Scope Local -ErrorAction Ignore -ValueOnly $paramName }
Bound = $bound
}
}
}
test -foo "foo!"
The above yields the following:
ParameterName ParameterValue Bound
------------- -------------- -----
foo foo! True
bar False
baz baz False
Note: This solution also handles dynamic parameters correctly:
Such parameters are reflected in $MyInvocation.MyCommand.Parameters if they situationally apply, but - unlike regular static parameters - are never reflected in scope-local variables. If they apply and are also bound, they and their values are reflected in $PSBoundParameters.
Thus - given that an applicable dynamic parameter may not be bound - the Get-Variable call to look for scope-local variables representing static parameters:
Must explicitly be limited to the current scope with -Scope Local so as not to accidentally pick up unrelated variables of the same name from ancestral scopes for unbound dynamic parameters.
Must ignore errors from the potentially resulting failure to find a variable in the current scope, using -ErrorAction Ignore.
I wanted a compact string of parameter key/value pairs that I can write out when catching error. Use content of the other answers, I came up with this:
$parameters = (Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) |
Select-Object Name, Value | ForEach-Object { "$($_.Name) : $($_.Value)" }) -join ' | '
Sample output:
ComputerName : SomePC | Directory : C:\Tools\LogExpert | UserName : Hans Wurst | YesOrNo : False
Thanks to all other posters with great & helpful answers above!
I'm cobbling together some of the above posts to try to meet my requirements.
I'd like to show Params in a Powershell-language-compatible format, so that one could easily see & display these params, and then also copy+pasta back into either a script as Param() Defaults (ParamsFormat1) or into a commandline/function call (CommandFormat2). I struggled with this for a while, so I hope this saves someone some time.
Collection of various Scripts from above:
# Note: there are some subtle differences between these versions below
# Specifically in the first line for *which* object you're checking for Parameters, and subsequently what type of Parameters you're looking at.
# PSCmdlet version REQUIRES [CmdletBinding()] on your function!!!
# $($(Get-Command -Name $($PSCmdlet.MyInvocation.InvocationName)).Parameters) | `
# %{ Get-Variable -Name $_.Values.Name -ErrorAction SilentlyContinue; }
# -Scope:'Local' FILTERS out any global params
# (Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys)) | ft
# PSCommandPath supposedly available/usable "in all situations" - so this may be the most useful of all of them, BUT it shows global params [Debug, ErrorAction, etc...]
# (Get-Command -Name $PSCommandPath).Parameters | `
# %{ Get-Variable -Name $_.Values.Name -ErrorAction SilentlyContinue; }
ParamsFormat1 / HYBRID VERSION: To output "Default Params" as you would see them in a function definition. Combines the -Scope:"Local" and "always available" versions to get BOTH param TYPES as well as Name, Value
Write-Host "Params("
# TBD Figure out how to expand #{} of System.Collections.Hashtable
# HYBRID VERSION: Combine the -Scope:"Local" and "always available" versions to get BOTH param TYPES as well as Name, Value
# If you remove LocalList Filter here, it will also show "Global" function properties like Debug, ErrorAction, etc...
$LocalList = $(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) | Select-Object -ExpandProperty Name) -join "|"
# VERSION: Wrapper script with DEFAULTS : normally DOES include Global vars [Debug, ErrorAction, etc]. but DOES preserve param order as-written.
((Get-Command -Name $PSCommandPath).Parameters | `
Select -ExpandProperty Values | `
Where-Object { $_.Name -Match $LocalList } | `
Format-Table -HideTableHeaders -AutoSize `
#{ Label="Type"; Expression={"[$($_.ParameterType )]"}; }, `
#{ Label="Name"; Expression={"`t`$$($_.Name)"}; }, `
#{ Label="Equals"; Expression={"="}; }, `
#{ Label="Value"; Expression={ If( $_.ParameterType -Match "String" ) { "`"$((Get-Variable -Name $_.Name -EA SilentlyContinue).Value)`"" } Else{ $((Get-Variable -Name $_.Name -EA SilentlyContinue).Value)}; }; }, `
#{ Label="RowEndComma"; Expression={ "," }; }
##{ Label="Value"; Expression={ $((Get-Variable -Name $_.Name -EA SilentlyContinue).Value) }; } # (OPTIONAL) Values only, no wrapping quotes
)
Write-Host ")";
CommandFormat2 / SIMPLER VERSION: Call with CommandLine Args (TYPES not needed). This filters out Global vars, does NOT preserve param ordering. (sorted alphabetically?)
# VERSION: Call with CommandLine Args (TYPES not available - TBD needs more work to display String, Array, and Hashmap/PsCustomObject ["", #() and {}] types better): filters out Global vars, does NOT preserve param ordering.
# If needed, cut+paste OPTIONAL lines down above Format-Table line.
# Where-Object { $_.Value } | ` # (Optional) remove any Null items.
# Sort-Object -Property Name | ` # (Optional) sort output
(Get-Variable -Scope:'Local' -Include:#($MyInvocation.MyCommand.Parameters.keys) | `
Format-Table -HideTableHeaders -AutoSize `
#{ Label="Name"; Expression={"`t-$($_.Name)"}; }, `
#{ Label="Equals"; Expression ={":"}; }, `
#{ Label="Value"; Expression={ If( $_.ParameterType -NotMatch "String" ) { $_.Value; } Else {"`"$($_.Value)`"";} }; Alignment="left"; }
)