【问题标题】:Is it possible to include functions only without executing the script?是否可以只包含函数而不执行脚本?
【发布时间】:2012-05-10 19:08:10
【问题描述】:

假设我有 MyScript.ps1

[cmdletbinding()]
param ( 
    [Parameter(Mandatory=$true)]
        [string] $MyInput
)

function Show-Input {
    param ([string] $Incoming)
    Write-Output $Incoming
}

function Save-TheWorld {
    #ToDo
}

Write-Host (Show-Input $MyInput)

是否可以仅以某种方式点源函数?问题是如果上面的脚本是点源的,它会执行整个事情......

我最好的选择是使用Get-Content 并解析出函数并使用 Invoke-Expression...?或者有没有办法以编程方式访问 PowerShell 的解析器?我看到使用 [System.Management.Automation.Language.Parser]::ParseInput 的 PSv3 可能会这样做,但这不是一个选项,因为它必须在 PSv2 上工作。

我问的原因是我正在尝试Pester PowerShell 单元测试框架,它对函数运行测试的方式是通过点源文件与测试夹具中的函数。测试夹具如下所示:

MyScript.Tests.ps1

$here = Split-Path -Parent $MyInvocation.MyCommand.Path
$sut = (Split-Path -Leaf $MyInvocation.MyCommand.Path).Replace(".Tests.", ".")
. "$here\$sut"

Describe "Show-Input" {

    It "Verifies input 'Hello' is equal to output 'Hello'" {
        $output = Show-Input "Hello"
        $output.should.be("Hello")
    }
}

【问题讨论】:

  • 你不能把函数放在他们自己的文件里吗?
  • @Lee 我想避免重新组织源代码(有一堆像 MyScript.ps1 文件这样的脚本)。

标签: powershell pester


【解决方案1】:

使用Doug's Get-Function function,您可以通过这种方式包含函数:

$script = get-item .\myscript.ps1
foreach ($function in (get-function $script))
{
  $startline = $function.line - 1
  $endline = $startline
  $successful = $false
  while (! $successful)
  {
    try {
      $partialfunction = ((get-content $script)[$startline..$endline]) -join [environment]::newline
      invoke-expression $partialfunction
      $successful = $true
    }
    catch [Exception] { $endline++ }
  }
}

编辑:可以使用 [System.Management.Automation.IncompleteParseException] 代替 Powershell V2 中的 [Exception]。

【讨论】:

  • 是的! [System.Management.Automation.PsParser]::Tokenize 是我需要的 :-)
  • 将多行函数转换为字符串时存在一些问题,因此我正在研究另一种方法,但先生您指出了正确的方向,谢谢!
  • 对不起乔恩,忽略我最后的评论,我的测试有问题。 Invoke-Expression 可以很好地与换行符连接。
  • “Doug 的 Get-Function 函数”是死链接。
【解决方案2】:

注意 -- 如果您觉得这个答案有帮助,请点赞 jonZ 的答案,因为如果不是他有帮助的答案,我将无法提出这个答案。

我根据链接到的脚本@jonZ 创建了这个函数提取器函数。这使用[System.Management.Automation.PsParser]::Tokenize 遍历输入脚本中的所有标记并将函数解析为函数信息对象并将所有函数信息对象作为数组返回。每个对象如下所示:

Start       : 99
Stop        : 182
StartLine   : 7
Name        : Show-Input
StopLine    : 10
StartColumn : 5
StopColumn  : 1
Text        : {function Show-Input {,     param ([string] $Incoming),     Write-Output $Incoming, }}

文本属性是一个字符串数组,可以写入临时文件和点源,或者使用换行符组合成一个字符串,并使用Invoke-Expression导入。

仅提取函数文本,因此如果一行有多个语句,例如:Get-Process ; function foo () {,则仅提取与函数相关的部分。

function Get-Functions {
    param (
        [Parameter(Mandatory=$true)]
        [System.IO.FileInfo] $File
    )

    try {
        $content = Get-Content $File
        $PSTokens = [System.Management.Automation.PsParser]::Tokenize($content, [ref] $null)

        $functions = @()

        #Traverse tokens.
        for ($i = 0; $i -lt $PSTokens.Count; $i++) {
            if($PSTokens[$i].Type -eq  'Keyword' -and $PSTokens[$i].Content -eq 'Function' ) {
                $fxStart = $PSTokens[$i].Start
                $fxStartLine = $PSTokens[$i].StartLine
                $fxStartCol = $PSTokens[$i].StartColumn

                #Skip to the function name.
                while (-not ($PSTokens[$i].Type -eq  'CommandArgument')) {$i++}
                $functionName = $PSTokens[$i].Content

                #Skip to the start of the function body.
                while (-not ($PSTokens[$i].Type -eq 'GroupStart') -and -not ($PSTokens[$i].Content -eq '{')) {$i++ }

                #Skip to the closing brace.
                $startCount = 1 
                while ($startCount -gt 0) { $i++ 
                    if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++}
                    if ($PSTokens[$i].Type -eq 'GroupEnd'   -and $PSTokens[$i].Content -eq '}') {$startCount--}
                }

                $fxStop = $PSTokens[$i].Start
                $fxStopLine = $PSTokens[$i].StartLine
                $fxStopCol = $PSTokens[$i].StartColumn

                #Extract function text. Handle 1 line functions.
                $fxText = $content[($fxStartLine -1)..($fxStopLine -1)]
                $origLine = $fxText[0]
                $fxText[0] = $fxText[0].Substring(($fxStartCol -1), $fxText[0].Length - ($fxStartCol -1))
                if ($fxText[0] -eq $fxText[-1]) {
                    $fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol - ($origLine.Length - $fxText[0].Length)))
                } else {
                    $fxText[-1] = $fxText[-1].Substring(0, ($fxStopCol))
                }

                $fxInfo = New-Object -TypeName PsObject -Property @{
                    Name = $functionName
                    Start = $fxStart
                    StartLine = $fxStartLine
                    StartColumn = $fxStartCol
                    Stop = $fxStop
                    StopLine = $fxStopLine
                    StopColumn = $fxStopCol
                    Text = $fxText
                }
                $functions += $fxInfo
            }
        }
        return $functions
    } catch {
        throw "Failed in parse file '{0}'. The error was '{1}'." -f $File, $_
    }
}

# Dumping to file and dot sourcing:
Get-Functions -File C:\MyScript.ps1 | Select -ExpandProperty Text | Out-File C:\fxs.ps1
. C:\fxs.ps1
Show-Input "hi"

#Or import without dumping to file:

Get-Functions -File  C:\MyScript.ps1 | % { 
    $_.Text -join [Environment]::NewLine | Invoke-Expression
}
Show-Input "hi"

【讨论】:

  • 我认为这不是真的。我刚刚用Write-Host blah 尝试过你的代码 - 'blah' 被打印到主机上。
  • @RomanKuzmin 你可能是对的,我只用上面的脚本进行了测试。它确实忽略了强制参数,但是必须使 $MyInput 为空,所以我可能会收到一个空白的新行而没有意识到。所以也许它只能得到我需要的一半。我真的很想避免创建自己的脚本解析树!
  • 我完全改变了答案,感谢您尝试原来的@RomanKuzmin :-)
  • if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -eq '{') {$startCount++} 应该是 if ($PSTokens[$i].Type -eq 'GroupStart' -and $PSTokens[$i].Content -in @('{', '@{')) { 才能正确解析 $hashmapFiltered = @{} 等代码
猜你喜欢
  • 2014-02-15
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-07-25
  • 1970-01-01
  • 1970-01-01
  • 2012-03-29
  • 1970-01-01
相关资源
最近更新 更多