PowerShell 技能连载 - 查找未使用(或使用过的)驱动器号

通过连接类型转换的结果,您可以轻松创建字母列表:

1
2
3
4
5
6
PS> [Char[]](65..90)
A
B
C
D
...

从此列表中,您可以生成正在使用的驱动器号的列表:

1
2
3
PS> [Char[]](65..90) | Where-Object { Test-Path "${_}:\" }
C
D

同样,要查找空闲的(未使用的)驱动器号,请尝试以下操作:

1
2
3
4
5
6
PS> [Char[]](65..90) | Where-Object { (Test-Path "${_}:\") -eq $false }
A
B
E
F
...

PowerShell 技能连载 - 了解 PowerShell 中的 REST Web 服务

Web 服务类型很多,PowerShell 可以使用 Invoke-RestMethod 访问其中的许多服务。这是一个快速入门,可帮助您起步。

这是最简单的形式,Web 服务就是上面带有结构化数据的网页。您可以在浏览器中使用相同的 URL 来查看数据。PowerShell 使用 Invoke-RestMethod 检索数据。这为您提供了最新的 PowerShell 团队博客:

1
2
Invoke-RestMethod -Uri https://blogs.msdn.microsoft.com/powershell/feed/ |
Select-Object -Property Title, pubDate

像上面的那种简单的 REST Web 服务可以接受参数。这些参数可以作为 URL 的一部分提交。这是一个可以接受任何数字并返回有关该数字的部分意义信息的 Web 服务。下次您被邀请参加 25 周年纪念日时,您可能想查询一些有关数字 25 的有趣信息:

1
2
3
4
5
6
7
8
PS> Invoke-RestMethod http://numbersapi.com/25
25 is the number of cents in a quarter.

PS> Invoke-RestMethod http://numbersapi.com/25
25 is the (critical) number of Florida electoral votes for the 2000 U.S. presidential election.

PS> Invoke-RestMethod http://numbersapi.com/25
25 is the minimum age of candidates for election to the United States House of Representatives.

其他 Web 服务通过不可见的 POST 数据传入用户数据(类似于网站上的表单数据)。它们可能还需要会话状态、cookie 和/或登录。

这是最后一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# https://4bes.nl/2020/08/23/calling-a-rest-api-from-powershell/

$Body = @{
Cook = "Freddy"
Meal = "PastaConCarne"
}

$Parameters = @{
Method = "POST"
Uri = "https://4besday4.azurewebsites.net/api/AddMeal"
Body = ($Body | ConvertTo-Json)
ContentType = "application/json"
}
Invoke-RestMethod @Parameters -SessionVariable cookie
Invoke-RestMethod "https://4besday4.azurewebsites.net/api/AddMeal" -WebSession $cookie

哈希表类似于您要发送到 Web 服务的参数。它们将转换为 JSON 格式。由 Web 服务确定接受用户输入的格式。然后,使用 POST方法将数据传输到 Web 服务。

如果您为向指定名字的厨师发送一个请求,则会从 Web 服务中获取一条通知,告知您正在准备食品。确保更改 Cook 和 Meal 的数据。

如您所见,Invoke-RestMethod 使用了两次。第一次调用获取会话状态和 cookie,并将其存储在使用 -SessionVariable 参数定义的 $cookie 变量中。

第二个调用通过 -WebSession 参数提交会话状态。这样,Web 服务可以保留每次调用之间的状态并清楚地标识您。

PowerShell 技能连载 - 更好的递归

当函数调用自身时,称为“递归”。当脚本想要遍历文件系统的一部分时,您会经常看到这种技术:一个函数处理文件夹内容,当它遇到子文件夹时,它会调用自身。

递归的功能很强大,但是却很难调试,并且有潜在的危险。因为当您犯错时,您将陷入无休止的循环。此外,递归深度过高时,始终存在堆栈溢出的风险。

许多通常需要递归的任务也可以通过使用“队列”来设计:当您的代码遇到新任务时,无需再次调用自身,而是将新任务放在队列中,一旦完成初始任务,开始解决队列中的任务。

感谢 Lee Holmes,这是一个遍历整个驱动器 C:\ 但不使用递归的简单示例。相反,您可以看到正在使用的队列:

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
# get a new queue
[System.Collections.Queue]$queue = [System.Collections.Queue]::new()
# place the initial search path(s) into the queue
$queue.Enqueue('c:\')
# add as many more search paths as you need
# they will eventually all be traversed
#$queue.Enqueue('D:\')

# while there are still elements in the queue...
while ($queue.Count -gt 0)
{
# get one item off the queue
$currentDirectory = $queue.Dequeue()
try
{
# find all subfolders and add them to the queue
# a classic recurse approach would have called itself right here
# this approach instead pushes the future tasks just onto
# the queue for later use
[IO.Directory]::GetDirectories($currentDirectory) | ForEach-Object {
$queue.Enqueue($_)
}
}
catch {}

try
{
# find all files in this folder with the given extensions
[IO.Directory]::GetFiles($currentDirectory, '*.psm1')
[IO.Directory]::GetFiles($currentDirectory, '*.ps1')
}
catch{}
}

以下是 UMU 提供的,不用递归实现本任务的方法:

1
[IO.Directory]::EnumerateFiles('path', '*', 1)

PowerShell 技能连载 - 管理本地组成员(第 2 部分)

在上一个技巧中,我们解释了为什么访问本地组成员并不总是与内置的 cmdlet(如 Get-LocalGroupMember)一起使用,以及使用旧的(但仍可正常使用)ADSI 接口解决该问题的方法。

如果您想以此为基础构建解决方案,您可能想知道如何将本地帐户添加到组或从组中删除,以及如何启用和禁用本地管理员帐户。

这是说明这些方法的几行有用的代码。您可以独立使用这些代码,也可以将它们集成到自己的脚本逻辑中。

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
# these examples use the data below - adjust to your needs
# DO NOT RUN THESE LINES UNLESS YOU CAREFULLY
# REVIEWED AND YOU KNOW WHAT YOU ARE DOING!

# use local machine
$ComputerName = $env:computername
# find name of local Administrators group
$Group = ([Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate([System.Security.Principal.NTAccount]).Value.Split('\')[-1]
# find name of local Administrator user
$Admin = Get-CimInstance -ClassName Win32_UserAccount -Filter "LocalAccount = TRUE and SID like 'S-1-5-%-500'"
$UserName = $Admin.Name
# examples

# find all local groups
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$computerObj.psbase.children |
Where-Object { $_.psbase.schemaClassName -eq 'group' } |
Select-Object -Property @{N='Name';E={$_.Name[0]}},
Path,
@{N='Sid';E={[Security.Principal.SecurityIdentifier]::new($_.objectSid.value,0).Value}}



# find members of local admin group
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$groupObj = $computerObj.psbase.children.find($Group, 'Group')
$groupObj.psbase.Invoke('Members') |
ForEach-Object { $_.GetType().InvokeMember('ADspath','GetProperty',$null,$_,$null) }

# add user to group/remove from group
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$groupObj = $computerObj.psbase.children.find($Group, 'Group')
# specify the user or group to add or remove
$groupObj.Add('WinNT://DOMAIN/USER,user')
$groupObj.Remove('WinNT://DOMAIN/USER,user')

# enabling/disabling accounts
$computerObj = [ADSI]("WinNT://$ComputerName,computer")
$userObj = $computerObj.psbase.children.find($UserName, 'User')
#enable
$userObj.UserFlags=$userObj.UserFlags.Value -band -bnot 512
$userObj.CommitChanges()

#disable
$userObj.UserFlags=$userObj.UserFlags.Value -bor 512
$userObj.CommitChanges()

PowerShell 技能连载 - 管理本地组成员(第 1 部分)

幸运的是,PowerShell 5 及更高版本附带了诸如 Get-LocalGroupMember 之类的 cmdlet,该 cmdlet 列出了本地组的成员。不幸的是,这些 cmdlet 有一个缺陷:如果组包含一个或多个孤立成员,则该 cmdlet 将无法列出任何组成员。

孤立的组成员可以是已添加到组中但后来在 Active Directory 中删除的用户或组。这样的孤儿显示为 SID 号,而不是对话框中的名称。

若要解决此问题并列出组成员而不考虑孤立帐户,请尝试使用 Get-GroupMemberLocal 函数,该函数使用 PowerShell 自带 cmdlet 之前前通常使用的,旧的 ADSI 方法:

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
function Get-GroupMemberLocal
{
[CmdletBinding(DefaultParameterSetName='Name')]
param
(
[Parameter(Mandatory,Position=0,ParameterSetName='Name')]
[string]
$Name,

[Parameter(Mandatory,Position=0,ParameterSetName='Sid')]
[System.Security.Principal.SecurityIdentifier]
$Sid,

[string]
$Computer = $env:COMPUTERNAME
)

if ($PSCmdlet.ParameterSetName -eq 'Sid')
{
$Name = $sid.Translate([System.Security.Principal.NTAccount]).Value.Split('\')[-1]
}

$ADSIComputer = [ADSI]("WinNT://$Computer,computer")
$group = $ADSIComputer.psbase.children.find($Name, 'Group')
$group.psbase.invoke("members") |
ForEach-Object {
try
{
$disabled = '-'
$disabled = $_.GetType().InvokeMember("AccountDisabled", 'GetProperty', $null, $_, $null)
} catch {}
[PSCustomObject]@{
Name = $_.GetType().InvokeMember("Name", 'GetProperty', $null, $_, $null)
SID = [Security.Principal.SecurityIdentifier]::new($_.GetType().InvokeMember("objectSid", 'GetProperty', $null, $_, $null),0)
Path = $_.GetType().InvokeMember("AdsPath", 'GetProperty', $null, $_, $null)
Type = $_.GetType().InvokeMember("Class", 'GetProperty', $null, $_, $null)
Disabled = $disabled
}
}
}

下面是一个实际示例:让我们转储本地管理员组。您可以按名称或不区分文化的 SID 来访问组:

1
2
3
4
5
6
7
8
PS> Get-GroupMemberLocal -Sid S-1-5-32-544 | Format-Table

Name SID Path Type Disabled
---- --- ---- ---- --------
Administrator S-1-5-21-2770831484-2260150476-2133527644-500 WinNT://WORKGROUP/DELL7390/Administrator User True
Presentation S-1-5-21-2770831484-2260150476-2133527644-1007 WinNT://WORKGROUP/DELL7390/Presentation User False
RemoteAdmin S-1-5-21-2770831484-2260150476-2133527644-1013 WinNT://WORKGROUP/DELL7390/RemoteAdmin User False
Management S-1-5-21-2770831484-2260150476-2133527644-1098 WinNT://WORKGROUP/DELL7390/Management Group -

PowerShell 技能连载 - 标识本地管理员帐户的名称

有时,PowerShell 脚本需要访问或使用内置的 Administrator 帐户或内置的 Administrators组。不幸的是,它们的名称已本地化,因此它们的名称可以根据 Windows 操作系统的语言进行更改。

但是,它们确实使用了恒定的(众所周知的)SID(安全标识符)。通过使用 SID,您可以获得名称。对于本地 Administrator组,这很简单,因为 SID 始终是已知的:S-1-5-32-544。使用一行代码,可以翻译 SID。这是从德文系统获得的结果:

1
2
PS> ([Security.Principal.SecurityIdentifier]'S-1-5-32-544').Translate([System.Security.Principal.NTAccount]).Value
VORDEFINIERT\Administratoren

使用内置管理员等帐户,就不那么简单了。在此,只有 RID(相对标识符)是已知的:-500。

通过简单的WMI查询,您将获得与过滤器匹配的帐户:

1
2
3
4
5
PS> Get-CimInstance -ClassName Win32_UserAccount -Filter "LocalAccount = TRUE and SID like 'S-1-5-%-500'"

Name Caption AccountType SID Domain
---- ------- ----------- --- ------
Administrator DELL7390\Administrator 512 S-1-5-21-2770831484-2260150476-2133527644-500 DELL7390

PowerShell 技能连载 - 读取已安装的软件(第 2 部分)

在上一个技能中,我们演示了 Get-ItemProperty 强大的功能,并且您可以通过读取多个注册表位置而仅用一行代码来创建整个软件清单。

今天,让我们在软件清单中添加两个重要的信息:范围(是为每个用户安装的软件还是为所有用户安装的软件?)和体系结构(x86或x64)。

每个信息都无法在注册表键值中找到,而是通过确定信息在注册表中的存储位置来找到。这是 Get-ItemProperty 提供的另一个巨大好处:它不仅返回给定注册表项的注册表值。它还添加了许多属性,例如 PSDrive(注册表配置单元)和 PSParentPath(正在读取注册表项的路径)。

以下是根据发现信息的地方将范围和体系结构信息添加到软件清单的解决方案:

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
# Registry locations for installed software
$paths =
# all users x64
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
# all users x86
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
# current user x64
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
# current user x86
'HKCU:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'

# calculated properties

# AllUsers oder CurrentUser?
$user = @{
Name = 'Scope'
Expression = {
if ($_.PSDrive -like 'HKLM')
{
'AllUsers'
}
else
{
'CurrentUser'
}
}
}

# 32- or 64-Bit?
$architecture = @{
Name = 'Architecture'
Expression = {
if ($_.PSParentPath -like '*\WOW6432Node\*')
{
'x86'
}
else
{
'x64'
}
}
}
Get-ItemProperty -ErrorAction Ignore -Path $paths |
# eliminate reg keys with empty DisplayName
Where-Object DisplayName |
# select desired properties and add calculated properties
Select-Object -Property DisplayName, DisplayVersion, $user, $architecture

PowerShell 技能连载 - 读取已安装的软件(第 1 部分)

Get-ItemProperty cmdlet 可以比大多数用户知道的功能强大得多的方式读取注册表值。该 cmdlet 支持多个注册表路径,并且支持通配符。这样,只需一行代码即可从四个注册表项读取所有已安装的软件(及其卸载字符串):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# list of registry locations where installed software is stored
$paths =
# all users x64
'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
# all users x86
'HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*',
# current user x64
'HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*',
# current user x86
'HKCU:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'

Get-ItemProperty -ErrorAction Ignore -Path $paths |
# eliminate all entries with empty DisplayName
Where-Object DisplayName |
# select some properties (registry values)
Select-Object -Property DisplayName, DisplayVersion, UninstallString, QuietUninstallString

PowerShell 技能连载 - 保持 Windows 和 PowerShell 持续运行

根据 Windows PC 的电源设置,即使您在运行冗长的脚本,您的计算机仍可能在一段时间后进入待机或休眠状态。

确保 Windows 在脚本忙时继续运行的一种方法是使用“演示模式”。您可以使用一个工具来启用和禁用它。在 PowerShell 中,运行以下命令:

1
PS> presentationsettings

这将打开一个窗口,您可以在其中检查和控制当前的演示文稿设置。要自动启动和停止演示模式,该命令支持参数 /start/stop

为了确保 Windows 在脚本运行时不会进入休眠状态,请将其放在第一行脚本中:

1
PS> presentationsettings /start

在脚本末尾,关闭演示模式,如下所示:

1
PS> presentationsettings /stop

要验证这两个命令的效果,请运行不带参数的命令,并选中对话框中最上方的复选框。

PowerShell 技能连载 - 读取事件日志(第 4 部分)

在上一个技巧中,我们鼓励您弃用 Get-EventLog cmdlet,而开始使用 Get-WinEvent,因为后者功能更强大,并且在PowerShell 7 中不再支持前者。

如上例所示,通过 Get-WinEvent 查询事件需要一个哈希表。例如,以下命令返回已安装更新的列表:

1
2
3
4
5
Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WindowsUpdateClient'
Id = 19
}

实际上,事件数据始终使用 XML 格式存储,并且所有查询都使用 XPath 过滤器查询来为您检索数据。如果您是 XML 和XPath专家,则可以直接使用以下命令来获得相同的结果:

1
2
3
Get-WinEvent -FilterXML @'
<QueryList><Query Id="0" Path="system"><Select Path="system">*[System/Provider[@Name='microsoft-windows-windowsupdateclient'] and (System/EventID=19)]</Select></Query></QueryList>
'@

哈希表是一个很方便的快捷方式。在内部,哈希表中包含的信息将转换为上面的 XML 语句。幸运的是,将哈希表转换为 XML 一点也不困难,因为 Get-WinEvent 会为您做到这一点:只需提交一个哈希表,并要求返回 XML 语句:

1
2
3
4
5
6
7
$result = Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WindowsUpdateClient'
Id = 19
} -MaxEvents 1 -Verbose 4>&1

$result | Where-Object { $_ -is [System.Management.Automation.VerboseRecord] }

本质上,通过提交 -Verbose 参数,要求 Get-WinEvent 将计算出的XML语句返回给您。通过将管道 4 重定向到输出管道 1,您可以将详细消息捕获到 $result 并过滤详细消息。这样,您可以捕获计算出的XML:

VERBOSE: Found matching provider: Microsoft-Windows-WindowsUpdateClient
VERBOSE: The Microsoft-Windows-WindowsUpdateClient provider writes events to the System log.
VERBOSE: The Microsoft-Windows-WindowsUpdateClient provider writes events to the Microsoft-Windows-WindowsUpdateClient/Operational log.
VERBOSE: Constructed structured query:
VERBOSE: <QueryList><Query Id="0" Path="system"><Select Path="system">*[System/Provider[@Name='microsoft-windows-windowsupdateclient'] and
VERBOSE: (System/EventID=19)]</Select></Query></QueryList>.