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

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

Related

Is it possible to implement IValidateSetValuesGenerator for script parameter validation?

In the example in the powershell documentation you need to implement a class, like:
Class SoundNames : System.Management.Automation.IValidateSetValuesGenerator { ... }
But, how can you do this in a script based cmdlet? (See "Parameters in scripts" in about_Scripts).
In a script cmdlet the first line must be param( ... ).
Unless "SoundNames" is already defined, then it fails with Unable to find type [SoundNames]
If this type is already defined in the powershell session, this works ... but I'd like a stand-alone script that doesn't require the user do something first to define this type (e.g. dot-sourcing some other file).
I want to define a parameter that accepts only *.txt filenames (for existing files in a specific directory).
You can't do this in a (stand-alone) script, for the reason you state yourself:
The param block (possibly preceded by a [CmdletBinding()] attribute, must be at the very start of the file (leaving using statements aside).
This precludes defining a custom class that implements the IValidateSetValuesGenerator interface for use in your param block.
To work around this limitation, use a [ValidateScript()] attribute:
param(
[ValidateScript({
if (-not (Get-Item -ErrorAction Ignore "$_.txt")) {
throw "$_ is not the (base) name of a *.txt file in the current dir."
}
return $true
})]
[string] $FileName
)
Note that this doesn't give you tab-completion the way that IValidateSetValuesGenerator-based validation would automatically give you.
To also provide tab-completion, additionally use an [ArgumentCompleter()] attribute:
param(
[ValidateScript({
if (-not (Get-Item -ErrorAction Ignore "$_.txt")) {
throw "$_ is not the (base) name of a *.txt file in the current dir."
}
return $true
})]
[ArgumentCompleter({
param($cmd, $param, $wordToComplete)
(Get-Item "$wordToComplete*.txt").BaseName
})]
[string] $FileName
)

Pass script variables to Powershell module

I guess this was answered a thousand times, but for love of all I can't find good (matching) answer to my problem.
I have a large PS script with a good dozen global variables that are used in various functions. For variables like $homedir I did not bother to include them in invocation of each function, since virtually all of them need to use it.
But now I need to write another script, and reuse ~80% of functions. Obviously I don't want to just copy&paste, since maintenance would be nightmare, so I told myself "let's finally learn to write PS modules" - basically cutting functions from the script and pasting them into module.
So far so good, but almost immediately I discovered that variables from the script are not passed to the module. I am not surprised by this, I just don't know what is the best practice to refactor my code (provided I don't really want to create functions with 10+ variables as input).
For now, I started adding necessary variables to each function, but the effect is that while before "working directory" variable was a given, now it has to be declared for each function. Hardly nice clean code there.
Is there a way to "init" a module, populating it with global variables?
EDIT:
Let's say I have a following code within a single script:
Function New-WorkDir {
if (Test-Path "$workDirectory") {
$null = Remove-Item -Recurse -Force $workDirectory
}
$null = New-Item -ItemType "directory" -Path "$workDirectory"
}
Function Set-Stage {
$null = New-Item -ItemType "file" -Force -Value $stage -Path "$workDirectory" -Name "ExecutionStage.txt"
}
$workDirectory = "C:\Temp"
New-WorkDir
$stage = "1"
Set-Stage
Now, I want to split the function be in a separate module. In order for this to work, I need to add function parameters explicitly, like so:
Function New-WorkDir {
Param(
[Parameter(Mandatory = $True, Position = 0)] [String] $WorkDirectory
)
if (Test-Path "$WorkDirectory") {
$null = Remove-Item -Recurse -Force $WorkDirectory
}
$null = New-Item -ItemType "directory" -Path "$WorkDirectory"
}
Function Set-Stage {
Param(
[Parameter(Mandatory = $True, Position = 0)] [String] $Stage,
[Parameter(Mandatory = $True, Position = 1)] [String] $WorkDirectory
)
$null = New-Item -ItemType "file" -Force -Value $Stage -Path "$WorkDirectory" -Name "ExecutionStage.txt"
}
And the main script becomes:
Import-Module new-module.psm1
$workDirectory = "C:\Temp"
New-WorkDir -WorkDirectory $workDirectory
$stage = "1"
Set-Stage -WorkDirectory $workDirectory -Stage $stage
So far, so good. My problem is that since virtually every function uses "$workDirectory", now I need to add an additional parameter to each of those functions, and what's worse - I need to add it everywhere in the code, severely impacting readability.
I was hoping that maybe there's some mechanism to "init" internal module variable, something like (pseudocode):
Import-Module new-module.psm1
Set-Variables -module new-module -WorkDirectory $workDirectory
$workDirectory = "C:\Temp"
New-WorkDir
$stage = "1"
Set-Stage -Stage $stage
Help, please?
While modules have state and you could set module variables through module functions that assign to $script:YourVariableName, I wouldn't recommend doing so. Although they are scoped to the module, module variables still smell like an anti-pattern similar to global variables. Having functions depend on state outside of the function makes maintenance and testing much harder. I recommend to use module variables only for constants.
A better pattern is to pass everything to the module functions via parameters. If it turns out that your functions have many common parameters, you could pass these via a single object parameter.
Say you have:
Function MyModuleFun1( $commonParam1, $commonParam2, $foo ) {
Write-Output $commonParam1 $commonParam2 $foo
}
Function MyModuleFun2( $commonParam1, $commonParam2, $bar ) {
Write-Output $commonParam1 $commonParam2 $bar
}
This could be refactored to...
Function MyModuleFun1( $commonParams, $foo ) {
Write-Output $commonParams.param1 $commonParams.param2 $foo
}
Function MyModuleFun2( $commonParams, $bar ) {
Write-Output $commonParams.param1 $commonParams.param2 $bar
}
... and called like this:
$common = [PSCustomObject]#{ param1 = 42; param2 = 21 }
MyModuleFun1 -commonParams $common -foo theFoo
MyModuleFun2 -commonParams $common -bar theBar
In this example the common parameter values are the same for all function calls, so we could use $PSDefaultParameterValues to pass them implicitly:
$PSDefaultParameterValues = #{
'MyModule*:commonParams' = [PSCustomObject]#{ param1 = 42; param2 = 21 }
}
MyModuleFun1 -foo theFoo
MyModuleFun2 -bar theBar
It is advisable to use a common prefix for all your module functions, to make sure that your $PSDefaultParameterValues don't leak into other functions. In my example all module functions start with prefix 'MyModule', so I could write MyModule*:commonParams to pass the common parameter values only to functions that start with 'MyModule' prefix.
For added type safety you could create a class for the common parameters within your module...
class MyModuleCommonParams {
[int] $param1
[String] $param2 = 'DefaultValue'
}
... and change your function signatures like this:
Function MyModuleFun1( [MyModuleCommonParams] $commonParams, $foo )
The function calls can stay the same, but now the function will check that only variables defined in the class, that have correct1 type, are passed:
$common = [PSCustomObject]#{ param1 = 42; xyz = 21 }
MyModuleFun1 -commonParams $common -foo theFoo
PowerShell will report an error, because the member xyz is not defined in class MyModuleCommonParams.
1 Actually it is sufficient that the argument type is convertible to the class member type. You could pass the string '42' to $param1, because it will be automatically converted to int.

How can you cascade a configuration file to submodules in powershell? [duplicate]

In the below sample module file, is there a way to pass the myvar value while importing the module.
For example,
import-module -name .\test.psm1 -?? pass a parameter? e.g value of myvar
#test.psm1
$script:myvar = "hi"
function Show-MyVar {Write-Host $script:myvar}
function Set-MyVar ($Value) {$script:myvar = $Value}
#end test.psm1
(This snippet was copied from another question.)
This worked for me:
You can use the –ArgumentList parameter of the import-module cmdlet to pass arguments when loading a module.
You should use a param block in your module to define your parameters:
param(
[parameter(Position=0,Mandatory=$false)][boolean]$BeQuiet=$true,
[parameter(Position=1,Mandatory=$false)][string]$URL
)
Then call the import-module cmdlet like this:
import-module .\myModule.psm1 -ArgumentList $True,'http://www.microsoft.com'
As may have already noticed, you can only supply values (no names) to –ArgumentList. So you should define you parameters carefully with the position argument.
Reference
The -ArgumentList parameter of Import-Module unfortunately does not accept a [hashtable] or [psobject] or something. A list with fixed postitions is way too static for my liking so I prefer to use a single [hashtable]-argument which has to be "manually dispatched" like this:
param( [parameter(Mandatory=$false)][hashtable]$passedVariables )
# this module uses the following variables that need to be set and passed as [hashtable]:
# BeQuiet, URL, LotsaMore...
$passedVariables.GetEnumerator() |
ForEach-Object { Set-Variable -Name $_.Key -Value $_.Value }
...
The importing module or script does something like this:
...
# variables have been defined at this point
$variablesToPass = #{}
'BeQuiet,URL,LotsaMore' -split ',' |
ForEach-Object { $variablesToPass[$_] = Get-Variable $_ -ValueOnly }
Import-Module TheModule -ArgumentList $variablesToPass
The above code uses the same names in both modules but you could of course easily map the variable names of the importing script arbitrarily to the names that are used in the imported module.

Powershell calling function with argument as string

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
}
}

Get-ChildItem recurse as a parameter in PowerShell

I am looking to create a function that could toggle the ability to recurse in cmdlet Get-ChildItem.
As a very basic example:
...
param
(
[string] $sourceDirectory = ".",
[string] $fileTypeFilter = "*.log",
[boolean] $recurse = $true
)
Get-ChildItem $sourceDirectory -recurse -filter $fileTypeFilter |
...
How does one conditionally add the -recurse flag to Get-ChildItem without having to resort to some if/else statement?
I thought perhaps one could just substitute the -recurse in the Get-ChildItem statement with a $recurseText parameter (set to "-recurse" if $recurse were true), but that does not seem to work.
A couple of things here. First, you don't want to use [boolean] for the type of the recurse parameter. That requires that you pass an argument for the Recurse parameter on your script e.g. -Recurse $true. What you want is a [switch] parameter as shown below. Also, when you forward the switch value to the -Recurse parameter on Get-ChildItem use a : as shown below:
param (
[string] $sourceDirectory = ".",
[string] $fileTypeFilter = "*.log",
[switch] $recurse
)
get-childitem $sourceDirectory -recurse:$recurse -filter $fileTypeFilter | ...
The PowerShell V1 way to approach this is to use the method described in the other answers (-recurse:$recurse), but in V2 there is a new mechanism called splatting that can make it easier to pass the arguments from one function to another.
Splatting will allow you to pass a dictionary or list of arguments to a PowerShell function. Here's a quick example.
$Parameters = #{
Path=$home
Recurse=$true
}
Get-ChildItem #Parameters
Inside of each function or script you can use $psBoundParameters to get the currently bound parameters. By adding or removing items to $psBoundParameters, it's easy to take your current function and call a cmdlet with some the functions' arguments.
I hope this helps.
I asked a similar question before... My accepted answer was basically that in v1 of PowerShell, just passing the named parameter through like:
get-childitem $sourceDirectory -recurse:$recurse -filter ...
Here's a good list of the types of parameters you can use:
param(
[string] $optionalparam1, #an optional parameter with no default value
[string] $optionalparam2 = "default", #an optional parameter with a default value
[string] $requiredparam = $(throw ""requiredparam required."), #throw exception if no value provided
[string] $user = $(Read-Host -prompt "User"), #prompt user for value if none provided
[switch] $switchparam; #an optional "switch parameter" (ie, a flag)
)
From here