PowerShell 技能连载 - 使用工作流来并发执行代码

如果您希望同时执行多个任务,以下有多种方法用 Powershell 实现。一种是使用工作流。它们是 PowerShell 3.0 中引入的:

#requires -Version 3
workflow Test-ParallelForeach
{
  param
  (
    [String[]]
    $ComputerName
  )

  foreach -parallel -throttlelimit 8 ($Machine in $ComputerName)
  {
    "Begin $Machine"
    Start-Sleep -Seconds (Get-Random -min 3 -max 5)
    "End $Machine"
  }
}

$list = 1..20

Test-ParallelForeach -ComputerName $list | Out-GridView

Test-ParallelForeach 处理一个计算机列表(在这个例子中,是一个数字列表)。它们同时执行。要控制资源的使用,并行循环将节流限制为 8,所以所以在这个例子中的 20 台计算机是 8 个一组处理的。

请注意使用工作流需要更多地了解它们的架构和限制。这个例子关注于工作流提供的并行循环技术。

PowerShell 技能连载 - 不要混合不同的对象

如果您连续输出完全不同的对象,您可能丢失信息。请看这个例子:

#requires -Version 2

$hash = @{
Name = 'PowerShell Conference EU'
Date = 'April 20, 2016'
City = 'Hannover'
URL = 'www.psconf.eu'
}
New-Object -TypeName PSObject -Property $hash

$b = Get-Process -Id $pid
$b

当您运行这段代码时,您将得到这样的结果:

Date           URL           Name                     City
----           ---           ----                     ----
April 20, 2016 www.psconf.eu PowerShell Conference EU Hannover
                             powershell_ise

看起来 $b (process) 的几乎所有属性都丢失了。原因是 PowerShell 是实时输出对象的,而且首次提交的对象决定了在表格中显示哪些属性。所有接下来的对象都将纳入这张表格中。

如果您必须要输出不同的对象,请将它们用管道输出到 Out-Host。每次您输出到 Out-Host,PowerShell 都将创建一个具有新的表格标题的输出。

PowerShell 技能连载 - 得到一个借口

以下是一个快速的方法来得到一个借口——假设您有 Internet 连接:

#requires -Version 3
function Get-Excuse
{
  $url = 'http://pages.cs.wisc.edu/~ballard/bofh/bofhserver.pl'
  $ProgressPreference = 'SilentlyContinue'
  $page = Invoke-WebRequest -Uri $url -UseBasicParsing
  $pattern = '(?m)<br><font size = "\+2">(.+)'
  if ($page.Content -match $pattern)
  {
    $matches[1]
  }
}

它演示了如何使用 Invoke-WebRequest 来下载一个网页的 HTML 内容,然后使用正则表达式来抓取网页的内容。

PowerShell 技能连载 - 谁在监听?(第二部分)

如果您的系统是 Windows 8 或 Windows Server 2012 或以上版本,您可以使用 Get-NetTcpConnection 来找出哪个网络端口正在使用中,以及谁在监听这些端口。

以下脚本不仅列出正在使用的端口而且列出了正在监听该端口的进程。如果进程是 “svchost”,该脚本还会找出是哪个服务启动了这个进程:

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
#requires -Version 3 -Modules NetTCPIP

$service = @{}
$Process = @{
Name = 'Name'
Expression = {
$id = $_.OwningProcess
$name = (Get-Process -Id $id).Name
if ($name -eq 'svchost')
{
if ($service.ContainsKey($id) -eq $false)
{
$service.$id = Get-WmiObject -Class win32_Service -Filter "ProcessID=$id" | Select-Object -ExpandProperty Name
}
$service.$id -join ','
}
else
{
$name
}
}
}

Get-NetTCPConnection |
Select-Object -Property LocalPort, OwningProcess, $Process |
Sort-Object -Property LocalPort, Name -Unique

结果类似如下:

LocalPort OwningProcess Name
--------- ------------- ----
      135           916 RpcEptMapper,RpcSs
      139             4 System
      445             4 System
     5354          2480 mDNSResponder
     5985             4 System
     7680           544 Appinfo,BITS,Browser,CertPropSvc,DoSvc,iphlpsvc,Lanm...
     7779             4 System
    15292          7364 Adobe Desktop Service
    27015          2456 AppleMobileDeviceService
(...)

PowerShell 技能连载 - 谁在监听?(第一部分)

一个过去十分好用的 netstat.exe 可以告诉您应用程序在监听哪些端口,不过结果是纯文本。PowerShell 可以用正则表达式将文本分割成 CSV 数据,ConvertFrom-Csv 可以将文本转换为真实的对象。

这是一个如何用 PowerShell 处理最基础数据的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#requires -Version 2
NETSTAT.EXE -anop tcp|
Select-Object -Skip 4|
ForEach-Object -Process {
[regex]::replace($_.trim(),'\s+',' ')
}|
ConvertFrom-Csv -d ' ' -Header 'proto', 'src', 'dst', 'state', 'pid'|
Select-Object -Property src, state, @{
name = 'process'
expression = {
(Get-Process -PipelineVariable $_.pid).name
}
} |
Format-List

结果类似如下:

src     : 0.0.0.0:135
state   : LISTEN
process : {Adobe CEF Helper, Adobe CEF Helper, Adobe Desktop Service,
          AdobeIPCBroker...}

src     : 0.0.0.0:445
state   : LISTEN
process : {Adobe CEF Helper, Adobe CEF Helper, Adobe Desktop Service,
          AdobeIPCBroker...}

src     : 0.0.0.0:5985
state   : LISTEN
process : {Adobe CEF Helper, Adobe CEF Helper, Adobe Desktop Service,
          AdobeIPCBroker...}

src     : 0.0.0.0:7680
state   : LISTEN
process : {Adobe CEF Helper, Adobe CEF Helper, Adobe Desktop Service,
          AdobeIPCBroker...}

src     : 0.0.0.0:7779
state   : LISTEN
process : {Adobe CEF Helper, Adobe CEF Helper, Adobe Desktop Service,
          AdobeIPCBroker...}

PowerShell 技能连载 - 将对象发送到记事本

在前一个技能里我们演示了如何将文本发送到一个全新的记事本实例中。今天,您会获得一个增强版的 Out-Notepad:您现在可以通过管道传输任何东西到记事本了。如果内容不是字符串,Out-Notepad 会使用内置的 PowerShell ETS 将它转换为文本并且合适地显示出来:

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
#requires -Version 2
function Out-Notepad
{
param
(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[Object]
[AllowEmptyString()]
$Object,

[Int]
$Width = 150
)

begin
{
$al = New-Object System.Collections.ArrayList
}

process
{
$null = $al.Add($Object)
}
end
{
$text = $al |
Format-Table -AutoSize -Wrap |
Out-String -Width $Width

$process = Start-Process notepad -PassThru
$null = $process.WaitForInputIdle()


$sig = '
[DllImport("user32.dll", EntryPoint = "FindWindowEx")]public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("User32.dll")]public static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, string lParam);
'

$type = Add-Type -MemberDefinition $sig -Name APISendMessage2 -PassThru
$hwnd = $process.MainWindowHandle
[IntPtr]$child = $type::FindWindowEx($hwnd, [IntPtr]::Zero, "Edit", $null)
$null = $type::SendMessage($child, 0x000C, 0, $text)
}
}

您现在可以通过管道传输任何东西到 Out-Notepad。它将原样显示:

PS C:\> Get-Content $env:windir\system32\drivers\etc\hosts | Out-Notepad

如果您通过管道传送对象,Out-Notepad 会将它们转换为文本并且不会截断任何东西。您可能会希望用 -Width 参数来确定页宽,以便正常显示:

PS C:\> Get-EventLog -LogName System -EntryType Error, Warning -Newest 10 | Out-Notepad -Width 130

另外您可能需要最大化记事本或禁用换行来查看正确的格式。

PowerShell 技能连载 - 发送文本到记事本

记事本可以用来显示文本结果。通常,您需要将文本保存到文件,然后用记事本打开该文件。不过还有一个更好的办法:打开一个空白的记事本,然后用 Windows 消息直接把文本发送到未命名的记事本编辑器中。

这个函数称为 Out-Notepad。无论您传给这个函数什么文本,它都会在记事本的一个未命名实例中显示:

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 -Version 2
function Out-Notepad
{
param
(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[String]
[AllowEmptyString()]
$Text
)

begin
{
$sb = New-Object System.Text.StringBuilder
}

process
{
$null = $sb.AppendLine($Text)
}
end
{
$text = $sb.ToString()

$process = Start-Process notepad -PassThru
$null = $process.WaitForInputIdle()


$sig = '
[DllImport("user32.dll", EntryPoint = "FindWindowEx")]public static extern IntPtr FindWindowEx(IntPtr hwndParent, IntPtr hwndChildAfter, string lpszClass, string lpszWindow);
[DllImport("User32.dll")]public static extern int SendMessage(IntPtr hWnd, int uMsg, int wParam, string lParam);
'

$type = Add-Type -MemberDefinition $sig -Name APISendMessage -PassThru
$hwnd = $process.MainWindowHandle
[IntPtr]$child = $type::FindWindowEx($hwnd, [IntPtr]::Zero, "Edit", $null)
$null = $type::SendMessage($child, 0x000C, 0, $text)
}
}

这是一些示例调用:

PS C:\> Get-Content $env:windir\system32\drivers\etc\hosts | Out-Notepad

PS C:\> Get-Process | Out-String | Out-Notepad

PowerShell 技能连载 - 神奇的下划线变量

以下是一个非常特别(并且有详细文档的)的使用 PowerShell 变量的方法。请看这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#requires -Version 2

function Test-DollarUnderscore
{
param
(
[Parameter(Mandatory=$true, ValueFromPipelineByPropertyName=$true)]
[string]
$Test
)

process
{
"received: $Test"
}
}

它初看起来并没有什么特别之处。您可以将数值赋给 -Test 参数,并且该函数返回它们:

1
2
PS C:\> Test-DollarUnderscore -Test 'Some Data'
received: Some Data

但是请看当您通过管道传送一个数据给该函数时发生了什么:

1
2
3
4
5
PS C:\> 1..4 | Test-DollarUnderscore -Test { "I am receiving $_" }
received: I am receiving 1
received: I am receiving 2
received: I am receiving 3
received: I am receiving 4

-Test 参数瞬间自动神奇地接受脚本块了(虽然赋予的类型是 string)。而且在脚本块中,您可以存取输入管道的数据。

您能得到这个非常特别的参数支持功能是因为您为一个必选参数设置了 ValueFromPipelineByPropertyName=$true,并且输入的数据没有一个属性和该参数匹配。

PowerShell 技能连载 - 换算货币

PowerShell 是一个非常有用的语言,可以调用 Web Service 和访问网页。如果您将两者合并成一个动态参数,就能得到一个专业的,支持实时汇率的货币换算器。

以下 ConvertTo-Euro 函数可以输入其他货币并转换成欧元。该函数有一个 -Currency 参数,并可以动态地传入欧洲中央银行支持的货币。

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
function ConvertTo-Euro
{
[CmdletBinding()]
param(
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[Double]
$Value
)

dynamicparam
{
$Bucket = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

$Attributes = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]
$AttribParameter = New-Object System.Management.Automation.ParameterAttribute
$AttribParameter.Mandatory = $true
$Attributes.Add($AttribParameter)

if ($script:currencies -eq $null)
{
$url = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'
$result = Invoke-RestMethod -Uri $url
$script:currencies = $result.Envelope.Cube.Cube.Cube.currency
}

$AttribValidateSet = New-Object System.Management.Automation.ValidateSetAttribute($script:currencies)
$Attributes.Add($AttribValidateSet)

$Parameter = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter('Currency',[String], $Attributes)
$Bucket.Add('Currency', $Parameter)

$Bucket
}

begin
{
foreach ($key in $PSBoundParameters.Keys)
{
if ($MyInvocation.MyCommand.Parameters.$key.isDynamic)
{
Set-Variable -Name $key -Value $PSBoundParameters.$key
}
}

$url = 'http://www.ecb.europa.eu/stats/eurofxref/eurofxref-daily.xml'
$rates = Invoke-RestMethod -Uri $url
$rate = $rates.Envelope.Cube.Cube.Cube |
Where-Object { $_.currency -eq $Currency} |
Select-Object -ExpandProperty Rate
}

process
{
$result = [Ordered]@{
Value = $Value
Currency = $Currency
Rate = $rate
Euro = ($Value / $rate)
Date = Get-Date
}

New-Object -TypeName PSObject -Property $result
}
}

该函数演示了如何向动态参数填充动态数据,以及该数据如何缓存以免智能感知每次触发一个新的请求过程。

以下使一些您可能期待的例子(需要 Internet 连接):

PS C:\> 100, 66.9 | ConvertTo-Euro -Currency DKK

Value    : 100
Currency : DKK
Rate     : 7.4622
Euro     : 13,4008737369677
Date     : 26.01.2016 21:32:44

Value    : 66,9
Currency : DKK
Rate     : 7.4622
Euro     : 8,96518453003136
Date     : 26.01.2016 21:32:45



PS C:\>  ConvertTo-Euro -Currency USD -Value 99.78

Value    : 99,78
Currency : USD
Rate     : 1.0837
Euro     : 92,0734520623789
Date     : 26.01.2016 21:33:01

PowerShell 技能连载 - 统计一个 Word 文档中的页数

假设您有一系列 Word 文档,并且希望知道它们共有多少页。以下是一个函数,传入一个 Word 文件参数,便能得到它包含多少页:

#requires -Version 1

# adjust path to point to an existing Word file:
$Path = "C:\...\SomeChapter.doc"
$word = New-Object -ComObject Word.Application
$word.Visible = $true
$binding = 'System.Reflection.BindingFlags' -as [type]
$doc = $word.Documents.Open($Path)
$doc.Repaginate()
$prop = $doc.BuiltInDocumentProperties(14)
$pages = [System.__ComObject].invokemember('value',$binding::GetProperty,$null,$prop,$null)
$doc.Close(0)
$word.Quit()
"$Path has $Pages pages."

如果这对您有用,可以将它改为一个函数,您就可以用 PowerShell 来统计多个 Word 文档共有多少页。