PowerShell 技能连载 - 查找 PowerShell 缺省变量(第二部分)

在前一个技能里我们解释了如何使用独立全新的 PowerShell 实例来获取所有缺省变量。当您仔细查看这些变量,会发现还是丢失了某些变量。

以下是一个稍微修改过的版本,名为 Get-BuiltInPSVariable,能返回所有保留的 PowerShell 变量:

1
2
3
4
5
6
7
8
9
10
11
12
function Get-BuiltInPSVariable($Name='*')
{
# create a new PowerShell
$ps = [PowerShell]::Create()
# get all variables inside of it
$null = $ps.AddScript('$null=$host;Get-Variable')
$ps.Invoke() |
Where-Object Name -like $Name
# dispose new PowerShell
$ps.Runspace.Close()
$ps.Dispose()
}

为了不遗漏任何一个内置的 PowerShell 变量,这个做法使用了 AddScript() 方法来代替 AddCommand(),来执行多于一条命令。有一些 PowerShell 变量要等待至少一条命令执行之后才创建。

您现在可以获取所有的 PowerShell 内置变量,或搜索指定的变量:

1
2
3
4
5
6
7
8
9
10
11
12
PS> Get-BuiltInPSVariable -Name *pref*

Name Value
---- -----
ConfirmPreference High
DebugPreference SilentlyContinue
ErrorActionPreference Continue
InformationPreference SilentlyContinue
ProgressPreference Continue
VerbosePreference SilentlyContinue
WarningPreference Continue
WhatIfPreference False

PowerShell 技能连载 - 查找 PowerShell 缺省变量(第一部分)

有些时候识别出 PowerShell 管理的缺省变量十分有用,这样能帮您区分内置的变量和自定义的变量。Get-Variable 总是输出所有的变量。

以下是一个简单的技巧,使用一个独立、全新的 PowerShell 运行空间来确定内置的 PowerShell 变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
# create a new PowerShell
$ps = [PowerShell]::Create()
# get all variables inside of it
$null = $ps.AddCommand('Get-Variable')
$result = $ps.Invoke()
# dispose new PowerShell
$ps.Runspace.Close()
$ps.Dispose()

# check results
$varCount = $result.Count
Write-Warning "Found $varCount variables."
$result | Out-GridView

当您运行这段代码时,该代码输出找到的变量数量,以及这些变量。

PowerShell 技能连载 - 查找 PowerShell 类

从 PowerShell 5 开始,您可以定义 PowerShell 类。它们是动态定义的,并且存在于内存中。那么要如何知道这些类的名字?

我们首先定义一个简单的,没有任何内容的类:

1
2
3
4
class TestClass
{

}

如何确认内存中确实存在一个名为 “TestClass” 的类?以下是一个名为 Get-PSClass 的工具函数:

1
2
3
4
5
6
7
8
9
10
11
function Get-PSClass($Name = '*')
{
[AppDomain]::CurrentDomain.GetAssemblies() |
Where-Object { $_.GetCustomAttributes($false) |
Where-Object { $_ -is [System.Management.Automation.DynamicClassImplementationAssemblyAttribute]} } |
ForEach-Object { $_.GetTypes() |
Where-Object IsPublic |
Where-Object { $_.Name -like $Name } |
Select-Object -ExpandProperty Name
}
}

执行这个函数后,它会返回当前内存中所有定义的 PowerShell 类(在我们的 PowerShell 例子中,在前几个技能实验中有好几个 PowerShell 类):

1
2
3
4
5
6
PS> Get-PSClass
HelperStuff
Employee
TestClass

PS>

您也可以显示地测试一个类名:

1
2
3
4
5
6
7
8
PS> Get-PSClass -Name TestClass
TestClass

PS> (Get-PSClass -Name TestClass) -ne $null
True

PS> (Get-PSClass -Name TestClassNotExisting) -ne $null
False

您也可以使用通配符。一下代码将返回所有以 “A” 至 “H” 字母开头的类:

1
2
3
PS> Get-PSClass -Name '[A-H]*'
HelperStuff
Employee

PowerShell 技能连载 - 使用 PowerShell 类(二)

从 PowerShell 5 开始,您可以定义 PowerShell 类。您可以使用类来创建新对象,并通过创建一个或多个构造函数,您可以方便地初始化新创建的对象。

让我们看看效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class Employee
{
[int]$Id
[string]$Name

Employee([int]$Id, [string]$Name)
{
$this.Id = $Id
$this.Name = $Name
}
Employee ([string]$Name)
{
$this.Id = -1
$this.Name = $Name
}
Employee ()
{
$this.Id = -1
$this.Name = 'Undefined'
}
}

这段代码运行后,将创建一个包含三个构造函数的 “Employee” 新类。以下是如何使用新类的方法:

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
PS> [Employee]::new()

Id Name
-- ----
-1 Undefined



PS> [Employee]::new('Tobias')

Id Name
-- ----
-1 Tobias



PS> [Employee]::new(999, 'Tobias')

Id Name
-- ----
999 Tobias



PS>

每次调用都使用一个新的构造函数,并且该类根据需要创建相应的对象。

PowerShell 技能连载 - 使用 PowerShell 类(一)

从 PowerShell 5 开始,您可以定义类。它们有许多应用场景。一个是为有用的工具函数创建一个库来更好地整理它们。要实现这个功能,这个类要定义一些 “static” 方法。以下是一个简单的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class HelperStuff
{
# get first character of string and throw exception
# when string is empty or multi-line
static [char] GetFirstCharacter([string]$Text)
{
if ($Text.Length -eq 0) { throw 'String is empty' }
if ($Text.Contains("`n")) { throw 'String contains multiple lines' }
return $Text[0]
}

# get file extension in lower case
static [string] GetFileExtension([string]$Path)
{
return [Io.Path]::GetExtension($Path).ToLower()
}
}

“HelperStuff” 类定义了 “GetFirstCharacter“ 和 “GetFileExtension“ 两个静态方法。现在查找和使用这些工具函数非常方便:

1
2
3
4
5
6
7
8
9
10
PS> [HelperStuff]::GetFirstCharacter('Tobias')
T

PS> [HelperStuff]::GetFileExtension('c:\TEST.TxT')
.txt

PS> [HelperStuff]::GetFileExtension($profile)
.ps1

PS>

PowerShell 技能连载 - 读取注册表键值(临时解决办法)

在前一个技能中我们演示了 Get-ItemProperty 无法读取数据错误的注册表键值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS> $key =  "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group  Policy\History\{35378EAC-683F-11D2-A89A-00C04FBBCFA2}\0"


PS> Get-ItemProperty -Path $key
Get-ItemProperty : Specified cast is not valid.
At line:1 char:1
+ Get-ItemProperty -Path $key
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Get-ItemProperty], InvalidCastException
+ FullyQualifiedErrorId : System.InvalidCastException,Microsoft.PowerShell.Commands.GetItemPropertyComma
nd


PS>

有一个变通办法,您可以使用 Get-Item 代替,来存取注册表键,这将使用它的 .NET 成员来读取所有值:

1
2
3
4
5
6
7
8
9
10
11
$key = "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group Policy\History\{35378EAC-683F-11D2-A89A-00C04FBBCFA2}\0"

$key = Get-Item -Path $key

$hash = @{}
foreach ($prop in $key.Property)
{
$hash.$prop = $key.GetValue($prop)
}

$hash

结果看起来如下:

1
2
3
4
5
6
7
8
9
10
11
12
Name                           Value
---- -----
Extensions [{35378EAC-683F-11D2-A89A-00C04FBBCFA2}{0F6B957E-509E-11D1-A7CC-0000F87571E3}]
Link Local
Options 0
GPOLink 1
Version 65537
GPOName Guidelines of the local group
lParam 0
DSPath LocalGPO
FileSysPath C:\WINDOWS\System32\GroupPolicy\Machine
DisplayName Guidelines of the local group

PowerShell 技能连载 - 读取注册表键值失败

有些时候,读取注册表键值可能会失败,提示奇怪的错误信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS> $key =  "Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\Group  Policy\History\{35378EAC-683F-11D2-A89A-00C04FBBCFA2}\0"


PS> Get-ItemProperty -Path $key
Get-ItemProperty : Specified cast is not valid.
At line:1 char:1
+ Get-ItemProperty -Path $key
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Get-ItemProperty], InvalidCastException
+ FullyQualifiedErrorId : System.InvalidCastException,Microsoft.PowerShell.Commands.GetItemPropertyComma
nd


PS>

当发生这种情况时,用 regedit.exe 检查注册表发现一个或多个键值已破坏。在我们的例子中,”lParam” 的值似乎在所有的 Windows 机器中都是错误的。Regedit.exe 报告“(invalid … value)”。

在这个例子中,Get-ItemProperty 指令并不会读出任何值。您无法也排除该值:

1
2
3
4
5
6
7
8
9
10
PS>  Get-ItemProperty -Path $key -Include * -Exclude lParam
Get-ItemProperty : Specified cast is not valid.
At line:1 char:1
+ Get-ItemProperty -Path $key -Include * -Exclude lParam
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [Get-ItemProperty], InvalidCastException
+ FullyQualifiedErrorId : System.InvalidCastException,Microsoft.PowerShell.Commands.GetItemPropertyCommand


PS>

可以采取的措施是只读取合法的键值:

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> Get-ItemProperty -Path $key -Name DSPath


DSPath : LocalGPO
PSPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersi
on\Group Policy\History\{35378EAC-683F-11D2-A89A-00C04FBBCFA2}\0
PSParentPath : Microsoft.PowerShell.Core\Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersi
on\Group Policy\History\{35378EAC-683F-11D2-A89A-00C04FBBCFA2}
PSChildName : 0
PSProvider : Microsoft.PowerShell.Core\Registry


PS>

PowerShell 技能连载 - 每日问候(带语音)

在前一个技能中我们解释了如何在 PowerShell 配置文件中增加个人问候。这个问候信息也可以朗读出来,假设音量打开的情况下。这对所有的 PowerShell 宿主都有效,包括 VSCode。

这将把代码增加到您的配置文件脚本中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# create profile if it does not yet exist
$exists = Test-Path -Path $Profile.CurrentUserAllHosts
if (!$exists)
{
$null = New-Item -Path $Profile.CurrentUserAllHosts -ItemType File -Force
}

# add code to profile
@'
$greetings =
'Hello there!',
'Glad to see you!',
'Happy coding!',
'Have a great day!',
'May the PowerShell be with you!'

$text = $greetings | Get-Random
$null = (New-Object -COM Sapi.SpVoice).Speak($text)
'@ | Add-Content -Path $Profile.CurrentUserAllHosts -Encoding Default

要编辑用户配置文件,请运行这段代码:

1
PS> notepad $profile.CurrentUserAllHosts

PowerShell 技能连载 - 每日问候

以下是一个在 PowerShell 中接受一个字符串数组并返回一个随机的字符串,可以用作自定义问候语的简单方法:

1
2
3
4
5
6
7
8
$greetings =
'Hello there!',
'Glad to see you!',
'Happy coding!',
'Have a great day!',
'May the PowerShell be with you!'

$greetings | Get-Random

您所需要做的只是将这段代码加到您的 profile 脚本,例如这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# create profile if it does not yet exist
$exists = Test-Path -Path $Profile.CurrentUserAllHosts
if (!$exists)
{
$null = New-Item -Path $Profile.CurrentUserAllHosts -ItemType File -Force
}

# add code to profile
@'
$greetings =
'Hello there!',
'Glad to see you!',
'Happy coding!',
'Have a great day!',
'May the PowerShell be with you!'

$greetings | Get-Random
'@ | Add-Content -Path $Profile.CurrentUserAllHosts -Encoding Default

完成以后,PowerShell 将会使用自定义信息向您问候。

PowerShell 技能连载 - PowerShell 中 LINQ 的真实情况

不久前有一些关于 LINQ,一个 .NET 查询语言,在 PowerShell 中用来提升代码速度的报告。

直到 PowerShell 真正支持 Linq 之前,使用 Linq 是非常冗长的,并且需要使用强类型和没有文档的方法。另外,同样的事可以使用纯 PowerShell 方法来做,速度的提升很少——至少对 IPPro 相关的任务不明显。

以下是一个使用很简单的 Linq 语句对数字求和的测试用例。它接受 Windows 文件夹下的所有文件,然后对所有文件的长度求和:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
$numbers = Get-ChildItem -Path $env:windir -File | Select-Object -ExpandProperty Length

(Measure-Command {
$sum1 = [Linq.Enumerable]::Sum([int[]]$numbers)
}).TotalMilliseconds

(Measure-Command {
$sum2 = ($numbers | Measure-Object -Sum).Sum
}).TotalMilliseconds

(Measure-Command {
$sum3 = 0
foreach ($number in $numbers) { $sum3+=$number }
}).TotalMilliseconds

当您运行它多次的时候,您会观察到执行时间的输出。Linq 的方法可以使用,但是对数据类型十分敏感。例如,您需要将数字数组转换为 integer 数组,否则 Linq 的 Sum() 方法将不起作用。

可以提炼出两条法则:

  1. 这时不值得使用 Linq,因为它尚未集成到 PowerShell 中,并且会产生难读的代码。它几乎相当于在 PowerShell 使用 C# 源代码。

  2. 如果您想提升速度,请在所有可能的地方避免使用管道。foreach 循环的执行速度比用管道将许多对象通过管道传到 ForEach-Object 快许多。

If Linq was better integrated into PowerShell in the future, it would indeed be highly interesting.