Reputation: 350
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?
Upvotes: 2
Views: 2039
Reputation: 27796
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
.
Upvotes: 4