「负责人写的 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:$WhatIfPreference。4
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 命令基础 ── 应先掌握的操作与安全使用方法
- PowerShell 脚本进阶 ── 安全地实现日志排查、归档与报表自动化
- 用 Pester 完善 PowerShell 测试 ── 让运维脚本不易损坏的实务方法
- PowerShell 的执行策略与脚本签名 ── 从「用 Bypass 蒙混过关」的运维方式毕业的实务指南
- PowerShell 的错误处理与重试设计 ── 从 try/catch 失效的陷阱到 exit code、重试的实务定式
- PowerShell 中凭据的安全处理方式 ── 把明文密码逐出脚本
相关咨询领域
合同会社小村软件承接属人化 PowerShell 脚本的整理・模块化、运维自动化脚本的设计评审,以及内部分发・版本管理机制的构建。即使只是想先对「虽然在运行,但谁都不敢碰」的脚本资产做一次盘点,也欢迎垂询。
参考链接
-
Microsoft Learn,about_Functions_CmdletBindingAttribute。关于 CmdletBinding 属性会让函数像编译好的 cmdlet 一样运行,常见参数会自动添加,$PSCmdlet 变得可以使用,未知参数或不匹配的位置参数会导致绑定失败,以及 SupportsShouldProcess 会添加 Confirm/WhatIf 参数。 ↩ ↩2 ↩3 ↩4
-
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
-
Microsoft Learn,about_Comment_Based_Help。关于用 .SYNOPSIS/.DESCRIPTION/.PARAMETER/.EXAMPLE 等关键字编写基于注释的帮助后,Get-Help 会以与 XML 帮助相同的格式显示,以及脚本与函数各自的放置规则。 ↩ ↩2 ↩3
-
Microsoft Learn,Everything you wanted to know about ShouldProcess。关于只需指定 SupportsShouldProcess 就会自动创建 -WhatIf/-Confirm,用 $PSCmdlet.ShouldProcess() 分支的写法,以及不应过度信任 -WhatIf 的传播、推荐显式传给内部命令。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,How to Write a PowerShell Script Module。关于只需保存为 .psm1 扩展名即可成为脚本模块,要保存在与脚本同名的文件夹中,默认情况下所有函数都会被公开而变量不会,以及推荐用 Export-ModuleMember 明确指定要公开的函数。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,about_PSModulePath。关于 $env:PSModulePath 是模块搜索文件夹的列表,PowerShell 7 与 Windows PowerShell 5.1 中 CurrentUser/AllUsers 作用域的默认路径不同,以及 OneDrive 或文件夹重定向可能会改变 Documents 的位置。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn,about_Modules。关于 PSModulePath 下的模块会在命令首次执行时自动导入(模块自动加载),把模块文件夹整个复制过去的手动安装方法,以及默认的模块存放位置。 ↩ ↩2 ↩3 ↩4
-
Microsoft Learn,New-ModuleManifest。关于模块清单(.psd1)是记录模块内容・属性・前提条件的哈希表、并非必需,唯一必需的键是 ModuleVersion,以及 New-ModuleManifest 会生成可作为模板使用的雏形。 ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn,about_Execution_Policies。关于 RemoteSigned 允许本地创建的无签名脚本运行、但要求互联网来源的脚本必须签名,在不区分 UNC 路径与互联网路径的系统中,UNC 路径上的脚本有时不会被 RemoteSigned 允许执行,以及通过 Unblock-File 解除阻止的方法。 ↩ ↩2
-
Microsoft Learn,about_Functions_Advanced_Methods。关于高级函数中可用的 begin/process/end 输入处理方法,以及 ShouldProcess 方法要在 process 块内调用、且需要通过 CmdletBinding 属性声明。 ↩ ↩2
-
Microsoft Learn,Approved Verbs for PowerShell Commands。关于命令名应采用 Verb-Noun 形式,已批准动词的列表以及用 Get-Verb 确认的方法,还有包含未批准动词的模块在导入时会显示警告。 ↩
-
Microsoft Learn,about_Scripts。关于脚本默认会在自己独立的作用域中运行,其中创建的函数・变量・别名・驱动器只存在于脚本作用域内,使用在路径前加点和空格的「点源(dot-sourcing)」执行时会在当前作用域中运行,创建的项目在执行结束后仍会留在会话中。 ↩
-
Microsoft Learn,Export-ModuleMember。关于 Export-ModuleMember 是用于指定从脚本模块导出哪些成员的 cmdlet,未指定时函数与别名会被导出而变量不会,虽然可以省略,但明示作者意图是一种最佳实践。 ↩ ↩2
相关文章
共享相同标签的最新文章。可以围绕相近的主题进一步加深理解。
从 PowerShell 调用 COM 与 .NET 的实践 ── 一举拓宽脚本触达的范围
本文从实务角度讲解从 PowerShell 调用 .NET 类的方法、通过 Add-Type 嵌入 C# 与 Win32 API、COM 操作、Excel 进程残留与后续处理、Office 无人执行不受支持的原因,以及 5.1 与 7 之间的差异。
用 winget + PowerShell 自动化 PC 装机 ── 让操作手册可执行
本文整理了让新员工电脑的初始设置具备可复现性的方法,涵盖通过 winget 进行应用安装与 export/import、WinGet Configuration 的声明式配置、用 PowerShell 补充的设置,直至无人值守执行时的注意事项。
PowerShell脚本运行慢时该看哪里 ── 数组・管道・匹配的诀窍
整理 PowerShell 脚本变慢的常见原因。从数组 += 导致 O(n^2) 的原理、管道与 foreach 的差异、匹配的哈希表化、文件 I/O 的改善,到正确的测量方法,从实务角度进行讲解。
不再使用 Write-Host ── PowerShell 的输出流与日志设计
本文整理 PowerShell 六个输出流的用法区分、Write-Host 存在的问题与正确的使用场景、函数返回值被污染的原因、通过 -Verbose 与 -InformationVariable 实现调用方控制,以及结构化日志的留存方法。
PowerShell 的并行处理 ── ForEach-Object -Parallel 与作业(Job)的选用之道
本文从实务角度整理 ForEach-Object -Parallel、Start-ThreadJob、Start-Job 的区别与选用之道,$using: 与线程安全性,ThrottleLimit 的确定方法,以及反而会变慢的情形。
相关主题
与本文相近的主题页面。以本文为起点,可以进一步了解相关服务和其他文章。
Windows 技术主题
汇整 KomuraSoft LLC 关于 Windows 开发、故障调查与既有资产活用文章的主题中心。
常见问题
汇总了咨询这一主题时常见的问题。
- 在 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)。