PowerShell 技能连载 - 列出用户配置文件

我们收到很多反馈,关于如何处理用户配置文件的技能,所以我们决定增加几个额外的技能。

WMI 可以方便地枚举出系统中所有用户的配置文件,但是只列出了 SID (security identifier),而不是明文的用户名。

1
2
Get-CimInstance -ClassName Win32_UserProfile |
Out-GridView

要改进这个结果,以下是一小段将 SID 转换为用户名的示例代码:

1
2
$sid = "S-1-5-32-544"
(New-Object System.Security.Principal.SecurityIdentifier($sid)).Translate([System.Security.Principal.NTAccount]).Value

要向 Get-CimInstance 指令的输出结果添加明文的用户名,您可以使用 Add-Member 指令和 ScriptProperty 属性:

1
2
3
Get-CimInstance -ClassName Win32_UserProfile |
Add-Member -MemberType ScriptProperty -Name UserName -Value { (New-Object System.Security.Principal.SecurityIdentifier($this.Sid)).Translate([System.Security.Principal.NTAccount]).Value } -PassThru |
Out-GridView

网格视图显示了一个名为 UserName 的额外列,其中包括指定用户配置文件的明文用户名。

PowerShell 技能连载 - 理解和避免双跃点问题

当一个脚本在远程执行时,您可能会遇到“拒绝访问”的问题,这通常和双跃点 (double-hop) 问题有关。以下是一个例子,并且我们将演示如何解决它:

1
2
3
4
5
6
7
8
$target = 'serverA'

$code = {
# access data from another server with transparent authentication
Get-WmiObject -Class Win32_BIOS -ComputerName serverB
}

Invoke-Command -ScriptBlock $code -ComputerName $target

以上脚本在 ServerA 服务器上执行 PowerShell 代码。远程执行的代码试图连接 ServerB 来获取 BIOS 信息。请不要介意这是否有现实意义,有关系的是远程执行的代码无法透明地登录 ServerB,即便执行这段代码的用户可以直接访问 ServerB。

双跃点问题发生在您的认证信息没有从一个远程计算机传递给另一台远程计算机时。对于任何非域控制计算机,双跃点缺省都是禁止的。

如果您想使用上述代码,您需要使用 CredSSP 来验证(远程桌面也使用了这项技术)。这需要一次性设置您和您直接访问的计算机(在这个例子中是 ServerA)之间的信任关系:

1
2
3
4
5
6
7
8
9
10
11
#requires -RunAsAdministrator

$TargetServer = 'ServerA'

# configure the computer you directly connect to
Invoke-Command -ScriptBlock {
Enable-WSManCredSSP -Role Server -Force | Out-String
} -ComputerName $TargetServer

# establish CredSSP trust
Enable-WSManCredSSP -Role Client -DelegateComputer $TargetServer -Force

当这个信任存在时,您可以使用 CredSSP 规避双跃点问题。以下是如何在 CredSSP 启用的情况下如何远程运行代码的方法:

1
2
Invoke-Command -ScriptBlock $code -ComputerName $target
-Authentication Credssp -Credential mydomain\myUser

当您使用 CredSSP 时,您不再能使用透明的登录。取而代之的是,您必须使用 -Credential 来指定用户账户。

PowerShell 技能连载 - 用 Group-Object 区分远程处理结果

当您需要通过 PowerShell 远程处理来获取多台计算机的信息时,您可以单独查询每台机器。更快的方法是同时查询多台机器,这也产生一个问题,如何区分处理结果呢?

以下是一个例子(假设您的环境中启用了 PowerShell 远程操作):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$serverList = 'TRAIN11','TRAIN12'


$code = {
Get-Service
}

# get results from all machines in parallel...
$results = Invoke-Command -ScriptBlock $code -ComputerName $serverList |
# and separate results by PSComputerName
Group-Object -Property PSComputerName -AsHashTable -AsString


$results['TRAIN11']

$results['TRAIN12']

如您所见,Invoke-Command 命令从两台机器返回了所请求的服务信息。接下来 Group-Object 命令将数据分离到两个组里,并且使用 PSComputerName 属性来标识它。PSComputerName 是一个自动属性,通过 PowerShell 远程处理接收到的数据总是带有这个属性。

这样,即便 PowerShell 远程处理在所有机器上并行执行代码,我们也可以区分每台机器的执行结果。

PowerShell 技能连载 - 转换数字字符串

在 PowerShell 中转换一个包含数字的字符串非常简单:

1
2
3
4
PS C:\> [double]"77.234"
77,234

PS C:\>

不过,如果字符串包含的不只是纯数字,那么就比较有挑战性了。例如,您需要转换一个类似 “2763MB” 的字符串,PowerShell 无法自动将它转换为一个数字。这时候您需要一个类似这样的转换函数:

1
2
3
4
5
6
7
function Convert-MBToByte($MBString)
{
$number = $MBString.Substring(0, $MBString.Length-2)
1MB * $number
}

Convert-MBToByte -MBString '2433MB'

或者如果它是一个合法的 PowerShell 代码格式,您可以试着让 PowerShell 来做转换:

1
2
3
4
PS C:\> Invoke-Expression -Command '2615MB'
2742026240

PS C:\>

然而,不推荐使用 Invoke-Expression,因为它会带来安全风险。例如用户能够改变命令执行的表达式,类似 SQL 注入攻击。

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

您可能听说过 PowerShell 的缺省参数值和 $PSDefaultParameterValues。当您将一个哈希表赋给这个特殊的变量,则哈希表的键定义了命令和影响的参数,而值定对应新的缺省值。

请看这个例子:

1
2
3
$PSDefaultParameterValues = @{
'*:ComputerName' = 'testserver1'
}

这将会把所有命令 (*) 的 -ComputerName 设置为新的缺省值 “testserver1”。当您调用一个命令,且满足以下两个条件 (a) 有一个名为 ComputerName 的参数 (b) 没有显式地赋值给这个参数时,将会使用缺省值。

这对 PowerShell 函数也有效,然而只对 “Advanced Functions” 有效,对 “Simple Functions” 无效。

要验证区别,请按上述介绍定义 $PSDefaultParameterValues,然后运行这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function testSimple
{
param($Computername)

"Result Simple: $Computername"
}

function testAdvanced
{
[CmdletBinding()]
param($Computername)

"Result Advanced: $Computername"
}


testSimple

testAdvanced

如您所见,只有 testAdvanced 函数应用了缺省参数。”Advanced Functions” 至少要定义一个参数属性,例如 [CmdletBinding()][Parameter(Mandatory)]

PowerShell 技能连载 - 正确地导入 Excel 的 CSV 文件

如果您导出一个 Excel 工作表到 CSV 文件中,并且希望将这个文件导入 PowerShell,以下是实现方法:

1
2
3
$path = 'D:\sampledata.csv'

Import-Csv -Path $path -UseCulture -Encoding Default

重要的参数有 -UseCulture(自动使用和 Excel 在您系统中所使用一致的分隔符)和 -Encoding Default(只有使用此设置,所有特殊字符才能保持原样)。

PowerShell 技能连载 - 在 PowerShell 函数中支持风险缓解

当一个 PowerShell 函数进行一个可能有风险的系统变更时,推荐使用 -WhatIf-Configm 风险缓解参数。以下是基本的需求:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Test-WhatIf
{
[CmdletBinding(SupportsShouldProcess,ConfirmImpact='Low',HelpUri='http://www.myhelp.com')]
param()



if ($PSCmdlet.ShouldProcess($env:COMPUTERNAME,"Say 'Hello'"))
{
"I am executing..."
}
else
{
"I am simulating..."
}

}

当运行这个函数时,PowerShell 会遵从 -WhatIf-Confirm 参数设置:

1
2
3
4
5
6
7
8
PS C:\> Test-WhatIf -WhatIf
What if: Performing the operation "Say 'Hello'" on target "PC10".
I am simulating...

PS C:\> Test-WhatIf
I am executing...

PS C:\>

这个函数还定义了一个 ConfirmImpact 属性,它的值可以是 LowMediumHigh,表示这个函数的操作引起的改变有多严重。

ConfirmImpact 的值大于或等于 $ConfirmPreference 变量中定义的值时,PowerShell 自动向用户显示一个确认信息:

1
2
3
4
5
6
7
8
9
10
PS C:\> $ConfirmPreference = "Low"

PS C:\> Test-WhatIf
I am executing...


Confirmation
Do you really want to perform this action?
...
I am simulating...

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
function Test-Binding
{
[CmdletBinding(DefaultParameterSetName='Date')]
param
(
[Parameter(ParameterSetName='Integer', Position=0, Mandatory=$true)]
[int]
$Id,

[Parameter(ParameterSetName='String', Position=0, Mandatory=$true)]
[string]
$Name,

[Parameter(ParameterSetName='Date', Position=0, Mandatory=$true)]
[datetime]
$Date
)

$chosenParameterSet = $PSCmdlet.ParameterSetName
Switch ($chosenParameterSet)
{
'Integer' { 'User has chosen Integer' }
'String' { 'User has chosen String' }
'Date' { 'User has chosen Date' }
}

[PSCustomObject]@{
Integer = $Id
String = $Name
Date = $Date
}
}

现在用户可以测试 Test-Binding 并且提交参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
PS C:\> Test-Binding "Hello"
User has chosen String

Integer String Date
------- ------ ----
0 Hello



PS C:\> Test-Binding 12
User has chosen Integer

Integer String Date
------- ------ ----
12



PS C:\> Test-Binding (Get-Date)
User has chosen Date

Integer String Date
------- ------ ----
0 11/21/2017 11:44:33 AM

PowerShell 技能连载 - 删除环境变量

在前一个技能中我们解释了如何在所有可用的范围内设置环境变量的方法。但是如何移除环境变量呢?

巧合地,您可以用完全相同的方法做这件事情,只需要将一个空字符串赋给该变量。然而,前一个技能中的函数中的 -VariableValue 参数不能接受空字符串:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Set-EnvironmentVariable
{
[CmdletBinding()]
param
(
[Parameter(Mandatory)][String]
$VariableName,

[Parameter(Mandatory)][String]
$VariableValue,

[Parameter(Mandatory)][EnvironmentVariableTarget]
$Target
)

[Environment]::SetEnvironmentVariable($VariableName, $VariableValue, $Target)
}

当您尝试着赋值空字符串时,将会收到这样的提示:

1
2
3
PS C:\>  Set-EnvironmentVariable -VariableName test -VariableValue "" -Target  User
Set-EnvironmentVariable : Cannot bind Argument to Parameter "VariableValue" because it is an empty string.
...

这是因为当您将一个参数声明为 “Mandatory”,PowerShell 缺省情况下将拒绝空字符串和 null 值。

您可以将 VariableValue 参数设为可选的,但是这样当您调用该函数不传该参数时,PowerShell 将不再提示。如何使一个必选参数能接受 null 和空字符串呢?

只要稍微改一下,加上 [AllowNull()] 和/或 [AllowEmptyString()] 以上函数就可以支持删除环境变量:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function Set-EnvironmentVariable
{
[CmdletBinding()]
param
(
[Parameter(Mandatory)][String]
$VariableName,

[Parameter(Mandatory)][String]
[AllowEmptyString()]
$VariableValue,

[Parameter(Mandatory)][EnvironmentVariableTarget]
$Target
)

[Environment]::SetEnvironmentVariable($VariableName, $VariableValue, $Target)
}

以下是删除 “Test” 环境变量的方法:

1
PS C:\> Set-EnvironmentVariable -VariableName test -VariableValue "" -Target User

PowerShell 技能连载 - 设置环境变量

有些时候您会见到一些脚本使用 Select-Object 向现有的对象附加信息,类似以下代码:

1
2
3
4
5
6
Get-Process |
Select-Object -Property *, Sender|
ForEach-Object {
$_.Sender = $env:COMPUTERNAME
$_
}

它可以工作,但是 Select-Object 创建了一个完整的对象拷贝,所以这种方法速度很慢而且改变了对象的类型。您可能注意到了 PowerShell 不再使用正常的表格方式输出进程对象,也是因为这个原因。

如果您想设置环境变量,env: 驱动器只能修改进程级别的环境变量。要设置用户或者机器级别的环境变量,请试试这个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Set-EnvironmentVariable
{
[CmdletBinding()]
param
(
[Parameter(Mandatory)][String]
$VariableName,

[Parameter(Mandatory)][String]
$VariableValue,

[Parameter(Mandatory)][EnvironmentVariableTarget]
$Target
)

[Environment]::SetEnvironmentVariable($VariableName, $VariableValue, $Target)
}

请注意 $Target 变量如何使用特殊的数据类型 “EnvironmentVariableTarget” ,当您在 PowerShell ISE 或其它带有 IntelliSense 功能的编辑器中,-Target 参数的可选项有 “Process”、”User” 和 “Machine”。

以下是如何在用户级别创建名为 “Test”,值为 12 的环境变量的方法:

1
2
3
PS C:\> Set-EnvironmentVariable -VariableName test -VariableValue 12 -Target User

PS C:\>