2

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?

3
  • 1
    Without a concrete example this is difficult to answer but you might just share a single variable (e.g. $Config) and put your variable in the properties of the variable (e.g. $Config.CurrentUser). Commented Dec 16, 2020 at 21:58
  • Question is too broad. Unless you have a specific problem with code, you will propably be better off at codereview. Try to refactor one of the smaller functions and present your result at codereview then. Commented Dec 16, 2020 at 22:29
  • There you go, added a bit of code. Sorry, I thought what I'm trying to convene is clear enough :-) Commented Dec 17, 2020 at 8:33

1 Answer 1

4

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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thanks for detailed answers! In fact, all I need to pass are constants (variables that are init from config file once and then unchanged), so scoping them into module sounds like a solution to me :-)

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.