Powershell calling function with argument as string - powershell

I am trying to call Get-ChildItem function from custom function. The problem is the arguments to the function can be dynamic.
function Test {
Get-ChildItem $Args
}
When I try
Test .\ //this works as Path is taken as default argument value
Test .\ -Force //this doesn't work as expected as it still tries to consider entire thing as Path
Test -Path .\ -Force //same error
How to wrap around function and pass the arguments as it's?

$args is an array of arguments, and passing it to the Get-ChildItem wouldn't work, as you've noticed. The PowerShell-way for this would be the Proxy Command.
For a quick-and-dirty hack, you can use Invoke-Expression:
function Test {
Invoke-Expression "Get-ChildItem $Args"
}

Invoke-Expression will be difficult to work with because what's been passed as strings will need quoting all over again when expressed in a string. ProxyCommand is the better way as beatcracker has suggested.
There are a few alternatives for fun and interest. You might splat PSBoundParameters, but you will need to declare the parameters you expect to pass.
This is an incomplete example in that it will easily get upset if there are duplicate parameters (including common parameters if you set CmdletBinding on the function Test).
function Test {
dynamicparam {
$dynamicParams = New-Object Management.Automation.RuntimeDefinedParameterDictionary
foreach ($parameter in (Get-Command Microsoft.PowerShell.Management\Get-ChildItem).Parameters.Values) {
$runtimeParameter = New-Object System.Management.Automation.RuntimeDefinedParameter(
$parameter.Name,
$parameter.ParameterType,
$parameter.Attribtes
)
$dynamicParams.Add($parameter.Name, $runtimeParameter)
}
return $dynamicParams
}
end {
Get-ChildItem #psboundparameters
}
}

Related

What is the effect of "-ScriptBlock" in "New-Module -AsCustomObject"

I am new with powershell and I had been researching how to create custom objects.
With "New-Module" I always find results like "New-Module -AsCustomObject -ScriptBlock".
Like this for example:
$myObject = New-Module -AsCustomObject -ScriptBlock {
function myFunc(){
return "myFunc";
}
}
but when I try without "-ScriptBlock":
$myObject = New-Module -AsCustomObject {
function myFunc(){
return "myFunc";
}
}
it appears to have the same effect to me. I get a custom object with a function myFunc in both cases.
Am I missing something? Or actually it will have no difference in this particular case?
New-Module -AsCustomObject -ScriptBlock { function myFunc(){ return "myFunc"; } }
passes the script block { ... } as a named argument to the -ScriptBlock parameter. That is, the target parameter is explicitly named.
PowerShell commands can choose to alternatively support positional arguments, where the target parameter is not named, and instead the relative position among all unnamed arguments implies the target parameter.
The New-Module cmdlet's first positional parameter is indeed -ScriptBlock, which means that
-ScriptBlock before the { ... } block can be omitted here, which is why your two commands are equivalent:
# Same as above, except that -ScriptBlock is *implied*, positionally.
New-Module -AsCustomObject { function myFunc(){ return "myFunc"; } }
(Note that the -AsCustomObject switch parameter (flag) is by definition named, so it has no impact on positional parameter binding.)
You can learn which of a given command's parameters are positional by looking at its syntax diagrams, which you can obtain with New-Module -? or Get-Command New-Module -Syntax:
# Note: PowerShell commands can have multiple *parameter sets*, with
# distinct parameter combinations.
# Here, I've omitted the 2nd parameter set that doesn't apply to
# your call, because it doesn't use the -Name parameter.
PS> Get-Command -Syntax New-Module
New-Module [-ScriptBlock] <scriptblock> [-Function <string[]>] [-Cmdlet <string[]>] [-ReturnResult] [-AsCustomObject] [-ArgumentList <Object[]>] [<CommonParameters>]
The [...] around a parameter name tells you that a given parameter accepts a positional argument, and in the case at hand that only applies to -ScriptBlock, so it is the first and only positional parameter in that parameter set.
To learn more about:
how to read syntax diagrams, including a helper function that programmatically lists a command's positional parameters, see this answer
declaring your own positional parameters when authoring functions and scripts, see this answer.

How to check command-line parameter from a PowerShell module?

Is there a way to check if a command-line parameter was specified for a PowerShell script from a module (.psm1 file)? I do not need the value, just have to know if the parameter was specified. The $PSBoundParameters.ContainsKey method does not seem to work.
TestParam.psm1:
function Test-ScriptParameter {
[CmdletBinding()]
param ()
# This does not work (always returns false):
return $PSBoundParameters.ContainsKey('MyParam')
}
Export-ModuleMember -Function *
TestParam.ps1:
[CmdletBinding()]
param (
$MyParam= "Default"
)
$path = Join-Path (Split-Path -Path $PSCommandPath -Parent) 'TestParam.psm1'
Import-Module $path -ErrorAction Stop -Force
Test-ScriptParameter
This must return false:
PS>.\TestParam.ps1
This must return true:
PS>.\TestParam.psq -MyParam ""
This must return true:
PS>.\TestParam.ps1 -MyParam "Runtime"
It cannot be done like you are thinking about it. The PSBoundParameters variable is native to the cmdlet's execution and as such depends on the param block of the cmdlet's definition. So in your case, the Test-ScriptParameter is checking if it was invoked with the parameter MyParam but since it doesn't specify it, then it will be always false.
To achieve what I believe you want, you need to create a function that checks into a hash structure like the PSBoundParameters for a specific key. The key needs to be provided by name. But then a simple $PSBoundParameters.ContainsKey('MyParam') wherever you need it should suffice.
The problem with your code is that you are checking the $PSBoundParameters value of the Function itself, which has no parameters.
You could make the function work by sending the $PSBoundParameters variable from the script in to the function via a differently named parameter.
For example:
TestParam.psm1:
function Test-ScriptParameter ($BoundParameters) {
return $BoundParameters.ContainsKey('MyParam')
}
Export-ModuleMember -Function *
TestParam.ps1:
[CmdletBinding()]
param (
$MyParam = "Default"
)
$path = Join-Path (Split-Path -Path $PSCommandPath -Parent) 'TestParam.psm1'
Import-Module $path -ErrorAction Stop -Force
Test-ScriptParameter $PSBoundParameters

Default value of parameter is not used in function

I have a very basic PowerShell script:
Param(
[string]$MyWord
)
function myfunc([string] $MyWord) {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
This is how I execute it:
PS C:\> .\test.ps1 -MyWord 'hello'
hello
All fine. But I want to set a default value if -MyWord isn't specified.
I tried this:
Param(
[string]$MyWord='hi'
)
function myfunc([string] $MyWord) {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
But than the output of my script was just empty. It was printing nothing when I did not describe my parameter. (it only showed 'hello' if I specified the parameter).
I also tried:
Param(
[string]$MyWord
)
function myfunc([string] $MyWord) {
[string]$MyWord='hi'
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
But than the output was of course always 'hi' and never 'hello'. Even when I executed the script with the parameter -MyWord 'hello'
Can someone explaining what I'm doing wrong?
When I'm not using the function it is working as expected:
Param(
[string]$MyWord='hi'
)
Write-Host $MyWord
Output:
PS C:\> .\test.ps1 -MyWord 'hallo'
hallo
PS C:\> .\test.ps1
hi
Automatic variable $PSBoundParameters, as the name suggests, contains only bound parameters, where bound means that an actual value was supplied by the caller.
Therefore, a parameter default value does not qualify as binding the associated parameter, so $MyWord with its default value of 'hi' does not become part of $PSBoundParameters.
Note: Arguably, a parameter with a default value should also be considered bound (it is bound by its default value, as opposed to by a caller-supplied value). Either way, it would be convenient to have an automatic variable that includes default values too, so as to enable simple and comprehensive passing through of arguments. A suggestion has been submitted to the PowerShell repository as GitHub issue #3285.
Workarounds
The following solutions assume that you want to pass the default value through, and don't want to simply duplicate the default value in function myfunc (as demonstrated in Ansgar Wiecher's helpful answer), because that creates a maintenance burden.
Regarding function syntax: The following two forms are equivalent (in this case), though you may prefer the latter for consistency and readability.[1]
function myfunc([string] $MyWord = 'hi') { ... }
parameter declaration inside (...) after the function name.
function myfunc { param([string] $MyWord = 'hi') ... }
parameter declaration inside a param(...) block inside the function body.
A simple fix would be to add the default value explicitly to $PSBoundParameters:
Param(
[string]$MyWord = 'hi'
)
function myfunc ([string] $MyWord){
Write-Host "$MyWord"
}
# Add the $MyWord default value to PSBoundParameters.
# If $MyWord was actually bound, this is effectively a no-op.
$PSBoundParameters.MyWord = $MyWord
myfunc #PSBoundParameters
To achieve what you want generically, you must use reflection (introspection):
param(
[alias('foop')]
[string]$MyWord = 'hi'
)
function myfunc ([string] $MyWord) {
Write-Host "$MyWord"
}
# Add all unbound parameters that have default values.
foreach ($paramName in $MyInvocation.MyCommand.Parameters.Keys) {
if (-not $PSBoundParameters.ContainsKey($paramName)) {
$defaultVal = Get-Variable -Scope Local $paramName -ValueOnly
# A default value is identified by either being non-$null or
# by being a [switch] parameter that defaults to $true (which is bad practice).
if (-not ($null -eq $defaultVal -or ($defaultVal -is [switch] -and -not $defaultVal))) {
$PSBoundParameters[$paramName] = $defaultVal
}
}
}
myfunc #PSBoundParameters
[1] The param(...) form is required if you need to use the [CmdletBinding()] attribute with non-default values, as well as in scripts (.ps1). See this answer.
A parameter is bound only if you actually pass it a value, meaning that a parameter's default value does not show up in $PSBoundParameters. If you want to pass script parameters into a function, you must replicate the script parameter set in the function parameter set:
Param(
[string]$MyWord = 'hi'
)
function myfunc([string]$MyWord = 'hi') {
Write-Host "$MyWord"
}
myfunc #PSBoundParameters
Maintaining something like this is easier if you define both parameter sets the same way, though, so I'd put the function parameter definition in a Param() block as well:
Param(
[string]$MyWord = 'hi'
)
function myfunc {
Param(
[string]$MyWord = 'hi'
)
Write-Host "$MyWord"
}
If you want to use "Param" enclose it in the function like this:
function myfunc {
Param(
[string]$MyWord='hi'
)
Write-Host "$MyWord"
}
Very simple way is,
function myfunc([string]$MyWord = "hi") {
Write-Output $MyWord
}

Powershell accessing multiple parameters in a for loop

Hi I am very new to powershell and I am writing a script that accepts multiple parameters. These parameters are being accessed in a for loop inside the file.
It looks something like this
$numOfArgs = args.Length
for ($i=3; $i -le $numOfArgs; $i++)
{
write-host "folder: $args[$i]"
# does something with the arguments
}
However, the output gives me all the parameters as a whole instead of just one parameter specified in the array as an array element? Can someone tell me where is the mistake here? Thanks!
EDIT: Thanks Duncan to point this out, missing a $ in a variable.
Try this:
$numOfArgs = $args.Length
for ($i=3; $i -lt $numOfArgs; $i++)
{
write-host "folder: $($args[$i])"
# does something with the arguments
}
When placing a variable in a string, the variable is evaluated, not the entire expression. So by surrounding it with $() Powershell will evaluate the whole expression.
In other words, only $args was evaluated instead of $args[$i]
The preferred Powershell way is to use a bind parameter, like this;
param(
[Parameter(Mandatory=$true)][string[]]$Paths
)
# not sure why we're skipping some elements
$Paths | foreach-object { write-host "folder: $_" }
Which you can specify an array or arguments, like this;
.\myScript.ps1 -Paths c:\,c:\Users\,'c:\Program Files\'
This way it will work with -argument TAB completion and will even give you a brief usage using the get-help cmdlet.
get-help .\myscript.ps1
myScript.ps1 [-Paths] <string[]> [<CommonParameters>]

How do you support PowerShell's -WhatIf & -Confirm parameters in a Cmdlet that calls other Cmdlets?

I have a PowerShell script cmdlet that supports the -WhatIf & -Confirm parameters.
It does this by calling the $PSCmdlet.ShouldProcess() method before performing the change.
This works as expected.
The problem I have is that my Cmdlet is implemented by calling other Cmdlets and the -WhatIf or -Confirm parameters are not passed along to the Cmdlets I invoke.
How can I pass along the values of -WhatIf and -Confirm to the Cmdlets I call from my Cmdlet?
For example, if my Cmdlet is Stop-CompanyXyzServices and it uses Stop-Service to implement its action.
If -WhatIf is passed to Stop-CompanyXyzServices I want it to also be passed to Stop-Service.
Is this possible?
Passing parameters explicitly
You can pass the -WhatIf and -Confirm parameters with the $WhatIfPreference and $ConfirmPreference variables. The following example achieves this with parameter splatting:
if($ConfirmPreference -eq 'Low') {$conf = #{Confirm = $true}}
StopService MyService -WhatIf:([bool]$WhatIfPreference.IsPresent) #conf
$WhatIfPreference.IsPresent will be True if the -WhatIf switch is used on the containing function. Using the -Confirm switch on the containing function temporarily sets $ConfirmPreference to low.
Passing parameters implicitly
Since the -Confirm and -WhatIf temporarily set the $ConfirmPreference and $WhatIfPreference variables automatically, is it even necessary to pass them?
Consider the example:
function ShouldTestCallee {
[cmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')]
param($test)
$PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Confirm?")
}
function ShouldTestCaller {
[cmdletBinding(SupportsShouldProcess=$true)]
param($test)
ShouldTestCallee
}
$ConfirmPreference = 'High'
ShouldTestCaller
ShouldTestCaller -Confirm
ShouldTestCaller results in True from ShouldProcess()
ShouldTestCaller -Confirm results in an confirm prompt even though I didn't pass the switch.
Edit
#manojlds answer made me realize that my solution was always setting $ConfirmPreference to 'Low' or 'High'. I have updated my code to only set the -Confirm switch if the confirm preference is 'Low'.
After some googling I came up with a good solution for passing common parameters along to called commands. You can use the # splatting operator to pass along all the parameters that were passed to your command. For example, if
Start-Service -Name ServiceAbc #PSBoundParameters
is in the body of your script powershell will pass all the parameters that were passed to your script to the Start-Service command. The only problem is that if your script contains say a -Name parameter it will be passed too and PowerShell will complain that you included the -Name parameter twice. I wrote the following function to copy all the common parameters to a new dictionary and then I splat that.
function Select-BoundCommonParameters
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true)]
$BoundParameters
)
begin
{
$boundCommonParameters = New-Object -TypeName 'System.Collections.Generic.Dictionary[string, [Object]]'
}
process
{
$BoundParameters.GetEnumerator() |
Where-Object { $_.Key -match 'Debug|ErrorAction|ErrorVariable|WarningAction|WarningVariable|Verbose' } |
ForEach-Object { $boundCommonParameters.Add($_.Key, $_.Value) }
$boundCommonParameters
}
}
The end result is you pass parameters like -Verbose along to the commands called in your script and they honor the callers intention.
Here is a complete solution based on #Rynant and #Shay Levy's answers:
function Stop-CompanyXyzServices
{
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='Medium')]
Param(
[Parameter(
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[string]$Name
)
process
{
if($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Stop XYZ services '$Name'")){
ActualCmdletProcess
}
if([bool]$WhatIfPreference.IsPresent){
ActualCmdletProcess
}
}
}
function ActualCmdletProcess{
# add here the actual logic of your cmdlet, and any call to other cmdlets
Stop-Service $name -WhatIf:([bool]$WhatIfPreference.IsPresent) -Confirm:("Low","Medium" -contains $ConfirmPreference)
}
We have to see if -WhatIf is passed separately as well so that the whatif can be passed on to the individual cmdlets. ActualCmdletProcess is basically a refactoring so that you don't call the same set of commands again just for the WhatIf. Hope this helps someone.
Updated per #manojlds comment
Cast $WhatIf and $Confirm to Boolean and pass the values to the the underlying cmdlet:
function Stop-CompanyXyzServices
{
[CmdletBinding(SupportsShouldProcess=$true,ConfirmImpact='High')]
Param(
[Parameter(
Position=0,
ValueFromPipeline=$true,
ValueFromPipelineByPropertyName=$true
)]
[string]$Name
)
process
{
if($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Stop service '$Name'"))
{
Stop-Service $name -WhatIf:([bool]$WhatIf) -Confirm:([bool]$confirm)
}
}
}
Just so you wont get run around the block for hours by this question and the answers here, I would suggest that you read this article instead:
https://powershellexplained.com/2020-03-15-Powershell-shouldprocess-whatif-confirm-shouldcontinue-everything/#suppressing-nested-confirm-prompts
The answers presented here does not work for many cases and I see a danger in people implementing the answers here, without understanding the fundamentals.
Here is how a hacked it to work across scriptmodules: