考虑一个名为内容的PowerShell 脚本s.ps1
1/0
和假设函数
NAME
New-GlobalFunction
SYNOPSIS
Creates a new function in global scope from a script.
SYNTAX
New-GlobalFunction [-ScriptPath] <string> [-FunctionName] <string>
我希望能够使用这样的方法定义全局函数,从而产生显示正确文件的错误位置信息,类似于以下内容:Invoke-S
New-GlobalFunction
Invoke-S
PS C:\> New-GlobalFunction -ScriptPath .\s.p1 -FunctionName Invoke-S; Invoke-S
RuntimeException: C:\s.ps1:1
Line |
1 | 1/0
| ~~~
| Attempted to divide by zero.
----------
Attempted to divide by zero.
at <ScriptBlock>, C:\s.ps1: line 1
请留意提及的原始文件c.ps1
。
PowerShell 的架构表明这应该是可能的,因为脚本和函数只是脚本块:
在 PowerShell 编程语言中,脚本块是可用作单个单元的语句或表达式的集合。语句集合可以用括号 ({}) 括起来,定义为函数,或保存在脚本文件中。
实际上, 定义的脚本块s.ps1
可以通过 访问Get-Command .\s.ps1 | % ScriptBlock
。 上面带有正确位置信息的错误是通过调用该脚本块产生的。 事实上,可以使用以下命令将该脚本块转换为本地函数
New-Item -Path Function: -Name Invoke-S -Value (Get-Command .\s.ps1 | % ScriptBlock)
然而,我还没有成功创建这样一个全局函数。
有没有办法定义,New-GlobalFunction
使得文件中定义的脚本块产生的错误在-ScriptPath
其错误位置消息中包含正确的文件?
无效的方法
下面的代码尝试了我尝试过的各种创建方法Invoke-S
。它输出以下内容:
method new_error invoke_error stacktrace
------ --------- ------------ ----------
iex function global Attempted to divide by zero. at global:Invoke-S, <No file>…
function global: $sb Missing function body in… The term 'Invoke-S' is not recognized… at <ScriptBlock>, C:\Users\Us…
New-Item Function: The term 'Invoke-S' is not recognized… at <ScriptBlock>, C:\Users\Us…
New-Item global:Function: Cannot find drive. A dri… The term 'Invoke-S' is not recognized… at <ScriptBlock>, C:\Users\Us…
Get-Variable function: Cannot find a variable w… The term 'Invoke-S' is not recognized… at <ScriptBlock>, C:\Users\Us…
这些方法要么没有创建函数以使其在调用者的范围内可用(错误消息'Invoke-S' is not recognized
,要么堆栈跟踪没有提到正确的文件(错误消息at global:Invoke-S, <No file>
)。
function New-GlobalFunction {
param(
[Parameter(Mandatory)][string] $ScriptPath ,
[Parameter(Mandatory)][string] $FunctionName,
[Parameter(Mandatory)][ValidateSet(
'iex function global' ,
'function global: $sb' ,
'New-Item Function:' ,
'New-Item global:Function:',
'Get-Variable function:' )]$Method
)
switch ($Method) {
'iex function global' {
Invoke-Expression `
-Command "function global:$FunctionName {
$(Get-Content $ScriptPath)
}"
}
'function global: $sb' {
$sb =
Get-Command `
-Name $ScriptPath |
% ScriptBlock
# function global:Invoke-S $sb
throw 'Missing function body in function declaration.'
}
'New-Item Function:' {
New-Item `
-ErrorAction Stop `
-Path Function: `
-Name $FunctionName `
-Value (Get-Command $ScriptPath |
% ScriptBlock )
}
'New-Item global:Function:' {
New-Item `
-ErrorAction Stop `
-Path global:Function: `
-Name $FunctionName `
-Value (Get-Command $ScriptPath |
% ScriptBlock )
}
'Get-Variable function:' {
Get-Variable `
-Name 'function:' `
-ErrorAction Stop
}
}}
$(foreach ($method in 'iex function global' ,
'function global: $sb',
'New-Item Function:' ,
'New-Item global:Function:' ,
'Get-Variable function:' ) {
[pscustomobject]@{
method = $method
new_error = $(try {$null =
New-GlobalFunction `
-ScriptPath .\s.ps1 `
-FunctionName Invoke-S `
-Method $method }
catch {$_})
invoke_error = ($e =
try {Invoke-S -ErrorAction Stop}
catch {$_} )
stacktrace = $e.ScriptStackTrace
e = $e
}
Remove-Item Function:Invoke-S -ErrorAction SilentlyContinue
}) |
Format-Table `
-Property @{e='method' ;width=25},
@{e='new_error' ;width=25},
@{e='invoke_error';width=38},
@{e='stacktrace' ;width=30}
下面使用命名空间变量表示法似乎有效:
在这种情况下,基于 cmdlet 的上述等效方法实际上更简单,因为函数名称可以直接用变量来表示:
在您的功能上下文中:
解释:
具体来说,变量表达式(由[1]
${Function:global:Invoke-S}
解释的扩展形式)/传递给的参数(位置隐含的)分解如下:Invoke-Expression
-Path
Function:global:Invoke-S
Set-Content
Function:
引用提供程序Function:
公开的内置驱动器Function
global:Invoke-S
Invoke-S
指的是全局作用域内命名的函数;也global:
就是作用域修饰符。function global:Test-Me { 'hi!' }
定义一个函数。Test-Me
函数名包含
-
,这是一个需要将整个(命名空间符号)变量名表达式括起来{...}
才能被识别为单个变量表达式的字符。Set-Content
Function:global:Invoke-S
通过分配/
${Function:global:Invoke-S}
使用该名称作为路径Set-Content
,可以创建或更新全局Invoke-S
函数;必须分配/传递给(位置隐含的)参数-Value
的Set-Content
是函数体(类似地,获取此变量的值/使用Get-Content Function:global:Invoke-S
将返回函数的主体作为脚本块,假设该函数存在)。Get-Command
即可保留完整的来源信息,即包括来源文件的路径.ps1
。在调用者范围内定义函数的变体解决方案:
上述解决方案取决于能否在函数名称中使用范围修饰符,即
global:
但是,没有办法通过范围修饰符来指定相对范围,如果你想在调用者的范围中定义一个函数就需要这样做 - 无论调用者位于哪个范围。
相对范围(例如
1
引用父范围)仅通过实体特定的-Scope
cmdlet的参数支持,例如和;没有Set-Variable
Set-Alias
删除指定名称的命令。Set-Function
New-Item
和Set-Content
)也可以扩展以提供特定于实体的动态参数(此类参数的现有示例是特定于提供程序的-File
和开关)。-Directory
FileSystem
这个遗漏是GitHub 讨论 #16881的主题(由于语言不当,建议读者谨慎使用),目前唯一的(次优的)解决方法是使用 .NET反射来访问非公共成员(不能保证其 [以该形式] 的持续存在),特别是内部
.GetScopeByID()
方法,正如您所说,它本身只能通过通过另一个非公共 API 获得的对象来访问。下面的解决方案使用了链接讨论中的代码的简化版本,并且与前者不同,它还支持将函数放入模块
New-FunctionInCallerScope
内,尽管假设它只能从该模块外部调用。[1] 请注意,虽然在这种特殊情况下使用
Invoke-Expression
(iex
)是安全的,但通常应避免使用。存储
CommandInfo
在值脚本块中的另一种方法。