PowerShell:使用 PS 5 类 时无法找到类型

PowerShell: Unable to find type when using PS 5 classes

我在 PS 中使用 类 和 WinSCP PowerShell 程序集。在其中一种方法中,我使用了 WinSCP 中的各种类型。

只要我已经添加了程序集,这就可以正常工作 - 但是,由于 PowerShell 在使用 类 时读取脚本的方式(我假设?),在程序集可能被抛出之前会抛出一个错误已加载。

事实上,即使我在顶部放置一个Write-Host,它也不会加载。

在解析文件的其余部分之前,是否有任何方法可以强制某些内容 运行?

Transfer() {
    $this.Logger = [Logger]::new()
    try {

        Add-Type -Path $this.Paths.WinSCP            
        $ConnectionType = $this.FtpSettings.Protocol.ToString()
        $SessionOptions = New-Object WinSCP.SessionOptions -Property @{
            Protocol = [WinSCP.Protocol]::$ConnectionType
            HostName = $this.FtpSettings.Server
            UserName = $this.FtpSettings.Username
            Password = $this.FtpSettings.Password
        }

导致这样的错误:

Protocol = [WinSCP.Protocol]::$ConnectionType
Unable to find type [WinSCP.Protocol].

但是我在哪里加载程序集并不重要。即使我将 Add-Type cmdlet 放在最顶行并直接指向 WinSCPnet.dll,它也不会加载 - 它似乎会在 运行 之前检测到缺失的类型。

虽然它本身不是解决方案,但我解决了它。但是,我会保留这个问题,因为它仍然存在

我没有使用 WinSCP 类型,而是使用了字符串。看到我已经有了与 WinSCP.Protocol

相同的枚举
Enum Protocols {
    Sftp
    Ftp
    Ftps
}

并在 FtpSettings 中设置协议

$FtpSettings.Protocol = [Protocols]::Sftp

我可以这样设置协议

$SessionOptions = New-Object WinSCP.SessionOptions -Property @{
            Protocol = $this.FtpSettings.Protocol.ToString()
            HostName = $this.FtpSettings.Server
            UserName = $this.FtpSettings.Username
            Password = $this.FtpSettings.Password
        }

我在 [WinSCP.TransferMode]

上使用了类似的
$TransferOptions.TransferMode = "Binary" #[WinSCP.TransferMode]::Binary

如您所见,PowerShell 拒绝 运行 包含 class 引用当时不可用(尚未加载)类型的定义的脚本 - 脚本解析阶段失败。

  • 从 PSv5.1 开始,即使脚本顶部的 using assembly 语句在这种情况下也无济于事,因为在您的情况下,类型是在PS class 定义 - 这个may get fixed in PowerShell Core, however; the required work, along with other class-related issues, is being tracked in GitHub issue #6652.

正确的解决方案是 创建一个脚本 模块 (*.psm1),其关联的清单 (*.psd1) 声明程序集包含引用的类型 先决条件,通过 RequiredAssemblies 键。

如果无法使用模块,请参阅底部的替代解决方案。

这是简化的演练

创建测试模块tm如下:

  • 创建模块文件夹 ./tm 并在其中显示 (*.psd1):

      # Create module folder (remove a preexisting ./tm folder if this fails).
      $null = New-Item -Type Directory -ErrorAction Stop ./tm
    
      # Create manifest file that declares the WinSCP assembly a prerequisite.
      # Modify the path to the assembly as needed; you may specify a relative path, but
      # note that the path must not contain variable references (e.g., $HOME).
      New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 `
        -RequiredAssemblies C:\path\to\WinSCPnet.dll
    
  • 在模块文件夹中创建脚本模块文件(*.psm1):

使用您的 class 定义创建文件 ./tm/tm.psm1;例如:

    class Foo {
      # As a simple example, return the full name of the WinSCP type.
      [string] Bar() {
        return [WinSCP.Protocol].FullName
      }
    }

注意:在现实世界中,模块通常放置在$env:PSMODULEPATH中定义的标准位置之一,这样模块只能通过name引用,无需指定(相对)路径。

使用模块:

PS> using module ./tm; [Foo]::new().Bar()
WinSCP.Protocol

using module 语句导入模块并且 - 不像 Import-Module - 还使模块中定义的 class 可用于当前会话。

由于模块清单中的 RequiredAssemblies 键,导入模块后隐式加载了 WinSCP 程序集,实例化 class Foo 引用了程序集的类型,成功了。


如果您需要动态确定依赖程序集的路径以便加载它甚至临时编译一个(在这种情况下使用 RequiredAssemblies 清单条目不是一种选择), 应该 能够使用 [=47 中推荐的方法=] - 即,使用指向 *.ps1 脚本的 ScriptsToProcess 清单条目调用 Add-Type 动态加载依赖程序集 before 脚本模块 (*.psm1) 已加载 - 但此 从 PowerShell 7.2.0-preview.9 开始实际上不起作用:而 [=85= *.psm1 文件中 class 的 ]definition 依赖依赖程序集的类型成功,调用者没有看到 class 直到带有 using module ./tm 语句的脚本执行了 时间:

  • 创建示例模块:
# Create module folder (remove a preexisting ./tm folder if this fails).
$null = New-Item -Type Directory -ErrorAction Stop ./tm

# Create a helper script that loads the dependent
# assembly.
# In this simple example, the assembly is created dynamically,
# with a type [demo.FooHelper]
@'
Add-Type @"
namespace demo {
  public class FooHelper {
  }
}
"@
'@ > ./tm/loadAssemblies.ps1

# Create the root script module.
# Note how the [Foo] class definition references the
# [demo.FooHelper] type created in the loadAssemblies.ps1 script.
@'
class Foo {
  # Simply return the full name of the dependent type.
  [string] Bar() {
    return [demo.FooHelper].FullName
  }
}
'@ > ./tm/tm.psm1

# Create the manifest file, designating loadAssemblies.ps1
# as the script to run (in the caller's scope) before the
# root module is parsed.
New-ModuleManifest ./tm/tm.psd1 -RootModule tm.psm1 -ScriptsToProcess loadAssemblies.ps1
  • 现在,从 PowerShell 7.2.0-preview.9 开始,尝试 使用 模块的 [Foo] class 仅在调用 using module ./tm twice - 你不能在 single 脚本中做到这一点,使这种方法暂时无用:
# As of PowerShell 7.2.0-preview.9:
# !! First attempt FAILS:
PS> using module ./tm; [Foo]::new().Bar()
InvalidOperation: Unable to find type [Foo]

# Second attempt: OK
PS> using module ./tm; [Foo]::new().Bar()
demo.FooHelper

问题是一个已知问题,事实证明,它可以追溯到 2017 年 - 请参阅 GitHub issue #2962


如果您的用例不允许使用 模块:

  • 在紧要关头,您可以使用 Invoke-Expression,但请注意,为了稳健性,通常最好避免使用 Invoke-Expression避免安全风险[1] .
# Adjust this path as needed.
Add-Type -LiteralPath C:\path\to\WinSCPnet.dll

# By placing the class definition in a string that is invoked at *runtime*
# via Invoke-Expression, *after* the WinSCP assembly has been loaded, the
# class definition succeeds.
Invoke-Expression @'
class Foo {
  # Simply return the full name of the WinSCP type.
  [string] Bar() {
    return [WinSCP.Protocol].FullName
  }
}
'@

[Foo]::new().Bar()
  • 或者,使用两个脚本方法
    • 加载依赖程序集的主脚本,
    • 然后点源第二个脚本,其中包含 class 依赖于依赖程序集类型的定义。

此方法在 中进行了演示。


[1] 在 这个 案例中这不是问题,但一般来说,鉴于 Invoke-Expression 可以调用 any 命令存储在字符串中,将其应用于字符串 不完全在您的控制下 可能会导致执行恶意命令 - 请参阅 了解更多信息。 此警告类似地适用于其他语言,例如 Bash 的内置 eval 命令。

首先,我会推荐 mklement0 的答案。

但是,您可以采取一些 运行 的方法来获得大致相同的效果,但工作量会少一些,这对较小的项目或早期阶段很有帮助。

仅 .在您的代码中获取另一个 ps1 文件,其中包含您的 classes 在您加载引用的程序集后引用尚未加载的库。

##########
MyClasses.ps1

Class myClass
{
     [3rdParty.Fancy.Object] $MyFancyObject
}

然后您可以使用 .

从您的主脚本调用您的自定义 class 库
#######
MyMainScriptFile.ps1

#Load fancy object's library
Import-Module Fancy.Module #If it's in a module
Add-Type -Path "c:\Path\To\FancyLibrary.dll" #if it's in a dll you have to reference

. C:\Path\to\MyClasses.ps1

原始解析将通过集合,脚本将启动,您的引用将被添加,然后随着脚本的继续,.源文件将被读取和解析,添加您的自定义 classes 没有问题,因为在解析代码时它们的参考库已在内存中。

制作和使用具有适当清单的模块仍然要好得多,但这会很容易,而且非常容易记住和使用。

另一种解决方案是将您的 Add-Type 逻辑放入单独的 .ps1 文件(将其命名为 AssemblyBootStrap.ps1 或其他名称),然后将其添加到 ScriptsToProcess 部分你的模块清单。 ScriptsToProcess 在根脚本模块 (*.psm1) 之前运行,程序集将在 class 定义查找它们时加载。