StanTastic
StanTastic

Reputation: 350

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?

Upvotes: 2

Views: 2039

Answers (1)

zett42
zett42

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

Related Questions