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-SubnetTest-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|IEXiexInvoke-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.