PowerShell 技能连载 - 删除日期最早的日志文件

如果您正在将日志活动写入文件,可能需要清除一些东西,例如在增加一个新文件的时候总是需要删除最旧的日志文件。

以下是一个简单的实现:

1
2
3
4
5
6
7
8
9
10
11
12
# this is the folder keeping the log files
$LogFileDir = "c:\myLogFiles"

# find all log files...
Get-ChildItem -Path $LogFileDir -Filter *.log |
# sort by last change ascending
# (oldest first)...
Sort-Object -Property LastWriteTime |
# take the first (oldest) one
Select-Object -First 1 |
# remove it (remove -whatif to actually delete)
Remove-Item -WhatIf

如果只希望保留最新的 5 个文件,请像这样更改:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# this is the folder keeping the log files
$LogFileDir = "c:\myLogFiles"
$Keep = 5

# find all log files...
$files = @(Get-ChildItem -Path $LogFileDir -Filter *.log)
$NumberToDelete = $files.Count - $Keep

if ($NumberToDelete -gt 0)
{
$files |
# sort by last change ascending
# (oldest first)...
Sort-Object -Property LastWriteTime |
# take the first (oldest) one
Select-Object -First $NumberToDelete |
# remove it (remove -whatif to actually delete)
Remove-Item -WhatIf
}

PowerShell 技能连载 - 用 ForEach 实现实时流

传统的 foreach 循环是最快速的循环方式,但是它有一个严重的限制。foreach 循环不支持管道。用户只能等待整个 foreach 循环结束才能处理结果。

以下是一些演示这个情况的示例。在以下代码中,您需要等待一段很长的时间才能“看见”执行结果:

1
2
3
4
5
6
7
8
$result = foreach ($item in $elements)
{
"processing $item"
# simulate some work and delay
Start-Sleep -Milliseconds 50
}

$result | Out-GridView

您无法直接通过管道输出结果。以下代码会产生语法错误:

1
2
3
4
5
6
7
8
$elements = 1..100

Foreach ($item in $elements)
{
"processing $item"
# simulate some work and delay
Start-Sleep -Milliseconds 50
} | Out-GridView

可以使用 $() 语法来使用管道,但是仍然要等待循环结束并且将整个结果作为一个整体发送到管道:

1
2
3
4
5
6
7
8
$elements = 1..100

$(foreach ($item in $elements)
{
"processing $item"
# simulate some work and delay
Start-Sleep -Milliseconds 50
}) | Out-GridView

一下是一个鲜为人知的技巧,向 foreach 循环增加实时流功能:只需要使用一个脚本块!

1
2
3
4
5
6
7
8
$elements = 1..100

& { foreach ($item in $elements)
{
"processing $item"
# simulate some work and delay
Start-Sleep -Milliseconds 50
}} | Out-GridView

现在您可以“看到”它们处理的结果,并且享受实时流的效果。

让然,您可以一开始就不使用 foreach,而是使用 ForEach-Object 管道 cmdlet 来代替:

1
2
3
4
5
6
7
8
9
$elements = 1..100

$elements | ForEach-Object {
$item = $_

"processing $item"
# simulate some work and delay
Start-Sleep -Milliseconds 50
} | Out-GridView

但是,ForEach-Objectforeach 关键字慢得多,并且有些场景无法使用 ForEach-Object。例如,在许多数据库代码中,代码需要一次次地检测结束标记,所以无法使用 ForEach-Object

PowerShell 技能连载 - 列出网络驱动器

有着许多方法可以创建网络驱动器的列表。其中一个需要调用 COM 接口。这个接口也可以通过 VBScript 调用。我们将利用它演示一个特殊的 PowerShell 技术。

要列出所有网络驱动器,只需要运行这几行代码:

1
2
$obj = New-Object -ComObject WScript.Network
$obj.EnumNetworkDrives()

结果类似这样:

1
2
3
4
5
6
PS> $obj.EnumNetworkDrives()

X:
\\storage4\data
Z:
\\127.0.0.1\c$

这个方法对每个网络驱动器返回两个字符串:挂载的驱动器号,以及原始 URL。要将它转为有用的东西,您需要创建一个循环,每次迭代返回两个元素。

以下是一个聪明的方法来实现这个目标:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
$obj = New-Object -ComObject WScript.Network
$result = $obj.EnumNetworkDrives()

Foreach ($entry in $result)
{
$letter = $entry
$null = $foreach.MoveNext()
$path = $foreach.Current


[PSCustomObject]@{
DriveLetter = $letter
UNCPath = $path
}
}

foreach 循环中,有一个很少人知道的自动变量,名为 $foreach,它控制着迭代。当您调用 MoveNext() 方法时,它对整个集合迭代,移动到下一个元素。通过 Current 属性,可以读取到迭代器的当前值。

通过这种方法,循环每次处理两个元素,而不仅仅是一个。两个元素合并为一个自定义对象。结果看起来类似这样:

DriveLetter UNCPath
----------- -------
X:          \\storage4\data
Z:          \\127.0.0.1\c$

PowerShell 技能连载 - 通过 Outlook 发送邮件

您可以用 Send-MailMessage 通过任何 SMTP 服务器放松右键。不过,如果您希望使用 Outlook 客户端,例如要访问地址簿、使用公司的 Exchange 服务器,或者将邮件保存到邮件历史中,那么可以通过这个快速方法来实现:

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
function Send-OutlookMail
{
param
(
[Parameter(Mandatory=$true)]
$To,

[Parameter(Mandatory=$true)]
$Subject,

[Parameter(Mandatory=$true)]
$BodyText,

[Parameter(Mandatory=$true)]
$AttachmentPath
)


$outlook = New-Object -ComObject Outlook.Application
$mail = $outlook.CreateItem(0)
$mail.Subject = $Subject
$mail.to = $To

$mail.BodyFormat = 1 # use 2 for HTML mails
$mail.Attachments.Add([object]$AttachmentPath, 1)
$mail.building = $BodyText
$mail.Display($false)
}

这只是一个基本的模板,您可以投入一些时间使它变得更好。例如,当前版本总是需要一个附件。在更复杂的版本中,您可以使附件变成可选的,并且支持多个附件。或者寻找一种方法发送邮件而不需要用户交互。

PowerShell 技能连载 - 首字母大写

修饰一段文本并不太简单,如果您希望名字或者文本格式正确,并且每个单词都以大写开头,那么工作量通常很大。

有趣的是,每个 CultureInfo 对象有一个内置的 ToTitleCase() 方法,可以完成上述工作。如果您曾经将纯文本转换为全部消协,那么它也可以处理所有大写的单词:

1
2
3
4
5
$text = "here is some TEXT that I would like to title-case (all words start with an uppercase letter)"

$textInfo = (Get-Culture).TextInfo
$textInfo.ToTitleCase($text)
$textInfo.ToTitleCase($text.ToLower())

以下是执行结果:

Here Is Some TEXT That I Would Like To Title-Case (All Words Start With An Upper Letter
Here Is Some Text That I Would Like To Title-Case (All Words Start With An Upper Letter

This method may be especially useful for list of names.
这个方法对于姓名列表很有用。

PowerShell 技能连载 - 连接文本文件

假设一个脚本已经向某个文件夹写入了多个日志文件,所有文件名都为 *.log。您可能希望将它们合并为一个大文件。以下是一个简单的实践:

1
2
3
4
5
6
$OutPath = "$env:temp\summary.log"

Get-Content -Path "C:\Users\tobwe\Documents\ScriptOutput\*.log" |
Set-Content $OutPath

Invoke-Item -Path $OutPath

然而,这个方法并不能提供充分的控制权:所有文件需要放置在同一个文件夹中,并且必须有相同的文件扩展名,而且您无法控制它们合并的顺序。

一个更多功能的方法类似这样:

1
2
3
4
5
6
7
8
$OutPath = "$env:temp\summary.log"

Get-ChildItem -Path "C:\Users\demouser\Documents\Scripts\*.log" -Recurse -File |
Sort-Object -Property LastWriteTime -Descending |
Get-Content |
Set-Content $OutPath

Invoke-Item -Path $OutPath

它利用了 Get-ChildItem 的灵活性,而且可以在读取内容之前对文件排序。通过这种方法,日志保持了顺序,并且最终的日志信息总是在日志文件的最上部。

PowerShell 技能连载 - 查看 Windows 通用唯一识别码 (UUID)

每个 Windows 的安装都有一个唯一的 UUID,您可以用它来区分机器。计算机名可能会改变,但 UUID 不会。

1
2
PS> (Get-CimInstance -Class Win32_ComputerSystemProduct).UUID
4C4C4544-004C-4710-8051-C4C04F443732

In reality, the UUID is just a GUID (Globally Unique Identifier), which comes in different formats:
实际中,UUID 只是一个 GUID(全局唯一标识符),它的格式有所不同:

1
2
3
4
5
6
7
$uuid = (Get-CimInstance -Class Win32_ComputerSystemProduct).UUID
[Guid]$guid = $uuid

"d","n","p","b","x" |
ForEach-Object {
'$guid.ToString("{0}") = {1}' -f $_, $guid.ToString($_)
}

以下是执行结果:

$guid.ToString("d")= 4c4c4544-004c-4710-8051-c4c04f443732
$guid.ToString("n")= 4c4c4544004c47108051c4c04f443732
$guid.ToString("p")= (4c4c4544-004c-4710-8051-c4c04f443732)
$guid.ToString("b")= {4c4c4544-004c-4710-8051-c4c04f443732}
$guid.ToString("x")= {0x4c4c4544,0x004c,0x4710,{0x80,0x51,0xc4,0xc0,0x4f,0x44,0x37,0x32}}

如果您希望为某个想标记的东西创建一个新的 UUID(或 GUID),例如临时文件名,那么可以用 PowerShell 5 新带来的 New-Guid 命令:

1
2
3
4
5
PS> New-Guid

Guid
----
16750457-9a7e-4510-96ab-f9eef7273f3e

它实质上是在后台调用了这个 .NET 方法:

1
2
3
4
5
PS> [Guid]::NewGuid()

Guid
----
6cb3cb1a-b094-425b-8ccb-e74c2034884f

PowerShell 技能连载 - 格式化日期和时间(包含区域性)

在前一个技能中我们演示了 Get-Date 如何用格式化字符串将 DateTime 值转换为字符串。不过该字符串转换总是使用操作系统中的语言。这可能不是您想要的。现在我们来解决这个问题:

以下是输出 2018 圣诞前夕是周几的示例:

1
2
3
$christmasEve = Get-Date -Date '2018-12-24'

Get-Date -Date $christmasEve -Format '"Christmas Eve in" yyyy "will be on" dddd.'

显然这是在德文系统上做的转换,所以结果中的周几是用德文显示的:

Christmas Eve in 2018 will be on Montag.

如果您的脚本需要输出不同语言的结果,例如以英语(或其他语言)的方式来输出星期几。要控制语言,您需要意识到两件事:第一,Get-Date-Format 的格式化选项只是通用的 .NET 方法 ToString() 的简单封装,所以您也可以运行这段代码获得相同的结果:

1
2
3
$christmasEve = Get-Date -Date '2018-12-24'

$christmasEve.ToString('"Christmas Eve in" yyyy "will be on" dddd.')

第二,ToString() 方法有许多重载,其中一个能接受任何实现了 IFormatProvider 接口的对象,它们恰好包含了 “CultureInfo“ 对象:

1
2
3
4
5
6
7
8
9
10
PS> $christmasEve.ToString

OverloadDefinitions
-------------------
string ToString()
string ToString(string format)
string ToString(System.IFormatProvider provider)
string ToString(string format, System.IFormatProvider provider)
string IFormattable.ToString(string format, System.IFormatProvider formatProvider)
string IConvertible.ToString(System.IFormatProvider provider)

以下是无论在什么语言的操作系统上都以英文输出周几的解决方案:

1
2
3
4
5
6
$christmasEve = Get-Date -Date '2018-12-24'
$culture = [CultureInfo]'en-us'
$christmasEve.ToString('"Christmas Eve in" yyyy "will be on" dddd.', $culture)


Christmas Eve in 2018 will be on Monday.

如果要显示其它地区的语言,例如要查看中文或泰文中“星期一”的表达:

1
2
3
4
5
6
7
8
9
10
$christmasEve = Get-Date -Date '2018-12-24'
$culture = [CultureInfo]'zh'
$christmasEve.ToString('"Monday in Chinese: " dddd.', $culture)
$culture = [CultureInfo]'th'
$christmasEve.ToString('"Monday in Thai: " dddd.', $culture)



Monday in Chinese: 星期一.
Monday in Thai: จันทร์.

PowerShell 技能连载 - 格式化日期和时间

通过 Get-Date-Format 参数可以方便地将日期和时间格式化为您所需的格式。您可以对当前时间使用它,也可以对外部的 DateTime 变量使用它。只需要使用日期和时间的格式化字符串就可以转换为您所需的输出格式。

以下是一些例子。例如要将当前日期暗 ISO 格式输出,请运行以下代码:

1
2
PS> Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
2018-12-02 11:36:37

使用现有的或从别处读取的 datetime 对象的方法是 将它传给 Get-Date-Date 属性:

1
2
3
4
5
6
7
8
9
# find out last boot time
$os = Get-CimInstance -ClassName Win32_OperatingSystem
$lastBoot = $os.LastBootUpTime

# raw datetime output
$lastBoot

# formatted string output
Get-Date -Date $lastBoot -Format '"Last reboot at" MMM dd, yyyy "at" HH:mm:ss "and" fffff "Milliseconds.

格式化字符串既可以包括日期时间的通配符,也可以包括静态文本。只需要确保用双引号将静态文本包括起来。以下是执行结果(在德语系统上):

Donnerstag, 22. November 2018 01:13:44

Last reboot at Nov 22, 2018 at 01:13:44 and 50000 Milliseconds.

PowerShell 技能连载 - 将 PowerShell 结果发送到 PDF(第 4 部分)

在前一个技能中我们将建了 Out-PDFFile 函数,能够接受任意 PowerShell 的结果数据并将它们转换为 PDF 文件——使用 Windows 10 和 Windows Server 2016 内置的打印驱动。

我们使用了一个简单的函数,用 $Input 自动变量来读取管道输入的数据,从而达到上述目的。如果您更希望使用高级函数,利用它们的必选参数等功能,我们将该工程包装成一个更优雅的高级函数,它能够在检测到未安装 PDF 打印机的情况下先安装它:

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
function Out-PDFFile
{
param
(
[Parameter(Mandatory)]
[String]
$Path,

[Parameter(ValueFromPipeline)]
[Object]
$InputObject,

[Switch]
$Open
)

begin
{
# check to see whether the PDF printer was set up correctly
$printerName = "PrintPDFUnattended"
$printer = Get-Printer -Name $printerName -ErrorAction SilentlyContinue
if (!$?)
{
$TempPDF = "$env:temp\tempPDFResult.pdf"
$port = Get-PrinterPort -Name $TempPDF -ErrorAction SilentlyContinue
if ($port -eq $null)
{
# create printer port
Add-PrinterPort -Name $TempPDF
}

# add printer
Add-Printer -DriverName "Microsoft Print to PDF" -Name $printerName -PortName $TempPDF
}
else
{
# this is the file the print driver always prints to
$TempPDF = $printer.PortName

# is the port name is the output file path?
if ($TempPDF -notlike '?:\*')
{
throw "Printer $printerName is not set up correctly. Remove the printer, and try again."
}
}

# make sure old print results are removed
$exists = Test-Path -Path $TempPDF
if ($exists) { Remove-Item -Path $TempPDF -Force }

# create an empty arraylist that takes the piped results
[Collections.ArrayList]$collector = @()
}

process
{
$null = $collector.Add($InputObject)
}

end
{
# send anything that is piped to this function to PDF
$collector | Out-Printer -Name $printerName

# wait for the print job to be completed, then move file
$ok = $false
do {
Start-Sleep -Milliseconds 500
Write-Host '.' -NoNewline

$fileExists = Test-Path -Path $TempPDF
if ($fileExists)
{
try
{
Move-Item -Path $TempPDF -Destination $Path -Force -ea Stop
$ok = $true
}
catch
{
# file is still in use, cannot move
# try again
}
}
} until ( $ok )
Write-Host

# open file if requested
if ($Open)
{
Invoke-Item -Path $Path
}
}
}

假设您使用的是 Windows 10 或 Windows 2016,并且 “Microsoft Print to PDF” 打印机可用,那么您可以像这样方便地创建 PDF 文档:

1
2
3
PS> Get-Service | Out-PDFFile -Path $home\desktop\services.pdf -Open

PS> Get-ComputerInfo | Out-PDFFile -Path $home\Desktop\computerinfo.pdf -Open

如果指定的 “PrintPDFUnattended” 打印机还未安装,该函数也会事先安装该打印机。