第 10 章 - 脚本模块

如果你发现自己经常使用相同的 PowerShell 单行程序或脚本,则将它们转换为可重用的工具更为重要。 将函数打包在脚本模块中,可以使其更具专业性,并更易于维护和分享给他人。

点源函数

我们在上一章没有介绍的一个内容是点源函数。 在脚本中定义函数而不是模块的一部分时,将函数加载到内存中的唯一方法是点溯源其 .ps1 文件。

例如,将以下函数保存在名为Get-MrPSVersion.ps1的文件中。

function Get-MrPSVersion {
    $PSVersionTable
}

运行脚本时,似乎没有发生任何事情。

.\Get-MrPSVersion.ps1

尝试调用函数会导致错误,因为它未加载到内存中。

Get-MrPSVersion
Get-MrPSVersion : The term 'Get-MrPSVersion' is not recognized as the name
of a cmdlet, function, script file, or operable program. Check the spelling
of the name, or if a path was included, verify that the path is correct and
try again.
At line:1 char:1
+ Get-MrPSVersion
+ ~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Get-MrPSVersion:String) [],
   CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

可以通过核实 Function: PSDrive 上是否存在函数来确认函数是否已加载到内存中。

Get-ChildItem -Path Function:\Get-MrPSVersion
Get-ChildItem : Cannot find path 'Get-MrPSVersion' because it does not
exist.
At line:1 char:1
+ Get-ChildItem -Path Function:\Get-MrPSVersion
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Get-MrPSVersion:String) [Get
   -ChildItem], ItemNotFoundException
    + FullyQualifiedErrorId : PathNotFound,Microsoft.PowerShell.Commands.Ge
   tChildItemCommand

运行定义函数的脚本的问题是,它会将其加载到 脚本 范围。 脚本完成执行后,PowerShell 会放弃该范围以及函数。

若要使函数在脚本运行后可用,需要将其加载到 全局 范围内。 可以通过点源化(将脚本文件作为子脚本执行)来实现此目的。 为此,可以使用相对路径。

. .\Get-MrPSVersion.ps1

您还可以在点号导入脚本时使用脚本的完整路径。

. C:\Demo\Get-MrPSVersion.ps1

如果路径的一部分存储在变量中,则可以将其与路径的其余部分合并。 无需使用字符串串联来执行此作。

$Path = 'C:\'
. $Path\Get-MrPSVersion.ps1

现在,如果检查 函数 PSDrive,则会看到该 Get-MrPSVersion 函数可用。

Get-ChildItem -Path Function:\Get-MrPSVersion
CommandType     Name                                               Version
-----------     ----                                               -------
Function        Get-MrPSVersion

脚本模块

在 PowerShell 中,脚本模块只是包含一 .psm1 个或多个函数的文件,就像常规脚本一样,但文件扩展名不同。

如何创建脚本模块? 你可能会假设有一个名为 New-Module 的命令。 该假设是合理的猜测,但该命令实际上会创建动态模块,而不是脚本模块。

此方案是始终阅读帮助文档的好提醒,即使命令名称看起来与所需内容完全相同。

help New-Module
NAME
    New-Module

SYNOPSIS
    Creates a new dynamic module that exists only in memory.


SYNTAX
    New-Module [-Name] <System.String> [-ScriptBlock]
    <System.Management.Automation.ScriptBlock> [-ArgumentList
    <System.Object[]>] [-AsCustomObject] [-Cmdlet <System.String[]>]
    [-Function <System.String[]>] [-ReturnResult] [<CommonParameters>]


DESCRIPTION
    The `New-Module` cmdlet creates a dynamic module from a script block.
    The members of the dynamic module, such as functions and variables, are
    immediately available in the session and remain available until you
    close the session.

    Like static modules, by default, the cmdlets and functions in a dynamic
    module are exported and the variables and aliases are not. However, you
    can use the Export-ModuleMember cmdlet and the parameters of
    `New-Module` to override the defaults.

    You can also use the **AsCustomObject** parameter of `New-Module` to return
    the dynamic module as a custom object. The members of the modules, such
    as functions, are implemented as script methods of the custom object
    instead of being imported into the session.

    Dynamic modules exist only in memory, not on disk. Like all modules,
    the members of dynamic modules run in a private module scope that is a
    child of the global scope. Get-Module cannot get a dynamic module, but
    Get-Command can get the exported members.

    To make a dynamic module available to `Get-Module`, pipe a `New-Module`
    command to Import-Module, or pipe the module object that `New-Module`
    returns to `Import-Module`. This action adds the dynamic module to the
    `Get-Module` list, but it does not save the module to disk or make it
    persistent.


RELATED LINKS
    Online Version: https://learn.microsoft.com/powershell/module/microsoft.
    powershell.core/new-module?view=powershell-5.1&WT.mc_id=ps-gethelp
    Export-ModuleMember
    Get-Module
    Import-Module
    Remove-Module
    about_Modules

REMARKS
    To see the examples, type: "Get-Help New-Module -Examples".
    For more information, type: "Get-Help New-Module -Detailed".
    For technical information, type: "Get-Help New-Module -Full".
    For online help, type: "Get-Help New-Module -Online"

上一章提到函数应使用已批准的谓词。 否则,PowerShell 会在导入模块时生成警告。

以下示例使用 New-Module cmdlet 在内存中创建动态模块,具体演示不使用批准的谓词时会发生什么情况。

New-Module -Name MyModule -ScriptBlock {

    function Return-MrOsVersion {
        Get-CimInstance -ClassName Win32_OperatingSystem |
        Select-Object -Property @{Label='OperatingSystem';Expression={$_.Caption}}
    }

    Export-ModuleMember -Function Return-MrOsVersion

} | Import-Module
WARNING: The names of some imported commands from the module 'MyModule' include
unapproved verbs that might make them less discoverable. To find the commands with
unapproved verbs, run the Import-Module command again with the Verbose parameter. For a
list of approved verbs, type Get-Verb.

尽管前面示例中使用了 New-Module cmdlet,但这不是在 PowerShell 中创建脚本模块的命令。

若要创建脚本模块,请将函数 .psm1 保存在文件中。 例如,将以下两个函数保存在名为 MyScriptModule.psm1 的文件中。

function Get-MrPSVersion {
    $PSVersionTable
}

function Get-MrComputerName {
    $env:COMPUTERNAME
}

尝试运行其中一个函数。

Get-MrComputerName

调用函数时,会收到一条错误消息,指出 PowerShell 找不到它。 与之前一样,检查 函数: PSDrive 确认它未加载到内存中。

Get-MrComputerName : The term 'Get-MrComputerName' is not recognized as the
name of a cmdlet, function, script file, or operable program. Check the
spelling of the name, or if a path was included, verify that the path is
correct and try again.
At line:1 char:1
+ Get-MrComputerName
+ ~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : ObjectNotFound: (Get-MrComputerName:String) [
   ], CommandNotFoundException
    + FullyQualifiedErrorId : CommandNotFoundException

若要使函数可用,可以使用 cmdlet 手动导入 MyScriptModule.psm1 文件 Import-Module

Import-Module C:\MyScriptModule.psm1

PowerShell 引入了版本 3 中的模块自动加载。 若要利用此功能,脚本模块必须保存在与文件具有相同基名称 .psm1 的文件夹中。 该文件夹必须位于环境变量中指定的 $env:PSModulePath 其中一个目录中。

$env:PSModulePath

$env:PSModulePath 的输出难以读取。

C:\Users\mike-ladm\Documents\WindowsPowerShell\Modules;C:\Program Files\Wind
owsPowerShell\Modules;C:\Windows\system32\WindowsPowerShell\v1.0\Modules;C:\
Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\

若要使结果更具可读性,请拆分分号路径分隔符上的路径,以便每个路径在其自己的行上显示。

$env:PSModulePath -split ';'

列表中的前三个路径是默认模块位置。 安装 SQL Server Management Studio 时添加了最后一个路径。

C:\Users\mike-ladm\Documents\WindowsPowerShell\Modules
C:\Program Files\WindowsPowerShell\Modules
C:\Windows\system32\WindowsPowerShell\v1.0\Modules
C:\Program Files (x86)\Microsoft SQL Server\130\Tools\PowerShell\Modules\

若要使模块自动加载正常工作,必须将文件放置在 MyScriptModule.psm1 名为 MyScriptModule的文件夹中,并且该文件夹必须直接驻留在所列路径之一中
$env:PSModulePath

并非所有路径都同样有用。 例如,我的系统上的当前用户路径不是列表中的第一个路径。 这是因为我使用与用于运行 PowerShell 的帐户不同的帐户登录到 Windows。 因此,它不会指向用户的文档文件夹。

第二个路径是 AllUsers 路径,即存储所有模块的位置。

第三个路径指向 C:\Windows\System32受保护系统位置。 只有Microsoft应该将模块放在其中,因为它位于作系统的目录结构之下。

将文件放置在 .psm1 其中一个路径中的相应文件夹中后,PowerShell 会在首次调用其中一个命令时自动加载模块。

模块清单

每个模块都应包含一个 .psd1 模块清单,该清单是包含有关该模块的元数据的文件。 .psd1虽然扩展用于清单,但并非所有.psd1文件都是模块清单。 还可以将它们用于其他目的,例如在 DSC 中定义环境数据
配置。

可以使用 New-ModuleManifest cmdlet 创建模块清单。 唯一必需的参数是 Path,但要使模块正常工作,还必须指定 RootModule 参数。

最佳做法是包含 作者说明等值,尤其是在计划使用 PowerShellGet 将模块发布到 NuGet 存储库时。 在这种情况下,这些字段是必需的。

判断模块是否缺少清单的一种快速方法是检查其版本。

Get-Module -Name MyScriptModule

版本号 0.0 是模块缺少清单的明确标志。

ModuleType Version    Name                                ExportedCommands
---------- -------    ----                                ----------------
Script     0.0        MyScriptModule                      {Get-MrComputer...

在创建模块清单时,应包括所有建议的详细信息,以确保模块已做好了记录,并已准备好共享或发布。

$moduleManifestParams = @{
    Path = "$env:ProgramFiles\WindowsPowerShell\Modules\MyScriptModule\MyScriptModule.psd1"
    RootModule = 'MyScriptModule'
    Author = 'Mike F. Robbins'
    Description = 'MyScriptModule'
    CompanyName = 'mikefrobbins.com'
}

New-ModuleManifest @moduleManifestParams

如果在最初创建模块清单时省略任何值,则可以稍后使用 Update-ModuleManifest cmdlet 添加或更新它。 避免在首次创建清单 New-ModuleManifest 后重新生成它,因为这样做会生成新的 GUID。

定义公共函数和专用函数

有时,模块可能包含不希望向用户公开的帮助程序函数。 这些专用函数由模块中的其他函数在内部使用,但不会向用户公开。 可通过几种方法来处理此方案。

如果您不遵循最佳实践,并且只有一个没有适当模块结构的.psm1文件,您唯一的选择是使用Export-ModuleMember cmdlet 控制可见性。 使用此选项,可以显式定义应直接从 .psm1 脚本模块文件中公开哪些函数,默认将其他所有函数保留为私有。

在以下示例中,只有 Get-MrPSVersion 函数向模块的用户公开,而该 Get-MrComputerName 函数在内部仍可供模块中的其他函数访问。

function Get-MrPSVersion {
    $PSVersionTable
}

function Get-MrComputerName {
    $env:COMPUTERNAME
}

Export-ModuleMember -Function Get-MrPSVersion

确定 MyScriptModule 模块中公开可用的命令。

Get-Command -Module MyScriptModule
CommandType     Name                                               Version
-----------     ----                                               -------
Function        Get-MrPSVersion                                    1.0

如果将模块清单添加到模块,最佳做法是在 FunctionsToExport 节中显式列出要导出的函数。 此选项允许你控制从 .psd1 模块清单文件向用户公开的内容。

FunctionsToExport = 'Get-MrPSVersion'

您无需同时在Export-ModuleMember文件和模块清单中的.psm1节中使用FunctionsToExport。 这两种方法本身就足够了。

概要

本章介绍了如何在 PowerShell 中将函数转换为脚本模块。 你还探索了创建脚本模块的最佳做法,包括添加模块清单以定义元数据和管理导出的命令的重要性。

回顾

  1. 如何在 PowerShell 中创建脚本模块?
  2. 为什么对函数名称使用批准的谓词很重要?
  3. 如何在 PowerShell 中创建模块清单?
  4. 仅从模块导出特定函数的两种方法是什么?
  5. 运行其中一个命令时,模块必须满足哪些条件才能自动加载?

参考文献