Powershell hashtable in function not accessible - function

I created a function to import HR records so they're easily accessible by indexing the employeeID.
function hrDataImport {
$HRFile=Import-Csv $rawHRFile #$rawHRFile path previously defined
Write-Host "Creating HR hash table..."
$hrData = #{}
$HRFile | ForEach-Object { $hrData[$_.employeeID] = $_ }
Write-Host "Done. " $hrData.Count
}
I'd like to use that $hrData variable outside the function. I can't reference $hrData['employeeID'] as I've read that variables in functions don't exist outside the function. One thing I tried is creating another function to pass an employeeID into the $hrData hashtable:
function showHRData {
param ([string]$hrUser)
$hrData[$hrUser]
}
I put the functions in a module and imported successfully. I'm able to execute importHRData fine but when I try showHRData -hrUser $employeeID I get "Cannot index into a null array". Seems like the function does not see the hashtable variable from the previous function. What am I doing wrong or what would you suggest so I can access that $hrData hashtable across various scripts?

You might create a Hashtable within your function using a $global (or $script) scope like this:
function hrDataImport {
$HRFile = Import-Csv $rawHRFile #$rawHRFile path previously defined
Write-Host "Creating HR hash table..."
if (!$hrData) { $Global:hrData = #{} }
$HRFile | ForEach-Object { $Global:hrData[$_.employeeID] = $_ }
Write-Host "Done. " $hrData.Count
}
But in general it is advised to avoid the global (or script) scope mainly because it might conflict with variables in the current scope.
To minimize this possibility, I would consider to change the responsibility of your function to not only loading the information but also to receiving the concerned employeeID, e.g.:
function Get-EmployeeData ($EmployeeID) {
if (!$StaticEmployeeData) {
Write-Host "Creating HR hash table..."
$HRFile = Import-Csv $rawHRFile #$rawHRFile path previously defined
$Global:StaticEmployeeData = #{}
$HRFile | ForEach-Object { $Global:StaticEmployeeData[$_.employeeID] = $_ }
Write-Host "Done. " $StaticEmployeeData.Count
}
$Global:StaticEmployeeData[$employeeID]
}
function showHRData {
param ([string]$hrUser)
Get-EmployeeData $hrUser
}
In that case you might choose for a more specific global variable name (e.g. $StaticEmployeeData, knowing that PowerShell doesn't support something like a static variable) and as an extra bonus the data is only loaded the first time you really need it (lazy evaluation).

Related

How to achieve #args splatting in an advanced function in Powershell?

Consider the following simple function:
function Write-HostIfNotVerbose()
{
if ($VerbosePreference -eq 'SilentlyContinue')
{
Write-Host #args
}
}
And it works fine:
Now I want to make it an advanced function, because I want it to inherit the verbosity preference:
function Write-HostIfNotVerbose([Parameter(ValueFromRemainingArguments)]$MyArgs)
{
if ($VerbosePreference -eq 'SilentlyContinue')
{
Write-Host #MyArgs
}
}
But it does not work:
And what drives me nuts is that I am unable to identify how $args in the first example is different from $args in the second.
I know that the native #args splatting does not work for advanced functions by default - https://learn.microsoft.com/en-us/powershell/module/microsoft.powershell.core/about/about_splatting?view=powershell-7.2#notes
But I hoped it could be simulated, yet it does not work either. My question is - what is wrong with the way I am trying to simulate it and whether it is possible to fix my code without surfacing all the Write-Host parameters at Write-HostIfNotVerbose
Santiago Squarzon's helpful answer contains some excellent sleuthing that reveals the hidden magic behind #args, i.e. splatting using the automatic $args variable, which is available in simple (non-advanced) functions only.
The solution in Santiago's answer isn't just complex, it also isn't fully robust, as it wouldn't be able to distinguish -ForegroundColor (a parameter name) from '-ForegroundColor' a parameter value that happens to look like a parameter name, but is distinguished from it by quoting.
As an aside: even the built-in #args magic has a limitation: it doesn't correctly pass a [switch] parameter specified with an explicit value through, such as
-NoNewLine:$false[1]
A robust solution requires splatting via the automatic $PSBoundParameters variable, which in turn requires that the wrapping function itself also declare all potential pass-through parameters.
Such a wrapping function is called a proxy function, and the PowerShell SDK facilitates scaffolding such functions via the PowerShell SDK, as explained in this answer.
In your case, you'd have to define your function as follows:
function Write-HostIfNotVerbose {
[CmdletBinding()]
param(
[Parameter(Position = 0, ValueFromPipeline, ValueFromRemainingArguments)]
[Alias('Msg', 'Message')]
$Object,
[switch] $NoNewline,
$Separator,
[System.ConsoleColor] $ForegroundColor,
[System.ConsoleColor] $BackgroundColor
)
begin {
$scriptCmd =
if ($VerbosePreference -eq 'SilentlyContinue') { { Write-Host #PSBoundParameters } }
else { { Out-Null } }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
}
process {
$steppablePipeline.Process($_)
}
end {
$steppablePipeline.End()
}
}
[1] Such an argument is invariably passed through as two arguments, namely as parameter name -NoNewLine by itself, followed by a separate argument, $false. The problem is that at the time the original arguments are parsed into $args, it isn't yet known what formally declared parameters they will bind to. The NoteProperty tagging applied to $args for marking elements as parameter names doesn't preserve the information as to whether the subsequent argument was separated from the parameter name with :, which for a [switch] parameter is necessary to identify that argument as belonging to the switch. In the absence of this information, two separate arguments are always passed during splatting.
This is too obscure for me to explain, but for the sake of answering what PowerShell could be doing with $args you can test this:
function Write-HostIfNotVerbose {
param(
[parameter(ValueFromRemainingArguments)]
[object[]]$MagicArgs
)
$params = #{
NotePropertyName = '<CommandParameterName>'
PassThru = $true
InputObject = ''
}
$z = foreach($i in $MagicArgs) {
if($i.StartsWith('-')) {
$params.NotePropertyValue = $i
Add-Member #params
continue
}
$i
}
if ($VerbosePreference -eq 'SilentlyContinue') {
Write-Host #z
}
}
Write-HostIfNotVerbose -ForegroundColor Green Hello world! -BackgroundColor Yellow
A way of seeing what $args is doing automatically for us could be to serialize the variable:
function Test-Args {
[System.Management.Automation.PSSerializer]::Serialize($args)
}
Test-Args -Argument1 Hello -Argument2 World
Above would give us the serialized representation of $args where we would observe the following:
<LST>
<Obj RefId="1">
<S>-Argument1</S>
<MS>
<S N="<CommandParameterName>">Argument1</S>
</MS>
</Obj>
<S>Hello</S>
<Obj RefId="2">
<S>-Argument2</S>
<MS>
<S N="<CommandParameterName>">Argument2</S>
</MS>
</Obj>
<S>World</S>
</LST>

Cannot use variable in a Where-Object in a function

I am trying to have a function that can count jobs based on the LatestStatus value that I would pass a parameter. So far what I got:
Function JobCountStatus {
Write-Output (Get-VBRJob | ?{$_.Info.LatestStatus -eq $args} | Measure-Object).Count
}
The issue is that as I've read somewhere there will be a subshell(?) executing the where so the argument is not passed.
If I replace the $args with a specific string like "Failed" it will work.
Is there a way to overcome this? I do not want to write separate functions for all possible values.
I would appreciate any comments - Thanks
Well you can just name the value when you run the function as $args is an Automatic Variable
JobCountStatus "Failed"
You can use an advanced function with a parameter, named or not:
function JobCountStatus {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[System.String]
$Status
)
Process {
(Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } | Measure-Object).Count
}
}
And call it like so:
JobCountStatus -Status "Failed"
# OR
JobCountStatus "Failed"
The latter having the same end result as just using $args. The only possible advantage to specifying your own parameter here is you could define a ValidateSet of statuses or an Enum of Status values so you can tab through them. An example of the former would be like so:
function JobCountStatus {
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true, Position = 0)]
[ValidateSet("Failed", "Running", "Successful", "Unknown")]
[System.String]
$Status
)
Process {
(Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } | Measure-Object).Count
}
}
$args is an array, not a single value. In any case, a ScriptBlock {} is an unnamed function and $args has it's own meaning within it, so you can't use it without some modification in something like Where-Object. You would have to store the elements of $args as another variable or multiple variables to reference within a child ScriptBlock. Not a huge change for this function, but for one where more parameters are expected this can result in a lot of unnecessary code which can be difficult to maintain.
I prefer to recommend defining named parameters in most cases, and this would be a simpler change than making the parent $args work in a child ScriptBlock:
Function JobCountStatus {
Param(
[string]$Status
)
( Get-VBRJob | Where-Object { $_.Info.LatestStatus -eq $Status } ).Count
}
I've also made a few more changes in this function, I'll explain below:
Use Param() to strongly define parameters by name. You could use the simpler syntax of function JobCountStatus([string]$Status) {} but for this case it's really a matter of preference for which technique to use. Using Param() is something I recommend as a matter of convention, but you'll need to use either technique to get a named parameter.
I replaced the $args reference with $Status.
Your use of Measure-Object is extraneous and so I've removed it. Where-Object returns a collection which already has the Count property.
You can use ? if you want but it's considered best practice to omit aliases and use full cmdlet names in scripts and modules, so I've replaced ? with Where-Object.
Note that you can invoke the function/cmdlet (the difference is minimal for PowerShell-defined cmdlets) with or without the parameter, as when you don't define a positional order, the order is automatically determined in the order of declaration:
# Either will work
JobCountStatus -Status Running
JobCountStatus Running
Here is some more documentation you may find useful:
About Functions
About Advanced Functions
#Ash's answer gives some more advanced examples of what you can do with param() which are mentioned in the the Advanced Functions link above. You cannot use advanced parameter attributes with the simple function syntax I mentioned in the first bullet point.

PowerShell adds int indexes of items when returning object

I've seen a lot of similar questions on here but none of the solutions are working for me.
I have a function that looks like this
function Get-ManagedDevices {
# ...(params and stuff here)...
$batchResponse = Invoke-RestMethod #restParams
$responses = #{}
foreach ($response in $batchResponse.responses) {
$responses.Add($response.id, $response.body) > $null
}
return , $responses
}
that gets called like this
$data = Get-Devices -AccessToken $AccessToken -DeviceIds $DeviceIds
When I'm debugging inside of the function call I am getting what I expect when I look at the $response variable which is something like
PS> $responses
Name Value
---- -----
d1290207-c693-4663-88bf-58512… #{#odata.context=https://graph.microsoft.com/v1.0/$metadat…
7ad71b70-b992-490f-8822-1561a… #{#odata.context=https://graph.microsoft.com/v1.0/$metadat…
PS> $responses.count
1
However, when I try to use the $data variable from after the function call I get this
PS> $data
0
1
Name Value
---- -----
d1290207-c693-4663-88bf-58512… #{#odata.context=https://graph.microsoft.com/v1.0/$metadat…
7ad71b70-b992-490f-8822-1561a… #{#odata.context=https://graph.microsoft.com/v1.0/$metadat…
PS> $data.count
3
I'm so confused. I piped the addition of the variable to the hashtable to $null with >$null.
I added the unary operator with return like return , $responses which is what's recommended in the 'about_return' PowerShell documentation .
I tried omitting the return keyword, using an ArrayList instead, using an array instead, etc...
What on earth is going on and why can't I return a hashtable from this function?! I feel like I'm going crazy.
The about_Return Documentation makes no mention of returning a hash table directly from a function. The common problem your referring to is the ability to return an array in whole rather than iterated down the pipeline.
Its common and straight forward to return a hash table. At a glance your code could be:
function Get-ManagedDevices {
# ...(params and stuff here)...
$batchResponse = Invoke-RestMethod #restParams
$responses = #{}
foreach ($response in $batchResponse.responses) {
$responses.Add($response.id, $response.body)
}
return $responses
}
It shouldn't be more complicated that that. Remove the comma it has no effect when returning a hash. >$null has no effect on the HashTable .Add(...) method.
Given your results and the example, there may be some artifact from your earlier attempts using an ArrayList. The .Add(...) method on an ArrayList will emit the index # that it's adding to. When we forget to [void] or | Out-Null or $null = ...Add(...) this usually results in unwanted output polluting the pipeline. As you're getting the 2 index numbers then your data...

Declaring parameters does not work if anything is above param

I have a script with parameters. In order to ease the debug of the script I create a small function I found on the net to list all my variables. In order to do so, I start by getting all existing variables at the top of the script, then I create a function which compares recorded variables before and after getting parameters
Problem is when I put the $AutomaticVariables and the function before param declaration, PowerShell gives me the following error for any parameter where I set a default value. Is there anyway to workaround this … bug? If it's not a bug, why the hell this behavior. I don't see the point.
The assignment expression is not valid. The input to an assignment operator must be an object that is able to accept assignments, such as a
variable or a property.
# Array and function to debug script variable content
$AutomaticVariables = Get-Variable
function check_variables {
Compare-Object (Get-Variable) $AutomaticVariables -Property Name -PassThru |
Where -Property Name -ne "AutomaticVariables"
}
param(
[String]$hostname,
[String]$jobdesc,
[String]$type = "standard",
[String]$repo,
[String]$ocred,
[String]$site,
[String]$cred = "SRC-$($site)-adm",
[String]$sitetype,
[String]$room,
[String]$chsite = "chub"
)
# TEST - Display variables
check_variables
As mentioned in the comments, you should gather the variables you want to exclude in the calling scope:
Define function (could as well be a script), notice the $DebugFunc parameter I've added at the end:
function Do-Stuff
{
param(
[String]$hostname,
[String]$jobdesc,
[String]$type = "standard",
[String]$repo,
[String]$ocred,
[String]$site,
[String]$cred = "SRC-$($site)-adm",
[String]$sitetype,
[String]$room,
[String]$chsite = "chub",
[scriptblock]$DebugFunc
)
if($PSBoundParameters.ContainsKey('DebugFunc')){
. $DebugFunc
}
}
Now, gather the variables and define your function, then inject it into Do-Stuff:
# Array and function to debug script variable content
$AutomaticVariables = Get-Variable
function check_variables {
Compare-Object (Get-Variable) $AutomaticVariables -Property Name -PassThru | Where -Property Name -ne "AutomaticVariables"
}
Do-Stuff -DebugFunc $Function:check_variables
It's not a bug. The param section defines the input parameter of your script thus has to be the first statement (same as with functions). There is no need to perform any action before the param block.
If you explain what you want to achieve with your check_variables (not what it does). We probably can show you how to do it right.

How do I pass a collection of object types to a function as a parameter in PowerShell?

Here is my code:
function Get-OSInfo {
param([string]$Computer)
$OS = gwmi -class Win32_OperatingSystem -computer $Computer
$OS | Add-Member –MemberType NoteProperty –Name OSType –Value ""
$OS.OSType = Get-OSType -Input $OS
write $OS
}
function Get-OSType {
param([?]$Input)
if ($Input.ProductType -eq 1) {
write "Client OS"
}
}
$blah = Get-OSInfo -Computer mypc
$blah | fl *
I realize that this could be done with a single function (or in the body of the script itself), but I have simplified the functions to highlight the trouble I'm having. What I want to do is pass the gwmi dataset from the Get-OSInfo function as a parameter variable in the Get-OSType so I can reference all of its properties in the second function without passing them individually from the first. Clear as mud?
I have tried multiple parameter accelerator types, [ref], [array], [object[]], etc., but I haven't found anything that works as a parameter. The only thing that has proven to work is to change the second function to use args[0] for accepting input, but that is not as clean as using parameters, and since it works, I can't help but think there is a parameter that should work as well.
Avoid using $input as that has special meaning in functions (representing pipeline input). Just rename the parameter to something like $OS.
The docs (man about_automatic_variables) on $input say:
Contains an enumerator that enumerates all input that is
passed to a function. The $input variable is available only to
functions and script blocks (which are unnamed functions). In the
Process block of a function, the $input variable enumerates the
object that is currently in the pipeline. When the Process block
completes, there are no objects left in the pipeline, so the $input
variable enumerates an empty collection. If the function does not
have a Process block, then in the End block, the $input variable
enumerates the collection of all input to the function.