Home > Software engineering >  Error when running a dot-sourced script after executing it
Error when running a dot-sourced script after executing it

Time:12-22

I've got very weird behaviour which I suppose is related to dot-sourcing somehow, but I cannot wrap my head around it. Here's what I have:

A script sourced.ps1 which contains the two functions and a class:

class MyData {
    [string] $Name
}

function withClass() {
    $initialData = @{
        Name1 = "1";
        Name2 = "2";
    }
    $list = New-Object Collections.Generic.List[MyData]
    foreach ($item in $initialData.Keys) {
        $d = [MyData]::new()
        $d.Name = $item
        $list.Add($d)
    }
}

function withString() {
    $initialData = @{
        Name1 = "1";
        Name2 = "2";
    }
    $list = New-Object Collections.Generic.List[string]
    foreach ($item in $initialData.Keys) {
        $list.Add($item)
    }
}

I also have a script caller.ps1 which dot-sources the one above and calls the function:

$ErrorActionPreference = 'Stop'

. ".\sourced.ps1"
withClass

I then call the caller.ps1 by executing .\caller.ps1 in the shell (Win terminal with PS Core).

Here's the behaviour I cannot explain: if I call .\caller.ps1, then .\sourced.ps1 and then caller.ps1 again, I get the error:

Line |
  14 |          $list.Add($d)
     |          ~~~~~~~~~~~~~
     | Cannot find an overload for "Add" and the argument count: "1".

However, if I change the caller.ps1 to call withString function instead, everything works fine no matter how many times I call caller.ps1 and sourced.ps1.

Furthermore, if I first call caller.ps1 with withString, then change it to withClass, there is no error whatsoever.

I suppose using modules would be more correct, but I'm interested in the reason for such weird behaviour in the first place.

CodePudding user response:

Written as of PowerShell 7.2.1

  • A given script file that is both dot-sourced and directly executed (in either order, irrespective of how often) creates successive versions of the class definitions in it - these are distinct .NET types, even though their structure is identical. Arguably, there's no good reason to do this, and the behavior may be a bug.

  • These versions, which have the same full name (PowerShell class definitions created in the top-level scope of scripts have only a name, no namespace) but are housed in different dynamic (in-memory) assemblies that differ by the last component of their version number, shadow each other, and which one is effect depends on the context:

    • Other scripts that dot-source such a script consistently see the new version.
    • Inside the script itself, irrespective of whether it is itself executed directly or dot-sourced:
      • In PowerShell code, the original version stays in effect.
      • Inside binary cmdlets, notably New-Object, the new version takes effect.
      • If you mix these two ways to access the class inside the script, type mismatches can occur, which is what happened in your case - see sample code below.
  • While you can technically avoid such errors by consistently using ::new() or New-Object to reference the class, it is better to avoid performing both direct execution and dot-sourcing of script files that contain class definitions to begin with.

Sample code:

  • Save the code to a script file, say, demo.ps1

  • Execute it twice.

    • First, by direct execution: .\demo.ps1
    • Then, via dot-sourcing: . .\demo.ps1
  • The type-mismatch error that you saw will occur during that second execution.

    • Note: The error message, Cannot find an overload for "Add" and the argument count: "1", is a bit obscure; what it is trying to express that is that the .Add() method cannot be called with the argument of the given type, because it expects an instance of the new version of [MyData], whereas ::new() created an instance of the original version.
# demo.ps1

# Define a class 
class MyData { }

# Use New-Object to instantiate a generic list based on that class.
# This passes the type name as a *string*, and instantiation of the 
# type happens *inside the cmdlet*.
# On the second execution, this will use the *new* [MyData] version.
Write-Verbose -Verbose 'Constructing list via New-Object'
$list = New-Object System.Collections.Generic.List[MyData]

# Use ::new() to create an instance of [MyData]
# Even on the second execution this will use the *original* [MyData] version
$myDataInstance = [MyData]::new()

# Try to add the instance to the list.
# On the second execution this will *fail*, because the [MyData] used
# by the list and the one that $myDataInstance is an instance of differ.
$list.Add($myDataInstance)

Note that if you used $myDataInstance = New-Object MyData, the type mismatch would go away.

Similarly, it would also go away if you stuck with ::new() and also used it to instantiate the list: $list = [Collections.Generic.List[MyData]]::new()

  • Related