PowerShell 技能连载 - 查找重复的文件

在前一个技能中我们介绍了如何用 Get-FileHash cmdlet(PowerShell 5 新增的功能)来生成脚本文件的 MD5 哈希。

哈希可以用来查找重复的文件。大体上,哈希表可以用来检查一个文件哈希是否已经发现过。以下代码检查您的用户配置文件中的所有脚本并且报告重复的文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$dict = @{}

Get-ChildItem -Path $home -Filter *.ps1 -Recurse |
ForEach-Object {
$hash = ($_ | Get-FileHash -Algorithm MD5).Hash
if ($dict.ContainsKey($hash))
{
[PSCustomObject]@{
Original = $dict[$hash]
Duplicate = $_.FullName
}
}
else
{
$dict[$hash]=$_.FullName
}
} |
Out-GridView

PowerShell 技能连载 - 创建 MD5 文件哈希

MD5 文件哈希可以唯一确定文件内容,并且可以用来检测文件内容是否唯一。在 PowerShell 5 中,有一个新的 cmdlet 可以创建文件哈希。以下代码将在您的用户配置文件中查找所有 PowerShell 脚本,并且为每个文件生成 MD5 哈希:

1
2
3
Get-ChildItem -Path $home -Filter *.ps1 -Recurse |
Get-FileHash -Algorithm MD5 |
Select-Object -ExpandProperty Hash

一个更好的方法是将哈希值关联到原始路径上:

1
2
3
4
5
6
7
Get-ChildItem -Path $home -Filter *.ps1 -Recurse |
ForEach-Object {
[PSCustomObject]@{
Hash = ($_ | Get-FileHash -Algorithm MD5).Hash
Path = $_.FullName
}
}

输出结果类似如下:

1
2
3
4
5
6
7
8
9
10
11
Hash                             Path
---- ----
2AE5CA30DCF6550903B994E61A714AC0 C:\Users\tobwe\.nuget\packages\Costura.Fody...
46CB505EECEC72AA8D9104A6263D2A76 C:\Users\tobwe\.nuget\packages\Costura.Fody...
2AE5CA30DCF6550903B994E61A714AC0 C:\Users\tobwe\.nuget\packages\Costura.Fody...
46CB505EECEC72AA8D9104A6263D2A76 C:\Users\tobwe\.nuget\packages\Costura.Fody...
930621EE040F82392017D240CAE13A97 C:\Users\tobwe\.nuget\packages\Fody\2.1.2\T...
39466FE42CE01CC7786D8B446C4C11C2 C:\Users\tobwe\.nuget\packages\MahApps.Metr...
2FF7910807634C984FC704E52ABCDD36 C:\Users\tobwe\.nuget\packages\microsoft.co...
C7E3AAD4816FD98443A7F1C94155954D C:\Users\tobwe\.nuget\packages\microsoft.co...
...

PowerShell 技能连载 - Creating Balloon Tips Safely

受到 MVP 同行 Boe Prox 一篇文章的启发,编写了一个精致的创建气球状提示对话框的函数。您可以在 Boe 的原始文章中找到背景信息:https://mcpmag.com/articles/2017/09/07/creating-a-balloon-tip-notification-using-powershell.aspx

您可以找到许多关于如何显示气球状提示的使用技巧,但大多数都只是一个不能操作的任务栏图标。

以下函数是基于 Boe 的点子,但可以确保不需要全局变量或任何其它会污染 PowerShell 环境的东西。当您调用 Show-BalloonTip 时,一个气球状提示将从桌面的右下角滑入。您可以点击打开这个工具提示并再次关闭它,或者取消它。当您取消它时,它的图标会留在任务栏的托盘区域。当您点击托盘图标,该气球状图标会再次显示。

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
function Show-BalloonTip
{
param
(
[Parameter(Mandatory=$true)][string]$Text,
[string]$Title = "Message from PowerShell",
[ValidateSet('Info','Warning','Error','None')][string]$Icon = 'Info'
)

Add-Type -AssemblyName System.Windows.Forms

# we use private variables only. No need for global scope
$balloon = New-Object System.Windows.Forms.NotifyIcon
$cleanup =
{
# this gets executed when the user clicks the balloon tip dialog

# take the balloon from the event arguments, and dispose it
$event.Sender.Dispose()
# take the event handler names also from the event arguments,
# and clean up
Unregister-Event -SourceIdentifier $event.SourceIdentifier
Remove-Job -Name $event.SourceIdentifier
$name2 = "M" + $event.SourceIdentifier
Unregister-Event -SourceIdentifier $name2
Remove-Job -Name $name2
}
$showBalloon =
{
# this gets executed when the user clicks the tray icon
$event.Sender.ShowBalloonTip(5000)
}

# use unique names for event handlers so you can open multiple balloon tips
$name = [Guid]::NewGuid().Guid

# subscribe to the balloon events
$null = Register-ObjectEvent -InputObject $balloon -EventName BalloonTipClicked -Source $name -Action $cleanup
$null = Register-ObjectEvent -InputObject $balloon -EventName MouseClick -Source "M$name" -Action $showBalloon

# use the current application icon as tray icon
$path = (Get-Process -id $pid).Path
$balloon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($path)

# configure the balloon tip
$balloon.BalloonTipIcon = $Icon
$balloon.BalloonTipText = $Text
$balloon.BalloonTipTitle = $Title

# make the tray icon visible
$balloon.Visible = $true
# show the balloon tip
$balloon.ShowBalloonTip(5000)
}

PowerShell 技能连载 - 补零

您是否曾需要将数字转换为以零开头的字符串,例如生成服务器名?只需要使用 PowerShell 的 -f 操作符:

1
2
$id = 12
'server{0:d4}' -f $id

以下是输出结果:

1
server0012

-f 操作符左边是文本模板,右边是数值。在文本模板中,用 {x} 作为右侧数值的占位符。占位符的下标从 0 开始。

要在左侧补零,使用 d(digit 的缩写)加上您需要的数字位数即可。

PowerShell 技能连载 - 揭开错误处理的秘密

PowerShell 代码中所有的错误信息都包含在错误记录对象中。请看以下的函数,它可以从这样的错误记录中解析所有相关的错误信息:

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
function Get-ErrorInfo
{
param
(
[Parameter(ValueFrompipeline)]
[Management.Automation.ErrorRecord]$errorRecord
)


process
{
$info = [PSCustomObject]@{
Exception = $errorRecord.Exception.Message
Reason = $errorRecord.CategoryInfo.Reason
Target = $errorRecord.CategoryInfo.TargetName
Script = $errorRecord.InvocationInfo.ScriptName
Line = $errorRecord.InvocationInfo.ScriptLineNumber
Column = $errorRecord.InvocationInfo.OffsetInLine
Date = Get-Date
User = $env:username
}

$info
}
}

这个函数使得错误处理代码更短更容易理解。如果您需要立即处理一个错误,请使用 try/catch 概念,并且确保使用 -ErrorAction 通知 cmdlet 当发生错误时立即停止:

1
2
3
4
5
6
7
8
try
{
Stop-Service -Name someservice -ErrorAction Stop
}
catch
{
$_ | Get-ErrorInfo
}

如果您希望代码完成,并且在过后检查发生了哪些错误,请使用 -ErrorAction SilentlyContinue 以及 -ErrorVariable。同时,Get-ErrorInfo 函数有很大帮助:

1
2
$result = Get-ChildItem -Path C:\Windows -Filter *.ps1 -Recurse -ErrorAction SilentlyContinue -ErrorVariable myErrors
$myErrors | Get-ErrorInfo

PowerShell 技能连载 - 远程确定启动时间点和启动以来的时间

Get-CimInstance 是一个用来获取 WMI 信息的有用的 cmdlet,因为它使用标准的 .NET DateTime 对象,而不是奇怪的 WMI datetime 格式。然而,Get-CimInstance 使用 WinRM 来进行远程访问,而传统的 Get-WmiObject 使用 DCOM 来进行远程访问。

非常古老的系统可能还没有配置为使用 WinRM 远程处理,并且可能仍然需要 DCOM。以下是演示如何使用 Get-CimInstance 和 DOM 来查询非常古老的机器的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# change computer name to a valid remote system that you
# can access remotely
$computername = 'server12'

# use DCOM for older systems that do not run with WinRM remoting
$option = New-CimSessionOption -Protocol Dcom
$session = New-CimSession -ComputerName $computername -SessionOption $option

$bootTime = Get-CimInstance -ClassName Win32_OperatingSystem -CimSession $session | Select-Object -ExpandProperty LastBootupTime
$upTime = New-TimeSpan -Start $bootTime

$min = [int]$upTime.TotalMinutes
"Your system is up for $min minutes now."

Remove-CimSession -CimSession $session

PowerShell 技能连载 - 确定启动时间点和启动以来的时间

WMI 可以告诉您系统是什么时候启动的,还可以利用这个信息计算启动以来经历的时间:

1
2
3
4
5
$bootTime = Get-CimInstance -ClassName Win32_OperatingSystem | Select-Object -ExpandProperty LastBootupTime
$upTime = New-TimeSpan -Start $bootTime

$min = [int]$upTime.TotalMinutes
"Your system is up for $min minutes now."

请注意当使用 -ComputerName 访问远程系统时,Get-CimInstance 默认使用 WinRM 远程处理。旧的系统可能没有启用 WinRM 远程处理,而仍然使用 DCOM 技术。

PowerShell 技能连载 - 用管道将信息输出到 Excel

以下是一个短小但是十分有用的函数,能够从其它 cmdlet 接收数据并发送到 Excel:

1
2
3
4
5
6
7
8
9
10
11
function Out-Excel
{

param(
$path = "$env:temp\report$(Get-Date -Format yyyyMMddHHmmss).csv"
)

$Input |
Export-Csv $path -NoTypeInformation -UseCulture -Encoding UTF8
Invoke-Item $path
}

只需要将任何数据通过管道输出至 Out-Excel。例如:

1
PS C:\> Get-Process | Out-Excel

PowerShell 技能连载 - 评价事件日志信息

Get-EventLog 可以访问传统的 Windows 事件日志写入的内容。可以在一个名为 ReplacementStrings 的属性中找到最有价值的信息。以下是一个使该信息可视化并且可以利用它来生成报告的方法。

在这个例子中,将获取 Windows Update 客户端写入的 ID 为 44 的事件,并且这段代码输出替换的字符串。它们将精确地告知我们何时下载了哪些更新:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Get-EventLog -LogName System -InstanceId 44 -Source Microsoft-Windows-WindowsUpdateClient |
ForEach-Object {

$hash = [Ordered]@{}
$counter = 0
foreach($value in $_.ReplacementStrings)
{
$counter++
$hash.$counter = $value
}
$hash.EventID = $_.EventID
$hash.Time = $_.TimeWritten
[PSCustomObject]$hash


}

始终确保查询一个唯一的事件 ID:对于每个事件 ID,ReplacementStrings 中的信息是唯一的,您一定不希望将不同的事件 ID 类型中的信息混在一起。

PowerShell 技能连载 - 转换奇怪的数据格式

有些时候,您会被奇怪的数据格式难住,例如在 log 文件中,它无法自动转换为 DateTime 对象。以下是一个快速的解析此类日期时间信息的方法:

1
2
3
$weirdDate = '03 12 --- 1988'

[DateTime]::ParseExact($weirdDate, 'MM dd --- yyyy', $null)

如您所见,ParseExact() 用标准的 .NET 日期和时间字符,如您所愿处理自定义日期和时间格式。以下是大小写敏感的:

yy,yyyy: Year
M, MM, MMM, MMMM: Month
d,dd,ddd,dddd: Day
H, HH: Hour (24hr clock)
h,hh: Hour (12hr clock)
m,mm: Minute
s,ss: Second