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 别名。下一个技巧中,我们将演示一些更有用的例子。

PowerShell 技能连载 - 简单解析设置文件(第三部分)

在前一个技能中我们了解了 ConvertFrom-StringData 如何将纯文本的键值对转换为哈希表。还缺少另一个方向的操作:将哈希表转为纯文本。有了它以后,您就拥有了一个将设置和信息保存到文件的小型框架。

我们首先创建一个包含一些数据的哈希表:

1
2
3
4
5
6
7
$test = @{
Name = 'Tobias'
ID = 12
Conf = 'PowerShell Conference EU'
}

$test

结果看起来如下:

1
2
3
4
5
Name                           Value
---- -----
Conf PowerShell Conference EU
Name Tobias
ID 12

以下是名为 ConvertFrom-Hashtable 的函数,传入一个哈希表,并将它转换为纯文本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
filter ConvertFrom-Hashtable
{
$_.GetEnumerator() |
ForEach-Object {
# get hash table key and value
$value = $_.Value
$name = $_.Name

# escape "\" in strings
if ($value -is [string]) { $value = $value.Replace('\','\\') }

# compose key-value pair as plain text
'{0}={1}' -f $Name, $value
}
}

让我们看看哈希表是如何转换的:

1
2
3
4
5
6
PS> $test | ConvertFrom-Hashtable
Conf=PowerShell Conference EU
Name=Tobias
ID=12

PS>

您可以用 ConvertFrom-StringData 转换到另一种形式:

1
2
3
4
5
6
7
8
9
10
11
PS> $test | ConvertFrom-Hashtable | ConvertFrom-StringData

Name Value
---- -----
Conf PowerShell Conference EU
Name Tobias
ID 12



PS>

所以基本上,您可以将哈希表保存为纯文本,并在稍后使用它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$test = @{
Name = 'Tobias'
ID = 12
Conf = 'PowerShell Conference EU'
}

$path = "$env:temp\settings.txt"

# save hash table as file
$test | ConvertFrom-Hashtable | Set-Content -Path $path -Encoding UTF8
notepad $path

# read hash table from file
Get-Content -Path $path -Encoding UTF8 |
ConvertFrom-StringData |
Out-GridView

请注意这种方法对简单的字符串和数字型数据有效。它不能处理复杂数据类型,因为这个转换操作并不能序列化对象。

PowerShell 技能连载 - 简单解析设置文件(第二部分)

在前一个技能中我们使用了 ConvertFrom-StringData 来将纯文本键值对转换为哈希表。

以下是一个转换失败的例子:

1
2
3
4
5
6
$settings = @'
Machine=Server12
Path=c:\test
'@

$settings | ConvertFrom-StringData

当您查看结果时,很快能发现失败的原因:

1
2
3
4
Name                           Value
---- -----
Machine Server12
Path c: est

显然,ConvertFrom-StringData 将 “\“ 视为一个转义符,在上述例子中增加了一个制表符 (“\t“),并吃掉了字面量 “t”。

要解决这个问题,请始终将 “\“ 转义为 “\\“。以下是正确的代码:

1
2
3
4
5
6
$settings = @'
Machine=Server12
Path=c:\\test
'@

$settings | ConvertFrom-StringData

现在结果看起来正确了:

1
2
3
4
Name                           Value
---- -----
Machine Server12
Path c:\test

PowerShell 技能连载 - 简单解析设置文件(第一部分)

假设您需要将设置用最简单的方式保存到一个文件中。设置文件可能看起来像这样:

1
2
3
4
5
6
7
$settings = '
Name=Weltner
FirstName=Tobias
ID=12
Country=Germany
Conf=psconf.eu
'

您可以将这些设置用 Set-Content 保存到文件中,并用 Get-Content 再把它们读出来。

那么,如何解析该信息,来存取独立的项目呢?有一个名为 ConvertFrom-StringData 的 cmdlet,可以将键值对转化为哈希表:

1
2
3
4
5
6
7
8
9
10
11
12
$settings = @'
Name=Weltner
FirstName=Tobias
ID=12
Country=Germany
Conf=psconf.eu
'@

$hash = $settings | ConvertFrom-StringData

$hash.Name
$hash.Country

PowerShell 技能连载 - 在 PowerShell 标题栏中添加实时时钟(第二部分)

在前一个技能中我们演示了一段代码,可以在后台线程中更新 PowerShell 标题栏,显示一个实时时钟。

难道不能更完美一点,显示当前的路径位置?挑战之处在于,如何让后台线程知道前台 PowerShell 的当前路径?

有一个名为 $ExecutionContext 的 PowerShell 变量可以提供关于上下文状态的各种有用信息,包括当前路径。我们将 $ExecutionContext 从前台传递给后台线程,该后台线程就可以显示当前前台的路径。

试试看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$code =
{
# submit the host process RawUI interface and the execution context
param($RawUi, $ExecContext)

do
{
# find the current location in the host process
$location = $ExecContext.SessionState.Path.CurrentLocation
# compose the time and date display
$time = Get-Date -Format 'HH:mm:ss dddd MMMM d'
# compose the title bar text
$title = "$location $time"
# output the information to the title bar of the host process
$RawUI.WindowTitle = $title
# wait a half second
Start-Sleep -Milliseconds 500
} while ($true)
}
$ps = [PowerShell]::Create()
$null = $ps.AddScript($code).AddArgument($host.UI.RawUI).AddArgument($ExecutionContext)
$handle = $ps.BeginInvoke()

当您运行这段代码时,PowerShell 的状态栏显示当前路径和实时时钟。当您切换当前路径时,例如运行 “cd c:\windows“,标题栏会立刻更新。

用以上代码可以处理许多使用场景:

  • 您可以在午餐时间快到时显示通知
  • 您可以在指定时间之后结束 PowerShell 会话
  • 您可以在标题栏中显示 RSS 订阅项目

PowerShell 技能连载 - 在 PowerShell 标题栏中添加实时时钟(第一部分)

要持续地更新 PowerShell 的标题栏,例如显示当前的日期和时间,您需要一个后台线程来处理这个工作。如果没有后台线程,PowerShell 会一直不停地忙于更新标题栏,导致无法使用。

以下是一个演示了如何在标题栏显示实时时钟的代码片段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$code =
{
# submit the host process RawUI interface and the execution context
param($RawUi)

do
{
# compose the time and date display
$time = Get-Date -Format 'HH:mm:ss dddd MMMM d'
# compose the title bar text
$title = "Current Time: $time"
# output the information to the title bar of the host process
$RawUI.WindowTitle = $title
# wait a half second
Start-Sleep -Milliseconds 500
} while ($true)
}
$ps = [PowerShell]::Create()
$null = $ps.AddScript($code).AddArgument($host.UI.RawUI)
$handle = $ps.BeginInvoke()

这里最关键的是将 $host.UI.RawUI 对象从 PowerShell 前台传递给后台线程代码。只有这样,后台线程才能存取 PowerShell 前台拥有的标题栏对象。