4

在 Powershell 中是否可以点源或重用脚本函数而不执行它?我试图重用脚本的功能,而不执行脚本本身。我可以将函数分解为仅函数文件,但我试图避免这样做。


点源文件示例:

function doA
{
    Write-Host "DoAMethod"
}

Write-Host "reuseme.ps1 main."

消费文件示例:

. ".\reuseme.ps1"

Write-Host "consume.ps1 main."
doA

执行结果:

reuseme.ps1 main.
consume.ps1 main.
DoAMethod

期望的结果:

consume.ps1 main.
DoAMethod
4

4 回答 4

8

必须执行函数定义才能使它们可用。没有其他办法了。

您可以尝试将 PowerShell 解析器扔到文件中,只执行函数定义而不执行其他任何操作,但我想更简单的方法是将可重用部分构建为模块或简单地构建为除了声明函数之外不做任何事情的脚本。

作为记录,一个粗略的测试脚本可以做到这一点:

$file = 'foo.ps1'

$tokens = @()
$errors = @()
$result = [System.Management.Automation.Language.Parser]::ParseFile($file, [ref]$tokens, [ref]$errors)

$tokens | %{$s=''; $braces = 0}{
    if ($_.TokenFlags -eq 'Keyword' -and $_.Kind -eq 'Function') {
        $inFunction = $true
    }
    if ($inFunction) { $s += $_.Text + ' ' }
    if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'LCurly') {
        $braces++
    }
    if ($_.TokenFlags -eq 'ParseModeInvariant' -and $_.Kind -eq 'RCurly') {
        $braces--
        if ($braces -eq 0) {
            $inFunction = $false;
        }
    }
    if (!$inFunction -and $s -ne '') {
        $s
        $s = ''
    }
} | iex

如果脚本中声明的函数引用脚本参数(因为不包括脚本的参数块),您将遇到问题。可能还有很多其他我现在想不到的问题。我最好的建议仍然是区分可重用的库脚本和打算调用的脚本。

于 2013-08-01T10:48:05.740 回答
4

重用代码的最佳方法是将函数放入 PowerShell 模块中。只需创建一个包含所有功能的文件并为其提供.psm1扩展名。然后,您导入该模块以使您的所有功能都可用。例如reuseme.psm1

function doA
{
    Write-Host "DoAMethod"
}

Write-Host "reuseme.ps1 main."

然后,在任何你想使用你的函数模块的脚本中,

# If you're using PowerShell 2, you have to set $PSScriptRoot yourself:
# $PSScriptRoot = Split-Path -Parent -Path $MyInvocation.MyCommand.Definition
Import-Module -Name (Join-Path $PSScriptRoot reuseme.psm1 -Resolve)

doA
于 2013-08-01T18:13:47.803 回答
4

在你的函数之后,行Write-Host "reuseme.ps1 main"。被称为“过程代码”(即它不在函数内)。您可以告诉脚本不要运行此过程代码,方法是将其包装在计算$MyInvocation.InvocationName -ne "."的 IF 语句中。

$MyInvocation.InvocationName 查看脚本是如何被调用的,如果您使用点 (.) 对脚本进行点源,它将忽略过程代码。如果您运行/调用不带点 (.) 的脚本,那么它将执行过程代码。下面的例子:

function doA
{
    Write-Host "DoAMethod"
}

If ($MyInvocation.InvocationName -ne ".")
{
    Write-Host "reuseme.ps1 main."
}

因此,当您正常运行脚本时,您将看到输出。当您对脚本进行点源时,您将看不到输出;但是,函数(但不是过程代码)将被添加到当前范围。

于 2018-08-09T21:14:48.987 回答
0

在进一步寻找此问题的解决方案时,我遇到了一个解决方案,该解决方案几乎是 Aaron 回答中提示的后续。意图有点不同,但可以用来达到相同的结果。

这就是我发现的: https ://virtualengine.co.uk/2015/testing-private-functions-with-pester/

它需要它来与 Pester 进行一些测试,我想避免在为逻辑编写任何测试之前更改文件的结构。

它工作得很好,让我有信心先为逻辑编写一些测试,然后再重构文件的结构,这样我就不再需要对函数进行点源。

Describe "SomeFunction" {
  # Import the ‘SomeFunction’ function into the current scope
  . (Get-FunctionDefinition –Path $scriptPath –Function SomeFunction)

  It "executes the function without executing the script" {
     SomeFunction | Should Be "fooBar"
 }
}

和代码Get-FunctionDefinition

#Requires -Version 3

<#
.SYNOPSIS
    Retrieves a function's definition from a .ps1 file or ScriptBlock.
.DESCRIPTION
    Returns a function's source definition as a Powershell ScriptBlock from an
    external .ps1 file or existing ScriptBlock. This module is primarily
    intended to be used to test private/nested/internal functions with Pester
    by dot-sourcsing the internal function into Pester's scope.
.PARAMETER Function
    The source function's name to return as a [ScriptBlock].
.PARAMETER Path
    Path to a Powershell script file that contains the source function's
    definition.
.PARAMETER LiteralPath
    Literal path to a Powershell script file that contains the source
    function's definition.
.PARAMETER ScriptBlock
    A Powershell [ScriptBlock] that contains the function's definition.
.EXAMPLE
    If the following functions are defined in a file named 'PrivateFunction.ps1'

    function PublicFunction {
        param ()

        function PrivateFunction {
            param ()
            Write-Output 'InnerPrivate'
        }

        Write-Output (PrivateFunction)
    }

    The 'PrivateFunction' function can be tested with Pester by dot-sourcing
    the required function in the either the 'Describe', 'Context' or 'It'
    scopes.

    Describe "PrivateFunction" {
        It "tests private function" {
            ## Import the 'PrivateFunction' definition into the current scope.
            . (Get-FunctionDefinition -Path "$here\$sut" -Function PrivateFunction)
            PrivateFunction | Should BeExactly 'InnerPrivate'
        }
    }
.LINK
    https://virtualengine.co.uk/2015/testing-private-functions-with-pester/
#>
function Get-FunctionDefinition {
    [CmdletBinding(DefaultParameterSetName='Path')]
    [OutputType([System.Management.Automation.ScriptBlock])]
    param (
        [Parameter(Position = 0,
          ValueFromPipeline = $true,
          ValueFromPipelineByPropertyName = $true,
          ParameterSetName='Path')]
        [ValidateNotNullOrEmpty()]
        [Alias('PSPath','FullName')]
        [System.String] $Path = (Get-Location -PSProvider FileSystem),

        [Parameter(Position = 0,
          ValueFromPipelineByPropertyName = $true,
          ParameterSetName = 'LiteralPath')]
        [ValidateNotNullOrEmpty()]
        [System.String] $LiteralPath,

        [Parameter(Position = 0,
          ValueFromPipeline = $true,
          ParameterSetName = 'ScriptBlock')]
        [ValidateNotNullOrEmpty()]
        [System.Management.Automation.ScriptBlock] $ScriptBlock,

        [Parameter(Mandatory = $true,
          Position =1,
          ValueFromPipelineByPropertyName = $true)]
        [Alias('Name')]
        [System.String] $Function        
    )

    begin {
        if ($PSCmdlet.ParameterSetName -eq 'Path') {
            $Path = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($Path);
        }
        elseif ($PSCmdlet.ParameterSetName -eq 'LiteralPath') {
            ## Set $Path reference to the literal path(s)
            $Path = $LiteralPath;          
        }
    } # end begin

    process {
        $errors = @();
        $tokens = @();
        if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
            $ast = [System.Management.Automation.Language.Parser]::ParseInput($ScriptBlock.ToString(), [ref] $tokens, [ref] $errors);
        } 
        else {
            $ast = [System.Management.Automation.Language.Parser]::ParseFile($Path, [ref] $tokens, [ref] $errors);
        }

        [System.Boolean] $isFunctionFound = $false;
        $functions = $ast.FindAll({ $args[0] -is [System.Management.Automation.Language.FunctionDefinitionAst] }, $true);
        foreach ($f in $functions) {
            if ($f.Name -eq $Function) {
                Write-Output ([System.Management.Automation.ScriptBlock]::Create($f.Extent.Text));
                $isFunctionFound = $true;
            }
        } # end foreach function

        if (-not $isFunctionFound) {
            if ($PSCmdlet.ParameterSetName -eq 'ScriptBlock') {
                $errorMessage = 'Function "{0}" not defined in script block.' -f $Function;
            }
            else {
               $errorMessage = 'Function "{0}" not defined in "{1}".' -f $Function, $Path;
            }
            Write-Error -Message $errorMessage;
        }
    } # end process
} #end function Get-Function
于 2018-11-12T15:33:16.823 回答