PowerShell 技能连载 - “Continue” 和标签

当您在循环中使用“Continue”语句时,您可以跳过循环中剩下的语句,然后继续下一次循环。“Break”的工作原理与之相似,不过它不仅结束循环而且将跳过所有剩下的循环。

这引出一个问题:当您使用嵌套的循环时,这些语句影响了哪层循环?缺省情况下,“Continue”针对的是内层的循环,但是通过使用标签,您可以使“Continue”和“Break”指向外层循环。

:outer
Foreach ($element in (1..10))
{
  for ($x = 1000; $x -lt 1500; $x += 100)
  {
    "Frequency $x Hz"
    [Console]::Beep($x, 500)
    continue outer
    Write-Host 'I am never seen unless you change the code...'
  }
}

由于这段示例代码的 continue 是针对外层循环的,所以您将见到(以及听到)10 次 1000Hz 的输出。

当您移除“Continue”之后的“outer”标签时,您会听到频率递增的蜂鸣,并且 Write-Host 语句不再被跳过。

PowerShell 技能连载 - 获取内存消耗值

要了解某个脚本占用内存的大约值,或是当您将结果存入变量时 PowerShell 写入多少内存,以下是一个辅助函数:

#requires -Version 2

$script:last_memory_usage_byte = 0

function Get-MemoryUsage
{
  $memusagebyte = [System.GC]::GetTotalMemory('forcefullcollection')
  $memusageMB = $memusagebyte / 1MB
  $diffbytes = $memusagebyte - $script:last_memory_usage_byte
  $difftext = ''
  $sign = ''
  if ( $script:last_memory_usage_byte -ne 0 )
  {
    if ( $diffbytes -ge 0 )
    {
      $sign = '+'
    }
    $difftext = ", $sign$diffbytes"
  }
  Write-Host -Object ('Memory usage: {0:n1} MB ({1:n0} Bytes{2})' -f  $memusageMB, $memusagebyte, $difftext)

  # save last value in script global variable
  $script:last_memory_usage_byte = $memusagebyte
}

您可以在任意时候运行 Get-MemoryUsage,它将返回当前的内存消耗以及和上一次调用之间的变化量。

关键点在于使用垃圾收集器:它是负责清理内存,但是平时并不是立即清理内存。要粗略计算内存消耗时,需要调用垃圾收集器立即释放所有无用的内存,然后报告当前占用的内存。

PowerShell 技能连载 - 使用闭包将变量保持在脚本块内

当您使用脚本块中的变量时,运行脚本块时变量会被求值。

要将变量内容保持住,您可以创建一个新的“闭包”。当创建一个闭包之后,该脚本块持有该变量的值,该值为创建闭包时刻的值。

$info = 1

$code =
{
    $info
}

$code = $code.GetNewClosure()

$info = 2

& $code

如果不使用闭包,该脚本块将显示“2”,因为执行时 $info 的值为 2。通过闭包的作用,该脚本块内包含的值为创建闭包时赋予 $info 的值。

PowerShell 技能连载 - 互斥参数 (2)

PowerShell 函数中的互斥参数使用“ParameterSetName”属性将参数指定到不同的参数集上(或是参数组上)。

很少人知道可以为一个参数指定多个参数集名。通过这种方法,某个参数可以在一个场景中为可选,但在另外一个场景中为必选。

function Test-ParameterSet
{
  [CmdletBinding(DefaultParameterSetName='NonCredential')]
  param
  (
    $id,

    [Parameter(ParameterSetName='LocalOnly', Mandatory=$false)]
    $LocalAction,

    [Parameter(ParameterSetName='Credential', Mandatory=$true)]
    [Parameter(ParameterSetName='NonCredential', Mandatory=$false)]
    $ComputerName,

    [Parameter(ParameterSetName='Credential', Mandatory=$false)]
    $Credential
  )

  $PSCmdlet.ParameterSetName
  $PSBoundParameters

  if ($PSBoundParameters.ContainsKey('ComputerName'))
  {
    Write-Warning 'Remote Call!'
  }
}

Test-ParameterSet 函数显示了如何实现该功能:当“NonCredential”参数集有效时 -ComputerName 是可选的。如果用户使用了 -Credential 参数,那么 -ComputerName 变成了必选的。而如果用户使用了 -LocalAction 参数,那么 -ComputerName-Credential 都变为不可用的了。

PowerShell 技能连载 - 互斥参数 (1)

有些时候,PowerShell 的函数参数必须是互斥的:用户只能用这类参数多个中的一个,而不能同时使用。

要创建互斥参数,请将他们指定到不同的参数集中,并且确保定义了一个缺省的参数集(当 PowerShell 无法自动选择正确的参数集时使用):

function Test-ParameterSet
{
  [CmdletBinding(DefaultParameterSetName='number')]
  param
  (
    [int]
    [Parameter(ParameterSetName='number', Position=0)]
    $id,

    [string]
    [Parameter(ParameterSetName='text', Position=0)]
    $name
  )

  $PSCmdlet.ParameterSetName
  $PSBoundParameters
}

Test-ParameterSet 函数有两个参数:-id-name。用户只能指定一个参数,而不能同时指定两个参数。这个例子也演示了如何知道用户选择了哪个参数集。

PowerShell 技能连载 - 解析 PowerShell 脚本

如果您希望为 PowerShell 脚本语法着色,例如用 HTML 将它们格式化,以下是一个起步示例:

这个例子将读取当前 ISE 编辑器中显示的脚本,然后调用 PowerShell 解析器返回所有 token 的信息。

$content = $psise.CurrentFile.Editor.Text
$token = $null
$errors = $null

$token = [System.Management.Automation.PSParser]::Tokenize($content, [ref]$errors)

$token | Out-GridView

您可以简单地使用 Get-Content 来读取任何脚本中的内容。读取到的结果是一个由 token 对象组成的数组。它们包含了语法元素类型,以及起止位置。

该信息是格式化 PowerShell 代码所需要的。您可以为 token 类型指定颜色,并为 PowerShell 代码创建自己的文档。

PowerShell 技能连载 - 跳出管道

如果您事先知道期望从管道中得到多少个对象,您可以用 Select-Object 命令来停止上游的 cmdlet 执行。这样可以节约很多时间。

这个例子试着在 Windows 文件夹中查找 explorer.exe 的第一个实例。由于 Select-Object 语句的作用,一旦找到第一个实例,管道就结束了。如果没有这个语句,即便已经查找到所需的数据,Get-ChildItem 也会不断地递归扫描 Windows 文件夹。

#requires -Version 3


Get-ChildItem -Path c:\Windows -Recurse -Filter explorer.exe -ErrorAction SilentlyContinue |
Select-Object -First 1

请注意只有在 PowerShell 3.0 以上版本中,Select-Object 才具有中断上游 cmdlet 的能力。在早期的版本中,您仍然会获得前 x 个元素,但是上游的 cmdlet 会得不到“已经获得足够的数据”通知而一直持续执行。

PowerShell 技能连载 - 向管道传递一个数组

如果一个函数返回多于一个值,PowerShell 会将它们封装为一个数组。然而如果您通过管道将它传递至另一个函数,该管道会自动地将数组“解封”,并且一次处理一个数组元素。

如果您需要原原本本地处理一个数组而不希望解封,那么请将返回值封装在另一个数组中。通过这种方式,管道会将外层的数组解封并处理内层的数组。

以下代码演示了这个技能:

#requires -Version 1


function Test-ArrayAsReturnValue1
{
  param($count)

  $array = 1..$count

  return $array
}

function Test-ArrayAsReturnValue2
{
  param($count)

  $array = 1..$count

  return ,$array
}

'Result 1:'
Test-ArrayAsReturnValue1 -count 10 | ForEach-Object -Process {
  $_.GetType().FullName
}

'Result 2:'
Test-ArrayAsReturnValue2 -count 10 | ForEach-Object -Process {
  $_.GetType().FullName
}

当您运行这段代码时,第一个例子将返回数组中的元素。第二个例子将会把整个数组传递给循环。

PS C:\>

Result 1:
System.Int32
System.Int32
System.Int32
System.Int32
System.Int32
System.Int32
System.Int32
System.Int32
System.Int32
System.Int32

Result 2:
System.Object[]

PowerShell 技能连载 - 面向管理员的免费 PowerShell 模块

上个技能中我们收到一个反馈,推荐使用一个名为“Carbon”的免费 PowerShell 模块,其中包含了一大堆有用的 PowerShell 函数。

其中一个是 Get-IPAddress,它能返回您计算机的所有 IP 地址。您可以在这儿找到和下载该模块:http://get-carbon.org/

请始终记着:从外部资源中下载 PowerShell 代码要记得检查一遍代码,确保是您所需要的内容。

PowerShell 技能连载 - 将 CSV 转换为 Excel 文件

PowerShell 可以用 Export-Csv 很容易创建 CSV 文件。如果您的系统中安装了 Microsoft Excel,PowerShell 可以调用 Excel 将一个 CSV 文件转换为一个 XLSX Excel 文件。

以下是一段示例代码。它使用 Get-Process 来获取一些数据,然后将数据写入一个 CSV 文件。Export-Csv 使用 -UseCulture 来确保 CSV 文件使用您所安装的 Excel 期望的分隔符。

$FileName = "$env:temp\Report"

# create some CSV data
Get-Process | Export-Csv -UseCulture -Path "$FileName.csv" -NoTypeInformation -Encoding UTF8

# load into Excel
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$excel.Workbooks.Open("$FileName.csv").SaveAs("$FileName.xlsx",51)
$excel.Quit()

explorer.exe "/Select,$FileName.xlsx"

下一步,Excel 打开该 CSV 文件,然后将数据保存为一个 XLSX 文件。

它工作得很好,不过可能会遇到一个类似这样的异常:

PS>  $excel.Workbooks.Open("$FileName.csv")
Exception  calling "Open" with "1" argument(s): "Old format or  invalid type library. (Exception from HRESULT: 0x80028018
(TYPE_E_INVDATAREAD))"
At line:1 char:1
+  $excel.Workbooks.Open("$FileName.csv")
+  ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
    + CategoryInfo          : NotSpecified: (:) [],  MethodInvocationException
    + FullyQualifiedErrorId :  ComMethodTargetInvocation

这是一个长期已知的问题。当您的 Excel 语言和您的 Windows 操作系统不一致时可能会碰到。当您的 Windows 操作系统使用一个本地化的 MUI 包时,也许根本不会遇到这个问题。

要解决这个问题,您可以临时改变该线程的语言文化设置来适应您的 Excel 版本:

$FileName = "$env:temp\Report"

# create some CSV data
Get-Process | Export-Csv -Path "$FileName.csv" -NoTypeInformation -Encoding UTF8

# load into Excel
$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true

# change thread culture
[System.Threading.Thread]::CurrentThread.CurrentCulture = 'en-US'

$excel.Workbooks.Open("$FileName.csv").SaveAs("$FileName.xlsx",51)
$excel.Quit()

explorer.exe "/Select,$FileName.xlsx"

这也会带来另外一个问题:当您以 en-US 语言文化设置运行 Excel 的 Open() 方法时,它不再需要 CSV 文件使用您的本地化分隔符。现在它需要的是一个以半角逗号分隔的文件,所以第二个脚本去掉了 -UseCulture 设置。