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)

PowerShell 技能连载 - Adding New Incrementing Number Column in a Grid View Window

Maybe you’d like to add a column with incrementing indices to your objects. Try this:

$startcount = 0
Get-Service |
  Select-Object -Property @{N='ID#';E={$script:startcount++;$startcount}}, * |
  Out-GridView

When you run this chunk of code, you get a list of services in a grid view window, and the first column “ID#” is added with incrementing ID numbers.

The technique can be used to add arbitrary columns. Simply use a hash table with key N[ame] for the column name, and key E[xpression] with the script block that generates the column content.

PowerShell 技能连载 - 改进 Group-Object

在每一个技能中我们解释了 Group-Object 能为您做什么,以及它有多么好用。不幸的是,Group-Object 的性能不理想。如果您试图对大量对象分组,该 cmdlet 可能会消耗大量时间。

以下是一行按文件大小对您的用户文件夹中所有文件排序的代码。当您希望检测重复的文件时,这将是一个十分重要的先决条件。由于这行代码将在最终返回结果,所以将会消耗大量的时间,甚至数小时:

1
2
3
4
5
6
$start = Get-Date
$result = Get-ChildItem -Path $home -Recurse -ErrorAction SilentlyContinue -File |
Group-Object -Property Length
$stop = Get-Date

($stop - $start).TotalSeconds

由于这些限制,我们创建了一个基于 PowerShell 的 Group-Object 的实现,并称它为 Group-ObjectFast。它基本上做相同的事请,只是速度更快。

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
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
function Group-ObjectFast
{
param
(
[Parameter(Mandatory,Position=0)]
[Object]
$Property,

[Parameter(ParameterSetName='HashTable')]
[Alias('AHT')]
[switch]
$AsHashTable,

[Parameter(ValueFromPipeline)]
[psobject[]]
$InputObject,

[switch]
$NoElement,

[Parameter(ParameterSetName='HashTable')]
[switch]
$AsString,

[switch]
$CaseSensitive
)


begin
{
# if comparison needs to be case-sensitive, use a
# case-sensitive hash table,
if ($CaseSensitive)
{
$hash = [System.Collections.Hashtable]::new()
}
# else, use a default case-insensitive hash table
else
{
$hash = @{}
}
}

process
{
foreach ($element in $InputObject)
{
# take the key from the property that was requested
# via -Property

# if the user submitted a script block, evaluate it
if ($Property -is [ScriptBlock])
{
$key = & $Property
}
else
{
$key = $element.$Property
}
# convert the key into a string if requested
if ($AsString)
{
$key = "$key"
}

# make sure NULL values turn into empty string keys
# because NULL keys are illegal
if ($key -eq $null) { $key = '' }

# if there was already an element with this key previously,
# add this element to the collection
if ($hash.ContainsKey($key))
{
$null = $hash[$key].Add($element)
}
# if this was the first occurrence, add a key to the hash table
# and store the object inside an arraylist so that objects
# with the same key can be added later
else
{
$hash[$key] = [System.Collections.ArrayList]@($element)
}
}
}

end
{
# default output are objects with properties
# Count, Name, Group
if ($AsHashTable -eq $false)
{
foreach ($key in $hash.Keys)
{
$content = [Ordered]@{
Count = $hash[$key].Count
Name = $key
}
# include the group only if it was requested
if ($NoElement -eq $false)
{
$content["Group"] = $hash[$key]
}

# return the custom object
[PSCustomObject]$content
}
}
else
{
# if a hash table was requested, return the hash table as-is
$hash
}
}
}

只需要将上述例子中的 Group-Object 替换为 Group-ObjectFast,就可以体验它的速度:

1
2
3
4
5
6
$start = Get-Date
$result = Get-ChildItem -Path $home -Recurse -ErrorAction SilentlyContinue -File |
Group-ObjectFast -Property Length
$stop = Get-Date

($stop - $start).TotalSeconds

在我们的测试中,Group-ObjectFastGroup-Object 快了大约 10 倍。

PowerShell 技能连载 - 探索 Group-Object

Group-Object 是一个好用的 cmdlet:它可以方便地可视化分组。请查看以下示例:

1
2
3
Get-Process | Group-Object -Property Company
Get-Eventlog -LogName System -EntryType Error | Group-Object -Property Source
Get-ChildItem -Path c:\windows -File | Group-Object -Property Extension

Basically, the cmdlet builds groups based on the content of a given property. You can also omit the group, and just look at the count if all that matters to you are the number distributions:
基本上,该 cmdlet 基于指定的属性内容创建分组。当您只关注数量分布时,也可以忽略该分组,而只查看总数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
PS C:\> Get-ChildItem -Path c:\windows -File | Group-Object -Property Extension -NoElement | Sort-Object -Property Count -Descending



Count Name
----- ----
10 .exe
10 .log
5 .ini
4 .xml
3 .dll
2 .txt
1 .dat
1 .bin
1 .tmp
1 .prx

如果您对实际对象的分组更感兴趣,可以通过 Group-Object 来返回一个哈希表。通过这种方式,您可以通过他们的键访问每个特定的分组:

1
2
3
4
$hash = Get-ChildItem -Path c:\windows -File |
Group-Object -Property Extension -AsHashTable

$hash.'.exe'

执行的结果将转储 Windows 目录下所有扩展名为 “.exe” 的文件。请注意键(查询的属性)不为字符串的情况。

类似地,如果您使用 PowerShell 远程操作并且扇出到多台计算机来平行地获取信息,当获取到结果时,Group-Object 将会把结果重新分组。这个示例同时从三台机器获取服务信息,而且结果将以随机顺序返回。Group-Object 将会对输入的数据分组,这样您可以操作计算机的结果。和哈希表的操作方法一样,您可以用方括号或点号来存取哈希表的键:

1
2
3
4
$services = Invoke-Command -ScriptBlock { Get-Service } -ComputerName server1, server2, server3 | Group-Object -Property PSComputerName -AsHashTable

$services["server1"]
$services."server2"

以下是一个类似的示例,但有一个 bug。您能指出错误吗?

1
2
3
4
$hash = Get-Service |
Group-Object -Property Status -AsHashTable

$hash.'Running'

当您查看哈希表时,您可能希望获取正在运行的服务:

1
2
3
4
5
6
PS C:\> $hash

Name Value
---- -----
Running {AdobeARMservice, AGMService, AGSService, App...
Stopped {AJRouter, ALG, AppIDSvc, AppMgmt...}

When you look at the hash table, you would expect to get back the running services:
然而,您并不会获得任何结果。那是因为哈希表中的那个键并不是字符串而是一个 “ServiceControllerStatus” 对象:

1
2
3
4
PS C:\> $hash.Keys | Get-Member


TypeName: System.ServiceProcess.ServiceControllerStatus

要确保获得到的是可存取的键,请总是将 -AsHashTable-AsString 合并使用。后者确保把键转换为字符串。现在示例代码可以按预期工作:

1
2
3
4
$hash = Get-Service |
Group-Object -Property Status -AsHashTable -AsString

$hash.'Running'

PowerShell 技能连载 - 自动化操作网站

有些时候,需要自动化操作某些已经人工打开的网站。也许您需要先用 WEB 表单登录到内部的网页。假设网站是通过 Internet Explorer 加载的(不是 Edge 或任何第三方浏览器),您可以使用 COM 接口来访问浏览器的实时内容。

当您访问动态网页时,纯 HTML 元素可能会更有用。一个纯 WebClient(或是 Invoke-WebRequest cmdlet)只会返回静态 HTML,并不是用户在浏览器中看到的内容。当使用一个真实的浏览器显示网页内容时,您的脚本需要访问驱动显示内容的完整 HTML。

要测试这一点,请打开 Internet Explorer 或者 Edge,并浏览到需要的网站。在我们的例子中,我们导航到 www.powershellmagazine.com

1
2
3
4
5
6
7
8
$obj = New-Object -ComObject Shell.Application
$browser = $obj.Windows() |
Where-Object FullName -like '*iexplore.exe' |
# adjust the below to match your URL
Where-Object LocationUrl -like '*powershellmagazine.com*' |
# take the first browser that matches in case there are
# more than one
Select-Object -First 1

$browser 中,您可以访问打开的浏览器中的对象模型。如果 $browser 为空,请确保您调整了 LocationUrl 的过滤条件。不要忘了两端的星号。

如果您希望挖掘网页中的所有图片,以下是获取所有图片列表的方法:

1
$browser.Document.images | Out-GridView

类似地,如果您希望挖掘网页的内容信息,以下代码返回页面的 HTML:

1
PS> $browser.Document.building.innerHTML

您可以使用正则表达式来挖掘内容。不过有一个限制:如果您需要以已登录的 WEB 用户的上下文来进行额外的操作,那么别指望了。例如,如果您需要下载一个需要登录才能获取的文件,那么您需要通过对象模型调用 Internet Explorer 的下载操作。

您可能无法通过 Invoke-WebRequest 或是其它简单的 WEB 客户端来下载文件,因为 PowerShell 运行在它自己的上下文中。而对于网站而言,看到的是一个匿名访问者。

使用 Internet Explorer 对象模型来进行更多高级操作,例如下载文件或视频,并不是完全不可行。但基本上是十分复杂的,您需要向用户界面发送点击和按键动作。

PowerShell 技能连载 - 安装打印机

从 Windows 8 和 Server 2012 R2 起,这些操作系统附带发行了一个名为 PrintManagement 的 PowerShell 模块。该模块中的 cmdlet 可以实现脚本化安装和配置打印机。以下是一段帮助您起步的代码:

1
2
3
4
5
6
7
8
9
10
11
$PrinterName = "MyPrint"
$ShareName = "MyShare"
$DriverName = 'HP Designjet Z Series PS Class Driver'
$portname = "${PrinterName}:"

Add-PrinterDriver -Name $DriverName
Add-PrinterPort -Name $portname
Add-Printer -Name $PrinterName -DriverName $DriverName -PortName $portname -ShareName $ShareName

# requires Admin privileges
# Set-Printer -Name $PrinterName -Shared $true

它从驱动库中安装了一个新的打印机。新的打印机默认没有在网络上共享,因为需要管理员权限。作为管理员,您可以运行 Set-Printer 来启用共享,也可以向 Add-Printer 命令添加 -Shared 开关参数。

要探索 PrintManagement 模块中的其它 cmdlet,请使用这行代码:

1
PS> Get-Command -Module PrintManagement

请注意 Windows 7 中没有包含该模块,而且无法在 Windows 7 中安装,因为 Windows 7 缺少运行该模块中 cmdlet 的某些依赖项。

PowerShell 技能连载 - 通过 CSV 创建对象

有些时候通过简单的基于文本的 CSV 格式来批量创建对象是一种聪明的方法,尤其是原始数据已是基于文本的而且只需要少量重格式化。

以下是一个简单的例子,以这种方式输入信息并创建一个自定义对象的列表:

1
2
3
4
5
6
7
8
9
$text = 'Name,FirstName,Location
Weltner,Tobias,Germany
Nikolic,Aleksandar,Serbia
Snover,Jeffrey,USA
Special,ÄÖÜß,Test'

$objects = $text | ConvertFrom-Csv

$objects | Out-GridView

结果看起来类似这样:

1
2
3
4
5
6
Name    FirstName Location
---- --------- --------
Weltner Tobias Germany
Nikolic Aleksandar Serbia
Snover Jeffrey USA
Special ÄÖÜß Test

PowerShell 技能连载 - 快速查找 Active Directory 组成员

经常地,AD 管理员需要查找某个 AD 组的所有成员,包括嵌套的成员。以下是一个常常出现在示例中的代码片段,用于解决这个问题:

1
2
3
4
5
$groupname = 'External_Consultants'
$group = Get-ADGroup -Identity $groupname
$dn = $group.DistinguishedName
$all = Get-ADUser -filter {memberof -recursivematch $dn}
$all | Out-GridView

(请注意您需要来自 Microsoft 免费的 RSAT 工具来使用这些示例中的 cmdlet。)

当您将 $groupname 中的组名改为您组织中存在的 AD 组名后,该代码不仅返回组中的直接用户,而且包含既在该组又在其它组中的直接用户。

然而,该代码执行起来非常慢。以下是一个更简单的实现,能达到多于五倍的速度:

1
2
3
$groupname = 'External_Consultants'
$all = Get-ADGroupMember -Identity $groupname -Recursive
$all | Out-GridView

它的内部使用合适的 LDAP 过滤器,和以上直接的方法类似:

1
2
3
4
5
6
$groupname = 'External_Consultants'
$group = Get-ADGroup -Identity $groupname
$dn = $group.DistinguishedName
$ldap = "(memberOf:1.2.840.113556.1.4.1941:=$dn)"
$all = Get-ADUser -LDAPFilter $ldap
$all | Out-GridView