PowerShell 脚本的参数设计与模块化 ── 从「能运行的脚本」到「可以交给别人的脚本」

· · PowerShell, Windows, 脚本, 自动化, 运维改善, 业务效率化, 现有资产活用, 命令行

「负责人写的 PowerShell 脚本正在运行,但只有那个人能碰」「服务器名和路径直接写死在代码的各个角落,环境一变就要改动脚本本体」「传错了参数也照样悄悄运行,事后才发觉」── 在运维自动化的咨询中,比脚本本身更成问题的,往往是「脚本该怎么交出去、怎么养大」,这样的案例非常多。

如果只是在自己手头运行,变量直接写死的「能运行的脚本」并不会带来什么麻烦。但一旦要放到任务计划程序上、交给同事、或者在多台服务器上复用,参数设计与通用处理的整理程度就决定了品质。幸运的是,PowerShell 从一开始就把通往「可以交给别人的脚本」这条路上所需的工具,作为语言功能准备好了 ── param 块、验证属性、基于注释的帮助,以及模块。

本文以中小企业信息系统部门或运维负责人手头已有的「能运行的脚本」为起点,按照参数设计 → 输入验证 → 帮助与 -WhatIf 支持 → .psm1 模块化 → 内部共享的顺序,整理逐步提升品质的实务步骤。以 PowerShell 7.x 为基准,同时随时补充仅能使用 Windows PowerShell 5.1 的现场需要注意的地方。

1. 先说结论

  • 参数应在 param 块中声明,并加上 [CmdletBinding()] 使其成为「高级函数」,这是出发点。这样会自动附带常见参数(-Verbose、-ErrorAction 等),传入未定义的参数会导致绑定错误,从而避免打字错误被悄悄忽略的事故。1
  • 必需参数应用 [Parameter(Mandatory)] 声明,并且一定要指定类型。忘记指定必需参数的调用会在执行前就停下,类型不匹配的值也会在执行前被拒绝。2
  • 形式检查应交给验证属性(ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty),而不是 if 语句。验证失败时函数根本不会被调用,从结构上消除「执行到一半才出错」的情况。原则是在入口处尽早报错。2
  • 开/关类型的参数应以 [switch] 声明。用字符串接收 $true 或 $false 的自创标志参数,是调用方出事故的根源。2
  • 写好基于注释的帮助(.SYNOPSIS/.EXAMPLE),Get-Help 就能对自制命令生效。摆脱「用法请看代码」的状态,是把脚本交给别人的最低条件。3
  • 变更类函数应声明 SupportsShouldProcess,使其支持 -WhatIf/-Confirm。让调用方能在执行前确认影响范围,是运维脚本中性价比最高的安全装置。4
  • 在多个脚本中复用的函数应拆分到 .psm1 模块,并放到 $env:PSModulePath 下。只要放置位置正确,无需 Import-Module 即可自动加载。psd1 清单等到「要发布出去的阶段」再添加就足够了。5678
  • 模块与脚本应通过 Git 进行版本管理,通过共享文件夹分发时要留意与执行策略的关系。UNC 路径上的脚本有时会在 RemoteSigned 策略下被拒绝执行。9

2. param 块与 [CmdletBinding()] ── 通往「高级函数」的入口

首先,现场常见的典型「能运行的脚本」是这样的。

# 常见的例子:变量直接写死。环境一变就要改写脚本本体
$logDir = "D:\Logs\AppA"
$days = 90
Get-ChildItem $logDir -Filter *.log |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$days) }

把它改写成 param 块与 [CmdletBinding()] 的形式。

[CmdletBinding()]
param(
    # 必需。忘记指定的调用会在执行前停止
    # (注: [string] 是表明意图,而非拒绝其他类型。数值等会被自动转换为字符串后
    #  传入,如果要严格拒绝,请用后文的验证属性编写条件)
    [Parameter(Mandatory)]
    [string]$LogDir,

    # 可省略的参数应给予业务上安全的默认值
    [int]$Days = 90
)

Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
    Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-$Days) }

[CmdletBinding()] 是一个声明,意为「让这个函数(脚本)按照与编译好的 cmdlet 相同的方式运行」,加上它的函数就会成为高级函数(advanced function)。效果有三个。1

  • 自动附带常见参数。不需要自己实现 -Verbose、-Debug、-ErrorAction、-ErrorVariable 等,调用方就能直接使用。脚本内的 Write-Verbose 只在指定 -Verbose 时才显示这种标准行为,也会原样获得。
  • 参数的错误会在执行前就停止。在高级函数中,传入未定义的参数名,或者没有对应位置参数的多余参数,都会导致参数绑定失败。像 -Dyas 30 这样的打字错误被悄悄忽略、然后用默认值运行的事故不会再发生。1
  • 可以使用 $PSCmdlet。这是通向后文将介绍的 ShouldProcess 等面向 cmdlet 的功能的入口。10

加了 Mandatory 的参数如果被省略,PowerShell 会在执行前提示输入。这是防止「明明是必需参数却忘记传值、结果用默认值跑起来了」的第一道安全装置。2 需要注意的一点是,这种「提示输入」的行为与无人值守执行相性不佳。如果从任务计划程序启动时漏传了必需参数,作业就可能卡在一个谁都无法回答的提示上不动。在无人值守执行中,标准做法是加上 -NonInteractive 启动,让它以立即报错的方式失败,而不是弹出提示。

由于「该写在哪里」不太直观,这里也给出任务计划程序「操作」标签页的填写示例(以 PowerShell 7 运行为例)。

栏位 填写示例
程序/脚本 C:\Program Files\PowerShell\7\pwsh.exe
添加参数(可选) -NoProfile -NonInteractive -File "C:\Scripts\Remove-OldAppLog.ps1" -LogDir "D:\Logs\AppA" -Days 90
起始于(可选) C:\Scripts

如果要在 Windows PowerShell 5.1 上运行,程序栏应填 powershell.exe。要点有三个:用 -NoProfile 消除因加载配置文件带来的环境差异与启动延迟;用 -NonInteractive 防止前面提到的等待输入;以及只要脚本路径或参数值中可能包含空格,就要用引号括起来-File 之后写的内容会作为脚本的参数传入,因此脚本自身的参数要排在 -File 后面。「起始于(可选)」留空时工作目录会是默认位置,因此使用相对路径的脚本务必要指定这一项。

另外,给函数命名并公开时应采用 Verb-Noun 形式,动词要从 Get-Verb 可以确认的已批准动词中选取。使用未获批准的动词也能运行,但在导入模块时会出现警告。11

3. 输入验证放在入口处 ── 用 Validate 属性做到「尽早报错」

如果把参数的形式检查写在函数本体的 if 语句中,很容易混入检查遗漏,或者「副作用在检查之前就发生了」这类 bug。在 PowerShell 中,可以用验证属性把验证与参数声明写在一起。验证会在函数被调用之前进行,一旦失败,本体连一行都不会执行。2

[CmdletBinding()]
param(
    # 只接受实际存在的文件夹。$_ 是被验证的值
    [Parameter(Mandatory)]
    [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
    [string]$LogDir,

    # 保留天数限制在 1~3650 天。防止 0 或负数导致「以全部文件为对象」的事故
    [ValidateRange(1, 3650)]
    [int]$Days = 90,

    # 固定可选项。同时也会启用 Tab 补全
    [ValidateSet('Zip', 'Move', 'ReportOnly')]
    [string]$Mode = 'ReportOnly',

    # 拒绝空字符串・$null。字符串参数的基本配备
    [ValidateNotNullOrEmpty()]
    [string]$ReportName = 'log-report',

    # 开/关用 switch。指定则为 $true,省略则为 $false
    [switch]$IncludeSubfolders
)

整理一下各属性的使用场景。

属性 用途 现场中的典型例子
ValidateSet 把值限制在固定的可选项中,并启用 Tab 补全2 运行模式、环境名(Dev/Test/Prod)
ValidateRange 数值・日期的范围限制2 保留天数、重试次数、端口号
ValidateScript 用任意脚本进行验证,返回 $false 或抛出异常即为失败2 路径是否存在、日期的前后关系
ValidateNotNullOrEmpty 拒绝 $null・空字符串・空集合2 几乎所有字符串参数
ValidatePattern 用正则表达式做形式检查2 单据编号、主机名的命名规则

补充两点。第一,验证属性的声明顺序需要注意。如果把验证属性写在类型之后,验证的就是类型转换前的值,有时会导致意料之外的失败,因此属性 → 类型 → 变量名的顺序,才是官方文档推荐的最佳实践。2 第二,ValidateScript 的 ErrorMessage 参数(自定义错误消息)是 PowerShell 6 以后才有的功能,在 Windows PowerShell 5.1 中无法使用。2 在混用 5.1 的环境中,稳妥的做法是在验证脚本内用 throw 抛出自己的消息,或者干脆保留默认消息。

当 ValidateScript 里开始写起复杂的验证逻辑,这也是该写测试的信号了。把验证逻辑本身的动作确认放到《用 Pester 完善 PowerShell 测试 ── 让运维脚本不易损坏的实务方法》整理的模式上,会更不容易出问题。

4. 管道输入的基础 ── ValueFromPipeline 与 process 块

如果自制函数也能像 Get-Content servers.txt | Test-AppServer 这样通过管道使用,就能以和 PowerShell 标准命令相同的感觉去组合它们。需要的只有两样:ValueFromPipeline 的声明,以及 process 块。2

function Test-AppServer {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$ComputerName
    )
    begin   { $results = @() }   # 在管道处理开始前只执行一次
    process {
        # 会对从管道流入的每个元素执行一次
        foreach ($name in $ComputerName) {
            $results += [pscustomobject]@{
                ComputerName = $name
                # -ComputerName 在 5.1 与 7 中都能使用(7 中新增的 -TargetName 在 5.1 中没有)
                Reachable    = Test-Connection -ComputerName $name -Count 1 -Quiet
            }
        }
    }
    end     { $results }         # 最后只执行一次
}

要记住的要点只有一个:如果要接受管道输入,就要把处理逻辑写在 process 块里。没有 process 块的话,即便通过管道流入多个值,也会变成只处理最后一个值这种典型 bug。10 begin 和 end 都是可以省略的,拿不准的时候记住「主体写在 process,需要汇总时才用 begin/end」,在实务中就已经够用了。

这个陷阱不太容易有实感,所以把错误示例并排放在一起。

# 错误示例:没有 process 块。即使通过管道流入 3 个值,也只会处理最后一个
function Test-AppServerBad {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline)]
        [string[]]$ComputerName
    )
    # 如果 begin/process/end 一个都不写,本体就会被整体当作 end 块处理,只会「在最后运行一次」。
    # 管道的元素是逐个绑定到参数上的,所以到达 end 时刻时,剩下的只有最后一个
    foreach ($name in $ComputerName) {
        [pscustomobject]@{ ComputerName = $name }
    }
}

'SV01', 'SV02', 'SV03' | Test-AppServerBad   # → 只输出 SV03 一行。SV01 和 SV02 被悄悄丢弃
'SV01', 'SV02', 'SV03' | Test-AppServer      # → 返回 3 行(即前面带有 process 块的版本)

麻烦的是,这个过程一个错误都不会报。而且如果像 Test-AppServerBad -ComputerName 'SV01','SV02','SV03' 这样以参数方式传入,3 件都会被正确处理,所以只用参数调用做动作确认的话根本发现不了。写了接受管道输入的函数之后,请务必加入一项流入多个值的确认。

5. 让 Get-Help 与 -WhatIf 生效 ── 交给别人的最低条件

5.1. 基于注释的帮助

PowerShell 用户遇到不认识的命令,第一反应就是敲 Get-Help。自制函数能不能融入这种文化,取决于有没有写基于注释的帮助。只需要写上带有特殊关键字的注释,Get-Help 就会以和标准 cmdlet 相同的格式显示帮助。3

把常用的关键字列成一览表。不需要全部都写,按照最低限度是 .SYNOPSIS 和 .EXAMPLE、要交给别人用则再加上 .DESCRIPTION 和 .PARAMETER 这样的顺序逐步补充就足够了。3

关键字 写什么
.SYNOPSIS 一行摘要,会显示在 Get-Help 的开头
.DESCRIPTION 详细说明,前提条件与副作用写在这里
.PARAMETER 参数名 每个参数的说明,关键字后面接参数名
.EXAMPLE 使用示例,第一行写要执行的命令,接下来的行写说明。可以写多个
.INPUTS 可以通过管道接收的对象类型
.OUTPUTS 返回的对象类型
.NOTES 补充信息,如作者、更新日期、已知限制等
.LINK 相关命令或 URL。第一个 URL 会成为 Get-Help -Online 的跳转目标

5.2. SupportsShouldProcess 与 -WhatIf

对进行删除・移动・设置变更的函数,应声明 [CmdletBinding(SupportsShouldProcess)]。只需这样做,就会自动添加 -WhatIf 与 -Confirm 参数,函数本体则根据 $PSCmdlet.ShouldProcess() 的返回值来判断是否真正执行变更。4

把两者都组合进去,达到可以交给别人的品质的函数完成形是这样的。

function Remove-OldAppLog {
    <#
    .SYNOPSIS
    从指定文件夹中删除超过保留期限的日志文件。

    .DESCRIPTION
    删除 LastWriteTime 早于保留天数的 *.log 文件。
    使用 -WhatIf 时只确认删除对象,不会实际删除。

    .EXAMPLE
    Remove-OldAppLog -LogDir 'D:\Logs\AppA' -Days 90 -WhatIf
    仅显示删除对象,不会实际删除。
    #>
    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory)]
        [ValidateScript({ Test-Path -LiteralPath $_ -PathType Container })]
        [string]$LogDir,

        [ValidateRange(1, 3650)]
        [int]$Days = 90
    )
    process {
        $limit = (Get-Date).AddDays(-$Days)
        # 用 -File 排除文件夹(避免误删名称以 ".log" 结尾的文件夹)
        Get-ChildItem -LiteralPath $LogDir -Filter *.log -File |
            Where-Object { $_.LastWriteTime -lt $limit } |
            ForEach-Object {
                # ShouldProcess 返回 $false 的情况是:使用 -WhatIf 时,以及被 -Confirm 拒绝时
                if ($PSCmdlet.ShouldProcess($_.FullName, "删除")) {
                    Remove-Item -LiteralPath $_.FullName
                }
            }
    }
}

只要敲入 Remove-OldAppLog -LogDir D:\Logs\AppA -WhatIf,就只会显示「What if: …」的一览,不会删除任何东西。把变更类脚本交给别人时,连同 -WhatIf 的预演步骤一并交出── 这是本站反复推荐的运维套路。实际整合进日志整理脚本的例子,详见《PowerShell 脚本进阶 ── 安全地实现日志排查、归档与报表自动化》。

另外,官方的说明文章中也提醒不要过度信任 -WhatIf 一定会传播到被调用的命令那一层。要做到万无一失,就要给内部的 Remove-Item 等命令显式传入 -WhatIf:$WhatIfPreference4

6. 把通用处理做成 .psm1 模块

6.1. .psm1 与 Export-ModuleMember

函数逐渐增多之后,就会想在多个脚本中使用同一个函数。用复制粘贴的方式增加,修改就不会波及到所有副本,所以通用函数应该统一管理。

在此之前还有一个选项,就是点源(dot-sourcing)。在脚本路径前面加上一个点和一个空格再执行,该脚本就会在调用方的作用域中运行,其中定义的函数和变量会原样留在调用方那里。12

# 加载写有通用函数的 Common.ps1(开头的点和空格就是点源)
. C:\Scripts\Common.ps1

# 可以直接调用 Common.ps1 中定义的函数
Remove-OldAppLog -LogDir 'D:\Logs\AppA' -WhatIf

这种方式很方便,但调用方需要知道文件的物理路径,也无法选择公开范围(函数、变量、别名会原样全部流入)。如果只是把一个脚本拆分开来倒是足够了,但当第二个脚本也想用同一个函数时,就是该转向模块化的判断标准。

模块化的做法简单得让人有点意外 ── 只需把写有函数的文件保存为 .psm1 扩展名即可5

# AppOpsTools.psm1 ── 公司内部运维工具的通用模块
function Remove-OldAppLog { <# 上一章的函数 #> }
function Get-AppLogSummary { <# 汇总函数 #> }

# 内部辅助函数,不对外暴露
function ConvertTo-InternalPath { <# ... #> }

# 明确指定要公开的函数。不写的话所有函数都会被公开
Export-ModuleMember -Function Remove-OldAppLog, Get-AppLogSummary

如果不写 Export-ModuleMember,模块内的所有函数和别名都会被导出(变量不会)。虽然可以省略,但明确指定公开范围被认为是最佳实践。13 把内部辅助函数隐藏起来,就能为以后自由重构留出余地。

6.2. 存放位置 ── $env:PSModulePath 与自动加载

模块应放在 $env:PSModulePath 中列出的文件夹下,建立一个与模块同名的文件夹再放进去(AppOpsTools\AppOpsTools.psm1)。文件夹名与文件的基础名不一致时,就不会被识别为模块。65 默认的存放位置如下表所示,Windows PowerShell 5.1 与 PowerShell 7 的路径不同,是现场容易踩坑的地方。6

作用域 PowerShell 7 Windows PowerShell 5.1
自己专用(CurrentUser) $HOME\Documents\PowerShell\Modules $HOME\Documents\WindowsPowerShell\Modules
所有用户(AllUsers) $env:ProgramFiles\PowerShell\Modules $env:ProgramFiles\WindowsPowerShell\Modules

与其死记这张表,不如在实际环境中确认更可靠。一行命令就能看到。

# 以每行一条的方式确认自己环境的搜索路径(Windows 的分隔符是分号)
$env:PSModulePath -split ';'

# 从环境中取分隔符的写法。如果在 PowerShell 7 中也会用到 macOS/Linux,选这种写法
$env:PSModulePath -split [System.IO.Path]::PathSeparator

即便同样叫 $env:PSModulePath,在 5.1 与 7 中内容其实是不同的东西。「找不到模块」的原因大多是放置的位置不在该环境的搜索路径中,所以请先执行这一行命令,再去怀疑别的原因。

只要放在正确的位置,就算不写 Import-Module,PowerShell 也会在第一次执行模块内的命令时自动导入(模块自动加载)。7 也就是说,使用者可以「像命令本来就装在那儿一样」直接使用。实务中的节奏通常是:在反复调试模块的阶段用完整路径 Import-Module 来做动作确认,定型之后再放到 PSModulePath 下。

有两点与环境相关的注意事项。Documents 文件夹的实体有时会因 OneDrive 的文件夹重定向而被移动,此时用户作用域的模块也会被放到 OneDrive 下面。6 另外,放到所有用户作用域需要管理员权限。如果要放在服务器上,建议采用 AllUsers 作用域,并确保任务计划程序的执行账户也能看到,这样就能避免「自己的电脑上能跑,服务器上却跑不动」的情况。

6.3. psd1 清单留到「要发布出去的阶段」

模块清单(.psd1)是一个记录模块版本、依赖关系等元数据的哈希表文件,并非必需。清单中唯一必需的键只有 ModuleVersion。8 在只于自己团队内部使用的阶段,单独一个 .psm1 就足够了,到了要分发给其他部门、或需要严格进行版本管理的阶段,再用 New-ModuleManifest 生成清单。8

New-ModuleManifest -Path .\AppOpsTools\AppOpsTools.psd1 `
    -RootModule 'AppOpsTools.psm1' `
    -ModuleVersion '1.0.0' `
    -FunctionsToExport 'Remove-OldAppLog', 'Get-AppLogSummary' `
    -PowerShellVersion '5.1'

生成出来的 psd1 是带有注释的模板,可以只把需要的键逐步补充完善。8

6.4. 内部共享与版本管理的要点

  • 原始版本放在 Git 中。脚本和模块都是文本,与 Git 天然合拍,能够追溯「何时、由谁、为何而改」,这本身就是运维脚本可信度的一部分。让 ModuleVersion 的更新与提交对应起来,能让确定服务器上跑的是哪个版本变得轻松。
  • 分发的基本形态是「从共享文件夹复制到各台机器的 PSModulePath 下」。把模块整个文件夹复制过去就能完成手动安装。7 把共享文件夹上的路径直接加入 PSModulePath 的做法,考虑到共享存在「慢・会断・有时不在」的前提(详见《网络驱动器与 UNC 路径的陷阱 ── 业务应用中处理文件服务器(共享文件夹)的实务》),并不建议常态化使用。
  • 注意与执行策略之间的关系。默认的 RemoteSigned 策略允许本地创建的无签名脚本运行,但在不区分 UNC 路径与互联网路径的系统上,UNC 路径上的脚本有时会被拒绝执行。此外,带有下载来源标记的文件会被阻止,需要用 Unblock-File 解除阻止,或者对其签名。9 如果要正式推进内部分发,请在《PowerShell 的执行策略与脚本签名 ── 从「用 Bypass 蒙混过关」的运维方式毕业的实务指南》中确认与代码签名组合使用的方法。

7. 实务定式(判断表)

论点 选项 判断标准
参数的接收方式 变量直接写死 / param 块 只要用两次以上、或有他人使用,就非 param 莫属。默认值应向「安全一侧」倾斜2
[CmdletBinding()] 不加 / 加上 要交给别人、要放到运维中的脚本一律加上。打字错误会在执行前就停止1
输入检查 本体的 if 语句 / 验证属性 单个参数的形式检查交给属性,只有组合验证才放在本体2
变更类的安全装置 自创的 -TestMode 参数 / SupportsShouldProcess 不要自己造标志参数,搭上标准的 -WhatIf/-Confirm4
通用处理的持有方式 复制粘贴 / 点源 / .psm1 模块 到第二个脚本也要共用时就该模块化。公开的函数用 Export-ModuleMember 明确指定513
模块的存放位置 任意文件夹 + Import-Module / PSModulePath 下 常用的模块放到规定位置,搭上自动加载。服务器用 AllUsers 作用域67
psd1 清单 一开始就做 / 发布阶段再做 要发给团队之外・要严格进行版本管理的阶段再用 New-ModuleManifest8

8. 总结

  • 用 param 块与 [CmdletBinding()] 使其成为高级函数,是「可以交给别人的脚本」的出发点。附带常见参数,参数错误会在执行前就停止。
  • 通过指定类型・[Parameter(Mandatory)]・偏向安全一侧的默认值・验证属性,让错误在入口处尽早出现。ValidateSet 还能提升 Tab 补全带来的易用性。
  • 管道输入要用 ValueFromPipeline 与 process 块搭配接收。忘记写 process 就只会处理最后一个值。
  • 用基于注释的帮助让 Get-Help 生效,变更类函数用 SupportsShouldProcess 支持 -WhatIf。能够进行预演,本身就是运维中的安全装置。
  • 通用函数拆分到 .psm1 中,用 Export-ModuleMember 明确公开范围,放置到 PSModulePath 下。请注意 5.1 与 7 的路径不同。
  • psd1 清单留到发布阶段再做。原始版本用 Git 管理,通过共享文件夹分发时要确认执行策略(UNC 路径与 RemoteSigned 之间的关系)。

相关文章

相关咨询领域

合同会社小村软件承接属人化 PowerShell 脚本的整理・模块化、运维自动化脚本的设计评审,以及内部分发・版本管理机制的构建。即使只是想先对「虽然在运行,但谁都不敢碰」的脚本资产做一次盘点,也欢迎垂询。

参考链接

  1. Microsoft Learn,about_Functions_CmdletBindingAttribute。关于 CmdletBinding 属性会让函数像编译好的 cmdlet 一样运行,常见参数会自动添加,$PSCmdlet 变得可以使用,未知参数或不匹配的位置参数会导致绑定失败,以及 SupportsShouldProcess 会添加 Confirm/WhatIf 参数。  2 3 4

  2. Microsoft Learn,about_Functions_Advanced_Parameters。关于 Parameter 属性与 Mandatory、ValueFromPipeline、switch 参数,以及 ValidateSet/ValidateRange/ValidateScript/ValidateNotNullOrEmpty/ValidatePattern 等验证属性的规范,验证失败时函数不会被调用,把属性声明在类型之前是最佳实践,以及 ValidateScript 的 ErrorMessage 参数是 PowerShell 6 以后才有的功能。  2 3 4 5 6 7 8 9 10 11 12 13 14 15

  3. Microsoft Learn,about_Comment_Based_Help。关于用 .SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE 等关键字编写基于注释的帮助后,Get-Help 会以与 XML 帮助相同的格式显示,以及脚本与函数各自的放置规则。  2 3

  4. Microsoft Learn,Everything you wanted to know about ShouldProcess。关于只需指定 SupportsShouldProcess 就会自动创建 -WhatIf/-Confirm,用 $PSCmdlet.ShouldProcess() 分支的写法,以及不应过度信任 -WhatIf 的传播、推荐显式传给内部命令。  2 3 4

  5. Microsoft Learn,How to Write a PowerShell Script Module。关于只需保存为 .psm1 扩展名即可成为脚本模块,要保存在与脚本同名的文件夹中,默认情况下所有函数都会被公开而变量不会,以及推荐用 Export-ModuleMember 明确指定要公开的函数。  2 3 4

  6. Microsoft Learn,about_PSModulePath。关于 $env:PSModulePath 是模块搜索文件夹的列表,PowerShell 7 与 Windows PowerShell 5.1 中 CurrentUser/AllUsers 作用域的默认路径不同,以及 OneDrive 或文件夹重定向可能会改变 Documents 的位置。  2 3 4 5

  7. Microsoft Learn,about_Modules。关于 PSModulePath 下的模块会在命令首次执行时自动导入(模块自动加载),把模块文件夹整个复制过去的手动安装方法,以及默认的模块存放位置。  2 3 4

  8. Microsoft Learn,New-ModuleManifest。关于模块清单(.psd1)是记录模块内容・属性・前提条件的哈希表、并非必需,唯一必需的键是 ModuleVersion,以及 New-ModuleManifest 会生成可作为模板使用的雏形。  2 3 4 5

  9. Microsoft Learn,about_Execution_Policies。关于 RemoteSigned 允许本地创建的无签名脚本运行、但要求互联网来源的脚本必须签名,在不区分 UNC 路径与互联网路径的系统中,UNC 路径上的脚本有时不会被 RemoteSigned 允许执行,以及通过 Unblock-File 解除阻止的方法。  2

  10. Microsoft Learn,about_Functions_Advanced_Methods。关于高级函数中可用的 begin/process/end 输入处理方法,以及 ShouldProcess 方法要在 process 块内调用、且需要通过 CmdletBinding 属性声明。  2

  11. Microsoft Learn,Approved Verbs for PowerShell Commands。关于命令名应采用 Verb-Noun 形式,已批准动词的列表以及用 Get-Verb 确认的方法,还有包含未批准动词的模块在导入时会显示警告。 

  12. Microsoft Learn,about_Scripts。关于脚本默认会在自己独立的作用域中运行,其中创建的函数・变量・别名・驱动器只存在于脚本作用域内,使用在路径前加点和空格的「点源(dot-sourcing)」执行时会在当前作用域中运行,创建的项目在执行结束后仍会留在会话中。 

  13. Microsoft Learn,Export-ModuleMember。关于 Export-ModuleMember 是用于指定从脚本模块导出哪些成员的 cmdlet,未指定时函数与别名会被导出而变量不会,虽然可以省略,但明示作者意图是一种最佳实践。  2

共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。

与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。

本文与以下服务页面相关联,欢迎从最接近的入口查看。

常见问题

汇总了咨询这一主题时常见的问题。

在 PowerShell 的 param 块上加了 [CmdletBinding()] 之后会有什么变化?
函数或脚本会被当作「高级函数(advanced function)」处理,从而获得与编译好的 cmdlet 相同的行为。具体来说,会自动添加 -Verbose、-ErrorAction 等常见参数,$PSCmdlet 变量变得可以使用,传入未定义的参数或多余的位置参数会产生绑定错误。由于打字错误的参数不会再被悄悄忽略,在运维脚本中加上它是基本做法。
脚本的参数检查应该用 Validate 属性还是 if 语句来写?
参数的形式检查,标准做法是交给 ValidateSet、ValidateRange、ValidateScript 等验证属性。验证会在函数本体执行之前进行,如果是不合法的值,处理连一行都不会运行就直接报错,从而防止「执行到一半才出错」的事故。ValidateSet 还有 Tab 补全生效这一实际好处。另一方面,多个参数的组合验证,或依赖运行时状态的检查,则应该放在本体的 if 语句中进行。
PowerShell 的自制模块(.psm1)应该放在哪里?
应该在 $env:PSModulePath 所包含的文件夹下建立「与模块同名的文件夹」再放进去。仅供自己使用时,PowerShell 7 的默认位置是 $HOME\Documents\PowerShell\Modules;所有用户共用时默认是 $env:ProgramFiles\PowerShell\Modules。请注意在 Windows PowerShell 5.1 中,对应的路径分别是 WindowsPowerShell\Modules,与 7 不同。放在这个位置后,即便不写 Import-Module,也会在命令首次执行时自动加载。
模块清单(psd1)是否一定要创建?
并非必需。即便没有清单,单独一个 .psm1 也能作为模块正常工作。需要清单的时机是「要发布出去的阶段」,当你想赋予版本号、所需的 PowerShell 版本、依赖模块、明确要导出的命令等元数据时,就用 New-ModuleManifest 生成。清单中必需的键只有 ModuleVersion 一个,所以先从最小配置开始,再按需要逐步完善就足够了。
放在公司内部共享文件夹中的脚本,为什么会被执行策略阻止?
默认的 RemoteSigned 策略,对本地创建的脚本即使无签名也允许执行,但对被标记为来自互联网的脚本则要求签名。官方文档中也明确写到,在不区分 UNC 路径与互联网路径的系统配置下,共享文件夹上的脚本有时会被 RemoteSigned 拒绝执行。如果要正式推进内部分发,请考虑代码签名与 AllSigned 的组合,或者配置内网区域(intranet zone)。

作者简介

本文作者的个人简介页面。

Go Komura

小村软件有限公司 代表

以 Windows 软件开发、技术咨询与故障排查为中心,擅长难以复现的故障调查,以及既有资产仍在运行的项目。

返回博客列表