PowerShell 技能连载 - Real-Time Processing for Language Structures

In the previous tip we looked at queues and how they can search the entire file system:

# create a new queue
$dirs = [System.Collections.Queue]::new()

# add an initial path to the queue
# any folder path in the queue will later be processed
$dirs.Enqueue('c:\windows')

# process all elements on the queue until all are taken
While ($current = $dirs.Dequeue())
{
    # find subfolders of current folder, and if present,
    # add them all to the queue
    try
    {
        foreach ($_ in [IO.Directory]::GetDirectories($current))
        {
                $dirs.Enqueue($_)
        }
    } catch {}

    try
    {
        # find all files in the folder currently processed
        [IO.Directory]::GetFiles($current, "*.exe")
        [IO.Directory]::GetFiles($current, "*.ps1")
    } catch { }
}

How would you process the data created by the loop though, i.e. to display it in a grid view window? You cannot pipe it in real-time, so this fails:

$dirs = [System.Collections.Queue]::new()
$dirs.Enqueue('c:\windows')

While ($current = $dirs.Dequeue())
{
    try
    {
        foreach ($_ in [IO.Directory]::GetDirectories($current))
        {
                $dirs.Enqueue($_)
        }
    } catch {}

    try
    {
        [IO.Directory]::GetFiles($current, "*.exe")
        [IO.Directory]::GetFiles($current, "*.ps1")
    } catch { }
# this fails
} | Out-GridView

You can save the results produced by do-while to a variable. That works but takes forever because you’d have to wait for the loop to complete until you can do something with the variable:

$dirs = [System.Collections.Queue]::new()
$dirs.Enqueue('c:\windows')

# save results to variable...
$all = while ($current = $dirs.Dequeue())
{
    try
    {
        foreach ($_ in [IO.Directory]::GetDirectories($current))
        {
                $dirs.Enqueue($_)
        }
    } catch {}

    try
    {
        [IO.Directory]::GetFiles($current, "*.exe")
        [IO.Directory]::GetFiles($current, "*.ps1")
    } catch { }
}

# then process or output
$all | Out-GridView

The same limitation applies when you use $() or other constructs. To process the results emitted by do-while in true real-time, use a script block instead:

$dirs = [System.Collections.Queue]::new()
$dirs.Enqueue('c:\windows')

# run the code in a script block
& { while ($current = $dirs.Dequeue())
    {
        try
        {
            foreach ($_ in [IO.Directory]::GetDirectories($current))
            {
                    $dirs.Enqueue($_)
            }
        } catch {}

        try
        {
            [IO.Directory]::GetFiles($current, "*.exe")
            [IO.Directory]::GetFiles($current, "*.ps1")
        } catch { }
    }
} | Out-GridView

With this approach, results start to show in the grid view window almost momentarily, and you don’t have to wait for the loop to complete.


psconf.eu – PowerShell Conference EU 2019 – June 4-7, Hannover Germany – visit www.psconf.eu There aren’t too many trainings around for experienced PowerShell scripters where you really still learn something new. But there’s one place you don’t want to miss: PowerShell Conference EU - with 40 renown international speakers including PowerShell team members and MVPs, plus 350 professional and creative PowerShell scripters. Registration is open at www.psconf.eu, and the full 3-track 4-days agenda becomes available soon. Once a year it’s just a smart move to come together, update know-how, learn about security and mitigations, and bring home fresh ideas and authoritative guidance. We’d sure love to see and hear from you!

Twitter This Tip!ReTweet this Tip!

PowerShell 技能连载 - 用队列代替嵌套

与其使用递归函数,您可能会希望使用一个 Queue 对象,这样在加载新的任务时可以卸载已处理的数据。

Lee Homes 最近贴出了以下示例,它不使用递归调用的方式而搜索了整个文件系统的文件夹树:

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
# create a new queue
$dirs = [System.Collections.Queue]::new()

# add an initial path to the queue
# any folder path in the queue will later be processed
$dirs.Enqueue('c:\windows')

# process all elements on the queue until all are taken
While ($current = $dirs.Dequeue())
{
# find subfolders of current folder, and if present,
# add them all to the queue
try
{
foreach ($_ in [IO.Directory]::GetDirectories($current))
{
$dirs.Enqueue($_)
}
} catch {}

try
{
# find all files in the folder currently processed
[IO.Directory]::GetFiles($current, "*.exe")
[IO.Directory]::GetFiles($current, "*.ps1")
} catch { }
}

try-catch 语句块是必要的,因为当没有文件或文件夹权限时,.NET 方法会抛出异常。

PowerShell 技能连载 - 查找服务特权

Get-Service 可以提供 Windows 服务的基础信息但是并不会列出所需要的特权。以下是一段简短的 PowerShell 函数,输入一个服务名并返回服务特权:

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
function Get-ServicePrivilege
{

param
(
[Parameter(Mandatory)]
[string]
$ServiceName
)

# find the service
$Service = @(Get-Service -Name $ServiceName -ErrorAction Silent)
# bail out if there is no such service
if ($Service.Count -ne 1)
{
Write-Warning "$ServiceName unknown."
return
}

# read the service privileges from registry
$Path = 'HKLM:\SYSTEM\CurrentControlSet\Services\' + $service.Name
$Privs = Get-ItemProperty -Path $Path -Name RequiredPrivileges

# output in custom object
[PSCustomObject]@{
ServiceName = $Service.Name
DisplayName = $Service.DisplayName
Privileges = $privs.RequiredPrivileges
}
}



PS C:\> Get-ServicePrivilege spooler

ServiceName DisplayName Privileges
----------- ----------- ----------
spooler Druckwarteschlange {SeTcbPrivilege, SeImpersonatePrivilege, SeAuditPrivilege, SeChangeNotifyPrivilege...}



PS C:\> Get-ServicePrivilege XboxGipSvc

ServiceName DisplayName Privileges
----------- ----------- ----------
XboxGipSvc Xbox Accessory Management Service {SeTcbPrivilege, SeImpersonatePrivilege, SeChangeNotifyPrivilege, SeCreateGlobalPrivilege}

PowerShell 技能连载 - 使用变量断点(第 2 部分)

在前一个技能中我们试验了用 Set-PSBreakpoint 在 PowerShell 中创建动态变量断点。我们演示了当一个变量改变时,如何触发一个断点。

然而,如果您希望监视对象的属性呢?假设您希望监视数组的大小,当数组元素变多时自动进入调试器。

在这个场景中,PowerShell 变量并没有改变。实际上是变量中的对象发生了改变。所以您需要一个“读”模式的断点而不是一个“写”模式的断点:

1
2
3
4
5
6
7
8
9
10
11
# break when $array’s length is greater than 10
Set-PSBreakpoint -Variable array -Action { if ($array.Length -gt 10) { break }} -Mode Read -Script $PSCommandPath

$array = @()
do
{
$number = Get-Random -Minimum -20 -Maximum 20
"Adding $number to $($array.count) elements"
$array += $number

} while ($true)

$array 数组的元素超过 10 个时,脚本会中断下来并进入调试器。别忘了按 SHIFT+F5 退出调试器。

PowerShell 技能连载 - 使用变量断点(第 1 部分)

在调试过程中,变量断点可能非常有用。当一个变量改变时,变量断点能够自动生效并进入调试器。如果您知道当异常发生时某个变量变为某个设置的值(或是 NULL 值),那么可以让调试器只在那个时候介入。

以下例子演示如何使用变量断点。最好在脚本的顶部定义它们,因为您可以用 $PSCommandPath 来检验断点所需要的实际脚本文件路径:

1
2
3
4
5
6
7
8
9
10
# initialize variable breakpoints (once)
# break when $a is greater than 10
Set-PSBreakpoint -Variable a -Action { if ($a -gt 10) { break }} -Mode Write -Script $PSCommandPath

# run the code to debug
do
{
$a = Get-Random -Minimum -20 -Maximum 20
"Drawing: $a"
} while ($true)

请确保执行之前先保存脚本:调试始终需要一个物理文件。

如您所见,当变量 $a 被赋予一个大于 10 的值时,调试器会自动中断下来。您可以使用 “exit“ 命令继续,用 “?“ 查看所有调试器选项,并且按 SHIFT+F4 停止。

要移除所有断点,运行这行代码:

1
PS C:\> Get-PSBreakpoint | Remove-PSBreakpoint

PowerShell 技能连载 - 隐藏返回结果的属性

默认情况下,PowerShell 会精简对象并且只显示最重要的属性:

1
2
3
4
5
6
7
8
PS C:\> Get-WmiObject -Class Win32_BIOS


SMBIOSBIOSVersion : 1.9.0
Manufacturer : Dell Inc.
Name : 1.9.0
SerialNumber : DLGQD72
Version : DELL - 1072009

要查看真实的信息,需要使用 Select-Object 并显示要求显示所有信息:

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
PS C:\> Get-WmiObject -Class Win32_BIOS | Select-Object -Property *


PSComputerName : DESKTOP-7AAMJLF
Status : OK
Name : 1.9.0
Caption : 1.9.0
SMBIOSPresent : True
__GENUS : 2
__CLASS : Win32_BIOS
__SUPERCLASS : CIM_BIOSElement
__DYNASTY : CIM_ManagedSystemElement
__RELPATH : Win32_BIOS.Name="1.9.0",SoftwareElementID="1.9.0",SoftwareElementState=3,TargetOperatingSystem=0,Version="DELL - 1072009"
__PROPERTY_COUNT : 31
__DERIVATION : {CIM_BIOSElement, CIM_SoftwareElement, CIM_LogicalElement, CIM_ManagedSystemElement}
__SERVER : DESKTOP-7AAMJLF
__NAMESPACE : root\cimv2
__PATH : \\DESKTOP-7AAMJLF\root\cimv2:Win32_BIOS.Name="1.9.0",SoftwareElementID="1.9.0",SoftwareElementState=3,TargetOperatingSystem=0,Version="D
ELL - 1072009"
BiosCharacteristics : {7, 9, 11, 12...}
BIOSVersion : {DELL - 1072009, 1.9.0, American Megatrends - 5000B}
BuildNumber :
CodeSet :

ClassPath : \\DESKTOP-7AAMJLF\root\cimv2:Win32_BIOS
Properties : {BiosCharacteristics, BIOSVersion, BuildNumber, Caption...}
SystemProperties : {__GENUS, __CLASS, __SUPERCLASS, __DYNASTY...}
Qualifiers : {dynamic, Locale, provider, UUID}
Site :
Container :

如何在自己的 PowerShell 函数中实现相同的内容并且返回自己的对象?

只需要告诉 PowerShell 缺省情况下需要可见的最重要的属性。以下是一个示例。Get-Info 函数创建一个有五个属性的自定义对象。在函数返回这个对象之前,它使用一些 PowerShell 的魔法对这个对象进行标记并且列出缺省的属性:

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
function Get-Info
{

# prepare the object returned by the function
$result = [PSCustomObject]@{
Name = $env:username
Date = Get-Date
BIOS = Get-WmiObject -Class Win32_BIOS | Select-Object -ExpandProperty SMBIOSBIOSVersion
Computername = $env:COMPUTERNAME
Random = Get-Date
}

#region Define the VISIBLE properties
# this is the list of properties visible by default
[string[]]$visible = 'Name','BIOS','Random'
$typ = 'DefaultDisplayPropertySet'
[Management.Automation.PSMemberInfo[]]$info =
New-Object System.Management.Automation.PSPropertySet($typ,$visible)

# add the information about the visible properties to the return value
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $info -InputObject $result
#endregion


# return the result object
return $result
}

以下是执行结果:

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

Name BIOS Random
---- ---- ------
tobwe 1.9.0 01.04.2019 19:32:44



PS C:\> Get-Info | Select-Object -Property *


Name : tobwe
Date : 01.04.2019 19:32:50
BIOS : 1.9.0
Computername : DESKTOP-7AAMJLF
Random : 01.04.2019 19:32:50

PowerShell 技能连载 - 锁定工作站

PowerShell 可以通过 C# 形式的代码操作底层 API。通过这种方法,可以在内存中编译 API 函数并添加新类型。以下例子使用一个 API 函数来锁定工作站:

1
2
3
4
5
6
7
Function Lock-WorkStation
{
$signature = '[DllImport("user32.dll",SetLastError=true)]
public static extern bool LockWorkStation();'
$t = Add-Type -memberDefinition $signature -name api -namespace stuff -passthru
$null = $t::LockWorkStation()
}

要锁定当前用户,请运行以下代码:

1
PS C:\> Lock-WorkStation

PowerShell 技能连载 - 命令发现机制揭秘(第 2 部分)

当您在 PowerShell 中键入一条命令,引擎将触发三个事件来发现您想执行的命令。这为您提供了许多机会来拦截并改变命令的发现机制。让我们教 PowerShell 当在命令中加入 >> 时将命令输出结果发送到 Out-GridView

一下是代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
$ExecutionContext.InvokeCommand.PreCommandLookupAction = {
param
(
[string]
$Command,

[Management.Automation.CommandLookupEventArgs]
$Obj
)

# when the command ends with ">>"...
if ($Command.EndsWith('>>'))
{
# ...remove the ">>" from the command...
$RealCommand = $Command.Substring(0, $Command.Length-2)
# ...run the original command with its original arguments,
# and pipe the results to a grid view window
$obj.CommandScriptBlock = {
& $RealCommand @args | Out-GridView
# use a new "closure" to make the $RealCommand variable available
# inside the script block when it is later called
}.GetNewClosure()
}
}

接下来,输入两条命令:

1
2
PS C:\> Get-Process -Id $PID
PS C:\> Get-Process>> -Id $PID

第一条命令只是输出当前进程。第二条命令自动将执行结果输出到 Out-GridView

如果您希望取消这种行为,请重新启动 PowerShell(或将一个空脚本块赋值给该事件)。如果您希望使该行为永久生效,请将以上代码加入到您的 profile 脚本中。

PowerShell 技能连载 - 命令发现机制揭秘(第 1 部分)

当您在 PowerShell 键入一个命令,将触发一系列事件来指定命令所在的位置。这从 PreCommandLookupAction 开始,您可以用它来记录日志。请看如下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$ExecutionContext.InvokeCommand.PreCommandLookupAction = {
param
(
[string]
$Command,

[Management.Automation.CommandLookupEventArgs]
$Obj
)
$whitelist = @(
'prompt',
'out-default',
'psconsolehostreadline',
'Microsoft.PowerShell.Core\Set-StrictMode'
)

if ($Command -notin $whitelist -and $Obj.CommandOrigin -eq 'Runspace')
{
$host.UI.WriteLine('Yellow','White',$Command)
}
}

当您运行这段代码,所有键入的命令都将回显到控制台中——除了白名单中列出命令。这演示了 PreCommandLookupAction 的工作方式:每当您键入一条命令时,将自动触发它,而且您也可以将命令写入一个日志文件。

PowerShell 技能连载 - 向字符串添加数字(第 2 部分)

在前一个技能中我们演示了一系列安全地将变量加入到字符串中的方法。将变量变量添加到双引号包围的文本中会导致

# this is the desired output:
# PowerShell Version is 5.1.17763.316

# this DOES NOT WORK:
"PowerShell Version is $PSVersionTable.PSVersion"

当您运行这段代码,输出结果并不是大多数人想象的那样。语法着色已经暗示了错误的地方:双引号括起来的字符串只会解析变量。他们不关心后续的任何信息。所以由于 $PSVersionTable 是一个哈希表对象,PowerShell 输出的是对象类型名称,然后在后面加上 “.PSVersion”:

1
2
PS> "PowerShell Version is $PSVersionTable.PSVersion"
PowerShell Version is System.Collections.Hashtable.PSVersion

以下是四种有效的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
# use a subexpression
"PowerShell Version is $($PSVersionTable.PSVersion)"

# use the format (-f) operator
'PowerShell Version is {0}' -f $PSVersionTable.PSVersion


# concatenate (provided the first element is a string)
'PowerShell Version is ' + $PSVersionTable.PSVersion

# use simple variables
$PSVersion = $PSVersionTable.PSVersion
"PowerShell Version is $PSVersion"