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” 打印机还未安装,该函数也会事先安装该打印机。

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

在前一个技能中我们解释了如何在 Windows 10 和 Windows Server 2016 中使用 PowerShell 来设置一个能将任何东西打印到 PDF 文件的打印机,当然,是无人值守的。要使它真的发挥作用,我们将它封装为一个名为 Out-PDFFile 的函数。任何通过管道传给这个新命令的内容都会被转换为一个 PDF 文件。

注意:要让这个函数生效,您必须按前一个技能介绍的方法先创建一个名为 PrintPDFUnattended 的打印机!

以下是 Out-PDFFile 函数:

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
function Out-PDFFile
{
param
(
$Path = "$env:temp\results.pdf",

[Switch]
$Open
)

# check to see whether the PDF printer was set up correctly
$printerName = "PrintPDFUnattended"
$printer = Get-Printer -Name $printerName -ErrorAction SilentlyContinue
if (!$?)
{
Write-Warning "Printer $printerName does not exist."
Write-Warning "Make sure you have created this printer (see previous tips)!"
return
}

# this is the file the print driver always prints to
$TempPDF = $printer.PortName

# is the printer set up correctly and the port name is the output file path?
if ($TempPDF -notlike '?:\*')
{
Write-Warning "Printer $printerName is not set up correctly."
Write-Warning "Make sure you have created this printer as instructed (see previous tips)!"
return
}

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

# send anything that is piped to this function to PDF
$input | 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
}
}

现在导出结果到 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

哇哦,真简单!

请注意我们有意地创建了一个“简单函数”。通过这种方式,所有通过管道输入的数据都可以在 $Input 自动变量中见到。如果您向参数添加属性,例如要使参数成为必选的,这个函数就变成了“高级函数”,并且 $Input 就不存在了。我们将在明天解决这个问题。


psconf.eu – PowerShell Conference EU 2019 – June 4-7, Hannover Germany – visit www.psconf.eu There aren’t too many trainings around for experienced PowerShell scripters where you really still learn something new. But there’s one place you don’t want to miss: PowerShell Conference EU - with 40 renown international speakers including PowerShell team members and MVPs, plus 350 professional and creative PowerShell scripters. Registration is open at www.psconf.eu, and the full 3-track 4-days agenda becomes available soon. Once a year it’s just a smart move to come together, update know-how, learn about security and mitigations, and bring home fresh ideas and authoritative guidance. We’d sure love to see and hear from you!

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

在前一个示例中我们延时了如何使用内置的 “Microsoft Print to PDF” 打印机来将 PowerShell 输出结果发送到 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
# requires Windows 10 / Windows Server 2016 or better

# choose a name for your new printer
$printerName = 'PrintPDFUnattended'
# choose a default path where the PDF is saved
$PDFFilePath = "$env:temp\PDFResultFile.pdf"
# choose whether you want to print a test page
$TestPage = $true

# see whether the driver exists
$ok = @(Get-PrinterDriver -Name "Microsoft Print to PDF" -ea 0).Count -gt 0
if (!$ok)
{
Write-Warning "Printer driver 'Microsoft Print to PDF' not available."
Write-Warning "This driver ships with Windows 10 or Windows Server 2016."
Write-Warning "If it is still not available, enable the 'Printing-PrintToPDFServices-Features'"
Write-Warning "Example: Enable-WindowsOptionalFeature -Online -FeatureName Printing-PrintToPDFServices-Features"
return
}

# check whether port exists
$port = Get-PrinterPort -Name $PDFFilePath -ErrorAction SilentlyContinue
if ($port -eq $null)
{
# create printer port
Add-PrinterPort -Name $PDFFilePath
}

# add printer
Add-Printer -DriverName "Microsoft Print to PDF" -Name $printerName -PortName $PDFFilePath

# print a test page to the printer
if ($TestPage)
{
$printerObject = Get-CimInstance Win32_Printer -Filter "name LIKE '$printerName'"
$null = $printerObject | Invoke-CimMethod -MethodName printtestpage
Start-Sleep -Seconds 1
Invoke-Item -Path $PDFFilePath
}

当这段脚本执行以后,可以获得一个全新的名为 PrintPDFUnattended 的打印机。并且当打印到该打印机不会产生提示,而是永远输出到临时文件夹的 PDFResultFile.pdf

以下是一段演示如何从 PowerShell 打印 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
# specify the path to the file you want to create
# (adjust if you want)
$OutPath = "$home\desktop\result.pdf"

# this is the file the print driver always prints to
$TempPDF = "$env:temp\PDFResultFile.pdf"

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

# send PowerShell results to PDF
Get-Service | Out-Printer -Name "PrintPDFUnattended"

# 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 $OutPath -Force -ErrorAction Stop
$ok = $true
}
catch
{
# file is still in use, cannot move
# try again
}
}
} until ( $ok )
Write-Host

# show new PDF file in explorer
explorer "/select,$OutPath"

当运行这段代码时,将会在桌面上创建一个新的 result.pdf 文件。它包含了所有服务的列表。您可以将任何结果通过管道输出到 Out-Printer 来创建 PDF 文件。