PowerShell,按需从互联网自动加载功能
PowerShell, auto load functions from internet on demand
有人向我指出(在 PowerShell, replicate bash parallel ping 中)我可以按如下方式从 Internet 加载函数:
iex (irm https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1)
引用Test-ConnectionAsync.ps1
的url包含两个函数:Ping-Subnet
和Test-ConnectionAsync
这让我想知道我是否可以在我的个人模块中定义旁路函数,这些函数是虚拟函数,一旦被调用就会被永久覆盖。例如
function Ping-Subnet <mimic the switches of the function to be loaded> {
if <function is not already loaded from internet> {
iex (irm https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1)
}
# Now, somehow, permanently overwrite Ping-Subnet to be the function that loaded from the URL
Ping-Subnet <pass the switches that we mimicked to the required function that we have just loaded>
}
这将非常简单地允许我直接从我的模块引用一些有用的脚本,但是 而 不必在加载模块时从互联网上加载它们(即函数仅在我调用它们时按需加载,除非我需要它们,否则我通常不会调用这些函数。
是的,应该可以。从 with-in 函数调用 Test-ConnectionAsync.ps1
将在包装函数的范围内创建定义 with-in 的函数。在函数作用域结束之前,您将能够调用任何包装函数。
如果您以不同的方式命名包装器和包装函数,您可以检查该函数是否已被声明为...
否则,您需要发挥更大的创意。
这表示,请谨慎行事。像这样的远程代码执行充满了安全问题,尤其是在我们谈论它的方式中,即不验证 Test-ConnectionAsync.ps1
.
您可以使用 Parser 查找远程脚本中的函数并将它们加载到您的范围中。这将不是 self-updating 函数,但应该比您要完成的更安全。
using namespace System.Management.Automation.Language
function Load-Function {
[cmdletbinding()]
param(
[parameter(Mandatory, ValueFromPipeline)]
[uri] $URI
)
process {
try {
$funcs = Invoke-RestMethod $URI
$ast = [Parser]::ParseInput($funcs, [ref] $null, [ref] $null)
foreach($func in $ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true)) {
if($func.Name -in (Get-Command -CommandType Function).Name) {
Write-Warning "$($func.Name) is already loaded! Skipping"
continue
}
New-Item -Name "script:$($func.Name)" -Path function: -Value $func.Body.GetScriptBlock()
}
}
catch {
Write-Warning $_.Exception.Message
}
}
}
Load-Function https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1
Ping-Subnet # => now is available in your current session.
function Ping-Subnet{
$toImport = (IRM "https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1").
Replace([Text.Encoding]::UTF8.GetString((239,187,191)),"")
NMO([ScriptBlock]::Create($toImport))|Out-Null
$MyInvocation.Line|IEX
}
function Test-ConnectionAsync{
$toImport = (IRM "https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1").
Replace([Text.Encoding]::UTF8.GetString((239,187,191)),"")
NMO([ScriptBlock]::Create($toImport))|Out-Null
$MyInvocation.Line|IEX
}
Ping-Subnet -Result Success
Test-ConnectionAsync -Computername $env:COMPUTERNAME
结果:
Computername Result
------------ ------
192.168.1.1 Success
192.168.1.2 Success
192.168.1.146 Success
Computername IPAddress Result
------------ --------- ------
HOME-PC fe80::123:1234:ABCD:EF12 Success
值得称赞 提出了方法的巧妙基础:
在使用
New-Module
创建的 动态模块 中下载并执行远程脚本的内容(其 built-in别名是 nmo
),这导致脚本的功能是 auto-exported 并变得可用 session-globally[1]
注意动态模块不容易发现,因为它们没有显示在
Get-Module
's output; however, you can discover them indirectly, via the .Source
property of the command-info objects output by Get-Command
:
Get-Command | Where Source -like __DynamicModule_*
下载的函数可用 session-globally 如果您尝试在不应该使用的脚本中使用该技术,则可能不需要影响会话的全局状态 - 请参阅底部的解决方案。
然后是re-invoke函数,假设原来的存根函数已经被同名的下载版本替换,将接收到的参数传递过去。
虽然 Fors1k 的解决方案通常会起作用,但这里有一个简化的、强大的替代方案,可以防止潜在的、无意的 re-execution 代码:
function Ping-Subnet{
$uri = 'https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1'
# Define and session-globally import a dynamic module based on the remote
# script's content.
# Any functions defined in the script would automatically be exported.
# However, unlike with persisted modules, *aliases* are *not* exported by
# default, which the appended Export-ModuleMember call below compensates for.
# If desired, also add -Variable * in order to export variables too.
# Conversely, if you only care about functions, remove the Export-ModuleMember call.
$dynMod = New-Module ([scriptblock]::Create(
((Invoke-RestMethod $uri)) + "`nExport-ModuleMember -Function * -Alias *")
)
# If this stub function shadows the newly defined function in the dynamic
# module, remove it first, so that re-invocation by name uses the new function.
# Note: This happens if this stub function is run in a child scope, such as
# in a (non-dot-sourced) script rather than in the global scope.
# If run in the global scope, curiously, the stub function seemingly
# disappears from view right away - not even Get-Command -All shows it later.
$myName = $MyInvocation.MyCommand.Name
if ((Get-Command -Type Function $myName).ModuleName -ne $dynMod.Name) {
Remove-Item -LiteralPath "function:$myName"
}
# Now invoke the newly defined function of the same name, passing the arguments
# through.
& $myName @args
}
具体而言,此实现可确保:
远程脚本中定义的 aliases 也被导出(如果不需要,只需从上面的代码中删除 + "`nExport-ModuleMember -Function * -Alias *"
。
re-invocation 稳健地针对 new,module-defined 函数的实现 - 即使存根函数 运行s 在子作用域中,例如在 (non-dot-sourced) 脚本中。
- 当 运行 在子作用域中时,
$MyInvocation.Line|IEX
(iex
是 Invoke-Expression
cmdlet 的 built-in 别名)将导致无限循环, 因为那个时候存根函数本身还是有效的。
所有收到的参数都在 re-invocation 上传递,没有 re-evaluation。
使用 built-in 展开自动 $args
变量 (@args
) 的魔法只传递接收到的、已经扩展的参数,同时支持命名和位置arguments.[2]
$MyInvocation.Line|IEX
有两个潜在问题:
如果调用命令行包含多个命令,它们将全部重复。
- 您可以通过将
(Get-PSCallStack)[1].Position.Text
替换为 $MyInvocation.Line
来解决这个特定问题,但这仍然无法解决下一个问题。
$MyInvocation.Line
和 (Get-PSCallStack)[1].Position.Text
都包含以 unexpanded(未计算)形式传递的参数,这导致它们的 re-evaluation by Invoke-Expression
,其风险在于,至少在假设上,这个 re-evaluation 可能涉及冗长的命令,其输出作为参数,或者更糟的是,命令具有 边不能或不应重复的效果。
将技术范围限定到给定的本地脚本:
下载的函数变得可用 session-globally 如果您尝试在脚本中使用技术 ,则可能不需要] 不应影响会话的全局状态;也就是说,您可能希望通过动态模块导出的函数在脚本退出时消失。
这需要两个额外的步骤:
将动态模块通过管道传输到 Import-Module
, which is the prerequisite for being able to unload it before exiting with Remove-Module
在退出前用动态模块调用Remove-Module
以卸载它。
function Ping-Subnet{
$uri = 'https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1'
# Save the module in a script-level variable, and pipe it to Import-Module
# so that it can be removed before the script exits.
$script:dynMod = New-Module ([scriptblock]::Create(
((Invoke-RestMethod $uri)) + "`nExport-ModuleMember -Function * -Alias *")
) | Import-Module -PassThru
# If this stub function shadows the newly defined function in the dynamic
# module, remove it first, so that re-invocation by name use the new function.
# Note: This happens if this stub function is run in a child scope, such as
# in a (non-dot-sourced) script rather than in the global scope.
# If run in the global scope, curiously, the stub function seemingly
# disappears from view right away - not even Get-Command -All shows it later.
$myName = $MyInvocation.MyCommand.Name
if ((Get-Command -Type Function $myName).ModuleName -ne $dynMod.Name) {
Remove-Item -LiteralPath "function:$myName"
}
# Now invoke the newly defined function of the same name, passing the arguments
# through.
& $myName @args
}
# Sample commands to perform in the script.
Ping-Subnet -?
Get-Command Ping-Subnet, Test-ConnectionAsync | Format-Table
# Before exiting, remove (unload) the dynamic module.
$dynMod | Remove-Module
[1] 这假定 New-Module
调用本身是在模块之外进行的;如果它是在模块内部创建的,至少该模块的命令会看到 auto-exported 函数;如果该模块使用 implicit 导出行为(这种情况很少见且不可取),动态模块中的 auto-exported 函数将包含在该模块的导出中,因此再次可用 session-globally.
[2] 这个魔法有一个限制,但是很少会出现:[switch]
参数 带有直接附加的布尔参数不受支持(例如 -CaseSensitive:$true
)- 请参阅 this answer.
有人向我指出(在 PowerShell, replicate bash parallel ping 中)我可以按如下方式从 Internet 加载函数:
iex (irm https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1)
引用Test-ConnectionAsync.ps1
的url包含两个函数:Ping-Subnet
和Test-ConnectionAsync
这让我想知道我是否可以在我的个人模块中定义旁路函数,这些函数是虚拟函数,一旦被调用就会被永久覆盖。例如
function Ping-Subnet <mimic the switches of the function to be loaded> {
if <function is not already loaded from internet> {
iex (irm https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1)
}
# Now, somehow, permanently overwrite Ping-Subnet to be the function that loaded from the URL
Ping-Subnet <pass the switches that we mimicked to the required function that we have just loaded>
}
这将非常简单地允许我直接从我的模块引用一些有用的脚本,但是 而 不必在加载模块时从互联网上加载它们(即函数仅在我调用它们时按需加载,除非我需要它们,否则我通常不会调用这些函数。
是的,应该可以。从 with-in 函数调用 Test-ConnectionAsync.ps1
将在包装函数的范围内创建定义 with-in 的函数。在函数作用域结束之前,您将能够调用任何包装函数。
如果您以不同的方式命名包装器和包装函数,您可以检查该函数是否已被声明为...
否则,您需要发挥更大的创意。
这表示,请谨慎行事。像这样的远程代码执行充满了安全问题,尤其是在我们谈论它的方式中,即不验证 Test-ConnectionAsync.ps1
.
您可以使用 Parser 查找远程脚本中的函数并将它们加载到您的范围中。这将不是 self-updating 函数,但应该比您要完成的更安全。
using namespace System.Management.Automation.Language
function Load-Function {
[cmdletbinding()]
param(
[parameter(Mandatory, ValueFromPipeline)]
[uri] $URI
)
process {
try {
$funcs = Invoke-RestMethod $URI
$ast = [Parser]::ParseInput($funcs, [ref] $null, [ref] $null)
foreach($func in $ast.FindAll({ $args[0] -is [FunctionDefinitionAst] }, $true)) {
if($func.Name -in (Get-Command -CommandType Function).Name) {
Write-Warning "$($func.Name) is already loaded! Skipping"
continue
}
New-Item -Name "script:$($func.Name)" -Path function: -Value $func.Body.GetScriptBlock()
}
}
catch {
Write-Warning $_.Exception.Message
}
}
}
Load-Function https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1
Ping-Subnet # => now is available in your current session.
function Ping-Subnet{
$toImport = (IRM "https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1").
Replace([Text.Encoding]::UTF8.GetString((239,187,191)),"")
NMO([ScriptBlock]::Create($toImport))|Out-Null
$MyInvocation.Line|IEX
}
function Test-ConnectionAsync{
$toImport = (IRM "https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1").
Replace([Text.Encoding]::UTF8.GetString((239,187,191)),"")
NMO([ScriptBlock]::Create($toImport))|Out-Null
$MyInvocation.Line|IEX
}
Ping-Subnet -Result Success
Test-ConnectionAsync -Computername $env:COMPUTERNAME
结果:
Computername Result
------------ ------
192.168.1.1 Success
192.168.1.2 Success
192.168.1.146 Success
Computername IPAddress Result
------------ --------- ------
HOME-PC fe80::123:1234:ABCD:EF12 Success
在使用
New-Module
创建的 动态模块 中下载并执行远程脚本的内容(其 built-in别名是nmo
),这导致脚本的功能是 auto-exported 并变得可用 session-globally[1]注意动态模块不容易发现,因为它们没有显示在
Get-Module
's output; however, you can discover them indirectly, via the.Source
property of the command-info objects output byGet-Command
:Get-Command | Where Source -like __DynamicModule_*
下载的函数可用 session-globally 如果您尝试在不应该使用的脚本中使用该技术,则可能不需要影响会话的全局状态 - 请参阅底部的解决方案。
然后是re-invoke函数,假设原来的存根函数已经被同名的下载版本替换,将接收到的参数传递过去。
虽然 Fors1k 的解决方案通常会起作用,但这里有一个简化的、强大的替代方案,可以防止潜在的、无意的 re-execution 代码:
function Ping-Subnet{
$uri = 'https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1'
# Define and session-globally import a dynamic module based on the remote
# script's content.
# Any functions defined in the script would automatically be exported.
# However, unlike with persisted modules, *aliases* are *not* exported by
# default, which the appended Export-ModuleMember call below compensates for.
# If desired, also add -Variable * in order to export variables too.
# Conversely, if you only care about functions, remove the Export-ModuleMember call.
$dynMod = New-Module ([scriptblock]::Create(
((Invoke-RestMethod $uri)) + "`nExport-ModuleMember -Function * -Alias *")
)
# If this stub function shadows the newly defined function in the dynamic
# module, remove it first, so that re-invocation by name uses the new function.
# Note: This happens if this stub function is run in a child scope, such as
# in a (non-dot-sourced) script rather than in the global scope.
# If run in the global scope, curiously, the stub function seemingly
# disappears from view right away - not even Get-Command -All shows it later.
$myName = $MyInvocation.MyCommand.Name
if ((Get-Command -Type Function $myName).ModuleName -ne $dynMod.Name) {
Remove-Item -LiteralPath "function:$myName"
}
# Now invoke the newly defined function of the same name, passing the arguments
# through.
& $myName @args
}
具体而言,此实现可确保:
远程脚本中定义的 aliases 也被导出(如果不需要,只需从上面的代码中删除
+ "`nExport-ModuleMember -Function * -Alias *"
。re-invocation 稳健地针对 new,module-defined 函数的实现 - 即使存根函数 运行s 在子作用域中,例如在 (non-dot-sourced) 脚本中。
- 当 运行 在子作用域中时,
$MyInvocation.Line|IEX
(iex
是Invoke-Expression
cmdlet 的 built-in 别名)将导致无限循环, 因为那个时候存根函数本身还是有效的。
- 当 运行 在子作用域中时,
所有收到的参数都在 re-invocation 上传递,没有 re-evaluation。
使用 built-in 展开自动
$args
变量 (@args
) 的魔法只传递接收到的、已经扩展的参数,同时支持命名和位置arguments.[2]$MyInvocation.Line|IEX
有两个潜在问题:如果调用命令行包含多个命令,它们将全部重复。
- 您可以通过将
(Get-PSCallStack)[1].Position.Text
替换为$MyInvocation.Line
来解决这个特定问题,但这仍然无法解决下一个问题。
- 您可以通过将
$MyInvocation.Line
和(Get-PSCallStack)[1].Position.Text
都包含以 unexpanded(未计算)形式传递的参数,这导致它们的 re-evaluation byInvoke-Expression
,其风险在于,至少在假设上,这个 re-evaluation 可能涉及冗长的命令,其输出作为参数,或者更糟的是,命令具有 边不能或不应重复的效果。
将技术范围限定到给定的本地脚本:
下载的函数变得可用 session-globally 如果您尝试在脚本中使用技术 ,则可能不需要] 不应影响会话的全局状态;也就是说,您可能希望通过动态模块导出的函数在脚本退出时消失。
这需要两个额外的步骤:
将动态模块通过管道传输到
Import-Module
, which is the prerequisite for being able to unload it before exiting withRemove-Module
在退出前用动态模块调用
Remove-Module
以卸载它。
function Ping-Subnet{
$uri = 'https://raw.githubusercontent.com/proxb/AsyncFunctions/master/Test-ConnectionAsync.ps1'
# Save the module in a script-level variable, and pipe it to Import-Module
# so that it can be removed before the script exits.
$script:dynMod = New-Module ([scriptblock]::Create(
((Invoke-RestMethod $uri)) + "`nExport-ModuleMember -Function * -Alias *")
) | Import-Module -PassThru
# If this stub function shadows the newly defined function in the dynamic
# module, remove it first, so that re-invocation by name use the new function.
# Note: This happens if this stub function is run in a child scope, such as
# in a (non-dot-sourced) script rather than in the global scope.
# If run in the global scope, curiously, the stub function seemingly
# disappears from view right away - not even Get-Command -All shows it later.
$myName = $MyInvocation.MyCommand.Name
if ((Get-Command -Type Function $myName).ModuleName -ne $dynMod.Name) {
Remove-Item -LiteralPath "function:$myName"
}
# Now invoke the newly defined function of the same name, passing the arguments
# through.
& $myName @args
}
# Sample commands to perform in the script.
Ping-Subnet -?
Get-Command Ping-Subnet, Test-ConnectionAsync | Format-Table
# Before exiting, remove (unload) the dynamic module.
$dynMod | Remove-Module
[1] 这假定 New-Module
调用本身是在模块之外进行的;如果它是在模块内部创建的,至少该模块的命令会看到 auto-exported 函数;如果该模块使用 implicit 导出行为(这种情况很少见且不可取),动态模块中的 auto-exported 函数将包含在该模块的导出中,因此再次可用 session-globally.
[2] 这个魔法有一个限制,但是很少会出现:[switch]
参数 带有直接附加的布尔参数不受支持(例如 -CaseSensitive:$true
)- 请参阅 this answer.