PowerShell 技能连载 - 在控制台输出中使用符号

您知道吗,控制台输出内容可以包括特殊字符,例如复选标记?您所需要做的只是将控制台设成 TrueType 字体,例如“Consolas”。

要显示特殊字体,请使用十进制或十六进制字符代码,例如:

[Char]8730


[Char]0x25BA

或者执行内置的“CharacterMap”程序以您选择的控制台字体选择另一个特殊字符。

以下是一个让您在 PowerShell 控制台中得到更复杂提示的示例代码:

function prompt
{

  $specialChar1 = [Char]0x25ba

  Write-Host 'PS ' -NoNewline
  Write-Host $specialChar1 -ForegroundColor Green -NoNewline
  ' '

  $host.UI.RawUI.WindowTitle = Get-Location
}

请注意“prompt”函数必须返回至少一个字符,否则 PowerShell 将使用它的默认提示信息。这是为什么该函数用一个空格作为返回值,并且使用 Write-Host 作为彩色输出的原因。

PowerShell 技能连载 - 测试嵌套深度

当您调用一个函数时,PowerShell 会增加嵌套的深度。当一个函数调用另一个函数,或是一段脚本时,将进一步增加嵌套的深度。以下是一个能够告诉您当前代码嵌套深度的函数:

function Test-NestLevel
{
  $i = 1
  $ok = $true
  do
  {
    try
    {
      $test = Get-Variable -Name Host -Scope $i
    }
    catch
    {
      $ok = $false
    }
    $i++
  } While ($ok)

  $i
}

当您设计递归(调用自身的函数)函数时,这种方法十分有用。

以下是使用该技术的示例代码:

function Test-Diving
{
    param($Depth)

    if ($Depth -gt 10) { return }

    "Diving deeper to $Depth meters..."

    $currentDepth = Test-NestLevel
    "calculated depth: $currentDepth"

    Test-Diving -depth ($Depth+1)
}

Test-Diving -depth 1

当您运行 Test-Diving 时,该函数将调用自身,直到达到 10 米深。该函数使用一个参数来控制嵌套深度,而 Test-NestLevel 的执行结果将返回相同的数字。

请注意它们的区别:Test-NestLevel 返回所有(绝对的)嵌套级别,而参数告诉您函数调用自己的次数。如果 Test-Diving 包含在其它函数中,那么绝对的级别和相对的级别将会不同:

PS C:\> Test-Diving -Depth 1
diving deeper to 1 meters...
calculated depth: 1
diving deeper to 2 meters...
calculated depth: 2
diving deeper to 3 meters...
calculated depth: 3
diving deeper to 4 meters...
calculated depth: 4
diving deeper to 5 meters...
calculated depth: 5
diving deeper to 6 meters...
calculated depth: 6
diving deeper to 7 meters...
calculated depth: 7
diving deeper to 8 meters...
calculated depth: 8
diving deeper to 9 meters...
calculated depth: 9
diving deeper to 10 meters...
calculated depth: 10

PS C:\> & { Test-Diving -Depth 1 }
diving deeper to 1 meters...
calculated depth: 2
diving deeper to 2 meters...
calculated depth: 3
diving deeper to 3 meters...
calculated depth: 4
diving deeper to 4 meters...
calculated depth: 5
diving deeper to 5 meters...
calculated depth: 6
diving deeper to 6 meters...
calculated depth: 7
diving deeper to 7 meters...
calculated depth: 8
diving deeper to 8 meters...
calculated depth: 9
diving deeper to 9 meters...
calculated depth: 10
diving deeper to 10 meters...
calculated depth: 11

PS C:\>

Test-NestLevel 总是从当前的代码中返回嵌套的级别到全局作用域。

PowerShell 技能连载 - 跳出管道

有些时候您可能希望当某些条件满足时跳出一个管道。

以下是一种实现该功能的创新方法。它适用于 PowerShell 2.0 以及更高版本。

以下是一段示例代码:

filter Stop-Pipeline
{
     param
     (
         [scriptblock]
         $condition = {$true}
     )

     if (& $condition)
     {
       continue
     }
     $_
}

do {
    Get-ChildItem c:\Windows -Recurse -ErrorAction SilentlyContinue | Stop-Pipeline { ($_.FullName.ToCharArray() -eq '\').Count -gt 3 }
} while ($false)

该管道方法递归扫描 Windows 文件夹。代码中有一个名为 Stop-Pipeline 的新命令。您可以将一个脚本块传给它,如果该脚本块的执行结果为 $true,该管道将会退出。

在这个例子中,您可以控制递归的深度。当路径中包含三个反斜杠(\)时,管道将会停止。将数字“3”改为更大的值可以在更深的文件夹中递归。

这个技巧的使用前提是管道需要放置在一个“do”循环中。因为 Stop-Pipeline 主要的功能是当条件满足时执行“Continue”语句,使 do 循环提前退出。

这听起来不太方便不过它工作得很优雅。以下是一个简单的改动。它将运行一个管道最多不超过 10 秒:

$start = Get-Date
$MaxSeconds = 10

do {
    Get-ChildItem c:\Windows -Recurse -ErrorAction SilentlyContinue | Stop-Pipeline { ((Get-Date) - $start).TotalSeconds -gt $MaxSeconds }
} while ($false)

如果您希望保存管道的结果而不是输出它们,只需要在“do”语句之前放置一个变量。

$result = do {
    Get-Chil...

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 会得不到“已经获得足够的数据”通知而一直持续执行。