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 文档共有多少页。

PowerShell 技能连载 - 将窗口置于前台

PowerShell 可以使用 Add-Type 来操作 Windows 内置的 API 功能。通过这种方法,可以很容易地将所有进程的窗口置于前台。以下是您需要的函数:

#requires -Version 2
function Show-Process($Process, [Switch]$Maximize)
{
  $sig = '
    [DllImport("user32.dll")] public static extern bool ShowWindowAsync(IntPtr hWnd, int nCmdShow);
    [DllImport("user32.dll")] public static extern int SetForegroundWindow(IntPtr hwnd);
  '

  if ($Maximize) { $Mode = 3 } else { $Mode = 4 }
  $type = Add-Type -MemberDefinition $sig -Name WindowAPI -PassThru
  $hwnd = $process.MainWindowHandle
  $null = $type::ShowWindowAsync($hwnd, $Mode)
  $null = $type::SetForegroundWindow($hwnd)
}

要测试 Show-Process,以下是一段示例代码,演示如何使用它:

# launch Notepad minimized, then make it visible
$notepad = Start-Process notepad -WindowStyle Minimized -PassThru
Start-Sleep -Seconds 2
Show-Process -Process $notepad
# switch back to PowerShell, maximized
Start-Sleep -Seconds 2
Show-Process -Process (Get-Process -Id $PID) -Maximize
# switch back to Notepad, maximized
Start-Sleep -Seconds 2
Show-Process -Process $notepad -Maximize
# switch back to PowerShell, normal window
Start-Sleep -Seconds 2
Show-Process -Process (Get-Process -Id $PID)

PowerShell 技能连载 - 处理数据(第 3 部分)

在第 1 和 第 2 部分,您学到了如何 PowerShell 函数如何处理通过参数和通过管道传入的信息。在这部分中,我们打算介绍一个函数如何接收多行文本并将它加工成一个字符串。

这个函数同时接受参数和管道传入的多行文本。该函数使用 StringBuilder 对象来收集所有的文本行,然后将收到的所有文本行合并成单个字符串:

#requires -Version 2
function Collect-Text
{
  param
  (
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
    [String[]]
    [AllowEmptyString()]
    $Text
  )

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

  process
  {
   foreach ($line in $Text)
   {
     $null = $sb.AppendLine($line)
   }
  }
  end
  {
    $result = $sb.ToString()
    $result
  }
}

请注意如何既支持通过参数,也支持通过管道传递文本行:

PS C:\> Collect-Text -Text 'Line 1', '', 'Line 2'
Line 1

Line 2


PS C:\> 'Line 1', '', 'Line 2' | Collect-Text
Line 1

Line 2

请注意参数:它使用了 [AllowEmptyString()] 属性。它确保可以接受空字符串参数。在 mandatory (必须)参数中,如果没有这个属性,是不允许空字符串的。

PowerShell 技能连载 - 处理数据(第 1 部分)

这是关于 PowerShell 函数如何通过管道或参数接受数据的三个技巧中的第一个。

在第一部分中,函数实时处理输入的信息。这消耗最少的内存并且快速提供结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#requires -Version 2
function Process-Data
{
param (
[Parameter(Mandatory=$true, ValueFromPipeline=$true)]
[Object[]]
$Object )

process {
foreach ($element in $Object)
{
"Processing received element $element..."
}
}
}

请注意如何通过参数调用函数:

1
2
3
4
5
6
7
8
PS C:\> Process-Data -Object 1
Processing received element 1...

PS C:\> Process-Data -Object 1,2,3,4
Processing received element 1...
Processing received element 2...
Processing received element 3...
Processing received element 4...

您也可以通过管道传送信息:

1
2
3
4
5
PS C:\> 1..4 | Process-Data
Processing received element 1...
Processing received element 2...
Processing received element 3...
Processing received element 4...

PowerShell 技能连载 - 处理数据(第 2 部分)

在第 1 部分中我们演示了一个 PowerShell 函数如何同时从参数和管道获取输入,并且实时处理它。这是最有效的方法并节省内存开销。

然而,有时需要先收集所有数据,待所有数据收集完成以后,一次性处理所有数据。以下是一个收集所有收到的数据并等所有数据都到齐以后才开始处理的例子:

#requires -Version 2
function Collect-Data
{
  param
  (
    [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
    [Object]
    [AllowEmptyString()]
    $Object
  )

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

  process
  {
    $null = $bucket.Add($Object)
  }
  end
  {
    $count = $bucket.Count
    Write-Host "Received $count objects." -ForegroundColor Yellow
    $bucket | Out-String
  }
}

请注意 Collect-Data 如何既从参数又从管道获取信息:

PS C:\>  Collect-Data -Object 1,2,3
Received 3 objects.
1
2
3


PS C:\> 1..3 |  Collect-Data
Received 3 objects.
1
2
3

有两件事值得一提:千万不要用一个纯数组来收集信息。而是使用一个 ArraryList 对象,因为它添加新的元素比较快。并且避免将 $input 用于类似用途的自动变量。$input 只能用于管道输入并且忽略提交到参数的值。

PowerShell 技能连载 - 查找当前文件系统路径

PowerShell 不仅支持文件系统,您可以将当前路径设置为别的 provider(用 Set-Location 命令)。以下是一个始终返回当前文件系统,无论当前激活的是那个 provider 的技巧:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PS C:\> cd hkcu:\

PS HKCU:\> $ExecutionContext.SessionState.Path

CurrentLocation CurrentFileSystemLocation
--------------- -------------------------
HKCU:\ C:\



PS HKCU:\> $ExecutionContext.SessionState.Path.CurrentFileSystemLocation

Path
----
C:\



PS HKCU:\> $ExecutionContext.SessionState.Path.CurrentFileSystemLocation.Path
C:\

PowerShell 技能连载 - 通过哈希表转换创建新的对象

从 PowerShell 3.0 开始,您可以通过哈希表创建预先初始化好的对象。只需要添加您希望预先初始化的属性,然后将哈希表转换为期望的类型。

以下是一个实际的例子:

1
2
3
4
5
6
7
8
9
10
#requires -Version 3

$preInit = @{
Rate = -10
Volume = 100
}

Add-Type -AssemblyName System.Speech
$speaker = [System.Speech.Synthesis.SpeechSynthesizer] $preInit
$null = $Speaker.SpeakAsync(“Oh boy, that was a New Year’s party. I guess I need a little break.”)

当您运行这段代码时,PowerShell 创建一个新的 System.Speech 对象并且预先初始化了 rate 和 volume 的值。当您用 SpeakAsync() 方法将文本输出到语音时,文本会被很慢地念出来。Rate 的取值在 -10 到 10 之间。

PowerShell 技能连载 - 使用 DCOM 协议运行 Get-CimInstance

PowerShell 3.0 增加了Get-WmiObject 的另一个选择:Get-CimInstance,它工作起来十分相似但可以从内部 的 WMI 服务中获取信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS C:\> Get-WmiObject -Class Win32_BIOS

SMBIOSBIOSVersion : A03
Manufacturer : Dell Inc.
Name : A03
SerialNumber : 5TQLM32
Version : DELL - 1072009

PS C:\> Get-CimInstance -Class Win32_BIOS

SMBIOSBIOSVersion : A03
Manufacturer : Dell Inc.
Name : A03
SerialNumber : 5TQLM32
Version : DELL - 1072009

虽然 Get-WmiObject 仍然存在,但 Get-CimInstance 绝对是未来的选择。这个 Cmdlet 支持 WMI 类的智能提示(在 PowerShell ISE 中),并且返回的数据可读性更好:例如日期是以人类可读的日期格式返回,而 Get-WmiObject 显示 WMI 内部原始的日期格式。

最重要的区别是它们远程工作的方法。Get-WmiObject 使用的是旧的 DCOM 协议,而 Get-CimInstance 缺省使用的是新的 WSMan 协议,不过它是灵活的,可以根据需要退回 DCOM 协议。

以下示例函数通过 Get-CimInstance 远程获取 BIOS 信息。该函数缺省采用 DCOM,通过 -Protocol 参数您可以选择希望的通信协议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#requires -Version 3
function Get-BIOS
{
param
(
$ComputerName = $env:COMPUTERNAME,

[Microsoft.Management.Infrastructure.CimCmdlets.ProtocolType]
$Protocol = 'DCOM'
)
$option = New-CimSessionOption -Protocol $protocol
$session = New-CimSession -ComputerName $ComputerName -SessionOption $option
Get-CimInstance -CimSession $session -ClassName Win32_BIOS
}

PowerShell 技能连载 - 理解 PowerShell 的流

PowerShell 提供七种不同的流,可以用来输出信息。流可以帮助筛选信息,因为流可以不输出。实际上一些流默认是不输出的。以下是一个名为 Test-Stream 的示例函数。它运行后会将信息发送给所有七种流。

请注意:Write-Information 是 PowerShell 5.0 新加入的。如果您想在早期的 PowerShell 版本中运行,请移除调用 Write-Information 的语句!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Test-Stream
{
#region These are all the same and define return values
'Return Value 1'
echo 'Return Value 2'
'Return Value 3' | Write-Output
#endregion

Write-Verbose 'Additional Information'
Write-Debug 'Developer Information'
Write-Host 'Mandatory User Information'
Write-Warning 'Warning Information'
Write-Error 'Error Information'

# new in PowerShell 5.0
Write-Information 'Auxiliary Information'
}

这应该是您运行 Test-Stream 能看到的结果:

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
PS C:\> Test-Stream
Return Value 1
Return Value 2
Return Value 3
Mandatory User Information
WARNING: Warning Information
Test-Stream : Error Information
At line:1 char:1
+ Test-Stream
+ ~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-Stream

PS C:\> $result = Test-Stream
Mandatory User Information
WARNING: Warning Information
Test-Stream : Error Information
At line:1 char:1
+ Test-Stream
+ ~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Test-Stream

PS C:\> $result
Return Value 1
Return Value 2
Return Value 3

PS C:\>

如您所见,echoWrite-Output 工作起来效果相同,而且实际上它们确实是相同的(因为echoWrite-Output 的别名)。它们定义了一个或多个返回值。它们可以赋值给一个变量。同理,这个规则适用于函数留下的未赋值的变量:它们也被送到 Write-Output 流中。

Write-Host 直接将输出送到控制台,所以它一定可见。这个 Cmdlet 只能用于向用户传递信息的场景。

其他的流是静默的。要查看其它流的输出,您首先需要打开它们:

1
2
3
$VerbosePreference = 'Continue'
$DebugPreference = 'Continue'
$InformationPreference = 'Continue'

当打开之后,Test-Stream 将输出这样的信息:

1
2
3
4
5
6
7
8
PS C:\> Test-Stream
Return Value 1
Return Value 2
Return Value 3
VERBOSE: Additional Information
DEBUG: Developer Information
Mandatory User Information
Auxiliary Information

要恢复缺省值,请复位 preference 变量:

1
2
3
$VerbosePreference = 'SilentlyContinue'
$DebugPreference = 'SilentlyContinue'
$InformationPreference = 'SilentlyContinue'

如果在函数中加入了通用参数,您就可以在调用函数时使用使用 -Verbose-Debug 开关。Test-CommonParameter 演示了如何添加通用参数支持。

1
2
3
4
5
6
7
8
function Test-CommonParameter
{
[CmdletBinding()]
param()

"VerbosePreference = $VerbosePreference"
"DebugPreference = $DebugPreference"
}

当运行 Test-CommonParameter 时,您将立即明白 -VerboseDebug 通用参数是如何工作的:它们只是改变了本地 preference 变量:

1
2
3
4
5
6
7
PS C:\> Test-CommonParameter
VerbosePreference = SilentlyContinue
DebugPreference = SilentlyContinue

PS C:\> Test-CommonParameters -Debug -Verbose
VerbosePreference = Continue
DebugPreference = Inquire