powershell 在不执行脚本的情况下对脚本进行点源化

c9qzyr3d  于 2023-06-23  发布在  Shell
关注(0)|答案(5)|浏览(184)

在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
mwg9r5ms

mwg9r5ms1#

您必须执行函数定义以使它们可用。这是没有办法的事。
你可以尝试在文件中抛出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

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

pwuypxnk

pwuypxnk2#

在你的函数后面,Write-Host "reuseme.ps1 main."行被称为"过程代码"(即,它不在函数内)。您可以通过将此过程代码 Package 在IF语句中来告诉脚本不要运行此过程代码,该IF语句计算$MyInvocation.InvocationName-ne "."
$MyInvocation.InvocationName查看脚本是如何被调用的,如果您使用点(.)来点源化脚本,它将忽略过程代码。如果运行/调用脚本时不带点(.),则它将执行过程代码。示例如下:

function doA
{
    Write-Host "DoAMethod"
}

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

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

cidc1ykv

cidc1ykv3#

重用代码的最佳方法是将函数放在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
z4iuyo4d

z4iuyo4d4#

在进一步寻找这个问题的解决方案时,我发现了一个解决方案,它几乎是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
ct2axkht

ct2axkht5#

下面是一个简单的方法,将函数从另一个文件导入到当前范围,而不执行该文件。在使用Pester进行单元测试时非常有用。

function Get-Functions($filePath)
{
    $script = Get-Command $filePath
    return $script.ScriptBlock.AST.FindAll({ $args[0] -is [Management.Automation.Language.FunctionDefinitionAst] }, $false)
}

Get-Functions ScriptPath.ps1 | Invoke-Expression

这是通过直接调用PowerShell解析器来实现的,就像前面的答案一样。
Answer的灵感来自于这条线索:Is there a way to show all functions in a PowerShell script?

相关问题