PowerShell 技能连载 - 提示符与界面定制

适用于 PowerShell 7.0 及以上版本

每天在终端里敲命令数小时,默认的 PS C:\> 提示符只能告诉你当前路径,其他信息一概欠奉。当你在多个 Git 仓库之间切换、管理不同的 Azure 订阅、激活不同的 Python 虚拟环境时,一个信息丰富的提示符可以让你瞬间掌握上下文状态,减少低级错误。

PowerShell 的提示符本质上就是一个名为 prompt 的函数——你可以自由重写它。无论是显示 Git 分支和脏状态、上一次命令的执行耗时、当前用户权限级别,还是用颜色区分不同的服务器环境,都可以通过几行代码实现。本文将带你从手写 prompt 函数开始,再到集成 Oh My Posh 这类成熟框架,最后补充一套提升日常效率的实用工具函数。

手写自定义 prompt 函数

最直接的方式是重写 prompt 函数。下面这段代码实现了一个多行提示符,第一行显示时间、路径和 Git 状态,第二行是实际的输入光标。同时它还记录上一条命令的执行时间,方便你判断某个操作是否太慢。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
# 保存到 $PROFILE 中即可生效
# 记录命令开始时间
$global:__LastCommandStart = $null

# 在命令执行前记录时间
$ExecutionContext.SessionState.InvokeCommand.AddEventHandler(
'CommandSearchAction', {
$global:__LastCommandStart = [DateTime]::Now
}
)

# 获取 Git 分支与状态信息
function Get-GitStatus {
try {
$branch = git rev-parse --abbrev-ref HEAD 2>$null
if (-not $branch) { return '' }

$status = git status --porcelain 2>$null
$dirty = if ($status) { '*' } else { '' }

$ahead = git log "@{upstream}..HEAD" --oneline 2>$null
$aheadCount = ($ahead | Where-Object { $_ }).Count

$aheadMark = if ($aheadCount -gt 0) { "+$aheadCount" } else { '' }

return " [$branch$dirty$aheadMark]"
} catch {
return ''
}
}

# 获取上一次命令执行耗时
function Get-LastCommandDuration {
if (-not $global:__LastCommandStart) { return '' }
$duration = [DateTime]::Now - $global:__LastCommandStart
if ($duration.TotalSeconds -gt 1) {
return " ($([math]::Round($duration.TotalSeconds, 1))s)"
}
return ''
}

# 自定义 prompt 函数
function prompt {
$path = Get-Location
$homePrefix = $HOME -replace '\\', '\\'
$displayPath = $path.Path -replace "^$homePrefix", '~'
$gitInfo = Get-GitStatus
$duration = Get-LastCommandDuration
$timeStamp = Get-Date -Format 'HH:mm:ss'

# 第一行:时间戳 + 路径 + Git 状态 + 执行耗时
Write-Host "`n" -NoNewline
Write-Host $timeStamp -ForegroundColor DarkGray -NoNewline
Write-Host " " -NoNewline
Write-Host $displayPath -ForegroundColor Cyan -NoNewline
Write-Host $gitInfo -ForegroundColor Yellow -NoNewline
Write-Host $duration -ForegroundColor DarkYellow -NoNewline

# 权限提示
if (
$IsWindows -and
([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()
).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
) {
Write-Host " [ADMIN]" -ForegroundColor Red -NoNewline
}

# 第二行:输入提示符
Write-Host ""
Write-Host ">" -ForegroundColor Green -NoNewline
return ' '
}

执行后的终端效果如下(纯文本模拟):

1
2
14:32:05 ~/projects/my-app [main*+2] (3.2s)
>

第一行显示了当前时间、相对主目录的路径、Git 分支名称(main)、脏标记(*表示有未提交的更改)、领先远程的提交数(+2)以及上一条命令耗时 3.2 秒。如果在管理员模式下运行,还会出现红色的 [ADMIN] 标记。

集成 Oh My Posh

手动写 prompt 函数虽然灵活,但维护成本不低——尤其是当你想要图标、颜色主题、多种 Segment(环境变量、云平台信息等)时。Oh My Posh 是一个跨 Shell 的提示符渲染引擎,配合 Nerd Font 可以实现非常精美的终端外观。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
# 安装 Oh My Posh(Windows 推荐使用 winget)
# winget install JanDeDobbeleer.OhMyPosh -s winget

# macOS / Linux 使用 Homebrew
# brew install jandedobbeleer/oh-my-posh/oh-my-posh

# 在 $PROFILE 中初始化 Oh My Posh
oh-my-posh init pwsh --config "$env:POSH_THEMES_PATH\jandedobbeleer.omp.json" |
Invoke-Expression

# 如果想使用自定义配置文件
# $ompConfig = Join-Path $HOME '.config' 'oh-my-posh' 'my-theme.omp.json'
# oh-my-posh init pwsh --config $ompConfig | Invoke-Expression

# 查看所有内置主题
Get-ChildItem -Path $env:POSH_THEMES_PATH -Filter '*.omp.json' |
Select-Object -ExpandProperty Name |
Sort-Object

# 快速预览主题(逐个浏览)
function Show-PoshThemePreview {
param([int]$Index = 0)

$themes = Get-ChildItem -Path $env:POSH_THEMES_PATH -Filter '*.omp.json' |
Sort-Object Name
$theme = $themes[$Index]
Write-Host "Theme [$Index/$($themes.Count)]: $($theme.Name)" -ForegroundColor Cyan
oh-my-posh init pwsh --config $theme.FullName | Invoke-Expression
}

# 导出当前配置并按需修改
function Export-PoshConfig {
param(
[string]$OutputPath = (Join-Path $HOME '.config' 'oh-my-posh')
)

$null = New-Item -ItemType Directory -Path $OutputPath -Force
$defaultConfig = Join-Path $env:POSH_THEMES_PATH 'jandedobbeleer.omp.json'
Copy-Item $defaultConfig (Join-Path $OutputPath 'my-theme.omp.json') -Force
Write-Host "配置已导出到 $OutputPath\my-theme.omp.json" -ForegroundColor Green
Write-Host '修改后更新 $PROFILE 中的 init 命令指向新文件即可。'
}

执行 Get-ChildItem 查看主题列表的部分输出:

1
2
3
4
5
6
1_shell.omp.json
agnoster.omp.json
agnosterplus.omp.json
atomic.omp.json
atomicBit.omp.json
...(共 100+ 内置主题)

执行 Export-PoshConfig 的输出:

1
2
配置已导出到 C:\Users\victor\.config\oh-my-posh\my-theme.omp.json
修改后更新 $PROFILE 中的 init 命令指向新文件即可。

Oh My Posh 的 JSON 配置文件支持丰富的 Segment 类型——Git、Az(Azure)、Python、Node、Docker、Kubectl 等等,你可以按需启用或禁用,调整颜色和图标。推荐从默认主题复制一份然后逐步微调,而不是从零开始编写。

实用工具函数集

提示符之外,Profile 里还可以放一些高频使用的辅助函数,它们与提示符配合让日常操作更加流畅。下面这组函数涵盖了目录快速跳转、增强的命令历史搜索,以及别名管理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
# --- 目录快速跳转 ---
# 使用书签机制在常用目录间跳转
$global:DirectoryBookmarks = @{}

function Set-Bookmark {
param([string]$Name)
$global:DirectoryBookmarks[$Name] = (Get-Location).Path
Write-Host "书签 '$Name' 已保存: $($global:DirectoryBookmarks[$Name])" -ForegroundColor Green
}

function Enter-Bookmark {
param([string]$Name)
if ($global:DirectoryBookmarks.ContainsKey($Name)) {
Set-Location $global:DirectoryBookmarks[$Name]
} else {
Write-Warning "书签 '$Name' 不存在。已保存的书签:"
$global:DirectoryBookmarks.GetEnumerator() |
ForEach-Object { Write-Host " $($_.Key) => $($_.Value)" }
}
}

# 简写别名
Set-Alias -Name bm -Value Set-Bookmark
Set-Alias -Name gb -Value Enter-Bookmark

# --- 增强的历史搜索 ---
# 使用 fzf(可选)或 PSFzf 模块进行模糊搜索
# 这里提供一个不依赖外部工具的方案
function Search-History {
param([string]$Pattern = '*')

Get-Content (Get-PSReadlineOption).HistorySavePath |
Where-Object { $_ -like "*$Pattern*" } |
Select-Object -Unique -Last 20
}

Set-Alias -Name hh -Value Search-History

# --- 别名管理 ---
# 列出所有自定义别名及其来源
function Get-MyAliases {
$builtIn = Get-Alias |
Where-Object { $_.Options -notcontains 'UserDefined' } |
Select-Object -ExpandProperty Name

Get-Alias |
Where-Object { $_.Name -notin $builtIn } |
Select-Object Name, Definition, Source |
Sort-Object Name |
Format-Table -AutoSize
}

# 快速进入 Profile 编辑模式
function Edit-Profile {
param([switch]$OpenFolder)
if ($OpenFolder) {
Invoke-Item (Split-Path $PROFILE)
} else {
code $PROFILE
}
}

Set-Alias -Name ep -Value Edit-Profile

使用书签功能的交互示例:

1
2
3
4
5
6
7
8
9
10
PS ~/projects/my-app> bm work
书签 'work' 已保存: /Users/victor/projects/my-app

PS ~> gb work
PS /Users/victor/projects/my-app>

PS ~> hh git
git status
git log --oneline -10
git push origin main

书签机制不依赖外部工具,设置简单,适合在少数几个高频目录之间切换。如果你的目录结构比较复杂,也可以考虑搭配 zzoxide 这类基于频率的跳转工具使用。

注意事项

  1. prompt 函数必须返回字符串:即使你只用 Write-Host 输出内容,函数也必须 return 一个字符串(哪怕是一个空格或空字符串),否则 PowerShell 会使用默认的提示符。
  2. Git 状态检测有性能开销:在大型仓库中,git status --porcelain 可能较慢。如果感到提示符延迟,可以在 Get-GitStatus 中加一个超时判断,或改用 git diff --quiet 做轻量级检测。
  3. Oh My Posh 需要 Nerd Font:图标符号依赖 Nerd Font 字体。如果终端中看到方框或乱码,说明字体未正确安装。推荐使用 CaskaydiaCove Nerd FontFiraCode Nerd Font
  4. Profile 分模块管理:随着自定义内容增多,建议把 prompt、别名、函数拆到不同的 .ps1 文件中,在 $PROFILE 里用 . $path 点源加载,保持主文件简洁。
  5. 跨平台兼容性:本文代码同时适配 Windows、macOS 和 Linux,但管理员检测部分([Security.Principal.WindowsIdentity])只在 Windows 上生效,非 Windows 平台会自动跳过该逻辑。
  6. PSReadLine 是好搭档:提示符定制之外,Set-PSReadlineOption 可以配置预测文本来源、颜色主题和快捷键。结合 CommandPrediction 插件,终端体验可以接近 IDE 级别。

PowerShell 技能连载 - 类型扩展与格式化

适用于 PowerShell 5.1 及以上版本

背景

PowerShell 的扩展类型系统(Extended Type System,简称 ETS)是它区别于传统 .NET 宿主的核心特性之一。当你对一个 FileInfo 对象调用 .Length 并看到它以 KB、MB 的友好格式输出时,或者当你对 DateTime 对象调用 .DayOfWeek 得到中文星期名时,背后都是 ETS 在起作用。ETS 允许你在不修改原始 .NET 类型定义的前提下,为任何对象动态添加属性和方法,甚至自定义它的显示格式。

这种”无侵入式扩展”在实际运维中非常有用。比如你可能希望每个进程对象额外携带一个”内存占用评分”属性,或者让所有服务器连接对象都自带一个 Test-Port 方法。这些需求如果靠继承或封装来实现会非常繁琐,而 ETS 可以用几行配置就搞定。同时,PowerShell 的格式化系统(Formatting System)与 ETS 紧密配合,决定了对象在控制台上以表格、列表还是宽格式显示,以及显示哪些列、列宽多少、对齐方式如何。

本文将从 Update-TypeData 动态添加属性和方法开始,接着介绍 Update-FormatData 自定义显示格式,最后展示如何将类型扩展和格式化配置持久化为 XML 文件,方便在模块中分发。

基础:使用 Update-TypeData 添加脚本属性

Update-TypeData cmdlet 可以为已存在的 .NET 类型动态注入脚本属性(ScriptProperty)和脚本方法(ScriptMethod)。脚本属性本质上是一段在访问时执行的 PowerShell 脚本,可以基于现有属性计算出新值。下面我们为 System.IO.FileInfo 类型添加一个 SizeCategory 属性,根据文件大小自动归类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
# 为 FileInfo 添加 SizeCategory 脚本属性
Update-TypeData -TypeName System.IO.FileInfo -MemberType ScriptProperty `
-MemberName SizeCategory -Value {
$len = $this.Length
if ($len -lt 1KB) {
"微型"
}
elseif ($len -lt 1MB) {
"小型"
}
elseif ($len -lt 100MB) {
"中型"
}
elseif ($len -lt 1GB) {
"大型"
}
else {
"超大"
}
}

# 验证新属性是否生效
$testFiles = @(
"/tmp/mini-note.txt"
"/tmp/report-q1.xlsx"
"/tmp/database-backup.bak"
"/tmp/archive.tar.gz"
)

# 创建测试文件(不同大小)
$testSizes = @(128, 524288, 52428800, 2147483648)
$index = 0

foreach ($filePath in $testFiles) {
$dir = Split-Path -Parent $filePath
if (-not (Test-Path -Path $dir)) {
$null = New-Item -ItemType Directory -Path $dir -Force
}

$size = $testSizes[$index]
$content = [byte[]]::new([math]::Min($size, 1024))
[System.IO.File]::WriteAllBytes($filePath, $content)

# 对于需要更大尺寸的文件,用 Stream 扩展
if ($size -gt 1024) {
$stream = [System.IO.File]::OpenWrite($filePath)
$stream.SetLength($size)
$stream.Close()
}

$index++
}

# 输出结果
foreach ($filePath in $testFiles) {
$file = Get-Item -Path $filePath -ErrorAction SilentlyContinue
if ($file) {
$sizeKB = [math]::Round($file.Length / 1KB, 2)
Write-Output "$($file.Name) | 大小: ${sizeKB} KB | 分类: $($file.SizeCategory)"
}
}

# 清理测试文件
foreach ($filePath in $testFiles) {
Remove-Item -Path $filePath -Force -ErrorAction SilentlyContinue
}

执行结果示例:

1
2
3
4
mini-note.txt | 大小: 0.13 KB | 分类: 微型
report-q1.xlsx | 大小: 512 KB | 分类: 小型
database-backup.bak | 大小: 51200 KB | 分类: 中型
archive.tar.gz | 大小: 2097152 KB | 分类: 超大

这段代码的关键在于 Update-TypeData-Value 参数。它接收一段脚本块,在脚本块内通过 $this 访问当前对象实例。每次访问 .SizeCategory 属性时,PowerShell 都会执行这段脚本并返回结果。注意 -TypeName 必须是完全限定的 .NET 类型名称(包含命名空间),不能使用别名。另外,如果同一个属性已经存在,需要加 -Force 参数才能覆盖,否则会报错。

进阶:添加脚本方法与 CodeMethod

除了属性,ETS 还支持添加脚本方法(ScriptMethod)和代码方法(CodeMethod)。脚本方法与脚本属性类似,用脚本块实现;代码方法则直接引用已编译 .NET 类型中的静态方法,性能更高。下面我们为 System.Diagnostics.Process 添加一个 GetUpTime 脚本方法和一个 SafeKill 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
# 为 Process 类型添加 GetUpTime 脚本方法
Update-TypeData -TypeName System.Diagnostics.Process `
-MemberType ScriptMethod `
-MemberName GetUpTime -Value {
$startTime = $this.StartTime
$uptime = (Get-Date) - $startTime
$hours = [math]::Floor($uptime.TotalHours)
$minutes = $uptime.Minutes
return "${hours}小时${minutes}分钟"
}

# 为 Process 类型添加 SafeKill 脚本方法(安全关闭)
Update-TypeData -TypeName System.Diagnostics.Process `
-MemberType ScriptMethod `
-MemberName SafeKill -Value {
param(
[int]$GracePeriodSeconds = 5
)

try {
$this.CloseMainWindow()
$this.WaitForExit($GracePeriodSeconds * 1000)

if (-not $this.HasExited) {
$this.Kill()
return "强制终止"
}
return "优雅关闭"
}
catch {
return "操作失败: $($_.Exception.Message)"
}
}

# 演示:获取当前 PowerShell 进程的运行时间
$procs = Get-Process -Id $PID
$uptime = $procs.GetUpTime()
Write-Output "当前进程 (PID: $PID) 已运行: $uptime"

# 演示:获取一组进程的运行时长
$targetNames = @("code", "Terminal", "Finder")
Write-Output ""
Write-Output "进程运行时长统计:"
Write-Output ("-" * 55)

foreach ($name in $targetNames) {
$found = Get-Process -Name $name -ErrorAction SilentlyContinue
if ($found) {
foreach ($p in $found) {
try {
$up = $p.GetUpTime()
$memMB = [math]::Round($p.WorkingSet64 / 1MB, 1)
Write-Output ("{0,-20} PID:{1,-8} 内存:{2,8} MB 运行:{3}" -f `
$p.ProcessName, $p.Id, $memMB, $up)
}
catch {
Write-Output ("{0,-20} PID:{1,-8} (无法获取运行时间)" -f `
$p.ProcessName, $p.Id)
}
}
}
else {
Write-Output ("{0,-20} (未运行)" -f $name)
}
}

执行结果示例:

1
2
3
4
5
6
7
8
当前进程 (PID: 48923) 已运行: 2小时34分钟

进程运行时长统计:
-------------------------------------------------------
code PID:31245 内存: 412.3 MB 运行:8小时12分钟
code PID:31246 内存: 38.7 MB 运行:8小时12分钟
Terminal PID:1024 内存: 156.2 MB 运行:48小时5分钟
Finder PID:245 内存: 289.1 MB 运行:120小时0分钟

这段代码演示了 ScriptMethod 的用法。与 ScriptProperty 不同,ScriptMethod 的脚本块可以接收 param 参数,通过 $this 访问当前对象。SafeKill 方法展示了在脚本方法中调用对象自身的其他方法(如 CloseMainWindowKill)来构建复合操作。值得注意的是,try/catch 在脚本方法中同样有效,可以用来处理进程已退出、权限不足等异常情况。在 Windows 系统上获取进程的 StartTime 需要管理员权限,部分系统进程可能会抛出”拒绝访问”异常。

实战:自定义格式化视图并持久化

类型扩展让你的对象拥有了新属性,但默认情况下 PowerShell 并不知道该如何展示它们。Update-FormatData 可以自定义对象的默认显示格式,而将类型定义和格式定义保存为 XML 文件则能实现跨会话持久化。下面我们创建一个自定义的 ServerStatus 对象类型,为它添加格式化视图,并将所有配置保存为可复用的模块文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
# 定义自定义对象类型名称
$TypeName = "MyTools.ServerStatus"

# 创建带类型名的自定义对象
function New-ServerStatus {
param(
[string]$Name,
[string]$IPAddress,
[string]$Status,
[int]$ResponseMs,
[string]$OS
)

$obj = [PSCustomObject]@{
Name = $Name
IPAddress = $IPAddress
Status = $Status
ResponseMs = $ResponseMs
OS = $OS
CheckedAt = Get-Date
}

$obj.PSTypeNames.Insert(0, $TypeName)
return $obj
}

# 使用 Update-TypeData 为自定义类型添加脚本属性
Update-TypeData -TypeName $TypeName -MemberType ScriptProperty `
-MemberName UptimeLabel -Value {
$ts = (Get-Date) - $this.CheckedAt
return "上次检查于 {0:N0} 秒前" -f $ts.TotalSeconds
} -Force

# 创建测试数据
$servers = @(
New-ServerStatus -Name "WEB-01" -IPAddress "10.0.1.10" `
-Status "在线" -ResponseMs 12 -OS "Windows Server 2022"
New-ServerStatus -Name "WEB-02" -IPAddress "10.0.1.11" `
-Status "在线" -ResponseMs 18 -OS "Windows Server 2022"
New-ServerStatus -Name "DB-01" -IPAddress "10.0.2.20" `
-Status "告警" -ResponseMs 245 -OS "Ubuntu 22.04"
New-ServerStatus -Name "CACHE-01" -IPAddress "10.0.3.30" `
-Status "离线" -ResponseMs -1 -OS "CentOS 7"
New-ServerStatus -Name "API-01" -IPAddress "10.0.4.40" `
-Status "在线" -ResponseMs 8 -OS "Windows Server 2025"
)

# 默认输出(无格式化,属性多时显示为列表)
Write-Output "=== 默认输出(List 格式)==="
$servers | Select-Object -First 1 | Format-List

# 使用 Format-Table 手动选择列
Write-Output ""
Write-Output "=== 自定义表格输出 ==="
$tableOutput = foreach ($svr in $servers) {
$color = switch ($svr.Status) {
"在线" { "Green" }
"告警" { "Yellow" }
"离线" { "Red" }
default { "White" }
}

[PSCustomObject]@{
名称 = $svr.Name
地址 = $svr.IPAddress
状态 = $svr.Status
响应时间 = if ($svr.ResponseMs -ge 0) { "$($svr.ResponseMs) ms" } else { "N/A" }
操作系统 = $svr.OS
}
}
$tableOutput | Format-Table -AutoSize

# 演示脚本属性
Write-Output ""
Write-Output "=== 脚本属性 UptimeLabel ==="
foreach ($svr in $servers) {
Write-Output " $($svr.Name): $($svr.UptimeLabel)"
}

# 导出类型定义到 XML(供模块加载使用)
$typeXmlPath = "/tmp/MyTools.Types.ps1xml"
$formatXmlPath = "/tmp/MyTools.Format.ps1xml"

# 生成类型 XML
$typeXmlContent = @"
<?xml version="1.0" encoding="utf-8"?>
<Types>
<Type>
<Name>MyTools.ServerStatus</Name>
<Members>
<ScriptProperty>
<Name>UptimeLabel</Name>
<GetScriptBlock>
`$ts = (Get-Date) - `$this.CheckedAt
"上次检查于 {0:N0} 秒前" -f `$ts.TotalSeconds
</GetScriptBlock>
</ScriptProperty>
<ScriptProperty>
<Name>ResponseGrade</Name>
<GetScriptBlock>
`$ms = `$this.ResponseMs
if (`$ms -lt 0) { return "不可达" }
if (`$ms -lt 50) { return "优秀" }
if (`$ms -lt 200) { return "良好" }
return "缓慢"
</GetScriptBlock>
</ScriptProperty>
</Members>
</Type>
</Types>
"@

Set-Content -Path $typeXmlPath -Value $typeXmlContent -Encoding UTF8
Write-Output ""
Write-Output "类型定义已导出: $typeXmlPath"
Write-Output "在模块中使用: Update-TypeData -AppendPath `"$typeXmlPath`""

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
=== 默认输出(List 格式)===

Name : WEB-01
IPAddress : 10.0.1.10
Status : 在线
ResponseMs : 12
OS : Windows Server 2022
CheckedAt : 2025/12/15 10:30:45

=== 自定义表格输出 ===

名称 地址 状态 响应时间 操作系统
---- ---- ---- -------- --------
WEB-01 10.0.1.10 在线 12 ms Windows Server 2022
WEB-02 10.0.1.11 在线 18 ms Windows Server 2022
DB-01 10.0.2.20 告警 245 ms Ubuntu 22.04
CACHE-01 10.0.3.30 离线 N/A CentOS 7
API-01 10.0.4.40 在线 8 ms Windows Server 2025

=== 脚本属性 UptimeLabel ===
WEB-01: 上次检查于 2 秒前
WEB-02: 上次检查于 2 秒前
DB-01: 上次检查于 2 秒前
CACHE-01: 上次检查于 2 秒前
API-01: 上次检查于 2 秒前

类型定义已导出: /tmp/MyTools.Types.ps1xml
在模块中使用: Update-TypeData -AppendPath "/tmp/MyTools.Types.ps1xml"

这段代码涵盖了 ETS 的完整工作流。首先通过 $obj.PSTypeNames.Insert(0, $TypeName) 将自定义类型名插入到对象的类型层次链头部,这样 PowerShell 就能识别并应用对应的类型扩展。Update-TypeData 为该类型添加了 UptimeLabel 脚本属性。最后展示了如何将类型定义导出为 .ps1xml 文件——这种 XML 格式是 PowerShell 模块分发类型扩展的标准方式,模块清单(.psd1)中的 TypesToProcess 字段可以声明这些文件,模块加载时自动注册。

注意事项

  1. Update-TypeData 在修改类型后对已存在的对象不一定立即生效。某些情况下需要重新创建对象实例或重新获取对象(例如再次调用 Get-Process)才能看到新添加的成员。如果发现新属性不可见,可以尝试先用 Remove-TypeData 清除再重新添加。

  2. .ps1xml 文件不会自动加载,必须显式注册。独立脚本中使用 Update-TypeData -AppendPath 加载,模块中则通过 .psd1 清单文件的 TypesToProcessFormatsToProcess 字段声明。如果模块有嵌套模块,确保 .ps1xml 文件路径正确——推荐使用相对于模块根目录的相对路径。

  3. 脚本属性和方法中的 $this 指向当前对象实例,但脚本块在独立的闭包中执行,无法直接访问外层作用域的变量。如果需要传入外部数据,可以使用 GetNewClosure() 方法捕获变量,或者将数据存储在对象本身的 NoteProperty 中再在脚本块内通过 $this 访问。

  4. PSTypeNames 的顺序决定类型解析优先级Insert(0, ...) 将自定义类型名放在最前面,PowerShell 会优先匹配第一个类型名对应的类型扩展和格式定义。如果多个模块为同一 .NET 类型注册了不同的扩展,后加载的模块可能覆盖先前的定义,建议为自定义类型使用带命名空间前缀的唯一名称(如 MyCompany.MyModule.MyType)。

  5. 格式化 XML 中不要嵌入复杂的条件逻辑.Format.ps1xml 支持通过 <Condition><ScriptBlock> 实现条件显示,但调试困难且性能较差。复杂的格式化需求建议在脚本中用 Format-Table/Format-List 配合计算属性(@{Name=...; Expression=...})来实现,既灵活又易于排错。

  6. Update-TypeData -Force 会静默覆盖已有成员。在生产环境使用前务必确认不会与 PowerShell 内置类型扩展或其他模块的扩展产生冲突。可以用 Get-TypeData -TypeName <名称> 先查看当前已注册的类型扩展,检查是否存在同名成员。

PowerShell 技能连载 - Windows Terminal 定制

适用于 PowerShell 7.0 及以上版本

Windows Terminal 已经成为 Windows 平台上最受欢迎的终端模拟器之一。它支持多标签页、GPU 加速渲染、Unicode 和 UTF-8 字符显示,以及对 WSL、CMD、PowerShell 等多种 Shell 的统一管理。但对于日常重度使用命令行的开发者来说,默认的 Terminal 外观和功能往往不够用——提示符单调、配色平庸、缺少上下文信息,这些都会影响工作效率。

好消息是,借助 PowerShell 7 的强大生态,我们可以通过 Oh My Posh 主题引擎、Profile 脚本自动化以及 Terminal 的 JSON 配置,打造一个既美观又实用的终端环境。从 Git 状态感知的提示符,到一键切换配色方案,再到自定义快捷键绑定,几乎所有的视觉和行为要素都可以按需调整。

本文将从实际场景出发,逐步展示如何用 PowerShell 脚本完成 Windows Terminal 的深度定制。每一段代码都可以直接复制到你的环境中运行,让你在几分钟内拥有一个令人印象深刻的终端工作区。

安装和初始化 Oh My Posh

Oh My Posh 是一个跨平台的提示符主题引擎,可以为 PowerShell 提供丰富的上下文信息,包括 Git 分支状态、Python 虚拟环境、执行耗时等。首先我们需要安装它并配置到 Profile 中。

1
2
3
4
5
6
7
8
# 安装 Oh My Posh(使用 winget)
winget install JanDeDobbeleer.OhMyPosh -s winget

# 如果 winget 不可用,也可以用 PowerShell 直接安装
Install-Module oh-my-posh -Scope CurrentUser -Force

# 查看 Oh My Posh 版本确认安装成功
oh-my-posh --version

安装完成后,输出类似如下:

1
24.5.0

接下来将 Oh My Posh 初始化命令写入 Profile,使其在每次启动 PowerShell 时自动加载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 查找或创建 Profile 文件
$profilePath = $PROFILE.CurrentUserAllHosts
if (-not (Test-Path $profilePath)) {
$null = New-Item -Path $profilePath -ItemType File -Force
Write-Host "已创建 Profile 文件: $profilePath"
} else {
Write-Host "Profile 文件已存在: $profilePath"
}

# 向 Profile 中追加 Oh My Posh 初始化行
$initLine = 'oh-my-posh init pwsh | Invoke-Expression'
$content = Get-Content -Path $profilePath -Raw -ErrorAction SilentlyContinue
if ($content -notmatch 'oh-my-posh init') {
Add-Content -Path $profilePath -Value $initLine
Write-Host '已添加 Oh My Posh 初始化命令'
} else {
Write-Host 'Oh My Posh 初始化命令已存在,跳过'
}

执行结果示例:

1
2
已创建 Profile 文件: C:\Users\dev\Documents\PowerShell\profile.ps1
已添加 Oh My Posh 初始化命令

浏览和应用主题

Oh My Posh 内置了大量开箱即用的主题,你可以通过脚本快速预览并切换。以下代码列出所有可用主题,并让你预览效果。

1
2
3
4
5
6
7
8
9
10
11
12
# 获取 Oh My Posh 主题目录
$themesDir = "$(oh-my-posh config export themes)"

# 列出所有主题文件名
$themes = Get-ChildItem -Path $themesDir -Filter '*.omp.json' |
Select-Object -ExpandProperty BaseName

Write-Host "共找到 $($themes.Count) 个主题"
Write-Host '---'
foreach ($t in $themes) {
Write-Host " - $t"
}

执行结果示例:

1
2
3
4
5
6
7
8
9
10
11
共找到 127 个主题
---
- 1_shell
- agnoster
- agnosterplus
- aliens
- amro
- atomic
- atomicBit
- avit
...

找到喜欢的主题后,将其写入 Profile:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# 选择主题名称
$selectedTheme = 'jandebuhr'

# 构建 Oh My Posh 初始化命令(指定主题)
$themeInit = "oh-my-posh init pwsh --config `"$themesDir\$selectedTheme.omp.json`" | Invoke-Expression"

# 读取当前 Profile 内容
$profileContent = Get-Content -Path $PROFILE.CurrentUserAllHosts -Raw

# 替换已有的 oh-my-posh init 行,或追加新行
if ($profileContent -match 'oh-my-posh init pwsh') {
$profileContent = $profileContent -replace 'oh-my-posh init pwsh.*Invoke-Expression', $themeInit
Set-Content -Path $PROFILE.CurrentUserAllHosts -Value $profileContent -NoNewline
Write-Host "已更新主题为: $selectedTheme"
} else {
Add-Content -Path $PROFILE.CurrentUserAllHosts -Value $themeInit
Write-Host "已添加主题: $selectedTheme"
}

Write-Host '请重新打开终端以查看效果'

执行结果示例:

1
2
已更新主题为: jandebuhr
请重新打开终端以查看效果

自动化管理 Terminal 配置文件

Windows Terminal 的设置存储在一个 JSON 文件中,路径通常是 settings.json。我们可以用 PowerShell 脚本直接读取和修改它,实现配色方案的批量管理和快捷键自定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 定位 Windows Terminal settings.json 路径
$settingsPath = Join-Path -Path $env:LOCALAPPDATA `
-ChildPath 'Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json'

# 如果是 Preview 版本
if (-not (Test-Path $settingsPath)) {
$settingsPath = Join-Path -Path $env:LOCALAPPDATA `
-ChildPath 'Packages\Microsoft.WindowsTerminalPreview_8wekyb3d8bbwe\LocalState\settings.json'
}

# 备份原始配置
$backupPath = $settingsPath + '.backup'
Copy-Item -Path $settingsPath -Destination $backupPath -Force
Write-Host "已备份配置到: $backupPath"

# 读取并解析 JSON
$settings = Get-Content -Path $settingsPath -Raw | ConvertFrom-Json
Write-Host "当前共有 $(($settings.schemes | Measure-Object).Count) 个配色方案"

执行结果示例:

1
2
已备份配置到: C:\Users\dev\AppData\Local\Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json.backup
当前共有 24 个配色方案

下面演示如何通过脚本添加自定义配色方案:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
# 定义一个新的配色方案
$newScheme = @{
name = 'PowerShell Dark Modern'
background = '#0C0C0C'
foreground = '#CCCCCC'
cursorColor = '#00FF00'
black = '#0C0C0C'
blue = '#0037DA'
cyan = '#3A96DD'
green = '#13A10E'
purple = '#881798'
red = '#C50F1F'
white = '#CCCCCC'
yellow = '#C19C00'
brightBlack = '#767676'
brightBlue = '#3B78FF'
brightCyan = '#61D6D6'
brightGreen = '#16C60C'
brightPurple = '#B4009E'
brightRed = '#E74856'
brightWhite = '#F2F2F2'
brightYellow = '#F9F1A5'
}

# 将哈希表转换为 PSCustomObject 并添加到 schemes 数组
$schemeObj = [PSCustomObject]$newScheme

if (-not $settings.schemes) {
$settings | Add-Member -MemberType NoteProperty -Name 'schemes' -Value @()
}
$settings.schemes += $schemeObj

# 将指定 Profile 的配色方案设置为新建的方案
foreach ($profile in $settings.profiles.list) {
if ($profile.name -match 'PowerShell') {
$profile | Add-Member -MemberType NoteProperty -Name 'colorScheme' `
-Value 'PowerShell Dark Modern' -Force
Write-Host "已为 Profile '$($profile.name)' 应用新配色"
}
}

# 写回 settings.json
$settings | ConvertTo-Json -Depth 10 | Set-Content -Path $settingsPath -Encoding UTF8
Write-Host '配置已保存,重新打开 Terminal 即可看到效果'

执行结果示例:

1
2
3
已为 Profile 'Windows PowerShell' 应用新配色
已为 Profile 'PowerShell 7' 应用新配色
配置已保存,重新打开 Terminal 即可看到效果

在 Profile 中添加实用函数

一个精心定制的终端不仅仅是好看,更要好用。以下是一组可以直接加入 Profile 的实用函数,提升日常操作效率。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
# 这段代码演示如何向 Profile 中批量添加实用函数
$functions = @(
@{
Name = 'Get-TerminalVersion'
Code = @'
function Get-TerminalVersion {
<# 获取当前 Windows Terminal 版本 #>
$pkg = Get-AppxPackage *WindowsTerminal*
if ($pkg) {
[PSCustomObject]@{
Name = $pkg.Name
Version = $pkg.Version
Status = '已安装'
}
} else {
Write-Warning '未检测到 Windows Terminal 安装'
}
}
'@
}
@{
Name = 'Export-TerminalSettings'
Code = @'
function Export-TerminalSettings {
<# 导出 Windows Terminal 配置到桌面 #>
$dest = Join-Path $env:USERPROFILE 'Desktop\wt-settings-backup.json'
$src = Join-Path $env:LOCALAPPDATA `
'Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json'
if (Test-Path $src) {
Copy-Item -Path $src -Destination $dest -Force
Write-Host "配置已导出到: $dest"
} else {
Write-Warning '未找到 Windows Terminal 配置文件'
}
}
'@
}
@{
Name = 'Set-TerminalOpacity'
Code = @'
function Set-TerminalOpacity {
<# 设置 Terminal 窗口透明度(需要 Terminal 设置中启用亚克力效果)#>
param([int]$Opacity = 80)
$settingsPath = Join-Path $env:LOCALAPPDATA `
'Packages\Microsoft.WindowsTerminal_8wekyb3d8bbwe\LocalState\settings.json'
$settings = Get-Content $settingsPath -Raw | ConvertFrom-Json
foreach ($p in $settings.profiles.list) {
$p | Add-Member -MemberType NoteProperty -Name 'opacity' `
-Value $Opacity -Force
}
$settings | ConvertTo-Json -Depth 10 | Set-Content $settingsPath -Encoding UTF8
Write-Host "已将所有 Profile 透明度设置为 ${Opacity}%"
}
'@
}
)

# 写入 Profile
$profileFile = $PROFILE.CurrentUserAllHosts
foreach ($func in $functions) {
if (-not (Select-String -Path $profileFile -Pattern "function $($func.Name)" -Quiet)) {
Add-Content -Path $profileFile -Value "`n$($func.Code)"
Write-Host "已添加函数: $($func.Name)"
} else {
Write-Host "函数已存在,跳过: $($func.Name)"
}
}

执行结果示例:

1
2
3
已添加函数: Get-TerminalVersion
已添加函数: Export-TerminalSettings
已添加函数: Set-TerminalOpacity

添加完成后,重新加载 Profile 即可使用这些函数:

1
2
3
4
. $PROFILE.CurrentUserAllHosts

# 测试获取 Terminal 版本
Get-TerminalVersion
1
2
3
Name                            Version        Status
---- ------- ------
Microsoft.WindowsTerminal 1.22.11141.0 已安装

注意事项

  1. 备份配置再修改:Terminal 的 settings.json 是唯一配置来源,修改前务必备份。本文中的脚本会自动创建 .backup 副本,但建议你也定期将配置纳入版本控制。

  2. Oh My Posh 字体依赖:大部分 Oh My Posh 主题需要 Nerd Font 字体才能正确显示图标。推荐安装 CascadiaCodeFiraCode 的 Nerd Font 版本,并在 Terminal 设置中将字体名填入 Profile 的 font.face 字段。

  3. Profile 执行策略:如果系统执行策略禁止运行脚本,Oh My Posh 和自定义函数都不会生效。需要以管理员身份执行 Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser 来放行本地脚本。

  4. JSON 序列化深度ConvertTo-Json 默认深度为 2,Terminal 的 settings.json 嵌套较深(特别是 actions 数组),务必使用 -Depth 10 或更高,否则部分配置会丢失。

  5. Preview 和 Stable 版本路径不同:Windows Terminal Preview 版的包名包含 Previewsettings.json 的路径也不同。脚本中应同时检测两个路径,避免修改错目标。

  6. 亚克力效果和透明度需要 GPU 支持useAcrylicopacity 设置依赖 GPU 加速渲染。在虚拟机或远程桌面会话中,这些视觉效果可能无法正常工作,但不影响核心功能使用。