Search code examples
powershellclasspowershell-module

PowerShell dot source within a dot sourced file - import classes


My project is structured like this:

MyScript.ps1
classes\
    Car.ps1
    Tesla.ps1

Car.ps1 is the base class of Tesla.ps1. I attempt to define Tesla like this in Tesla.ps1:

. "$PSScriptRoot\Car.ps1"

class Tesla : Car
{
}

MyScript.ps1 needs to use the Tesla class, but shouldn't need to know that it inherits from Car.

. "$PSScriptRoot\classes\Tesla.ps1"

$tesla = [Tesla]::new()

Dot sourcing to classes\Tesla.ps1 works fine, but this error is thrown from the Tesla file:

Unable to find type [Car]

If I import all the files in the correct order in MyScript.ps1, it works fine. Example:

. "$PSScriptRoot\classes\Car.ps1"
. "$PSScriptRoot\classes\Tesla.ps1"

$tesla = [Tesla]::new()

This is cumbersome, especially as the complexity grows. Am I dot sourcing incorrectly? Is there a better way to import a custom PowerShell class using a relative path that isn't in the PSModulePath?


Solution

  • PetSerAl, as countless times before, has provided the crucial pointers in a terse comment on the question:

    Due to your use of classes, you must use modules and import them with using module statements in order to use them.

    Specifically, in order to reference a type (class) in a class definition, that type must be known to PowerShell at parse time.

    In your example, in order to derive Tesla from class Car, type Car must be known to PowerShell when the script is parsed, before execution begins - that's why it is too late to try to import Car by dot-sourcing its containing script (. "$PSScriptRoot\Car.ps1")

    PowerShell knows about referenced types at parse time in one of two ways:

    • If the type is already loaded into the current PowerShell session

    • Via a using module statement that references a module in which the type (class) is defined (note that there's also a using assembly statement for loading types from DLLs, and using namespace to enable referring to types by their mere names).

      • Note that the related Import-Module cmdlet does not work in this case, because it executes at runtime.

    Therefore, as PetSerAl suggests:

    • Store your classes in module files (stand-alone *.psm1 files in the simplest case)

    • Then use using module to have the Tesla module import the Car module and the MyScript.ps1 script import the Tesla module, using paths that are relative to the enclosing script / module's location.

    MyScript.ps1
    classes\
        Car.psm1    # Note the .psm1 extension
        Tesla.psm1  # Ditto
    

    Car.psm1:

    class Car {}
    

    Tesla.psm1:

    using module .\Car.psm1
    
    class Tesla : Car {}
    

    MyScript.ps1:

    using module .\classes\Tesla.psm1
    
    $tesla = [Tesla]::new()