PowerShell 技能连载 - Getting File Extension

By converting a path to a FileInfo object, you can easily determine the path parent folder or file extension. Have a look:

([IO.FileInfo]'c:\test\abc.ps1').Extension

([IO.FileInfo]'c:\test\abc.ps1').DirectoryName

PowerShell 技能连载 - Working with [FileInfo] Object

Often, code needs to check on files, and for example test whether the file exists or exceeds a given size. Here is some commonly used code:

$logFile = "$PSScriptRoot\mylog.txt"

$exists = Test-Path -Path $logFile
if ($exists)
{
  $data = Get-Item -Path $logFile
  if ($data.Length -gt 100KB)
  {
    Remove-Item -Path $logFile
  }

}

By immediately converting a string path into a FileInfo object, you can do more with less:

[System.IO.FileInfo]$logFile = "$PSScriptRoot\mylog.txt"
if ($logFile.Exists -and $logFile.Length -gt 0KB) { Remove-Item -Path $logFile }

You can convert any path to a FileInfo object, even if it is not representing a file. That’s what the property “Exists” is for: it tells you whether the file is present or not.

Twitter This Tip!ReTweet this Tip!

PowerShell 技能连载 - 轻松记录脚本日志

从 PowerShell 5 开始,您可以在任何宿主中使用 Strart-Transcript 来记录脚本的所有输出内容。以下是向各种脚本轻松添加日志的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# add this: ############################
$logFile = "$PSScriptRoot\mylog.txt"
Start-Transcript -Path $logFile -Append
#########################################

"Hello"

($a = Get-Service)

"I received $($a.Count) services."
Write-Host "Watch out: direct output will not be logged!"


# end logging ###########################
Stop-Transcript
#########################################

只需要将注释块中的代码添加到脚本的开始和结束处。日志文件将会在脚本所在的目录创建。由于 $logFile 使用 $PSScriptRoot(脚本的当前文件夹),请确保已经将脚本保存并以脚本的方式运行。否则,$PSScriptRoot 变量可能为空。

只需要确保脚本输出所有您需要的信息,就可以在 logfile 中看到它。例如将赋值语句放在括号中,PowerShell 将不只是赋值,而且将它们输出到 output。

警告:除了用 Write-Host 直接写到宿主的信息,所有输入输出信息都将被记录下。这些信息只能在屏幕上看到。

PowerShell 技能连载 - 多语言语音输出

在 Windows 10 上,操作系统自带了一系列高质量的文本转语言引擎,而且不局限于英文。可用的 TTS 引擎数量依赖于您所安装的语言。

PowerShell 可以发送文本到这些 TTS 引擎,并且通过 tag 可以控制使用的语言。所以如果您同时安装了英语和德语的 TTS 引擎,您可以像下面这样混用不同的语言:

1
2
3
4
5
6
7
8
9
$text = "<LANG LANGID=""409"">Your system will restart now!</LANG>
<LANG LANGID=""407""><PITCH MIDDLE = '2'>Oh nein, das geht nicht!</PITCH></LANG>
<LANG LANGID=""409"">I don't care baby</LANG>
<LANG LANGID=""407"">Ich rufe meinen Prinz! Herbert! Tu was!</LANG>
"

$speaker = New-Object -ComObject Sapi.SpVoice
$speaker.Rate = 0
$speaker.Speak($text)

如果您希望使用不同的语言,只需要将 LANGID 数字调整为您希望使用的文化代号。

PowerShell 技能连载 - 从字符串中移除文本

有时候,您也许听说过 Trim()TrimStart()TrimEnd() 可以 移除字符串中的文本。并且它们工作起来很正常:

1
2
3
4
5
PS C:\> $testvalue = "this is strange"
PS C:\> $testvalue.TrimEnd("strange")
this is

PS C:\>

但是这个呢:

1
2
3
4
5
PS C:\> $testvalue = "this is strange"
PS C:\> $testvalue.TrimEnd(" strange")
this i

PS C:\>

实际情况是 Trim() 方法将您的参数视为一个字符的列表。所有这些字符都将被移除。

如果您只是想从字符串的任意位置移除文本,请使用 Replace() 来代替:

1
2
3
4
PS C:\> $testvalue.Replace(" strange", "")
this is

PS C:\>

如果您需要进一步的控制,请使用正则表达式和锚定。要只从字符串的尾部移除文本,以下代码可以实现这个功能。只有结尾部分的 “strange” 字符串会被移除。

1
2
3
4
5
6
7
$testvalue = "this is strange strange strange"

$searchText = [Regex]::Escape("strange")
$anchorTextEnd = "$"
$pattern = "$searchText$anchorTextEnd"

$testvalue -replace $pattern

PowerShell 技能连载 - 解压序列化的数据

在前一个技能中您学习到了如何使用 Export-CliXml 命令来序列化数据并且用 Compress-Archive 将巨大的 XML 文件压缩成远远小于原始文件的尺寸。

今天,我们进行相反的操作:假设获得一个包含 XML 序列化数据的 ZIP 文件,然后恢复序列化的对象。当然这假设您已基于昨天的技能创建了这样的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# path to existing ZIP file
$ZipPath = "$env:TEMP\data1.zip"

# by convention, XML file inside the ZIP file has the same name
$Path = [IO.Path]::ChangeExtension($ZipPath, ".xml")

# expand ZIP file
Expand-Archive -Path $ZipPath -DestinationPath $env:temp -Force

# deserialize objects
$objects = Import-Clixml -Path $Path

# remove XML file again
Remove-Item -Path $Path -Recurse -Force

$objects | Out-GridView

PowerShell 技能连载 - 压缩序列化的数据

通过 Export-CliXml 命令可以很方便地将处理结果保存到文件通过 Import-CliXml 命令,序列化的信息可以同样方便地恢复。然而,生成的 XML 文件可能非常大。

幸运的是,在 PowerShell 5 中有一个新的命令名叫 Compress-Archieve。当您创建了 XML 文件之后,您可以自动地将它转为一个 ZIP 文件。

以下是一些演示代码:它获取一个进程列表并且保存到 XML 文件。然后将 XML 文件压缩为 ZIP,并且删除原始的 XML 文件。

这么做的效率很高,因为 XML 是文本文件。您常常能看到压缩率在 3-5%(ZIP 的文件大小是原始文件的 3-5%):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
$Path = "$env:TEMP\data1.xml"

# serialize data (results in large text files)
Get-Process | Export-Clixml -Path $Path

$length1 = (Get-Item -Path $Path).Length

$ZipPath = [IO.Path]::ChangeExtension($Path, ".zip")

Compress-Archive -Path $Path -Destination $ZipPath -CompressionLevel Optimal -Force
Remove-Item -Path $Path

$length2 = (Get-Item -Path $ZipPath).Length

$compression = $length2 * 100 / $length1
"Compression Ratio {0:n2} %" -f $compression

PowerShell 技能连载 - Multipass: 安全存储多个凭据

If you’d like to safely store credentials (usernames and password) for your personal use in a file, here is a very simple yet extremely powerful approach. Take a look at this code:

$Path = "$home\Desktop\multipass.xml"

[PSCustomObject]@{
    User1 = Get-Credential -Message User1
    User2 = Get-Credential -Message User2
    User3 = Get-Credential -Message User3
} | Export-Clixml -Path $Path

When you run it, it asks for three credentials and saves them to a “multipass” file on your desktop. All passwords are safely encrypted with your identity and your machines identity (which is why the file can only be read by you, and only on the machine where it was created).

To later on use one of the credentials, this is how you read them back in:

$multipass = Import-Clixml -Path $Path

You can then access the credentials via the properties “User1”, “User2”, and “User3”, and use the credentials in your scripts wherever a cmdlet asks for a credential:

PS C:\> $multipass.User1

UserName                     Password
--------                     --------
AlbertK  System.Security.SecureString

PowerShell 技能连载 - 一个更好(更快)的 Start-Job

Start-Job 将一个脚本块发送到一个新的 PowerShell 进程,这样它可以独立并行运行。以下是一个非常简单的例子,演示 job 背后的概念:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# three separate "jobs" to do:
$job1 = { Start-Sleep -Seconds 6 ; 1 }
$job2 = { Start-Sleep -Seconds 8 ; 2 }
$job3 = { Start-Sleep -Seconds 5 ; 3 }

# execute two of them in background jobs
$j1 = Start-Job -ScriptBlock $job1
$j3 = Start-Job -ScriptBlock $job3

# execute one in our own process
$ej2 = & $job2

# wait for all to complete
$null = Wait-Job -Job $J1, $j3

# get the results and clean up
$ej1 = Receive-Job -Job $j1
$ej3 = Receive-Job -Job $j3
Remove-Job -Job $j1, $j3

# work with the results
$ej1, $ej2, $ej3

如果不用 job,那么需要等待 19 秒。幸好有了 job,这个过程可以缩短到 8 秒。

然而,也有副作用。由于 job 是在独立的应用中执行的,数据必须以 XML 序列化的方式来回传递。job 要传回越多的数据,就需要越多的时间。有些时候这个副作用会盖过了优点。

一个更好的方是在原来的 PowerShell 实例的子线程中运行 job。以下代码演示这种功能。它创建了一个新的名为 Start-MemoryJob 的命令,可以替代 Start-Job。其余的代码完全不用改变。

使用 Start-MemoryJob,不需要任何对象序列化。您的 job 可以快速平滑地运行,而没有返回大量的数据。而且,您现在获取到的是原始的对象。不再需要处理序列化过的对象。

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
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
$code = @'
using System;
using System.Collections.Generic;
using System.Text;
using System.Management.Automation;
using System.Management.Automation.Runspaces;
namespace InProcess
{
public class InMemoryJob : System.Management.Automation.Job
{
public InMemoryJob(ScriptBlock scriptBlock, string name)
{
_PowerShell = PowerShell.Create().AddScript(scriptBlock.ToString());
SetUpStreams(name);
}
public InMemoryJob(PowerShell PowerShell, string name)
{
_PowerShell = PowerShell;
SetUpStreams(name);
}
private void SetUpStreams(string name)
{
_PowerShell.Streams.Verbose = this.Verbose;
_PowerShell.Streams.Error = this.Error;
_PowerShell.Streams.Debug = this.Debug;
_PowerShell.Streams.Warning = this.Warning;
_PowerShell.Runspace.AvailabilityChanged +=
new EventHandler<RunspaceAvailabilityEventArgs>(Runspace_AvailabilityChanged);
int id = System.Threading.Interlocked.Add(ref InMemoryJobNumber, 1);
if (!string.IsNullOrEmpty(name))
{
this.Name = name;
}
else
{
this.Name = "InProcessJob" + id;
}
}
void Runspace_AvailabilityChanged(object sender, RunspaceAvailabilityEventArgs e)
{
if (e.RunspaceAvailability == RunspaceAvailability.Available)
{
this.SetJobState(JobState.Completed);
}
}
PowerShell _PowerShell;
static int InMemoryJobNumber = 0;
public override bool HasMoreData
{
get {
return (Output.Count > 0);
}
}
public override string Location
{
get { return "In Process"; }
}
public override string StatusMessage
{
get { return "A new status message"; }
}
protected override void Dispose(bool disposing)
{
if (disposing)
{
if (!isDisposed)
{
isDisposed = true;
try
{
if (!IsFinishedState(JobStateInfo.State))
{
StopJob();
}
foreach (Job job in ChildJobs)
{
job.Dispose();
}
}
finally
{
base.Dispose(disposing);
}
}
}
}
private bool isDisposed = false;
internal bool IsFinishedState(JobState state)
{
return (state == JobState.Completed || state == JobState.Failed || state ==
JobState.Stopped);
}
public override void StopJob()
{
_PowerShell.Stop();
_PowerShell.EndInvoke(_asyncResult);
SetJobState(JobState.Stopped);
}
public void Start()
{
_asyncResult = _PowerShell.BeginInvoke<PSObject, PSObject>(null, Output);
SetJobState(JobState.Running);
}
IAsyncResult _asyncResult;
public void WaitJob()
{
_asyncResult.AsyncWaitHandle.WaitOne();
}
public void WaitJob(TimeSpan timeout)
{
_asyncResult.AsyncWaitHandle.WaitOne(timeout);
}
}
}
'@
Add-Type -TypeDefinition $code
function Start-JobInProcess
{
[CmdletBinding()]
param
(
[scriptblock] $ScriptBlock,
$ArgumentList,
[string] $Name
)
function Get-JobRepository
{
[cmdletbinding()]
param()
$pscmdlet.JobRepository
}
function Add-Job
{
[cmdletbinding()]
param
(
$job
)
$pscmdlet.JobRepository.Add($job)
}
if ($ArgumentList)
{
$PowerShell = [PowerShell]::Create().AddScript($ScriptBlock).AddArgument($argumentlist)
$MemoryJob = New-Object InProcess.InMemoryJob $PowerShell, $Name
}
else
{
$MemoryJob = New-Object InProcess.InMemoryJob $ScriptBlock, $Name
}
$MemoryJob.Start()
Add-Job $MemoryJob
$MemoryJob
}

PowerShell 技能连载 - 结合时间戳服务器使用数字签名

当您对脚本文件签名时,会希望签名保持完整,即便为它签名的证书将来过期了。关键的是证书在签名的时候是合法的。

要确保这一点,您需要一个受信的机构提供的时间戳服务器。通过这种方式,您不仅是对一个脚本签名,而且添加了签名的时间。在证书有效期内,一切没有问题。

我们调整了前一个技能中的代码,并且添加了一个时间戳服务器的 URL。以下代码对用户数据文件中所有未签名的脚本文件增加签名。如果您确定要对您的脚本文件增加签名,请移除 -WhatIf 参数:

1
2
3
4
5
6
7
8
9
10
11
12
13
# read in the certificate from a pre-existing PFX file
$cert = Get-PfxCertificate -FilePath "$env:temp\codeSignCert.pfx"

# find all scripts in your user profile...
Get-ChildItem -Path $home\Documents -Filter *.ps1 -Include *.ps1 -Recurse -ErrorAction SilentlyContinue |
# ...that do not have a signature yet...
Where-Object {
($_ | Get-AuthenticodeSignature).Status -eq 'NotSigned'
} |
# and apply one
# (note that we added -WhatIf so no signing occurs. Remove this only if you
# really want to add digital signatures!)
Set-AuthenticodeSignature -Certificate $cert -TimestampServer http://timestamp.digicert.com -WhatIf