PowerShell 技能连载 - 创建 ASCII 艺术

PowerShell 的功能令人惊叹:只需要几行代码,就可以将任意照片和图片转化为一段 ASCII 艺术。PowerShell 只需要加载图片,然后然后逐行逐列扫描它,然后基于每个像素的亮度将每个像素替换为一个 ASCII 字符。

以下是该函数:

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
 function Convert-ImageToAsciiArt
{
param(
[Parameter(Mandatory)][String]
$Path,

[ValidateRange(20,20000)]
[int]$MaxWidth=80,

# character height:width ratio
[float]$ratio = 1.5
)

# load drawing functionality
Add-Type -AssemblyName System.Drawing

# characters from dark to light
$characters = '$#H&@*+;:-,. '.ToCharArray()
$c = $characters.count

# load image and get image size
$image = [Drawing.Image]::FromFile($path)
[int]$maxheight = $image.Height / ($image.Width / $maxwidth)/ $ratio

# paint image on a bitmap with the desired size
$bitmap = new-object Drawing.Bitmap($image,$maxwidth,$maxheight)


# use a string builder to store the characters
[System.Text.StringBuilder]$sb = ""

# take each pixel line...
for ([int]$y=0; $y -lt $bitmap.Height; $y++){
# take each pixel column...
for ([int]$x=0; $x -lt $bitmap.Width; $x++){
# examine pixel
$color = $bitmap.GetPixel($x,$y)
$brightness = $color.GetBrightness()
# choose the character that best matches the
# pixel brightness
[int]$offset = [Math]::Floor($brightness*$c)
$ch = $characters[$offset]
if (-not $ch){ $ch = $characters[-1] }
# add character to line
$null = $sb.Append($ch)
}
# add a new line
$null = $sb.AppendLine()
}

# clean up and return string
$image.Dispose()
$sb.ToString()
}

以下是它的使用方法:

1
2
3
4
5
6
7
8
$Path = "C:\Users\Tobias\Desktop\Somepic.jpg"
$OutPath = "$env:temp\asciiart.txt"

Convert-ImageToAsciiArt -Path $Path -MaxWidth 150 |
Set-Content -Path $OutPath -Encoding UTF8


Invoke-Item -Path $OutPath

请确保调整了代码中的路径。将会在缺省的文本编辑器中打开 ASCII 艺术。请确保禁用了换行,选择一个等宽的字体和一个足够小的字号!

                                         ;@&&&&&&@&&&&@+-
                                       :@&&&&&&&&&&&&&&&&&&:.
                                     ,&&&&&&&&&&&&&&&&&&&&&&&&&&*
                                  -@&&&&&&&&&&&HHHHHH&&&&&&&&&&&&&*
                               :&&&&&&&&&&&&H&&&&&&&&&&&&&&&&&&&&&&&@
                              *&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&,
                             :&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&-
                            *&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&*
                           &&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&
                          @&&&&&&&&H&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&-
                         &&&&&&&&H&&&@;:+&&&&&&&*-....,:@&&&&&&&&&&&&&&&&&&&@
                        &&&&&&&&H&&;........................*&&&&&&&H&&&&&&&&&.
                      .&&&&&&&&&H;...........................&&&&&&&&H&&&&&&&&&-
                     ;&&&&&&&&&&:............................@@&&&&&&H&&&&&&&&&&
                    &&&&&&&&&&&.+...........................-.;&&&H&&H&&&&&&&&&&@
                   *&H&&&&&&&&;;..............................+@+&&HH&&&&&&&&&&&&
                   ;&H&&&&&&&&................................@*.&&&H&&&&&&&&&&&&:
                   :&H&&&&&&&*...............................:*..&H&&&&&&&&&&&&&&@
                   ,&&H&&&@&@...................................,&&&&&&&&&&&&&&&&&,
                   ,&&&&&&:+....................................+*.;:&&&&&&&&&&&&&&
                   +&&&&&&............................................@*@+&&&&&&&&&,
                   &&&&&&;............................................,-..-+&&&&&&&
                  :&&&&&@-...................................................:&&&&&,
                  &&&&&&&.....................................................&&&&&-
                  &&&&&&+.....................................................&&&&&,,
                  :&&&&&;.....................................................&&&H&
                   &&&&&;.......................................,.,...........&&&&H&@
                  *-&&&&:...*&&&&&&&&&&&&&@*,.............-*&&&&&&&&&&&&:.....@&&&&+
                    &&&&:.+&&HHHHHHHHHHHHH&&&&..........+&&&&HHHHHHHHHHHHH*...+&&&&@
                   ,###H*+;;:,...........-;*&&##########&&&*:..........:;*@:;*&###H:
                   :##H*.......................#@;---+#;.......................@###.
                  ,@###+............+HH&+.....-&......-*......;&H&*............&##:.
                   ,&&#-...........HHH&+H;....@;......,#.....@HHH;H@...........HH&-.
                   ..;#,...........HHHHHH+....#........H,....&HHHHH&..........-#&:-
                   ...H*...........-;;;;;....-H........;@....,;;;;;,..........;#&..
                   ...&#.....................#-.........#.....................&H*..
                   ...:H&........,.........-#@..........-#;..........,,......:#&,..
                    ...-,&###&+::-,,::+&###+......  ......-###H*:::,,::+@####,....
                    ..........,,,,,,,,...........   ............,,,,,,,,..........
                     ............................   ..............................
                     ............................   .............................
                      .....................,.....   ............................
                       ....................,.....   .......,...................
                         ..................,,,...   .......,.................
                          ......................,.  ..,......................
                          ........................,,,.......................
                          ..................................................
                           .................................................
                           .............,-..................--.............
                            ...............--,,-----:-,. --...............
                             ..................,,----,,...................
                              ...........................................
                               .........................................
                                ......................................
                                 ....................................
                                   ................................
                                    ..............................
                                     .,,,,,,,,,,,,,,,,,,,,,,,,,,
                                   ,&...,,,,,,,,,,,,,,,,,,,,,,...@,
                                &HHH......,,,,,,,,,,,,,,,,,,.....-HHH&
                            :HHHHHHH&.........,,,,,,,,,,,........HHHHHHHH;
                         *HHHHHHHHHHHH+........................*HHHHHHHHHHHH@
                      -HHHHHHHHHHHHHHHHH&;..................;&HHHHHHHHHHHHHHHHH,
                     HHHHHHHHHHHHHHHHHHHHHHHH&*;:---::;*&HHHHHHHHHHHHHHHHHHHHHHH&
                    HHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHHH

这儿有一个很酷的优化技巧:在 Convert-ImageToAsciiArt 函数中,请注意 $characters。它是一个包含 ASCII 艺术中使用的字符的字符串,而这些字符是按亮度降序排列的。对于黑白作品,可以使用:

1
$characters = [char]0x2588, ' '

PowerShell 技能连载 - 验证本地用户账户密码

在前一个技能VS,我们通过 Active Directory 来验证用户账户密码。同样的,对于本地账户也可以。PowerShell 代码可以使用本地账户密码来管理对脚本的存取,或部分限制脚本的功能。当然,您也可以用以下代码来创建自己的基本的密码穷举工具。

缺省情况下,以下代码使用您当前的用户名。请确保 $UserName 是某个本地账户的用户名:

1
2
3
4
5
6
7
8
9
# specify local user name and password to test
$UserName = $env:USERNAME
$Password = Read-Host -Prompt "Enter password to test"

# test password
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
$type = [DirectoryServices.AccountManagement.ContextType]::Machine
$PrincipalContext = [DirectoryServices.AccountManagement.PrincipalContext]::new($type)
$PrincipalContext.ValidateCredentials($UserName,$Password)

今日的知识点:

  • PowerShell 可以请求本地的 Windows 用户数据库来验证密码。通过这种方式,您可以使用 Windows 维护的密码来决定一个脚本是否允许执行,或允许某个用户执行哪一部分。
  • 请注意向用户询问密码是一种不安全的做法,因为他们不知道密码将会被用在什么地方。

PowerShell 技能连载 - 校验域账户密码

PowerShell 可以轻松地校验一个域账号的密码。换句话说,您可以为 Active Directory 维护的密码绑定一个脚本逻辑。

以下是将密码发送到 AD 并获取回一个 Boolean 值的代码:如果密码正确,返回 $true,否则返回 $false

1
2
3
4
5
6
7
8
9
10
# specify user name and user domain
$UserDomain = $env:USERDOMAIN
$UserName = $env:USERNAME
$Password = Read-Host -Prompt "Enter password to test"

# test password
Add-Type -AssemblyName System.DirectoryServices.AccountManagement
$ContextType = [System.DirectoryServices.AccountManagement.ContextType]::Domain
$PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new($ContextType, $UserDomain)
$PrincipalContext.ValidateCredentials($UserName,$Password)

请注意这段代码需要 Active Directory 环境,并且不支持本地账号。缺省情况下,它使用您当前账号的详细信息。请根据实际情况调整 $UserDomain$UserName$Password 变量。也请注意 ValidateCredentials() 检查的是明文字符串密码。请谨慎处理并且不要在脚本存储明文密码。同时,不要要求用户以明文输入密码。

今日知识点:

  • PowerShell 可以轻松地连接到 Active Directory 并进行密码验证。

PowerShell 技能连载 - 等待服务状态变化

当您启动或停止一个服务时,可能需要一些时间才能确保服务进入指定的状态——或者它当然可能会失败。当您使用 Stop-Service 时,PowerShell 将等待该服务状态已确认。如果您希望获得其它地方初始化的服务响应,以下是一段监听代码,它将会暂停 PowerShell 直到服务变为指定的状态:

1
2
3
4
5
6
7
8
9
10
11
12
13
# wait 5 seconds for spooler service to stop
$serviceToMonitor = Get-Service -Name Spooler
$desiredStatus = [System.ServiceProcess.ServiceControllerStatus]::Stopped
$maxTimeout = New-TimeSpan -Seconds 5

try
{
$serviceToMonitor.WaitForStatus($desiredStatus, $maxTimeout)
}
catch [System.ServiceProcess.TimeoutException]
{
Write-Warning 'Service did not reach desired status within timeframe.'
}

您可以使用这段代码来响应由外部系统触发的服务改变,或者当您要求服务状态更改后做二次确认。

今日的知识点:

  • 您从 cmdlet 获得的多数对象(例如 Get-Service)有许多有用的方法。所有服务对象都有一个 WaitForStatus 方法。在我们的例子中演示了如何使用它。
  • 要发现隐藏在对象中的其它方法,请使用以下代码:
1
2
3
4
5
# get some object
$objects = Get-Process

# dump the methods
$objects | Get-Member -MemberType *method* | Select-Object -Property Name, Definition

PowerShell 技能连载 - 将文本转为图像

WPF (Windows Presentation Foundation) 不仅仅是创建 UI 的技术。您可以用它来创建任意类型的矢量图并将它保存为图形文件。

以下是一个简单的例子,输入任意的文字和字体,然后将它渲染为一个 PNG 文件:

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
65
66
67
function Convert-TextToImage
{
param
(
[String]
[Parameter(Mandatory)]
$Text,

[String]
$Font = 'Consolas',

[ValidateRange(5,400)]
[Int]
$FontSize = 24,

[System.Windows.Media.Brush]
$Foreground = [System.Windows.Media.Brushes]::Black,

[System.Windows.Media.Brush]
$Background = [System.Windows.Media.Brushes]::White
)

$filename = "$env:temp\$(Get-Random).png"

# take a simple XAML template with some text
$xaml = @"
<TextBlock
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">$Text</TextBlock>
"@

Add-Type -AssemblyName PresentationFramework

# turn it into a UIElement
$reader = [XML.XMLReader]::Create([IO.StringReader]$XAML)
$result = [Windows.Markup.XAMLReader]::Load($reader)

# refine its properties
$result.FontFamily = $Font
$result.FontSize = $FontSize
$result.Foreground = $Foreground
$result.Background = $Background

# render it in memory to the desired size
$result.Measure([System.Windows.Size]::new([Double]::PositiveInfinity, [Double]::PositiveInfinity))
$result.Arrange([System.Windows.Rect]::new($result.DesiredSize))
$result.UpdateLayout()

# write it to a bitmap and save it as PNG
$render = [System.Windows.Media.Imaging.RenderTargetBitmap]::new($result.ActualWidth, $result.ActualHeight, 96, 96, [System.Windows.Media.PixelFormats]::Default)
$render.Render($result)
Start-Sleep -Seconds 1
$encoder = [System.Windows.Media.Imaging.PngBitmapEncoder]::new()
$encoder.Frames.Add([System.Windows.Media.Imaging.BitmapFrame]::Create($render))
$filestream = [System.IO.FileStream]::new($filename, [System.IO.FileMode]::Create)
$encoder.Save($filestream)

# clean up
$reader.Close()
$reader.Dispose()

$filestream.Close()
$filestream.Dispose()

# return the file name for the generated image
$filename
}

以下是使用方法:

1
2
3
PS> $file = Convert-TextToImage -Text 'Red Alert!' -Font Stencil -FontSize 60 -Foreground Red -Background Gray

PS> Invoke-Item -Path $file

今日的知识点:

  • 通过 XAML,一个基于 XML 的 UI 描述语言,您可以定义图像。
  • PowerShell 可以使用 [Windows.Markup.XAMLReader] 类来快速地将任意合法的 XAML 转换为一个 UIElement 对象。
  • UIElement 对象可以保存成图形文件,例如 PNG 图像,可以在窗口中显示,或者打印出来。在这个例子中,我们主要是将它保存为文件,然后我们用了一个非常简单的 XAML 定义。您现在可能会感到很好奇。通过 Google 搜索一下这个例子中的方法,您将会找到很多知识。

PowerShell 技能连载 - 转换 IEEE754 (Float)(第 2 部分)

昨天我们研究了用 PowerShell 如何将传感器返回的 IEEE754 浮点值转换为实际值。这需要颠倒字节顺序并使用 BitConverter 类。

如果您收到了一个十六进制的 IEEE754 值,例如 0x3FA8FE3B,第一个任务就是将十六进制值分割为四个字节。实现这个目标简单得让人惊讶:将该十六进制值视为一个 IPv4 地址。这些地址内部也是使用四个字节。

以下是一个快速且简单的方法,能够将传感器的十六进制数值转换为一个有用的数值:

1
2
3
4
5
6
$hexInput = 0x3FA8FE3B

$bytes = ([Net.IPAddress]$hexInput).GetAddressBytes()
$numericValue = [BitConverter]::ToSingle($bytes, 0)

"Sensor: $numericValue"

今日的知识点:

  • 将数值转换为 IPAddress 对象来将数值分割为字节。这种方法也可以用来得到一个数字的最低有效位 (LSB) 或最高有效位 (MSB) 形式。

PowerShell 技能连载 - 转换 IEEE754 (Float)(第 1 部分)

PowerShell 是非常多功能的,并且现在常常用于 IoT 和传感器。一些 IEEE754 浮点格式往往是一系列四字节的十六进制数。

我们假设一个传感器以 IEEE754 格式返回一个十六进制的数值 3FA8FE3B。那么要如何获取真实值呢?

技术上,您需要颠倒字节顺序,然后用 BitConverter 来创建一个 “Single” 值。

以 3FA8FE3B 为例,将它逐对分割,颠倒顺序,然后转换为一个数字:

1
2
$bytes = 0x3B, 0xFE, 0xA8, 0x3F
[BitConverter]::ToSingle($bytes, 0)

实际结果是,十六进制数值 0x3FA8FE3B 返回了传感器值 1.320258。今天,我们研究 BitConverter 类,这个类提供了多个将字节数组转换为数值的方法。明天,我们将查看另一部分:将十六进制文本数值成对分割并颠倒顺序。

今日的知识点:

  • 使用 [BitConverter] 来将原始字节数组转换为其它数字格式。这个类有大量方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS> [BitConverter] | Get-Member -Static | Select-Object -ExpandProperty Name

DoubleToInt64Bits
Equals
GetBytes
Int64BitsToDouble
IsLittleEndian
ReferenceEquals
ToBoolean
ToChar
ToDouble
ToInt16
ToInt32
ToInt64
ToSingle
ToString
ToUInt16
ToUInt32
ToUInt64

要查看其中某个方法的语法,请键入它们的名称,不包括圆括号:

1
2
3
4
5
PS> [BitConverter]::ToUInt32

OverloadDefinitions
-------------------
static uint32 ToUInt32(byte[] value, int startIndex)

PowerShell 技能连载 - 小心“Throw”语句(第 2 部分)

在前一个技能中我们解释了将 $ErrorActionPreference 设为 “SilentlyContinue” 将如何影响 throw 语句,而且该 throw 将不会正常地退出函数代码。以下还是我们使用过的例子:

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
function Copy-Log
{
"Doing prerequisites"
"Testing whether target path exists"
"If target path does not exist, bail out"
throw "Target path does not exist"
"Copy log files to target path"
"Delete log files in original location"
}



PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out
Target path does not exist
In Zeile:8 Zeichen:3
+ throw "Target path does not exist"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Target path does not exist:String) [], RuntimeExceptio
n
+ FullyQualifiedErrorId : Target path does not exist


PS> $ErrorActionPreference = 'SilentlyContinue'

PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out
Copy log files to target path
Delete log files in original location

虽然 ErrorAction 为默认值时,throw 语句能够退出函数,但是当 ErrorAction 设为 “SilentlyContinue” 时代码将继续执行。这很可能是一个 bug,因为 ErrorAction 的值设为 “Continue” 和 “SilentlyContinue” 的唯一区别只应该是错误信息可见与否。这些设置不应该影响实际执行的代码。

这个 throw 中的 bug 只发生在 throw 抛出的终止错误没有被处理的时候。当您使用 try..catch 语句或者甚至添加一个简单的(完全为空的)trap 语句时,一切都正确了,throw 能够像预期地工作了:

1
2
3
4
5
6
7
# add a trap to fix
trap {}

$ErrorActionPreference = "SilentlyContinue"
Copy-Log
$ErrorActionPreference = "Continue"
Copy-Log

一旦添加了捕获语句,这段代码将会在 throw 语句处跳出,无论 $ErrorActionPreference 的设置为什么。您可以在 PowerShell 用户设置脚本中添加一个空白的捕获语句来防止这个 bug,或者重新考虑是否使用 throw 语句。

重要的知识点:

  • throw 是错误处理系统的一部分,通过 throw 抛出的异常需要用 try..catch 或者 trap 语句。如果异常没有捕获,throw 的工作方式可能和预期的不一致。
  • 因为存在这个问题,所以对于暴露给最终用户的函数,不要用 throw 来退出函数代码。对于最终用户,与其抛出一个(丑陋的)异常,还不如用 Write-WarningWrite-Host 抛出一条对人类友好的错误信息,然后用 return 语句友好地退出代码。
  • 如果您必须抛出一个异常,以便调用者能够在他们的错误处理器中捕获它,但也需要确保无论 $ErrorActionPreference 为何值都能可靠地退出代码,请使用 Write-Errorreturn 语句的组合:
1
2
3
4
5
6
7
8
9
function Copy-Log
{
"Doing prerequisites"
"Testing whether target path exists"
"If target path does not exist, bail out"
Write-Error "Target path does not exist"; return
"Copy log files to target path"
"Delete log files in original location"
}

由于 return 语句不受 $ErrorActionPreference 的影响,您的代码总是能够退出。让我们做个测试:

1
2
3
4
5
6
7
8
9
10
PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out
Copy-Log : Target path does not exist
In Zeile:1 Zeichen:1
+ Copy-Log
+ ~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Write-Error], WriteErrorException
+ FullyQualifiedErrorId : Microsoft.PowerShell.Commands.WriteErrorException,Copy-Log

如果将 $ErrorActionPreference 设为 SilentlyContinue,错误信息和预期一致地被屏蔽了,但是代码确实退出了:

1
2
3
4
5
6
PS> $ErrorActionPreference = 'SilentlyContinue'

PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out

PowerShell 技能连载 - 小心“Throw”语句(第 1 部分)

throw 是一个 PowerShell 语句,会抛出一个异常到调用者,并退出代码。至少理论上是这样。实际中,throw 可能不会退出代码,而且结果可能是毁灭性的。

要理解这个问题,请查看这个演示函数:

1
2
3
4
5
6
7
8
9
function Copy-Log
{
"Doing prerequisites"
"Testing whether target path exists"
"If target path does not exist, bail out"
throw "Target path does not exist"
"Copy log files to target path"
"Delete log files in original location"
}

当您运行 Copy-Log 时,它模拟了一个失败情况,假设一个目标路径不存在。当目标路径不存在时,不能复制日志文件。如果日志文件没有复制,那么不能删除它们。这是为什么调用 throw 时代码需要退出得原因。而且它确实有效:

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out
Target path does not exist
In Zeile:8 Zeichen:3
+ throw "Target path does not exist"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (Target path does not exist:String) [], RuntimeExceptio
n
+ FullyQualifiedErrorId : Target path does not exist

PS>

然而,这是基于 $ErrorActionPreference 为缺省值 “Continue” 时的行为。当一个用户恰好将它改为 “SilentlyContinue” 来禁止错误信息时,throw 会被彻底忽略,而且所有代码将会执行:

1
2
3
4
5
6
7
8
PS> $ErrorActionPreference = 'SilentlyContinue'

PS> Copy-Log
Doing prerequisites
Testing whether target path exists
If target path does not exist, bail out
Copy log files to target path
Delete log files in original location

在这个场景中,您可能会丢失所有日志文件,因为复制操作没有生效,而代码继续执行并删除了原始文件。

重要的知识点:

  • 如果退出函数对您来说很重要,throw 可能并不会真正地退出函数。您可能需要用其他方法来退出代码,例如 return 语句。

PowerShell 技能连载 - 读取新闻订阅

以下是一个针对有德语技能的用户的特殊服务——对于其他人修改代码会有所挑战:以下代码使用了德国主要新闻杂志的 RSS 订阅,打开一个选择窗口。在窗口中您可以选择一篇或多篇文章,然后在缺省的浏览器中打开选择的文章:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# URL to RSS Feed
$url = 'http://www.spiegel.de/schlagzeilen/index.rss'


$xml = New-Object -TypeName XML
$xml.Load($url)

# the subproperties (rss.channel.item) depend on the RSS feed you use
# and may be named differently
$xml.rss.channel.item |
Select-Object -Property title, link |
Out-GridView -Title 'What would you like to read today?' -OutputMode Multiple |
ForEach-Object {
Start-Process $_.link
}

基本的设计过程是一致的:要将代码改为另一个 RSS 订阅,只需要导航到相应的属性(背后的 XML 的嵌套结构)。