PowerShell 技能连载 - 使用 PowerShell 的帮助窗口作为通用输出

要显示文本信息,您当然可以启动 noteapd.exe,并且用编辑器来显示文本。不过,在编辑器中显示文本并不是一个好主意,如果您不希望文本被改变。

PowerShell 给我们带来了一个很棒的窗体,用来显示一小段或者中等长度的文本:内置的帮助窗口。

通过一些调整,该窗口可以重新编程,显示任意的文本信息。而且,您可以使用内置的全文搜索功能在您的文本中导航,甚至可以根据你的喜好设置前景色和背景色。

只需要将任意的文本通过管道传递给 Out-Window,并且根据需要可选地指定颜色、搜索词和标题。以下是 Out-Window 函数:

function Out-Window
{
  param
  (
    [String]
    $Title = 'PowerShell Output',

    [String]
    $FindText = '',

    [String]
    $ForegroundColor = 'Black',

    [String]
    $BackgroundColor = 'White'
  )

  # take all pipeline input:
  $allData = @($Input)

  if ($allData.Count -gt 0)
  {
    # open window in new thread to keep PS responsive
    $code = {
      param($textToDisplay, $FindText, $Title, $ForegroundColor, $BackgroundColor)

      $dialog = (New-Object –TypeName Microsoft.Management.UI.HelpWindow($textToDisplay))
      $dialog.Title = $Title
      $type = $dialog.GetType()
      $field = $type.GetField('Settings', 'NonPublic,Instance')
      $button = $field.GetValue($dialog)
      $button.Visibility = 'Collapsed'
      $dialog.Show()
      $dialog.Hide()
      $field = $type.GetField('Find', 'NonPublic,Instance')
      $textbox = $field.GetValue($dialog)
      $textbox.Text = $FindText
      $field = $type.GetField('HelpText', 'NonPublic,Instance')
      $RTB = $field.GetValue($dialog)
      $RTB.Background = $BackgroundColor
      $RTB.Foreground = $ForegroundColor
      $method = $type.GetMethod('MoveToNextMatch', [System.Reflection.BindingFlags]'NonPublic,Instance')
      $method.Invoke($dialog, @($true))
      $dialog.ShowDialog()
    }

    $ps = [PowerShell]::Create()
    $newRunspace = [RunSpaceFactory]::CreateRunspace()
    $newRunspace.ApartmentState = 'STA'
    $newRunspace.Open()

    $ps.Runspace = $newRunspace
    $null = $ps.AddScript($code).AddArgument(($allData | Format-Table -AutoSize -Wrap | Out-String -Width 100)).AddArgument($FindText).AddArgument($Title).AddArgument($ForegroundColor).AddArgument($BackgroundColor)
    $null = $ps.BeginInvoke()
  }
}

调用的示例如下:

Get-Content C:\Windows\windowsupdate.log  |
  # limit to first 100 lines (help window is not designed to work with huge texts)
  Select-Object -First 100 |
  Out-Window -Find Success -Title 'My Output' -Background Blue -Foreground White

请记住两件事:

  • 帮助窗口并不是设计为显示大量文本的。请确保使用该方法显示不超过几 KB 的文本。
  • 这只是试验性的代码。它并没有清除 PowerShell 用来显示窗体所创建的线程。当您关闭该线程时,该 PowerShell 线程将保持在后台运行,直到您关闭 PowerShell。我们需要为帮助窗口关闭事件增加一个事件处理器。该事件处理器可以清理该 PowerShell 线程。

PowerShell 技能连载 - 在后台播放声音

如果您的脚本执行起来需要较长时间,您可能会希望播放一段系统声音文件。以下是一个实现该功能的示例代码:

# find first available WAV file in Windows
$WAVPath = Get-ChildItem -Path $env:windir -Filter *.wav -Recurse -ErrorAction SilentlyContinue |
Select-Object -First 1 -ExpandProperty FullName

# load file and play it
$player = New-Object Media.SoundPlayer $WAVPath

try
{
  $player.PlayLooping()
  'Doing something...'

  1..100 | ForEach-Object {
    Write-Progress -Activity 'Doing Something. Hang in' -Status $_ -PercentComplete $_
    Start-Sleep -MilliSeconds (Get-Random -Minimum 300 -Maximum 1300)
  }
}

finally
{
  $player.Stop()
}

这段示例代码使用 Windows 文件夹中找到的第一个 WAV 文件,然后在脚本的执行期间播放它。您当然也可以指定其它 WAV 文件的路径。

PowerShell 技能连载 - 查找可执行程序

许多文件扩展名都被关联为可执行程序。您可以使用 Invoke-Item 来打开一个可执行的文件。

然而,查找哪些文件扩展名是可执行程序却不是那么简单。您可以读取 Windows 注册表,然后自己查找这些值。如果您采用这种方法,请注意 32/64 位的问题。

另外一个方法是使用 Windows API。以下示例代码演示了它是如何工作的。如果您使用了这种方法,您可以将重活交给操作系统做。付出的代价是一堆调用内部 API 函数的 C# 代码。

$Source = @"

using System;
using System.Text;
using System.Runtime.InteropServices;
public class Win32API
    {
        [DllImport("shell32.dll", EntryPoint="FindExecutable")]

        public static extern long FindExecutableA(string lpFile, string lpDirectory, StringBuilder lpResult);

        public static string FindExecutable(string pv_strFilename)
        {
            StringBuilder objResultBuffer = new StringBuilder(1024);
            long lngResult = 0;

            lngResult = FindExecutableA(pv_strFilename, string.Empty, objResultBuffer);

            if(lngResult >= 32)
            {
                return objResultBuffer.ToString();
            }

            return string.Format("Error: ({0})", lngResult);
        }
    }

"@

Add-Type -TypeDefinition $Source -ErrorAction SilentlyContinue

$FullName = 'c:\Windows\windowsupdate.log'
$Executable = [Win32API]::FindExecutable($FullName)

"$FullName will be launched by $Executable"

一个已知的限制是 FindExecutable() 的使用前提是该文件必须存在。您无法只通过文件扩展名来断定是否为一个可执行文件。

PowerShell 技能连载 - 根据大写字符分割文本

要在一段文本的每个大写字符出分割这段文本,而不用提供一个大写字符的列表,请试试这个例子:

$text = 'MapNetworkDriveWithCredential'

[Char[]]$raw = foreach ($character in $text.ToCharArray())
{
  if ([Char]::IsUpper($character))
  {
    ' '
  }
  $character
}

$newtext = (-join $raw).Trim()
$newtext

PowerShell 技能连载 - 查找大写字符

如果您希望查找大写字符,那么可以使用正则表达式。然而,您也可以提供一个大写字符的列表作为对比使用。一个更灵活的办法是使用 .NET 的 IsUpper() 函数。

以下是一段示例代码:它逐字符扫描一段文本,然后返回首个大写字符的位置:

$text = 'here is some text with Uppercase letters'

$c = 0
$position = foreach ($character in $text.ToCharArray())
{
  $c++
  if ([Char]::IsUpper($character))
  {
    $c
    break
  }
}

if ($position -eq $null)
{
  'No uppercase characters detected.'
}
else
{
  "First uppercase character at position $position"
  $text.Substring(0, $position) + "<<<" + $text.Substring($position)
}

执行的结果类似这样:

PS C:\>

First uppercase character at position 24
here is some text with U<<<ppercase letters

PowerShell 技能连载 - 在控制台输出中使用绿色的复选标记

在前一个技能中您已见到了如何使 PowerShell 控制台支持 TrueType 字体中所有可用的字符。您只需要将字符代码转换为“Char”类型即可。

以下是一个更高级的示例代码,使用了 splatting 技术将一个绿色的复选标记插入您的控制台输出中:

$greenCheck = @{
  Object = [Char]8730
  ForegroundColor = 'Green'
  NoNewLine = $true
  }

Write-Host "Status check... " -NoNewline
Start-Sleep -Seconds 1
Write-Host @greenCheck
Write-Host " (Done)"

这样当您需要一个绿色的复选标记时,使用这行代码:

Write-Host @greenCheck

如果该复选标记并没有显示出来,请确保您的控制台字体设置成了 TrueType 字体,例如“Consolas”。您可以点击控制台标题栏左上角的图标,并选择“属性”来设置字体。

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 语句不再被跳过。