正如您所发现的,PowerShell 拒绝运行包含引用当时不可用(尚未加载)类型的类定义的脚本 - 脚本解析阶段失败。
正确的解决方案是创建一个脚本模块 (*.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):
使用您的类定义创建文件./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 不同 -
还使模块中定义的类对当前会话可用。
由于模块清单中的 RequiredAssemblies 键,导入模块隐式加载了 WinSCP 程序集,因此实例化引用程序集类型的类 Foo 成功。
如果您需要动态地确定依赖程序集的路径以便加载它,甚至临时编译一个(在这种情况下使用RequiredAssemblies 清单条目不是一个选项),您应该能够使用 Justin Grote's helpful answer 中推荐的方法 - 即使用 ScriptsToProcess 清单条目指向 *.ps1 脚本,该脚本调用 Add-Type 以动态加载依赖程序集在脚本模块 (*.psm1) 被加载之前 - 但这个 实际上并不能在 PowerShell 中工作7.2.0-preview.9:虽然依赖依赖程序集类型的*.psm1 文件中class 的定义成功,但调用者看不到 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] 类只能在调用 using module ./tm 后莫名其妙地成功两次 -你不能在一个 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 定义。
Takophiliac's helpful answer 中演示了这种方法。
[1] 在 this 的情况下这不是问题,但一般来说,鉴于Invoke-Expression 可以调用存储在字符串中的 any 命令,应用它对字符串不完全由您控制可能会导致执行恶意命令 - 请参阅this answer 了解更多信息。
此警告类似地适用于其他语言,例如 Bash 的内置 eval 命令。