PowerShell 技能连载 - 创建 HTML 报表(第三部分 - 增加头部和尾部)

在前一个技能中我们开始将 PowerShell 的结果转换为 HTML 报告。现在,这份报告需要一些头部和尾部。以下是我们上一个版本的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#requires -Version 2.0

$Path = "$env:temp\eventreport.htm"
$startDate = (Get-Date).AddHours(-48)

$replacementStrings = @{
Name = 'ReplacementStrings'
Expression = { $_.ReplacementStrings -join ',' }
}

Get-EventLog -LogName System -EntryType Error -After $startDate |
Select-Object -Property EventId, Message, Source, InstanceId, TimeGenerated, $ReplacementStrings, UserName |
ConvertTo-Html |
Set-Content -Path $Path

Invoke-Item -Path $Path

要在数据前后加入内容,请使用 -PreContent-PostContent 参数。比如在头部加入机器名,在尾部加入版权信息,请使用以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#requires -Version 2.0

$Path = "$env:temp\eventreport.htm"
$today = Get-Date
$startDate = $today.AddHours(-48)
$startText = $startDate.ToString('MMMM dd yyyy, HH:ss')
$endText = $today.ToString('MMMM dd yyyy, HH:ss')

$preContent = "<h1>$env:computername</h1>
<h3>Error Events from $startText until $endText</h3>
"
$postContent = "<p><i>(C) 2017 SysAdmin $today</i></p>"

$replacementStrings = @{
Name = 'ReplacementStrings'
Expression = { $_.ReplacementStrings -join ',' }
}

Get-EventLog -LogName System -EntryType Error -After $startDate |
Select-Object -Property EventId, Message, Source, InstanceId, TimeGenerated, $ReplacementStrings, UserName |
ConvertTo-Html -PreContent $preContent -PostContent $postContent |
Set-Content -Path $Path

Invoke-Item -Path $Path

PowerShell 技能连载 - 创建 HTML 报表(第二部分 - 修复非字符串内容)

在前一个技能中我们开始使用 PowerShell 来将结果转换为 HTML 报告。目前,我们已经生成了报告,但报告的界面看起来很丑。我们从这里开始:

1
2
3
4
5
6
7
8
9
10
11
#requires -Version 2.0

$Path = "$env:temp\eventreport.htm"
$startDate = (Get-Date).AddHours(-48)

Get-EventLog -LogName System -EntryType Error -After $startDate |
Select-Object -Property EventId, Message, Source, InstanceId, TimeGenerated, ReplacementStrings, UserName |
ConvertTo-Html |
Set-Content -Path $Path

Invoke-Item -Path $Path

当您运行这段代码时,报告显示有一些属性包含非字符串内容。请看 “ReplacementStrings” 列:报告中含有 string[],也就是字符串数组类型,而不是真实数据。

要修复这个问题,请使用计算属性,并且将内容转换为可读的文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#requires -Version 2.0

$Path = "$env:temp\eventreport.htm"
$startDate = (Get-Date).AddHours(-48)

# make sure the property gets piped to Out-String to turn its
# content into readable text that can be displayed in the report
$replacementStrings = @{
Name = 'ReplacementStrings'
Expression = { ($_.ReplacementStrings | Out-String).Trim() }
}

Get-EventLog -LogName System -EntryType Error -After $startDate |
# select the properties to be included in your report
Select-Object -Property EventId, Message, Source, InstanceId, TimeGenerated, $ReplacementStrings, UserName |
ConvertTo-Html |
Set-Content -Path $Path

Invoke-Item -Path $Path

如您所见,该属性现在能正常显示它的内容了。

要如何将属性内容转换成可读的文本依赖于您的选择。如果将属性通过管道传给 Out-String,将把转换工作留给 PowerShell 自动完成。如果您希望更精细的控制,而且某个属性包含一个数组,您也可以使用 -join 操作符来连接数组元素。通过这种方式,您可以选择使用哪种分隔符来分割数组元素。以下例子使用逗号分隔:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#requires -Version 2.0

$Path = "$env:temp\eventreport.htm"
$startDate = (Get-Date).AddHours(-48)

# make sure the property gets piped to Out-String to turn its
# content into readable text that can be displayed in the report
$replacementStrings = @{
Name = 'ReplacementStrings'
Expression = { $_.ReplacementStrings -join ',' }
}

Get-EventLog -LogName System -EntryType Error -After $startDate |
# select the properties to be included in your report
Select-Object -Property EventId, Message, Source, InstanceId, TimeGenerated, $ReplacementStrings, UserName |
ConvertTo-Html |
Set-Content -Path $Path

Invoke-Item -Path $Path

PowerShell 技能连载 - 创建 HTML 报表(第一部分 - 创建 HTML)

要将 PowerShell 的处理结果输出为 HTML 报表,只需要将结果用管道传给 ConvertTo-Html,然后将结果保存到文件。所以它最基本的使用形式类似如下。它创建一个包含过去 48 小时发生的所有事件系统错误的报表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#requires -Version 2.0

# store report here
$Path = "$env:temp\eventreport.htm"
# set the start date
$startDate = (Get-Date).AddHours(-48)

# get data and convert it to HTML
Get-EventLog -LogName System -EntryType Error -After $startDate |
ConvertTo-Html |
Set-Content -Path $Path

# open the file with associated program
Invoke-Item -Path $Path

不过,输出的报告可能有点丑,因为包含了许多无用的信息。所以美化的第一步是选择报告中需要的属性。只需要在代码中加入 Select-Object

1
2
3
4
5
6
7
8
9
10
11
12
#requires -Version 2.0

$Path = "$env:temp\eventreport.htm"
$startDate = (Get-Date).AddHours(-48)

Get-EventLog -LogName System -EntryType Error -After $startDate |
# select the properties to be included in your report
Select-Object -Property EventId, Message, Source, InstanceId, TimeGenerated, ReplacementStrings, UserName |
ConvertTo-Html |
Set-Content -Path $Path

Invoke-Item -Path $Path

PowerShell 技能连载 - 在 Windows 10 中控制控制台的透明度

在 Windows 10 中,当您打开一个 PowerShell 控制体态,只需要按住 CTRL + SHIFT 键,然后滚动鼠标滚轮,就可以改变控制台的背景。

要实现这样的功能,请打开控制台的属性窗口,“使用旧版控制台”选项必须勾选。您可以单击控制台标题栏左上角的应用程序图标,然后选择“属性”来打开控制台属性。

PowerShell 技能连载 - 查找已安装和缺失的更新(第四部分)

有些时候,Microsoft.Update.Session 对象可以用来检查一台机器上是否安装了某个更新。有些作者用这种方法查询更新的标题字符串:

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
#requires -Version 3.0

function Get-UpdateInstalled([Parameter(Mandatory)]$KBNumber)
{
$Session = New-Object -ComObject "Microsoft.Update.Session"
$Searcher = $Session.CreateUpdateSearcher()
$historyCount = $Searcher.GetTotalHistoryCount()

$status = @{
Name="Operation"
Expression= {
switch($_.operation)
{
1 {"Installation"}
2 {"Uninstallation"}
3 {"Other"}
}
}
}

$Searcher.QueryHistory(0, $historyCount) |
Where-Object {$_.Title -like "*KB$KBNumber*" } |
Select-Object -Property Title, $status, Date
}

function Test-UpdateInstalled([Parameter(Mandatory)]$KBNumber)
{
$update = Get-UpdateInstalled -KBNumber $KBNumber |
Where-Object Status -eq Installation |
Select-Object -First 1

return $update -ne $null
}

Test-UpdateInstalled -KBNumber 2267602
Get-UpdateInstalled -KBNumber 2267602 | Out-GridView

请注意这个方法不仅更快,而且由于它将任务分成两个函数,所以您还可以读出所有已安装的更新标题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
PS> Get-UpdateInstalled -KBNumber 2267602

Title Operation Date
----- --------- ----
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.348.0) Installation 28.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.281.0) Installation 27.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.237.0) Installation 26.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.191.0) Installation 25.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.139.0) Installation 24.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.95.0) Installation 22.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.93.0) Installation 22.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.28.0) Installation 21.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.249.13.0) Installation 20.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.247.1068.0) Installation 19.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.247.1010.0) Installation 18.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.247.969.0) Installation 17.07.20...
Definitionsupdate für Windows Defender – KB2267602 (Definition 1.247.966.0) Installation 17.07.20...

PowerShell 技能连载 - 查找已安装和缺失的更新(第三部分)

当您想检查系统中已安装的更新,与其搜索在线更新,并和本地安装的更新比对,更好的方法是查询本地更新历史。

以下代码返回系统中所有已存在的更新。它不需要在线连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#requires -Version 2.0
$Session = New-Object -ComObject "Microsoft.Update.Session"
$Searcher = $Session.CreateUpdateSearcher()
$historyCount = $Searcher.GetTotalHistoryCount()

$status = @{
Name="Operation"
Expression= {
switch($_.operation)
{
1 {"Installation"}
2 {"Uninstallation"}
3 {"Other"}
}
}
}
$Searcher.QueryHistory(0, $historyCount) |
Select-Object Title, Description, Date, $status |
Out-GridView

PowerShell 技能连载 - 查找已安装和缺失的更新(第二部分)

当 PowerShell 通过 Microsoft.Update.Session 对象向 Windows 请求更新时,一些信息似乎无法读取。以下代码获取已安装的更新信息。而 KBArticleIDs 只是显示为 ComObject

1
2
3
4
5
6
7
8
9
#requires -Version 2.0

$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$updates = $Searcher.Search("IsInstalled=1").Updates

$updates |
Select-Object -Property Title, LastDeploy*, Desc*, MaxDownload*, KBArticleIDs |
Out-GridView

要解决这个问题,请使用计算属性。它能将无法读取的 COM 对象通过管道传给 Out-String 命令。通过这种方法,PowerShell 内部的内部逻辑使用它的魔力来解析 COM 对象内容:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#requires -Version 2.0

$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$updates = $Searcher.Search("IsInstalled=1").Updates

$KBArticleIDs = @{
Name = 'KBArticleIDs'
Expression = { ($_.KBArticleIDs | Out-String).Trim() }
}

$updates |
Select-Object -Property Title, LastDeploy*, Desc*, MaxDownload*, $KBArticleIDs |
Out-GridView

PowerShell 技能连载 - 查找已安装和缺失的更新(第一部分)

Windows 可以自动确定系统中缺失的更新,在有 Internet 连接的情况下。PowerShell 可以使用相同的系统接口来查询该信息。以下代码返回系统中所有已安装的更新:

1
2
3
4
5
6
7
8
9
#requires -Version 2.0

$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$updates = $Searcher.Search("IsInstalled=1").Updates

$updates |
Select-Object Title, LastDeployment*, Description, SupportUrl, MsrcSeverity |
Out-GridView

要查看缺失的更新,请将 IsInstalled=1 改为 IsInstalled=0

1
2
3
4
5
6
7
8
9
#requires -Version 2.0

$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$updates = $Searcher.Search("IsInstalled=0").Updates

$updates |
Select-Object Title, LastDeployment*, Description, SupportUrl, MsrcSeverity |
Out-GridView

PowerShell 技能连载 - 自动记录命令输出

在前一个技能中我们介绍了自 PowerShell 3 以上版本支持的 PreCommandLookupAction。今天我们将介绍一个特别的实现。

当您运行一下代码时,PowerShell 将会接受所有以 “*” 开头的命令并将命令的输出记录到一个文本文件中。当命令执行完毕,将会打开该文本文件。

现在您可以运行 *dir 来代替 dir,来保存结果,或用 *Get-Process 代替 Get-Process

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
$ExecutionContext.SessionState.InvokeCommand.PreCommandLookupAction = {
# is called whenever a command is ready to execute
param($command, $eventArgs)

# check commands that start with "*" and were not
# executed internally by PowerShell
if ($command.StartsWith('*') -and $eventArgs.CommandOrigin -eq 'Runspace')
{
# save command output here
$debugPath = "$env:temp\debugOutput.txt"
# clear text file if it exists
$exists = Test-Path $debugPath
if ($exists) { Remove-Item -Path $debugPath }
# remove leading "*" from a command name
$command = $command.Substring(1)
# tell PowerShell what to do instead of
# running the original command
$eventArgs.CommandScriptBlock = {
# run the original command without "*", and
# submit original arguments if there have been any
$(
if ($args.Count -eq 0)
{ & $command }
else
{ & $command $args }
) |
# log output to file
Tee-Object -FilePath $debugPath |
# open the file once all output has been processed
ForEach-Object -Process { $_ } -End {
if (Test-Path $debugPath) { notepad $debugPath }
}
}.GetNewClosure()
}
}

PowerShell 技能连载 - 替换命令

PowerShell 有一系列“秘密”的(更好的说法是没有在文档中体现的)设置。一个是 PreCommandLookupAction,它有一个很强的功能:当 PowerShell 一旦准备好执行一个命令时,就会先执行这个操作。

您的事件处理器可以调整、改变,操作护原始的命令,以及提交给它的参数。

今天我们将用这个简单的特性来秘密地将一个命令替换成另一个:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
$ExecutionContext.SessionState.InvokeCommand.PreCommandLookupAction = {
# is called whenever a command is ready to execute
param($command, $eventArgs)

# not executed internally by PowerShell
if ($command -eq 'Get-Service' -and $eventArgs.CommandOrigin -eq 'Runspace')
{
# tell PowerShell what to do instead of
# running the original command
$eventArgs.CommandScriptBlock = {
# run the original command without "*", and
# submit original arguments if there have been any
$command = 'dir'

$(
if ($args.Count -eq 0)
{ & $command }
else
{ & $command $args }
)
}.GetNewClosure()
}
}

这个事件处理器寻找 Get-Service 命令,并将它替换成 dir。所以当您运行 Get-Service 时,会变成获得一个文件夹列表。当然,这是没有实际意义的,正常情况下应该使用 alias 别名。下一个技巧中,我们将演示一些更有用的例子。