PowerShell 技能连载 - 通过 PowerShell 调用 Excel 宏

PowerShell 可以调用 Microsoft Excel 工作表,并执行其中的宏。由于这只对可见的 Excel 程序窗口有效,所以当您尝试进行安全敏感操作,例如宏时,最好保持 Excel 打开(参见以下代码)来查看警告信息:

1
2
3
4
5
6
7
8
9
# file path to your XLA file with macros
$FilePath = "c:\test\file.xla"
# macro name to run
$Macro = "AddData"

$excel = New-Object -ComObject Excel.Application
$excel.Visible = $true
$wb = $excel.Workbooks.Add($FilePath)
$excel.Run($Macro)

PowerShell 技能连载 - 编程列出所有 Cmdlet 或函数参数的列表

是否曾好奇如何列出一个函数或 cmdlet 暴露出的所有属性?以下是实现方法:

1
Get-Help Get-Service -Parameter * | Select-Object -ExpandProperty name

Get-Help 提供了一系列关于参数的有用的信息和元数据。如果您只希望转储支持管道输入的参数,以下是实现方法:

1
2
3
Get-Help Get-Service -Parameter * |
Where-Object { $_.pipelineInput.Length -gt 10 } |
Select-Object -Property name, pipelineinput, parameterValue

pipelineInput“ 属性暴露了通过管道接收到的一个属性的类型。不幸的是,它包含了一个本地化的字符串,所以一个区分的好方法是取字符串的长度。

输出的结果类似这样,并且可以从管道上游的命令中接受管道的输入,以及接受数据类型:

1
2
3
4
5
name         pipelineInput                  parameterValue
---- ------------- --------------
ComputerName True (ByPropertyName) String[]
InputObject True (ByValue) ServiceController[]
Name True (ByPropertyName, ByValue) String[]

PowerShell 技能连载 - 编程检查对象属性

当您用 Import-Csv 将一个 CSV 列表导入 PowerShell,或用任何其它类型的对象来处理时:如何自动确定对象的属性?以下是一个简单的方法:

1
2
3
4
5
# take any object, and dump a list of its properties
Get-Process -Id $pid |
Get-Member -MemberType *property |
Select-Object -ExpandProperty Name |
Sort-Object

为什么这种方法有用?有许多使用场景。例如,您可以检测一个注册表键的名称,支持用通配符转储所有的命令:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
$RegPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"

# get actual registry values from path
$Values = Get-ItemProperty -Path $RegPath

# exclude default properties
$default = 'PSChildName','PSDrive','PSParentPath','PSPath','PSProvider'

# each value surfaces as object property
# get property (value) names
$keyNames = $Values |
Get-Member -MemberType *Property |
Select-Object -ExpandProperty Name |
Where-Object { $_ -notin $default } |
Sort-Object

# dump autostart programs
$keyNames | ForEach-Object {
$values.$_
}

PowerShell 技能连载 - 存取隐藏(私有)成员

对象和类型中包括方法和属性等成员,但只有少数是公开可见和可使用的。还有许多隐藏(私有)的成员。在生产系统上使用这些成员是不明智的,当它们更新版本的时候,您并不会得到通知,所以可能会工作不正常。所以对于高级的 PowerShell 开发者来说是一个很好奇的地方。

有一个免费的 PowerShell 模块名为 ImpliedReflection,能将私有的成员变为可见,甚至在 PowerShell ISE 和 Visual Studio Code 的 IntelliSense 中,而且您可以运行那些成员。

例如,以下公有的类型只暴露了一个公有的方法,您可以用它来构造 PowerShell 的模块路径。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
PS> [System.Management.Automation.ModuleIntrinsics]::GetModulePath

OverloadDefinitions
-------------------
static string GetModulePath(string currentProcessModulePath, string hklmMachineModulePath, string
hkcuUserModulePath)




PS> [System.Management.Automation.ModuleIntrinsics] | Get-Member -Static


TypeName: System.Management.Automation.ModuleIntrinsics

Name MemberType Definition
---- ---------- ----------
Equals Method static bool Equals(System.Object objA, System.Object objB)
GetModulePath Method static string GetModulePath(string currentProcessModulePath, string hklmMa...
ReferenceEquals Method static bool ReferenceEquals(System.Object objA, System.Object objB)

现在我们像这样安装 ImpliedReflection

1
Install-Module -Name ImpliedReflection -Scope CurrentUser

当该模块安装以后,您需要先允许该扩展:

1
PS> Enable-ImpliedReflection -Force

现在,当您重新访问该类型并查看它的成员时,仍然只显示其公有成员。只有当您交互式输出该类型时,该扩展才起作用:

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
PS> [System.Management.Automation.ModuleIntrinsics] | Get-Member -Static


TypeName: System.Management.Automation.ModuleIntrinsics

Name MemberType Definition
---- ---------- ----------
Equals Method static bool Equals(System.Object objA, System.Object objB)
GetModulePath Method static string GetModulePath(string currentProcessModulePath, string hklmMa...
ReferenceEquals Method static bool ReferenceEquals(System.Object objA, System.Object objB)



PS> [System.Management.Automation.ModuleIntrinsics]

IsPublic IsSerial Name BaseType
-------- -------- ---- --------
True False ModuleIntrinsics System.Object



PS> [System.Management.Automation.ModuleIntrinsics] | Get-Member -Static


TypeName: System.Management.Automation.ModuleIntrinsics

Name MemberType Definition
---- ---------- ----------
AddToPath Method static string AddToPath(string basePath, string pathToAdd, ...
CombineSystemModulePaths Method static string CombineSystemModulePaths()
Equals Method static bool Equals(System.Object objA, System.Object objB)
ExportModuleMembers Method static void ExportModuleMembers(System.Management.Automatio...
GetDscModulePath Method static string GetDscModulePath()
GetExpandedEnvironmentVariable Method static string GetExpandedEnvironmentVariable(string name, S...
GetManifestGuid Method static guid GetManifestGuid(string manifestPath)
GetManifestModuleVersion Method static version GetManifestModuleVersion(string manifestPath)
GetModuleName Method static string GetModuleName(string path)
GetModulePath Method static string GetModulePath(string currentProcessModulePath...
GetPersonalModulePath Method static string GetPersonalModulePath()
GetSystemwideModulePath Method static string GetSystemwideModulePath()
IsModuleMatchingModuleSpec Method static bool IsModuleMatchingModuleSpec(psmoduleinfo moduleI...
IsPowerShellModuleExtension Method static bool IsPowerShellModuleExtension(string extension)
NewAliasInfo Method static System.Management.Automation.AliasInfo NewAliasInfo(...
PathContainsSubstring Method static int PathContainsSubstring(string pathToScan, string ...
PatternContainsWildcard Method static bool PatternContainsWildcard(System.Collections.Gene...
ProcessOneModulePath Method static string ProcessOneModulePath(System.Management.Automa...
ReferenceEquals Method static bool ReferenceEquals(System.Object objA, System.Obje...
RemoveNestedModuleFunctions Method static void RemoveNestedModuleFunctions(psmoduleinfo module)
SetModulePath Method static string SetModulePath()
SortAndRemoveDuplicates Method static void SortAndRemoveDuplicates[T](System.Collections.G...
_ctor Method static System.Management.Automation.ModuleIntrinsics _ctor(...
MaxModuleNestingDepth Property static int MaxModuleNestingDepth {get;}
PSModuleExtensions Property static string[] PSModuleExtensions {get;set;}
PSModuleProcessableExtensions Property static string[] PSModuleProcessableExtensions {get;set;}
SystemWideModulePath Property static string SystemWideModulePath {get;set;}
Tracer Property static System.Management.Automation.PSTraceSource Tracer {g...

现在您可以使用私有的成员了,好像它们是公有的一样:

1
2
3
4
5
PS> [System.Management.Automation.ModuleIntrinsics]::GetPersonalModulePath()
C:\Users\tobwe\Documents\WindowsPowerShell\Modules

PS> [System.Management.Automation.ModuleIntrinsics]::SystemWideModulePath
c:\windows\system32\windowspowershell\v1.0\Modules

再次强调,这仅仅适用于希望更深入了解对象和类型内部的工作机制的高级用户。ImpliedReflection 模块用于操作私有成员。在生产环境下,您需要十分谨慎地操作私有成员。

PowerShell 技能连载 - 性能(第 3 部分):更快的管道函数

在之前的技能中我们演示了如何改进循环,尤其是管道操作。要将这个知识点转为函数,请看一下这个简单的管道相关的函数,它的功能是统计管道传送过来的元素数量:

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
function Count-Stuff
{
param
(
# pipeline-aware input parameter
[Parameter(ValueFromPipeline)]
$InputElement
)

begin
{
# initialize counter
$c = 0
}
process
{
# increment counter for each incoming object
$c++
}
end
{
# output sum
$c
}
}

当您运行这个函数来统计一个非常大数量的对象时,可能会得到这样的结果:

1
2
3
4
5
6
7
8
9
PS> $start = Get-Date
1..1000000 | Count-Stuff
(Get-Date) - $start

1000000

...
TotalMilliseconds : 3895,5848
...

现在我们去掉属性,将这个函数转换为一个“简单函数”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Count-Stuff
{
begin
{
# initialize counter
$c = 0
}
process
{
# increment counter for each incoming object
$c++
}
end
{
# output sum
$c
}
}

由于没有定义管道参数,从管道的输入在 process 块中保存在 “$_“ 变量中以及在 end 块中,一个名为 “$input“ 的迭代器保存了所有收到的数据。请注意我们的计数示例并不需要这些变量,因为它只是统计了输入的数量。

以下是执行结果:

1
2
3
4
5
6
7
8
9
$start = Get-Date
1..1000000 | Count-Stuff
(Get-Date) - $start

1000000

...
TotalMilliseconds : 690,1558
...

显然,处理大量对象的时候,简单函数比管道函数的性能要高得多。

当然,仅当用管道传输大量对象的时候效果比较明显。但当做更多复杂操作的时候,效果会更加明显。例如,以下代码创建 5 位的服务器列表,用高级函数的时候,它在我们的测试系统上大约消耗 10 秒钟:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Get-Servername
{
param
(
# pipeline-aware input parameter
[Parameter(ValueFromPipeline)]
$InputElement
)

process
{
"Server{0:n5}" -f $InputElement
}
}



$start = Get-Date
$list = 1..1000000 | Get-ServerName
(Get-Date) - $start

使用简单函数可以在 2 秒之内得到相同的结果(在 PowerShell 5.1 和 6.1 相同):

1
2
3
4
5
6
7
8
9
10
11
12
13
function Get-ServerName
{
process
{
"Server{0:n5}" -f $InputElement
}
}



$start = Get-Date
$list = 1..1000000 | Get-ServerName
(Get-Date) - $start

PowerShell 技能连载 - 性能(第 2 部分):从 2 秒到 200 毫秒

在前一个技能中我们对一种常见的脚本模式提升了它的性能。现在,我们用一个更不常见的技巧进一步挤出更多性能。以下是我们上次的进展:

1
2
3
4
5
6
7
8
9
$start = Get-Date

$bucket = 1..100000 | ForEach-Object {
"I am adding $_"
}

$bucket.Count

(Get-Date) - $start

我们已经将执行时间从 6+ 分钟降到在 PowerShell 5.1 中 46 秒,在 PowerShell 6.1 中 1.46 秒。

现在我们看一看这个小改动——它返回完全相同的结果:

1
2
3
4
5
6
7
8
9
$start = Get-Date

$bucket = 1..100000 | & { process {
"I am adding $_"
} }

$bucket.Count

(Get-Date) - $start

这段神奇的代码在 PowerShell 5.1 中只花了 0.2 秒,在 PowerShell 6.1 中只花了 0.5 秒。

如您所见,这段代码只是将 ForEach-Object cmdlet 替换为等价的 $ { process { $_ }}。结果发现,由于 cmdlet 绑定的高级函数,管道操作符的执行效率被严重地拖慢了。如果使用一个简单的函数(或是一个纯脚本块),就可以显著地加速执行速度。结合昨天的技能,我们已经设法将处理从 6+ 分钟提速到 200 毫秒,而得到完全相同的结果。

请注意一件事情:这些优化技术只适用于大量迭代的循环。如果您的循环只迭代几百次,那么感受不到显著的区别。然而,一个循环迭代越多次,错误的设计就会导致越严重的消耗。

PowerShell 技能连载 - 性能(第 1 部分):从 6 分钟到 2 秒钟

以下是一个在许多 PowerShell 脚本中常见的错误:

1
2
3
4
5
6
7
8
9
10
11
$start = Get-Date

$bucket = @()

1..100000 | ForEach-Object {
$bucket += "I am adding $_"
}

$bucket.Count

(Get-Date) - $start

在这个设计中,脚本使用了一个空的数组,然后用某种循环向数组中增加元素。当运行它的时候,会发现它停不下来。实际上这段代码在我们的测试系统中需要超过 6 分钟时间,甚至有可能在您的电脑上要消耗更多时间。

以下是导致缓慢的元凶:对数组使用操作符 “+=“ 是有问题的。因为每次使用 “+=“ 时,它表面上动态扩展了该数组,实际上却是创建了一个元素数量更多一个的新数组。

要显著地提升性能,请让 PowerShell 来创建数组:当返回多个元素时,PowerShell 自动高速创建数组:

1
2
3
4
5
6
7
8
9
$start = Get-Date

$bucket = 1..10000 | ForEach-Object {
"I am adding $_"
}

$bucket.Count

(Get-Date) - $start

结果完全相同,但和消耗 6+ 分钟不同的是,它在 PowerShell 5.1 上只用了 46 秒钟,而在 PowerShell 6.1 上仅仅用了 1.46 秒。我们将会用另一个技巧来进一步提升性能。

PowerShell 技能连载 - 持续监视脚本的运行

以下是一段演示如何在 Windows 注册表中存储私人信息的代码:

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
# store settings here
$Path = "HKCU:\software\powertips\settings"

# check whether key exists
$exists = Test-Path -Path $Path
if ($exists -eq $false)
{
# if this is first run, initialize registry key
$null = New-Item -Path $Path -Force
}

# read existing value
$currentValue = Get-ItemProperty -Path $path
$lastRun = $currentValue.LastRun
if ($lastRun -eq $null)
{
[PSCustomObject]@{
FirstRun = $true
LastRun = $null
Interval = $null
}
}
else
{
$lastRunDate = Get-Date -Date $lastRun
$today = Get-Date
$timeSpan = $today - $lastRunDate

[PSCustomObject]@{
FirstRun = $true
LastRun = $lastRunDate
Interval = $timeSpan
}
}

# write current date and time to registry
$date = Get-Date -Format 'yyyy-MM-dd HH:mm:ss'
$null = New-ItemProperty -Path $Path -Name LastRun -PropertyType String -Value $date -Force

当运行这段代码时,它将返回一个对象,该对象告诉您上次运行此脚本是什么时候,以及从那以后运行了多长时间。

PowerShell 技能连载 - Retrieving Outlook Calendar Entries

If you use Outlook to organize your calendar events, here is a useful PowerShell function that connects to Outlook and dumps your calendar entries:

Function Get-OutlookCalendar
{
    # load the required .NET types
    Add-Type -AssemblyName 'Microsoft.Office.Interop.Outlook'

    # access Outlook object model
    $outlook = New-Object -ComObject outlook.application

    # connect to the appropriate location
    $namespace = $outlook.GetNameSpace('MAPI')
    $Calendar = [Microsoft.Office.Interop.Outlook.OlDefaultFolders]::olFolderCalendar
    $folder = $namespace.getDefaultFolder($Calendar)
    # get calendar items
    $folder.items |
      Select-Object -Property Start, Categories, Subject, IsRecurring, Organizer
}

Try this:

PS> Get-OutlookCalendar | Out-GridView

PowerShell 技能连载 - Getting AD Users with Selected First Letters

How would you query for all AD users with names that start with a “e”-“g”? You shouldn’t use a client-side filter such as Where-Object. One thing you can do is use the -Filter parameter with logical operators such as -and and -or:

Get-ADUser -filter {(name -lt 'E') -or (name -gt 'G')} |
  Select-Object -ExpandProperty Name

this example requires the free RSAT tools from Microsoft to be installed)