PowerShell 技能连载 - 在 Windows 10 中安装 Linux

Windows 中带有适用于 Linux 的 Windows 子系统 (WSL) 功能,您可以通过它来运行各种 Linux 发型版。通过提升权限的 PowerShell 来启用 WSL:

1
Enable-WindowsOptionalFeature -FeatureName Microsoft-Windows-Subsystem-Linux -Online

下一步,在 Windows 10 中打开 Microsoft Store,并且搜索 “Linux”。安装某个支持的 Linux 发行版(例如,Debian)!

这是全部的步骤。您现在可以在 PowerShell 里或通过开始菜单打开 Debian。只需要运行 “Debian” 命令。当您首次运行时,您需要设置一个 root 账号和密码。

您也许已经知道,PowerShell 是跨平台的,并且可以运行在 Linux 上。如果您希望使用新的 Debian 环境并且测试在 Linux 上运行 PowerShell,请在 Debian 控制台中运行以下代码:

1
2
3
4
5
6
7
sudo apt-get update
sudo apt-get install curl apt-transport-https
curl https://packages.microsoft.com/keys/microsoft.asc | sudo apt-key add -
sudo sh -c 'echo "deb https://packages.microsoft.com/repos/microsoft-debian-stretch-prod stretch main" > /etc/apt/sources.list.d/microsoft.list'
sudo apt-get update
sudo apt-get install -y powershell
pwsh

下一步,从 Debian 中,运行 “pwsh“ 命令切换到 PowerShell 6。

PowerShell 技能连载 - 使用本地化的用户名和组名

以下是一行返回由当前用户 SID 解析成名字的代码:

1
([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Translate( [System.Security.Principal.NTAccount]).Value

您可能会反对说查询 $env:username 环境变量来完成这个目的更容易,的确是这样的。然而在许多场景将 SID 转换为名字很有用。例如,如果您必须知道某个常见账户或组的确切(本地)名字,例如本地的 Administrators 组,只需要使用语言中性的 SID 来翻译它:

1
2
3
PS> ([System.Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate( [System.Security.Principal.NTAccount]).Value

VORDEFINIERT\Administratoren

类似地,以下代码总是列出本地的 Administrators,无论是什么语言的操作系统:

1
2
3
4
5
$admins = ([System.Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate( [System.Security.Principal.NTAccount]).Value
$parts = $admins -split '\\'
$groupname = $parts[-1]

Get-LocalGroupMember -Group $groupname

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
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
function Get-PortInfo
{
param
(
[Parameter(Mandatory)]
[Int]
$Port,

[Parameter(Mandatory)]
[Int]
$TimeoutMilliseconds,

[String]
$ComputerName = $env:COMPUTERNAME
)

# try and establish a connection to port async
$tcpobject = New-Object System.Net.Sockets.TcpClient
$connect = $tcpobject.BeginConnect($computername,$port,$null,$null)

# wait for the connection no longer than $timeoutMilliseconds
$wait = $connect.AsyncWaitHandle.WaitOne($timeoutMilliseconds,$false)

# return rich information
$result = @{
ComputerName = $ComputerName
}

if(!$wait) {
# timeout
$tcpobject.Close()
$result.Online = $false
$result.Error = 'Timeout'
} else {
try {
# try and complete the connection
$null = $tcpobject.EndConnect($connect)
$result.Online = $true
}
catch {
$result.Online = $false
}
$tcpobject.Close()
}
$tcpobject.Dispose()

[PSCustomObject]$result
}

扫描端口现在变得十分简单:

1
2
3
4
5
6
7
8
9
10
11
12
PS> Get-PortInfo -Port 139 -TimeoutMilliseconds 1000

ComputerName Online
------------ ------
DESKTOP-7AAMJLF True


PS> Get-PortInfo -Port 139 -TimeoutMilliseconds 1000 -ComputerName storage2

Error ComputerName Online
----- ------------ ------
Timeout storage2 False

PowerShell 技能连载 - 重制控制台颜色

在 PowerShell 控制台里很容易把控制台颜色搞乱。一个不经意的调用意外地改变了颜色值,或者脚本错误地设置了颜色,可能会导致意外的结果。要验证这种情况,请打开一个 PowerShell 控制台(不是编辑器!),并且运行这段代码:

1
PS> [Console]::BackgroundColor = "Green"

要快速地清除颜色,请运行这段代码:

1
PS> [Console]::ResetColor()

接着运行 Clear-Host 可以清空显示。

PowerShell 技能连载 - 解决 SSL 连接问题

有些时候当您尝试连接 WEB 服务时,PowerShell 可能会弹出无法设置安全的 SSL 通道提示。

让我们来深入了解一下这个问题。以下是一段调用德国铁路系统 WEB 服务的代码。它应该返回指定城市的火车站列表:

1
2
3
4
5
6
7
8
9
$city = "Hannover"
$url = "https://rit.bahn.de/webservices/rvabuchung?WSDL"

$object = New-WebServiceProxy -Uri $url -Class db -Namespace test

$request = [test.SucheBahnhoefeAnfrage]::new()
$request.bahnhofsname = $city
$response = $object.sucheBahnhoefe($request)
$response.bahnhoefe

不过,它弹出了一系列关于 SSL 连接问题的异常。通常,这些错误源于两种情况:

  • 该 WEB 网站用于确立 SSL 连接的 SSL 证书非法、过期,或不被信任。
  • 该 SSL 连接需要某种协议,而该协议没有启用。

要解决这些情况,只需要运行以下代码:

1
2
3
4
5
6
# ignore invalid SSL certificates
[System.Net.ServicePointManager]::ServerCertificateValidationCallback = {$true}

# allowing SSL protocols
$AllProtocols = [Net.SecurityProtocolType]'Ssl3,Tls,Tls11,Tls12'
[Net.ServicePointManager]::SecurityProtocol = $AllProtocols

只要执行一次,对当前的 PowerShell 会话都有效。以上代码现在能够正常返回 Hannover 地区的车站信息。而且如果深入地了解该对象的模型,您甚至可以通过 PowerShell 订火车票。

name                  nummer
----                  ------
Hannover Hbf         8000152
Hannover-Linden/Fi.  8002586
Hannover-Nordstadt   8002576
Hannover Bismarckstr 8002580
Hannover-Ledeburg    8002583
Hannover-Vinnhorst   8006089
Hannover-Leinhausen  8002585
Hannover Wiech-Allee 8002591
Hannover Ander.Misb. 8000578
Hannover Flughafen   8002589
Hannover-Kleefeld    8002584
Hannover-Bornum      8002581
Hann Münden          8006707
HannoverMesseLaatzen 8003487

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)
}

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