PowerShell 技能连载 - 从函数中返回富对象(第 2 部分)

当一个函数返回多于四个属性时,PowerShell 将输出结果格式化为列表,否则格式化为表格。在您学习新的方法来影响这种行为之前,请自己验证一下。以下函数返回一个多于 6 个属性的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Get-TestData
{
# if a function is to return more than one information kind,
# wrap it in a custom object

[PSCustomObject]@{
# wrap anything you'd like to return
ID = 1
Random = Get-Random
Date = Get-Date
Text = 'Hello'
BIOS = Get-WmiObject -Class Win32_BIOS
User = $env:username
}
}

结果是以表格形式呈现:

1
2
3
4
5
6
7
8
9
10
PS> Get-TestData


ID : 1
Random : 147704985
Date : 25.05.2018 13:09:26
Text : Hello
BIOS : \\DESKTOP-7AAMJLF\root\cimv2:Win32_BIOS.Name="1.6.1",SoftwareElementID="1.6.1",SoftwareElementState=3,TargetOperatingSys
tem=0,Version="DELL - 1072009"
User : tobwe

当移除掉一些属性,限制属性个数为 4 个或更少时,PowerShell 输出一个表格:

1
2
3
4
5
PS> Get-TestData

ID Random Text User
-- ------ ---- ----
1 567248729 Hello tobwe

通常,表格的形式比较容易阅读,特别是有多个数据集的时候。当您得到一个 4 个或更少属性的表格时,您可能不是始终希望返回值只有 4 个属性。所以为什么不像类似 cmdlet 一样处理它呢?

Cmdlet 默认情况下只显示属性的一部分:

1
2
3
4
5
PS> Get-Service | Select-Object -First 1

Status Name DisplayName
------ ---- -----------
Running AdobeARMservice Adobe Acrobat Update Service

使用 Select-Object 可以显示地获得所有属性的列表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
PS> Get-Service | Select-Object -First 1 -Property *


Name : AdobeARMservice
RequiredServices : {}
CanPauseAndContinue : False
CanShutdown : False
CanStop : True
DisplayName : Adobe Acrobat Update Service
DependentServices : {}
MachineName : .
ServiceName : AdobeARMservice
ServicesDependedOn : {}
ServiceHandle :
Status : Running
ServiceType : Win32OwnProcess
StartType : Automatic
Site :
Container

显然,有第一公民和第二公民之分。在您自己的函数中,您可以类似这样定义第一公民:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Get-TestData
{
# define the first-class citizen
[string[]]$visible = 'ID','Date','User'
$info = New-Object System.Management.Automation.PSPropertySet('DefaultDisplayPropertySet',$visible)


[PSCustomObject]@{
# wrap anything you'd like to return
ID = 1
Random = Get-Random
Date = Get-Date
Text = 'Hello'
BIOS = Get-WmiObject -Class Win32_BIOS
User = $env:username
} |
# add the first-class citizen info to your object
Add-Member -MemberType MemberSet -Name PSStandardMembers -Value $info -PassThru

}

现在,您的函数的行为类似 cmdlet,而且您没有定义多于 4 个一等公民,所以缺省情况下得到一个表格的形式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS> Get-TestData

ID Date User
-- ---- ----
1 25.05.2018 13:15:15 tobwe



PS> Get-TestData | Select-Object -Property *


ID : 1
Random : 1298877814
Date : 25.05.2018 13:15:22
Text : Hello
BIOS : \\DESKTOP-7AAMJLF\root\cimv2:Win32_BIOS.Name="1.6.1",SoftwareElementID="1.6.1",SoftwareElementState=3,TargetOperatingSys
tem=0,Version="DELL - 1072009"
User : tobwe

PowerShell 技能连载 - 从函数中返回富对象(第 1 部分)

如果一个 PowerShell 函数需要返回多于一类信息,请确保将这些信息集中到一个富对象中。最简单的实现方式是创建一个类似 [PSCustomObject]@{} 这样的自定义对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function Get-TestData
{
# if a function is to return more than one information kind,
# wrap it in a custom object

[PSCustomObject]@{
# wrap anything you'd like to return
ID = 1
Random = Get-Random
Date = Get-Date
Text = 'Hallo'
BIOS = Get-WmiObject -Class Win32_BIOS
User = $env:username
}
}

自定义对象的核心是一个哈希表:每个哈希表键将会转换为一个属性。这个方式的好处是您可以使用哈希表中的变量甚至命令,所以这样要收集您想返回的所有信息,将它合并为一个自描述的对象很容易:

1
2
3
4
5
6
7
8
9
10
PS> Get-TestData


ID : 1
Random : 1794057589
Date : 25.05.2018 13:06:57
Text : Hallo
BIOS : \\DESKTOP-7AAMJLF\root\cimv2:Win32_BIOS.Name="1.6.1",SoftwareElementID="1.6.1",SoftwareElementState=3,TargetOperatingSys
tem=0,Version="DELL - 1072009"
User : tobwe

PowerShell 技能连载 - 在函数内使用持久变量

默认情况下,当一个 PowerShell 函数退出时,它将“忘记”所有的内部变量。然而,有一种办法可以创建持久的内部变量。以下是实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# create a script block with internal variables
# that will persist
$c = & {
# define an internal variable that will
# PERSIST and keep its value even though
# the function exits
$a = 0

{
# use the internal variable
$script:a++
"You called me $a times!"
}.GetNewClosure()
}

这段代码创建一个包含内部变量的脚本块。当您多次运行这个脚本块时,计数器会累加:

1
2
3
4
5
6
7
8
PS> & $c
You called me 1 times!

PS> & $c
You called me 2 times!

PS> & $c
You called me 3 times!

然而,脚本内的 $a 变量的作用域既不是 global 也不是 scriptglobal。它的作用域只在脚本块的内部:

1
PS> $a

要将脚本块转换为函数,请加上这段代码:

1
2
3
4
5
6
7
PS> Set-Item -Path function:Test-Function -Value $c

PS> Test-Function
You called me 5 times!

PS> Test-Function
You called me 6 times!

PowerShell 技能连载 - 使用缺省参数

If you find yourself always using the same parameter values over again, try using PowerShell default parameters. Here is how:
如果您常常反复使用相同的参数值,试着使用 PowerShell 的缺省参数。以下是实现方法:

1
2
3
4
5
6
7
8
9
10
11
12
# hash table
# Key =
# Cmdlet:Parameter
# Value =
# Default value for parameter
# * (Wildcard) can be used

$PSDefaultParameterValues = @{
'Stop-Process:ErrorAction' = 'SilentlyContinue'
'*:ComputerName' = 'DC-01'
'Get-*:Path' = 'c:\windows'
}

在它的核心部分,有一个名为 $PSDefaultParametersValues 的哈希表。缺省情况下,这个变量不存在或者为空。如果您想将缺省参数复位为它们的缺省值,请运行以下代码:

1
PS> $PSDefaultParameterValues = $null

哈希表的键是 cmdlet 名和参数值,通过 *** 来分隔。支持通配符。哈希表的值是缺省参数值。

在上述例子中:

  • Stop-Process-ErrorAction 参数将总是使用 “SilentlyContinue” 值。
  • 所有使用 -ComputerName 参数的 cmdlet 将使用 “DC-01”。
  • 所有使用 Get 动词和 -Path 参数的 cmdlet 将默认使用 “C:\Windows” 值

缺省参数可能很方便,也可能很怪异。您不应在配置文件脚本中定义它们,因为您有可能忘了做过这件事,而下周会奇怪 cmdlet 执行的行为和预期不一致。

并且缺省参数只对 cmdlet 和高级函数有效。它们对简单函数是无效的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Start-SimpleFunction
{
param($ID=100)

"ID = $ID"
}

function Start-AdvancedFunction
{
[CmdletBinding()]
param($ID=100)

"ID = $ID"

}

$PSDefaultParameterValues = @{
"Start-*Function:ID" = 12345
}

以下是执行结果:

1
2
3
4
5
6
7
PS> Start-SimpleFunction
ID = 100

PS> Start-AdvancedFunction
ID = 12345

PS>

PowerShell 技能连载 - 速度差别:读取大型日志文件

需要读取大型日志文件时,例如,解析错误信息,PowerShell 既可以使用低内存占用的管道,也可以使用高内存占用的循环。不过,区别不仅在于内存的消耗,而且在速度上。

通过管道,不需要消耗太多的内存但是速度可能非常慢。通过传统的循环,脚本可以 10 倍甚至 100 倍的速度生成相同的结果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# make sure this file exists, or else
# pick a different text file that is very large
$path = 'C:\Windows\Logs\DISM\dism.log'
# SLOW
# filtering text file via pipeline (low memory usage)
Measure-Command {
$result = Get-Content -Path $Path | Where-Object { $_ -like '*Error*' }
}

# FAST
# filtering text by first reading in all
# content (high memory usage!) and then
# using a classic loop

Measure-Command {
$lines = Get-Content -Path $Path -ReadCount 0
$result = foreach ($line in $lines)
{
if ($line -like '*Error*') { $line }
}
}

PowerShell 技能连载 - 查看文件对应的可执行程序

多数事情可以由 PowerShell 的内置指令完成,但是那还不够,您总是可以借助内置的 Windows API。例如,如果您想查看某个文件关联的应用程序,请试试这段代码:

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
function Get-ExecutableForFile
{
param
(
[Parameter(Mandatory)]
[string]
$Path
)

$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
[Win32API]::FindExecutable($Path)
}

以下是使用这个函数的方法:

1
PS> Set-EnvironmentVariable -Name test -Value 123 -Target User

PowerShell 技能连载 - 理解脚本块日志(第 7 部分)

这是关于 PowerShell 脚本块日志的迷你系列的第 7 部分。我们现在只需要一些能清理脚本快日志记录的清理工具,您需要管理员特权。

清理日之前请注意:这将清理整个 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
function Clear-PowerShellLog
{
<#
.SYNOPSIS
Ckears the entire PowerShell operational log including
script blog logging entries.
Administrator privileges required.

.DESCRIPTION
Clears the complete content of the log
Microsoft-Windows-PowerShell/Operational.
This includes all logged script block code.

.EXAMPLE
Clear-PowershellLog
Clears the entire log Microsoft-Windows-PowerShell/Operational.
#>
[CmdletBinding(ConfirmImpact='High')]
param()

try
{
$ErrorActionPreference = 'Stop'
wevtutil cl Microsoft-Windows-PowerShell/Operational
}
catch
{
Write-Warning "Administrator privileges required. Run this command from an elevated PowerShell."

PowerShell 技能连载 - 理解脚本块日志(第 6 部分)

这是关于 PowerShell 脚本块日志的迷你系列的第 6 部分。该是时候介绍最后的部分了:当您执行一段非常大的 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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
function Get-LoggedCode
{
# to speed up SID-to-user translation,
# we use a hash table with already translated SIDs
# it is empty at first
$translateTable = @{}

# read all raw events
$logInfo = @{ ProviderName="Microsoft-Windows-PowerShell"; Id = 4104 }
Get-WinEvent -FilterHashtable $logInfo |
# take each raw set of data...
ForEach-Object {
# store the code in this entry

# if this is the first part, take it
if ($_.Properties[0].Value -eq 1)
{
$code = $_.Properties[2].Value
}
# else, add it to the $code variable
else
{
$code += $_.Properties[2].Value
}

# return the object when all parts have been processed
if ($_.Properties[0].Value -eq $_.Properties[1].Value)
{
# turn SID into user
$userSID = $_.UserId

# if the cache does not contain the user SID yet...
if (!$translateTable.ContainsKey($userSid))
{
try
{
# ...try and turn it into a real name, and add it
# to the cache
$identifier = New-Object System.Security.Principal.SecurityIdentifier($userSID)
$result = $identifier.Translate( [System.Security.Principal.NTAccount]).Value
$translateTable[$userSid] = $result
}
catch
{
# if this fails, use the SID instead of a real name
$translateTable[$userSid] = $userSID
}
}
else
{

}

# create a new object and extract the interesting
# parts from the raw data to compose a "cooked"
# object with useful data
[PSCustomObject]@{
# when this was logged
Time = $_.TimeCreated
# script code that was logged
Code = $code
# path of script file (this is empty for interactive
# commands)
Path = $_.Properties[4].Value
# log level
# by default, only level "Warning" will be logged
Level = $_.LevelDisplayName
# user who executed the code
# take the real user name from the cache of translated
# user names
User = $translateTable[$userSID]
}
}
}
}

本质上,这个函数检查该脚本是否是由多段组成。如果是,它将代码添加到 $code 中,直到当前块等于 最后一块。就是这么简单。

PowerShell 技能连载 - 理解脚本块日志(第 5 部分)

这是关于 PowerShell 脚本块日志的迷你系列的第 5 部分。我们已经几乎达到目标了,缺少的是一个更好的方式来读取记录的代码。在我们之前的方法中,执行代码的用户收到一个晦涩的 SID 而不是一个清晰的名称。以下是一个将用户 SID 转为真实名称的函数,并且使用智能缓存来加速 SID 的查询过程:

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
function Get-LoggedCode
{
# to speed up SID-to-user translation,
# we use a hash table with already translated SIDs
# it is empty at first
$translateTable = @{}

# read all raw events
$logInfo = @{ ProviderName="Microsoft-Windows-PowerShell"; Id = 4104 }
Get-WinEvent -FilterHashtable $logInfo |
# take each raw set of data...
ForEach-Object {
# turn SID into user
$userSID = $_.UserId

# if the cache does not contain the user SID yet...
if (!$translateTable.ContainsKey($userSid))
{
try
{
# ...try and turn it into a real name, and add it
# to the cache
$identifier = New-Object System.Security.Principal.SecurityIdentifier($userSID)
$result = $identifier.Translate( [System.Security.Principal.NTAccount]).Value
$translateTable[$userSid] = $result
}
catch
{
# if this fails, use the SID instead of a real name
$translateTable[$userSid] = $userSID
}
}
else
{

}

# create a new object and extract the interesting
# parts from the raw data to compose a "cooked"
# object with useful data
[PSCustomObject]@{
# when this was logged
Time = $_.TimeCreated
# script code that was logged
Code = $_.Properties[2].Value
# if code was split into multiple log entries,
# determine current and total part
PartCurrent = $_.Properties[0].Value
PartTotal = $_.Properties[1].Value

# if total part is 1, code is not fragmented
IsMultiPart = $_.Properties[1].Value -ne 1
# path of script file (this is empty for interactive
# commands)
Path = $_.Properties[4].Value
# log level
# by default, only level "Warning" will be logged
Level = $_.LevelDisplayName
# user who executed the code
# take the real user name from the cache of translated
# user names
User = $translateTable[$userSID]
}
}
}

PowerShell 技能连载 - 理解脚本块日志(第 4 部分)

这是关于 PowerShell 脚本块日志的迷你系列的第 4 部分。到目前为止,您已经了解了如何读取记录的 PowerShell 代码,以及如何打开详细模式。打开详细模式后,机器上运行的所有 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
function Set-SBLLogSize
{
<#
.SYNOPSIS
Sets a new size for the script block logging log.
Administrator privileges required.

.DESCRIPTION
By default, the script block log has a maximum size of 15MB
which may be too small to capture and log PowerShell activity
over a given period of time. With this command,
you can assign more memory to the log.

.PARAMETER MaxSizeMB
New log size in Megabyte

.EXAMPLE
Set-SBLLogSize -MaxSizeMB 100
Sets the maximum log size to 100MB.
Administrator privileges required.
#>


param
(
[Parameter(Mandatory)]
[ValidateRange(15,3000)]
[int]
$MaxSizeMB
)

$Path = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\WINEVT\Channels\Microsoft-Windows-PowerShell/Operational"
try
{
$ErrorActionPreference = 'Stop'
Set-ItemProperty -Path $Path -Name MaxSize -Value ($MaxSizeMB * 1MB)
}
catch
{
Write-Warning "Administrator privileges required. Run this command from an elevated PowerShell."
}
}

要将日志文件的尺寸从缺省的 15MB 增加到 100MB,请运行以下代码(需要管理员特权):

1
PS> Set-SBLLogSize -MaxSizeMB 100