【问题标题】:PowerShell functions load from functionPowerShell 函数从函数加载
【发布时间】:2019-07-30 09:00:49
【问题描述】:

我有一个包含多个文件的模块,其中包含函数和模块加载器。

示例函数:

Function1.ps1

function Init() {
    echo "I am the module initialization logic"
}

function DoStuff() {
    echo "Me performing important stuff"
}

模块加载器文件:

Module1.psm1:

$script:Functions = Get-ChildItem $PSScriptRoot\*.ps1 

function LoadModule {
    Param($path)
    foreach ($import in @($path)) {
        . $import.FullName
    }
}

LoadModule script:Functions

Init # function doesn't found 

所以我试图通过过程LoadModule 从文件Function1.ps1 加载函数。 调试LoadModule 显示加载了外部函数,但在完成LoadModule 过程后,函数变得不可访问,因此脚本在Init 行失败。

但是没有LoadModule函数的重写模块加载器工作正常

Module1.psm1:

Get-ChildItem $PSScriptRoot\*.ps1 | %{
    . $import.FullName
}
Init # In this case - all works fine

因此,据我了解,文件函数是从放置在某个隔离范围内的函数加载的并且能够访问它们,我需要添加一些范围标志。

也许有人知道我应该添加什么以使函数 Init() 可从 module.pm1 脚本主体访问,但不能使其外部可访问(不使用 Export-ModuleMember)?

【问题讨论】:

  • 函数点源文件在不同的范围内。为什么你首先需要那个包装函数?只需执行上一个代码 sn-p 中的操作即可。
  • 实际上,该模块加载私有/公共和共享函数文件,我将代码移动到单独的函数以不重复代码,因为LoadModule 包含额外的逻辑,如错误处理和日志记录
  • 如果您只需要调用Init一次,那么您可以在调用后将其删除。
  • @PetSerAl 您能否更深入地解释一下通话后删除的问题?也许这就是解决方案
  • Remove-Item Function:\Init

标签: powershell powershell-module


【解决方案1】:

注意:编辑 1,对点源实际作用的说明,包含在末尾。

首先,您将函数和模块的术语和用法混合在一起。扩展名为 .psm1 的模块应使用 Import-Module cmdlet 导入终端。在点源时,例如您在此处所做的,您应该只针对包含函数的脚本文件,即扩展名为 .ps1 的文件。

我对 PowerShell 也比较陌生,遇到了同样的问题。在花了大约一个小时阅读该问题后,我无法找到解决方案,但我发现的很多信息都表明这是一个范围问题。所以我创建了一个测试,使用了三个文件。

foo.ps1

function foo {
  Write-Output "foo"
}

bar.psm1

function bar {
  Write-Output "bar"
}

scoping.ps1

function loader {
    echo "dot sourcing file"
    . ".\foo.ps1"
    foo
    echo "Importing module"
    Import-Module -Name ".\bar.psm1"
    bar
}

foo
bar

loader

foo
bar

pause

让我们来看看这个脚本做了什么。

首先我们定义一个虚拟的loader 函数。这不是一个实用的加载器,但它足以测试范围和加载的文件中函数的可用性。这个函数 dot 获取包含函数 foo 的 ps1 文件,并将 Import-Module 用于包含函数 bar 的文件。

接下来,我们调用函数foobar,这将产生错误,以确定两者都不在当前范围内。虽然并非绝对必要,但这有助于说明他们的缺席。

接下来,我们调用loader 函数。在点源foo.ps1 之后,我们看到foo 成功执行,因为fooloader 函数的当前范围内。在将Import-Module 用于bar.psm1 之后,我们看到bar 也成功执行了。现在我们退出loader函数的作用域并返回到主脚本。

现在我们看到foo 的执行失败并出现错误。这是因为我们在函数范围内点源 foo.ps1。但是,因为我们导入了bar.psm1,所以 bar 成功执行。这是因为模块默认导入到全局范围内。


我们如何使用它来改进您的LoadModule 功能?此功能的主要内容是您需要切换到对导入的函数使用模块。请注意,根据我的测试,您不能 Import-Module 加载程序功能;这仅在您点源加载程序时才有效。

LoadModule.ps1

function LoadModule($Path) {
    Get-ChildItem -Path "$Path" -Filter "*.psm1" -Recurse -File -Name| ForEach-Object {
        $File = "$Path$_"
        echo "Import-Module -Name $File"
        Import-Module -Name "$File" -Force
    }
}

现在在终端中:

. ".\LoadModule.ps1"
LoadModule ".\"
foo
bar

编辑 1:对点源的进一步说明

点源相当于将指定文件的内容复制粘贴到形成点源的文件中。执行操作的文件逐字“导入”目标的内容,在继续执行“导入”代码之前不执行任何其他操作。例如

foo.ps1 Write-Output "I am foo"
. ".\bar.ps1"

bar.ps1

Write-Output "I am bar"

有效

Write-Output "I am foo"
Write-Output "I am bar"

【讨论】:

  • Import-Module 语法不适合我,因为Module1.psm1 实际上是一个模块,旨在将其他功能加载到模块中。如果模块使用Import-Module 导入的其他函数,模块将无法将它们导出到环境中。
  • 对了,非常感谢“modules load”和“a dot syntax”的详细解释
  • 如果您按照我的描述使用它,它应该适用于您的用例。获取包含模块加载器的 .ps1 文件,然后调用模块加载器将剩余的函数作为模块导入。导入一个模块然后导入更多模块似乎不起作用。采购一个然后导入模块的函数确实有效。
  • 我还应该注意 .ps1 和 .psm1 文件在技术上是相同的。你不会错过任何东西。区别在于语义上的区别,它鼓励更好的代码实践。您的可重用代码应该在您导入的模块中,并且您的特定于用例的代码应该在您执行的 .ps1 文件中。然后这些 .ps1 文件可以访问您的可重用模块。
【解决方案2】:

编辑:您实际上不需要使用 Import-Module。只要您的 $env:PSModulePath 中有模块,PowerShell 就会在首次调用它们时自动加载任何导出的函数。 Source.

根据您的用例的具体情况,您可以使用另一种方法。当您希望将模块大量导入到 PowerShell 会话中时,此方法可以解决。

当您启动 PowerShell 时,它会查看环境变量 $PSModulePath 的值以确定它应该在哪里查找模块。然后它在此目录下查找包含 psm1 和 psd1 文件的目录。您可以在会话期间修改此变量,然后按名称导入模块。这是一个示例,使用我添加到我的 PowerShell profile.ps1 文件中的内容:

$MyPSPath = [Environment]::GetFolderPath("MyDocuments") + "\WindowsPowerShell"

$env:PSModulePath = $env:PSModulePath + ";$MyPSPath\Custom\Modules"

Import-Module `
    -Name Confirm-Directory, `
    Confirm-File, `
    Get-FileFromURL, `
    Get-RedirectedURL, `
    Get-RemoteFileName, `
    Get-ReparseTarget, `
    Get-ReparseType, `
    Get-SpecialPath, `
    Test-ReparsePoint

如果您不熟悉 PowerShell 配置文件(它们与 Unix 的 ~/.profile 文件几乎相同),您可以找到:

  1. 有关 PowerShell 配置文件here 的更多信息。
  2. 使用了哪些配置文件以及何时使用here 的摘要。

虽然这可能不像自动加载器那样方便,但安装和导入模块是预期和接受的方法。除非您有特定的理由不这样做,否则您应该尽量遵循既定标准,以免日后努力改掉坏习惯。

你也可以modify the registry来实现这个。

【讨论】:

    【解决方案3】:

    经过一番研究,我发现:LoadModule函数在执行过程中,所有注册的函数都会添加到Functions Provider

    所以从LoadModule 函数体可以通过Get-ChildItem -Path Function: 枚举它们

    [DBG]: PS > Get-ChildItem -Path Function:
    CommandType     Name                                               Version    Source
    -----------     ----                                               -------    ------
    Function        C:
    Function        Close-VSCodeHtmlContentView                        0.2.0      PowerShellEditorServices.VSCode
    Function        Init                                               0.0        Module1
    Function        ConvertFrom-ScriptExtent                           0.2.0    
    Function        Module1                                            0.0        Module1
    

    所以我们可以在LoadModule的调用开始时将函数列表存储到变量中

    $loadedFunctions =  Get-ChildItem -Path Function:
    

    在点加载符号之后检索添加的函数列表

    Get-ChildItem -Path Function: |  where { $loadedFunctions -notcontains $_ } 
    

    所以修改后的LoadModule 函数将如下所示:

    function LoadModule {
        param ($path)
        $loadRef = Get-PSCallStack
    
        $loadedFunctions =  Get-ChildItem -Path Function:
        foreach ($import in @($path)) {
            . $import.FullName
        }
        $functions= Get-ChildItem -Path Function: | `
            Where-Object { $loadedFunctions -notcontains $_ } | `
            ForEach-Object{ Get-Item function:$_ }
    
        return $functions
    }
    

    下一步它只是将函数分配到列表More about this

    $script:functions = LoadModule $script:Private ##Function1.ps1
    $script:functions += LoadModule $script:PublicFolder
    

    经过这一步,我们可以

    • 调用初始化器:
        $initScripts = $script:functions| #here{ $_.Name -eq 'Initalize'} #filter
        $initScripts | ForEach-Object{ & $_ } ##execute
    
    • 并导出公共函数:
        $script:functions| `
        where { $_.Name -notlike '_*'  } |  ` # do not extport _Name functions
        %{ Export-ModuleMember -Function $_.Name}
    

    模块加载函数的完整代码我移到了ModuleLoader.ps1 文件中。并且可以在 GitHub repo 中找到PowershellScripts

    Moudule.psm1文件的完整版本是

    if($ModuleDevelopment){
        . $PSScriptRoot\..\Shared-Functions\ModuleLoader.ps1 "$PSScriptRoot"
    }
    else {
        . $PSScriptRoot\Shared\ModuleLoader.ps1 "$PSScriptRoot"
    }
    

    【讨论】:

      猜你喜欢
      • 2023-04-01
      • 1970-01-01
      • 2011-04-25
      • 1970-01-01
      • 2023-01-04
      • 2012-01-31
      • 1970-01-01
      • 2013-09-10
      • 1970-01-01
      相关资源
      最近更新 更多