PowerShell 技能连载 - 检测有问题的 Execution Policy 设置

PowerShell 用执行策略 (execution policy) 来决定是否执行某个脚本。实际上定义执行策略可以定义 5 种作用域。要查看这所有五种情况,请使用这个命令:

1
2
3
4
5
6
7
8
9
PS C:\> Get-ExecutionPolicy -List

Scope ExecutionPolicy
----- ---------------
MachinePolicy Undefined
UserPolicy Undefined
Process Undefined
CurrentUser RemoteSigned
LocalMachine Undefined

要确定生效的设置,PowerShell 会从上到下遍历这些作用域,然后取第一个非 “Undefined“ 设置。如果所有作用域都设置成 “Undefined“,那么 PowerShell 将使用 “Restricted“ 设置,并且阻止脚本执行。这是缺省的行为。

请永远保持 “MachinePolicy“ 和 “UserPolicy“ 作用域设置成 “Undefined“。这些作用域智能由组策略集中设置。如果它们设置成任何非 “Undefined“ 的值,用户则无法改变生效的设置。

有些公司使用这种方式来限制脚本的执行,它们用 execution policy 作为安全的屏障——而它并不是。”LocalMachine“ 作用域种的 Execution policy 永远应该是缺省值,而不应该强迫一个用户的设置。

如果 “MachinePolicy“ 或 “UserPolicy“ 有设置值,在 PowerShell 5 以及以下版本也有个 bug,可能会导致启动一个 PowerShell 脚本时延迟近 30 秒。这个延迟可能是内部导致的:PowerShell 使用 WMI 确定当前运行的进程,来决定一个 PowerShell 脚本是否作为一个组策略执行,而这种实现方式可能会造成过多的延迟。

所以如果您看见 “MachinePolicy“ 或 “UserPolicy“ 作用域中的设置不是 “Undefined“,您应该和 Active Directory 团队商量并和他们解释执行策略的目的:这是一个偏好设置,而不是一个限制设置。应该使用其它技术,例如 ““Software Restriction Policy” 来安全地限制脚本的使用。

PowerShell 技能连载 - 检查 Execution Policy

execution policy 决定了 PowerShell 能执行哪类脚本。您需要将 execution policy 设置成 UndefinedRestricted,或 Default 之外的值,来允许脚本执行。

对于没有经验的用户,推荐使用 “RemoteSigned“。它可以运行本地脚本,也可以运行位于您信任的网络域的文件服务器上的脚本。它不会运行从 internet 上下载的脚本,或从其它非信任的源获取的脚本,除非这些脚本包含合法的数字签名。

以下是查看和设置当前 execution policy 的方法。

1
2
3
4
5
6
7
8
9
PS C:\> Get-ExecutionPolicy
Restricted

PS C:\> Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned -Force

PS C:\> Get-ExecutionPolicy
RemoteSigned

PS C:\>

当您使用 “CurrentUser“ 作用域,那么不需要管理员权限来更改这个设置。这是您个人的安全带,而不是公司级别的安全边界。这个设置将会保持住直到您改变它。

如果您需要确保确保可以无人值守地运行任何地方的脚本,您可能需要使用 “Bypass“ 设置,而不是 “RemoteSigned“。”Bypass“ 允许运行任意位置的脚本,而且不像 ““Unrestricted”“ 那样会弹出确认对话框。

PowerShell 技能连载 - 使用类(静态成员 - 第六部分)

Class 可以定义所谓的“静态”成员。静态成员(属性和方法)可以通过类本身调用,而不需要对象实例。

看看这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#requires -Version 5.0
class TextToSpeech
{
# store the initialized synthesizer here
hidden static $synthesizer

# static constructor, gets called whenever the type is initialized
static TextToSpeech()
{
Add-Type -AssemblyName System.Speech
[TextToSpeech]::Synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer
}

# convert text to speech

static Speak([string]$text)
{
[TextToSpeech]::Synthesizer.Speak($text)
}
}

TextToSpeech“ 类包装了文本转语音的一切需要。它使用了静态的构造函数(当定义类型的时候执行)和一个静态方法,所以不需要实例化一个对象。立刻就可以使用 “Speak“ 方法:

1
2
3
# since this class uses static constructors and methods, there is no need
# to instantiate an object
[TextToSpeech]::Speak('Hello World!')

如果您不用“静态”成员来做相同的事情,这个类会长得十分相似。您只需要移除所有 “static“ 关键字,并且通过 $this 代替类型名来存取类的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#requires -Version 5.0
class TextToSpeech
{
# store the initialized synthesizer here
hidden $synthesizer

# static constructor, gets called whenever the type is initialized
TextToSpeech()
{
Add-Type -AssemblyName System.Speech
$this.Synthesizer = New-Object System.Speech.Synthesis.SpeechSynthesizer
}

# convert text to speech

Speak([string]$text)
{
$this.Synthesizer.Speak($text)
}
}

最显著的区别可能是在用户端:用户现在需要先实例化一个对象:

1
2
$speaker = [TextToSpeech]::new()
$speaker.Speak('Hello World!')

所以使用规则提炼如下:

  • 使用静态成员来实现只需要存在一次的功能(所以文本到语音转换器是一个静态类的好例子)
  • 使用动态成员来实现需要在多于一个实例中同时存在(这样用户可以根据需要实例化任意多个独立的对象)的功能。

PowerShell 技能连载 - 使用类(构造函数 - 第五部分)

Class 也可以称为构造函数。构造函数是创建一个新对象的方法。构造函数只是和类名相同的方法。通过构造函数,可以更简单地创建事先为属性赋过值的对象。以下是一个例子:“Person”类定义了一个 person。

以下是一个构造函数,输入姓和名,以及生日。当一个对象实例化的时候,构造函数会被调用,并且事先填充好对象的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#requires -Version 5.0
class Person
{
[string]$FirstName
[string]$LastName
[int][ValidateRange(0,100)]$Age
[DateTime]$Birthday

# constructor
Person([string]$FirstName, [string]$LastName, [DateTime]$Birthday)
{
# set object properties
$this.FirstName = $FirstName
$this.LastName = $LastName
$this.Birthday = $Birthday
# calculate person age
$ticks = ((Get-Date) - $Birthday).Ticks
$this.Age = (New-Object DateTime -ArgumentList $ticks).Year-1
}
}

有了这个类之后,您可以很方便地创建 person 对象的列表:

1
2
3
[Person]::new('Tobias','Weltner','2000-02-03')
[Person]::new('Frank','Peterson','1976-04-12')
[Person]::new('Helen','Stewards','1987-11-19')

结果类似如下:

1
2
3
4
5
FirstName LastName Age Birthday
--------- -------- --- --------
Tobias Weltner 16 2/3/2000 12:00:00 AM
Frank Peterson 40 4/12/1976 12:00:00 AM
Helen Stewards 29 11/19/1987 12:00:00 AM

PowerShell 技能连载 - 使用类(重载 - 第四部分)

类中的方法可以重载:您可以定义多个同名的方法,但是参数类型不同。它用起来和 cmdlet 中的参数集类似。请看:

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
#requires -Version 5.0
class StopWatch
{
# property is marked "hidden" because it is used internally only
# it is not shown by IntelliSense
hidden [DateTime]$LastDate = (Get-Date)

# when no parameter is specified, do not emit verbose info

[int] TimeElapsed()
{
return $this.TimeElapsedInternal($false)
}

# user can decide whether to emit verbose info or not
[int] TimeElapsed([bool]$Verbose)
{
return $this.TimeElapsedInternal($Verbose)
}

# this method is called by all public methods

hidden [int] TimeElapsedInternal([bool]$Verbose)
{
# get current date
$now = Get-Date
# and subtract last date, report back milliseconds
$milliseconds = ($now - $this.LastDate).TotalMilliseconds
# use $this to access internal properties and methods
# update the last date so that it now is the current date
$this.LastDate = $now
# output verbose information if requested
if ($Verbose) {
$VerbosePreference = 'Continue'
Write-Verbose "Last step took $milliseconds ms." }
# use "return" to define the return value
return $milliseconds
}

Reset()
{
$this.LastDate = Get-Date
}
}

# create instance
$stopWatch = [StopWatch]::new()

# do not output verbose info
$stopWatch.TimeElapsed()


Start-Sleep -Seconds 2
# output verbose info
$stopWatch.TimeElapsed($true)

$a = Get-Service
# output verbose info
$stopWatch.TimeElapsed($true)

结果类似如下:

1
2
3
4
5
0
VERBOSE: Last step took 2018.1879 ms.
2018
VERBOSE: Last step took 68.8883 ms.
69

PowerShell 技能连载 - 使用类(增加方法 - 第三部分)

相对于 [PSCustomObject],使用 class 的最大好处之一是它也可以定义方法(命令)。以下例子实现了秒表功能。秒表可以用来计算代码执行了多少时间:

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
#requires -Version 5.0
class StopWatch
{
# property is marked "hidden" because it is used internally only
# it is not shown by IntelliSense
hidden [DateTime]$LastDate = (Get-Date)

[int] TimeElapsed()
{
# get current date
$now = Get-Date
# and subtract last date, report back milliseconds
$milliseconds = ($now - $this.LastDate).TotalMilliseconds
# use $this to access internal properties and methods
# update the last date so that it now is the current date
$this.LastDate = $now
# use "return" to define the return value
return $milliseconds
}

Reset()
{
$this.LastDate = Get-Date
}
}

以下是秒表的使用方法:

1
2
3
4
5
6
7
8
9
10
# create instance
$stopWatch = [StopWatch]::new()

$stopWatch.TimeElapsed()

Start-Sleep -Seconds 2
$stopWatch.TimeElapsed()

$a = Get-Service
$stopWatch.TimeElapsed()

结果类似如下:

1
2
3
0
2018
69

当您在一个函数中定义方法时,要遵守一系列规则:

  • 如果一个方法有返回值,那么必须指定返回值的数据类型
  • 方法的返回值必须用关键字“return”来指定
  • 方法中不能使用未赋值的变量,也不能从父作用域中读取变量
  • 要引用这个类中的属性或方法,请在前面加上“$this.

PowerShell 技能连载 - 使用类(初始化属性 - 第二部分)

可以为类的属性手动指定一个数据类型和缺省值。当您从一个类实例化一个对象时,属性已经填充好并且只接受指定的数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#requires -Version 5.0
class Info
{
# strongly typed properties with default values
[String]
$Name = $env:USERNAME

[String]
$Computer = $env:COMPUTERNAME

[DateTime]
$Date = (Get-Date)
}

# create instance
$infoObj = [Info]::new()

# view default (initial) values
$infoObj

# change value
$infoObj.Name = 'test'
$infoObj

PowerShell 技能连载 - 使用类(创建对象 - 第一部分)

从 PowerShell 5.0 开始,引入了一个新的关键字 “class”。它能够创建新的类。您可以使用类作为新对象的模板。一下代码定义了一个名为 “Info” 的新类的模板,这个类有一系列属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#requires -Version 5.0
class Info
{
$Name
$Computer
$Date
}

# generic syntax to create a new object instance
$infoObj = New-Object -TypeName Info

# alternate syntax PS5 or better (shorter and faster)
$infoObj = [Info]::new()


$infoObj

$infoObj.Name = $env:COMPUTERNAME
$infoObj.Computer = $env:COMPUTERNAME
$infoObj.Date = Get-Date

$infoObj
$infoObj.GetType().Name

您可以使用 New-Object 来创建这个类的任意多个新实例。每个实例代表有三个属性的 “Info” 类型的一个新对象。

1
2
3
4
5
Name            Computer        Date
---- -------- ----

DESKTOP-7AAMJLF DESKTOP-7AAMJLF 1/2/2017 2:00:02 PM
Info

这是一个非常简单(但很有用)的例子,演示了如何使用类来生产对象。如果您只希望在新对象中存储一些零碎信息,您也可以使用 PowerShell 3.0 引入的 [PSCustomObject] 语法:

1
2
3
4
5
6
7
8
9
#requires -Version 3.0
$infoObj = [PSCustomObject]@{
Name = $env:COMPUTERNAME
Computer = $env:COMPUTERNAME
Date = Get-Date
}

$infoObj
$infoObj.GetType().Name

这种做法没有使用一个蓝本(类),而是根据哈希表创建独立的新对象:

1
2
3
4
Name            Computer        Date
---- -------- ----
DESKTOP-7AAMJLF DESKTOP-7AAMJLF 1/2/2017 2:02:39 PM
PSCustomObject

所以新创建的对象类型永远是 “PSCustomObject“而在前一个例子中,对象的类型是通过类名定义的。

PowerShell 技能连载 - 使用命名空间

使用 .NET 的类型名称很麻烦,因为这些名字很长。以下是一个例子:

1
2
3
4
5
#requires -Version 2.0

Add-Type -AssemblyName System.Speech
$speak = New-Object -TypeName System.Speech.Synthesis.SpeechSynthesizer
$speak.Speak('Hello I am PowerShell!')

在 PowerShell 5.0 以上版本,您可以定义希望使用的 .NET 命名空间。这些 “using namespace“ 语句必须放在脚本的开头。此时代码的可读性变得更好,并且 using 语句明确了该脚本使用了哪些 .NET 命名空间:

1
2
3
4
5
6
7
8
#requires -Version 5.0

using namespace System.Speech.Synthesis

Add-Type -AssemblyName System.Speech

$speak = New-Object -TypeName SpeechSynthesizer
$speak.Speak('Hello I am PowerShell!')

以下是另一个例子:System.IO.Path .NET 命名空间包含一系列有用的路径工具方法。以下是一些例子:

1
2
[System.IO.Path]::ChangeExtension('test.txt', 'bat')
[System.IO.Path]::GetExtension('test.txt')

现在可以不必重复地使用 [System.IO.Path] 来访问这些方法。添加一句 “using namespace System.IO“ 语句之后就可以直接访问 [Path] 类型:

1
2
3
4
5
6
#requires -Version 5.0

using namespace System.IO

[Path]::ChangeExtension('test.txt', 'bat')
[Path]::GetExtension('test.txt')