The Largest PowerShell Community in China has 1716 Members Now!

The largest PowerShell community “PowerShell Tech Interact” in China has 1716 members up to March 29, 2019!

The goal of this community is to:

  1. Help new PowerShellers to get up to PowerShell language
  2. Help every PowerSheller overcome any PowerShell technical difficulties
  3. Share code and information to accelerate the learning process

To contact with the community manager, please visit this MVP link.

Join Us Now! (you may need to install QQ client first)

PowerShell 技能连载 - 修复 PowerShell 上下文菜单

当您在文件管理器中右键点击一个 PowerShell 脚本文件时,通常会见到一个名为“使用 PowerShell 运行”的上下文菜单项,可以通过它快速地执行 PowerShell 脚本。

然而,在某些系统中,“使用 PowerShell 运行“命令缺失了。原因是当您定义了一个非缺省的“打开方式”命令,那么该命令就会隐藏。要修复它,您只需要删除这个注册表键:

1
HKEY_CURRENT_USER\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1\UserChoice

在 regedit.exe 中删除这个键很简单,而且当这个键移除之后,“使用 PowerShell 运行”上下文菜单就再次可见了。

不过实际中在 PowerShell 删除这个注册表键却不太容易。以下这些命令执行都会失败,报告某些子项无法删除:

1
2
3
4
5
6
7
8
9
PS C:\> Remove-Item HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1\UserChoice

PS C:\> Remove-Item Registry::HKEY:CURRENT_USER:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1\UserChoice

PS C:\> Remove-Item 'HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1\UserChoice'

PS C:\> Remove-Item HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\'.ps1'\UserChoice

PS C:\> Remove-Item HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1\UserChoice -Recurse -Force

我们明天会提供一个解决方案来应对这种情况。

PowerShell 技能连载 - 让新手运行 PowerShell 脚本

假设您要把一段 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
# specify the path to your PowerShell script
$ScriptPath = "C:\test\test.ps1"

# create a lnk file
$shortcutPath = [System.IO.Path]::ChangeExtension($ScriptPath, "lnk")
$filename = [System.IO.Path]::GetFileName($ScriptPath)

# create a new shortcut
$shell = New-Object -ComObject WScript.Shell
$scut = $shell.CreateShortcut($shortcutPath)
# launch the script with powershell.exe:
$scut.TargetPath = "powershell.exe"
# skip profile scripts and enable execution policy for this one call
# IMPORTANT: specify only the script file name, not the complete path
$scut.Arguments = "-noprofile -executionpolicy bypass -file ""$filename"""
# IMPORTANT: leave the working directory empty. This way, the
# shortcut uses relative paths
$scut.WorkingDirectory = ""
# optinally specify a nice icon
$scut.IconLocation = "$env:windir\system32\shell32.dll,162"
# save shortcut file
$scut.Save()

# open shortcut file in File Explorer
explorer.exe "/select,$shortcutPath"

这个快捷方式就放在您的 PowerShell 脚本相邻的位置。它使用相对路径,由于我们保证快捷方式和 PowerShell 脚本放在相同的路径,所以它能够完美地工作——这样您可以将两个文件打包,然后将它们发送给客户。当他解压了文件,快捷方式仍然可以工作。您甚至可以将快捷方式改为想要的名字,例如“双击我执行”。

重要:快捷方式使用相对路径来确保这个解决方案便携化。如果您将快捷方式移动到脚本之外的文件夹,那么该快捷方式显然不能工作。。

PowerShell 技能连载 - 修复 PowerShellGet 发布

如果您在使用 Publish-Module 来将您的模块发布到 PowerShell 仓库,并且您一直收到不支持的命令的错误信息,那么可能需要重新安装管理模块上传的可执行程序。当这些可执行程序太旧时,它们可能不能再能和最新的 PowerShellGet 模块同步。

运行这段代码,以管理员权限(对所有用户有效)下载并更新 nuget.exe:

1
2
3
4
5
6
7
$Path = "$env:ProgramData\Microsoft\Windows\PowerShell\PowerShellGet"
$exists = Test-Path -Path $Path
if (!$exists)
{
$null = New-Item -Path $Path -ItemType Directory
}
Invoke-WebRequest -Uri https://aka.ms/psget-nugetexe -OutFile "$Path\NuGet.exe"

运行这段代码只针对当前用户下载并安装 nuget.exe:

1
2
3
4
5
6
7
$Path = "$env:LOCALAPPDATA\Microsoft\Windows\PowerShell\PowerShellGet"
$exists = Test-Path -Path $Path
if (!$exists)
{
$null = New-Item -Path $Path -ItemType Directory
}
Invoke-WebRequest -Uri https://aka.ms/psget-nugetexe -OutFile "$Path\NuGet.exe"

PowerShell 技能连载 - 将大文件拆分成小片段(第 3 部分)

在前一个技能中我们延时了如何使用 PowerShell 将文件分割成小的分片,以及如何将这些分片合并起来,重建原始文件。我们甚至进一步扩展了这些函数,将它们发布到 PowerShell Gallery。所以要分割和合并文件,只需要获取该模块并像这样安装:

1
PS> Install-Module -Name FileSplitter -Repository PSGallery -Scope CurrentUser -Force

现在当您需要将一个大文件分割成多个小片时,只需要运行以下代码:

1
2
3
4
5
6
7
PS C:\> Split-File -Path 'C:\movies\Woman tries putting gas in a Tesla.mp4' -PartSizeBytes 10MB -AddSelfExtractor -Verbose
VERBOSE: saving to C:\users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.0.part...
VERBOSE: saving to C:\users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.1.part...
VERBOSE: saving to C:\users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.2.part...
VERBOSE: Adding extractor scripts...

PS C:\

Split-Path 将文件分割成不超过 PartSizeByte 参数指定的大小。感谢 -AddSelfExtractor,它还添加了一个可以将分片文件合并为原始文件的脚本,以及一个双击即可执行合并操作的快捷方式。以下是您获得的文件::

1
2
3
4
5
6
7
8
9
10
11
12
13
14
PS C:\users\tobwe\Downloads> dir *gas*


Folder: C:\movies


Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 03.03.2019 18:11 2004 Extract Woman tries putting gas in a Tesla.mp4.lnk
-a---- 03.03.2019 16:54 24081750 Woman tries putting gas in a Tesla.mp4
-a---- 03.03.2019 18:11 10485760 Woman tries putting gas in a Tesla.mp4.0.part
-a---- 03.03.2019 18:11 10485760 Woman tries putting gas in a Tesla.mp4.1.part
-a---- 03.03.2019 18:11 3110230 Woman tries putting gas in a Tesla.mp4.2.part
-a---- 03.03.2019 18:11 3179 Woman tries putting gas in a Tesla.mp4.3.part.ps1

如您所见,有许多包含 .part 扩展名的文件,以及一个扩展名为 .part.ps1 的文件。后者是合并脚本。当您运行这个脚本时,它读取这些分片文件并重建原始文件,然后将删除所有分片文件以及自身。最终,该合并脚本将打开文件管理器并选中恢复的文件。

由于对于普通用户来说可能不了解如何运行 PowerShell 脚本,所以还有一个额外的名为 “Extract…”,扩展名为 .lnk 的文件。这是一个快捷方式文件。当用户双击这个文件,它将运行 PowerShell 合并脚本并恢复原始文件。

如果您希望手工恢复原始文件,您可以手工调用 Join-File

1
2
3
4
5
6
7
PS> Join-File -Path 'C:\users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4' -Verbose -DeletePartFiles
VERBOSE: processing C:\users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.0.part...
VERBOSE: processing C:\users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.1.part...
VERBOSE: processing C:\users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.2.part...
VERBOSE: Deleting part files...

PS>

PowerShell 技能连载 - 将大文件拆分成小片段(第 2 部分)

在前一个技能中我们介绍了如何讲一个大文件分割成小块。今天,我们将完成一个函数,它能将这些小文件合并成原来的文件。

假设您已经按上一个技能用 Split-File 将一个大文件分割成多个小文件。现在拥有了一大堆扩展名为 “.part” 的文件。这是上一个技能的执行结果:

1
2
3
4
5
6
7
8
9
10
11
12
PS> dir "C:\Users\tobwe\Downloads\*.part"


Folder: C:\Users\tobwe\Downloads


Mode LastWriteTime Length Name
---- ------------- ------ ----
-a---- 03.03.2019 16:25 6291456 Woman tries putting gas in a Tesla.mp4.00.part
-a---- 03.03.2019 16:25 6291456 Woman tries putting gas in a Tesla.mp4.01.part
-a---- 03.03.2019 16:25 6291456 Woman tries putting gas in a Tesla.mp4.02.part
-a---- 03.03.2019 16:25 5207382 Woman tries putting gas in a Tesla.mp4.03.part

要合并这些部分,请使用我们新的 Join-File 函数(不要和内置的 Join-Path 命令混淆)。让我们先看看它是如何工作的:

1
2
3
4
5
6
7
8
PS C:\> Join-File -Path "C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4" -DeletePartFiles -Verbose
VERBOSE: processing C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.00.part...
VERBOSE: processing C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.01.part...
VERBOSE: processing C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.02.part...
VERBOSE: processing C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.03.part...
VERBOSE: Deleting part files...

PS C:\>

只需要提交文件名(不需要分片编号和分片扩展名)。当您指定了 -DeletePartFiles 参数,函数将会在创建完原始文件之后删除分片文件。

要使用 Join-File 函数,需要先运行这段代码:

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
function Join-File
{

param
(
[Parameter(Mandatory)]
[String]
$Path,

[Switch]
$DeletePartFiles
)

try
{
# get the file parts
$files = Get-ChildItem -Path "$Path.*.part" |
# sort by part
Sort-Object -Property {
# get the part number which is the "extension" of the
# file name without extension
$baseName = [IO.Path]::GetFileNameWithoutExtension($_.Name)
$part = [IO.Path]::GetExtension($baseName)
if ($part -ne $null -and $part -ne '')
{
$part = $part.Substring(1)
}
[int]$part
}
# append part content to file
$writer = [IO.File]::OpenWrite($Path)
$files |
ForEach-Object {
Write-Verbose "processing $_..."
$bytes = [IO.File]::ReadAllBytes($_)
$writer.Write($bytes, 0, $bytes.Length)
}
$writer.Close()

if ($DeletePartFiles)
{
Write-Verbose "Deleting part files..."
$files | Remove-Item
}
}
catch
{
throw "Unable to join part files: $_"
}
}

今日知识点:

  • 使用 [IO.Path] 类来分割文件路径
  • 使用 [IO.file] 类以字节的方式存取文件内容
  • 使用 OpenWrite() 以字节的方式写入文件

PowerShell 技能连载 - 将大文件拆分成小片段(第 1 部分)

PowerShell 可以将大文件拆分成多个小片段,例如将它们做为电子邮件附件发送。今天,我们关注如何分割文件。在下一个技能中,我们将演示如何将各个部分合并在一起。

要将大文件分割成小片段,我们创建了一个名为 Split-File 的函数。它工作起来类似这样:

1
2
3
4
5
6
7
PS> Split-File -Path "C:\Users\tobwe\Downloads\Woman putting gas in Tesla.mp4" -PartSizeBytes 6MB -Verbose
VERBOSE: saving to C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.00.part...
VERBOSE: saving to C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.01.part...
VERBOSE: saving to C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.02.part...
VERBOSE: saving to C:\Users\tobwe\Downloads\Woman tries putting gas in a Tesla.mp4.03.part...

PS C:\>

-PartSizeByte 参数设置最大的分片尺寸,在我们的例子中是 6MB。当您指定了 -Verbose 参数,该函数将在创建分片文件时显示分片文件名。

要使用 Split-File 函数,您需要运行以下代码:

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
function Split-File
{

param
(
[Parameter(Mandatory)]
[String]
$Path,

[Int32]
$PartSizeBytes = 1MB
)

try
{
# get the path parts to construct the individual part
# file names:
$fullBaseName = [IO.Path]::GetFileName($Path)
$baseName = [IO.Path]::GetFileNameWithoutExtension($Path)
$parentFolder = [IO.Path]::GetDirectoryName($Path)
$extension = [IO.Path]::GetExtension($Path)

# get the original file size and calculate the
# number of required parts:
$originalFile = New-Object System.IO.FileInfo($Path)
$totalChunks = [int]($originalFile.Length / $PartSizeBytes) + 1
$digitCount = [int][Math]::Log10($totalChunks) + 1

# read the original file and split into chunks:
$reader = [IO.File]::OpenRead($Path)
$count = 0
$buffer = New-Object Byte[] $PartSizeBytes
$moreData = $true

# read chunks until there is no more data
while($moreData)
{
# read a chunk
$bytesRead = $reader.Read($buffer, 0, $buffer.Length)
# create the filename for the chunk file
$chunkFileName = "$parentFolder\$fullBaseName.{0:D$digitCount}.part" -f $count
Write-Verbose "saving to $chunkFileName..."
$output = $buffer

# did we read less than the expected bytes?
if ($bytesRead -ne $buffer.Length)
{
# yes, so there is no more data
$moreData = $false
# shrink the output array to the number of bytes
# actually read:
$output = New-Object Byte[] $bytesRead
[Array]::Copy($buffer, $output, $bytesRead)
}
# save the read bytes in a new part file
[IO.File]::WriteAllBytes($chunkFileName, $output)
# increment the part counter
++$count
}
# done, close reader
$reader.Close()
}
catch
{
throw "Unable to split file ${Path}: $_"
}
}

明天我们将研究反向操作:如何将所有分片组合成原始文件。

今日知识点:

  • [IO.Path] 类来分割文件路径。
  • [IO.File] 类在字节级别处理文件内容。
  • Read() 函数将字节写入文件。

PowerShell 技能连载 - 通过 Outlook 发送邮件

您可以通过 Send-MailMessage 用 PowerShell 发送邮件。然而,这需要一个 SMTP 服务器,并且通过这种方式发送的邮件不会在您的邮箱中存档。

要通过 Outlook 发送邮件,请看这个函数:

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
function Send-OutlookMail
{

param
(
# the email address to send to
[Parameter(Mandatory=$true, Position=0, HelpMessage='The email address to send the mail to')]
[String]
$Recipient,

# the subject line
[Parameter(Mandatory=$true, HelpMessage='The subject line')]
[String]
$Subject,

# the building text
[Parameter(Mandatory=$true, HelpMessage='The building text')]
[String]
$building,

# a valid file path to the attachment file (optional)
[Parameter(Mandatory=$false)]
[System.String]
$FilePath = '',

# mail importance (0=low, 1=normal, 2=high)
[Parameter(Mandatory=$false)]
[Int]
[ValidateRange(0,2)]
$Importance = 1,

# when set, the mail is sent immediately. Else, the mail opens in a dialog
[Switch]
$SendImmediately
)

$o = New-Object -ComObject Outlook.Application
$Mail = $o.CreateItem(0)
$mail.importance = $Importance
$Mail.To = $Recipient
$Mail.Subject = $Subject
$Mail.building = $building
if ($FilePath -ne '')
{
try
{
$null = $Mail.Attachments.Add($FilePath)
}
catch
{
Write-Warning ("Unable to attach $FilePath to mail: " + $_.Exception.Message)
return
}
}
if ($SendImmediately -eq $false)
{
$Mail.Display()
}
else
{
$Mail.Send()
Start-Sleep -Seconds 10
$o.Quit()
Start-Sleep -Seconds 1
$null = [Runtime.Interopservices.Marshal]::ReleaseComObject($o)
}
}

现在在 Outlook 中很容易:

1
PS> Send-OutlookMail -Recipient frank@test.com -Subject 'Hi Frank!' -building 'Trying a new PS script. See attachment.' -FilePath 'c:\stuff\sample.zip' -Importance 0

假设您安装了 Outlook 并且设置了用户配置文件,这行代码将在一个对话框窗口中打开写好的邮件,这样您可以再次确认并做最终修改,然后按下“发送”按钮将邮件发送出去。

如果您指定了 -SendImmediately 开关参数,PowerShell 将会试图立即发送邮件。是否能够正确发送取决于您的 Outlook 关于自动操作的安全设置。自动发送邮件可能被禁用,或是会弹出一个对话框来征得您的同意。
z

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
function Convert-Text
{
param
(
$Text = 'Early to bed and early to rise makes a man healthy, wealthy and wise.'
)
$words = $Text -split ' '

$newWords = foreach($word in $words)
{
if ($word.Length -le 2)
{
$word
}
else
{
$firstChar = $word[0]
$lastChar = $word[-1]
$charLen = $word.Length -2
$inbetween = $word[1..$charLen]
$chars = $inbetween | Get-Random -Count $word.Length
$inbetweenScrambled = $chars -join ''
"$firstChar$inbetweenScrambled$lastChar"
}
}

$newWords -join ' '
}

如果没有输入文本,那么将采用默认文本。您可以猜出它的意思吗?

1
2
PS C:\> Convert-Text
Ealry to bed and erlay to rsie maeks a man hylhtea, wlhtaey and wies.

PowerShell 技能连载 - 提升 PowerShell 管道的速度

当 a) 需要处理许多项目时 b) 使用 PowerShell 管道时,PowerShell 脚本可能会变得非常缓慢。今天让我们找出它的原因,以及解决的方法。

要重现这个问题,让我们先创建一个用例,体现 PowerShell 如何明显变慢。我们需要准备许多项目。这里我们用代码生成 Windows 文件夹下所有文件的列表,这需要几秒钟才能生成完。

1
2
3
# get large data sets
$files = Get-ChildItem -Path c:\windows -File -Recurse -ErrorAction SilentlyContinue
$files.Count

我们将这些文件发送到管道,并且只挑出大于 1MB 的文件。在以下栗子中,我们将 $file 的内容全部发到管道,是为了有可复制的数据。实际情况中,当然不应该使用变量,而应该直接将结果输出到管道。

1
2
3
4
Measure-Command {
$largeFiles = $files | Where-Object { $_.Length -gt 1MB }
}
$largeFiles.Count

在我们的测试中,以上代码需要消耗 3-4 秒,并且产生了 3485 个“大”文件。在您的机器上结果可能不同。

Where-Object 实际上只是一个包含了 If 语句的 ForEach-Object 命令,那么让我们试着将 Where-Object 替换成 If

1
2
3
4
5
6
7
Measure-Command {
$largeFiles = $Files | ForEach-Object {
if ($_.Length -gt 1MB)
{ $_ }
}
}
$largeFiles.Count

结果是一样的,而时间减少到一半。

ForEach-Object 实际上只是一个有 process 块的匿名脚本块,所以接下来请试试这段代码:

1
2
3
4
5
6
7
8
9
10
Measure-Command {
$largeFiles = $Files | & {
process
{
if ($_.Length -gt 1MB)
{ $_ }
}
}
}
$largeFiles.Count

结果再次相同,但是结果从原来的 4 秒减少到大约 100 毫秒(四十分之一)。

可见,当通过管道传入数据时,PowerShell 对每个传入的对象调用绑定的参数方法,这将显著地增加时间开销。由于 ForEach-ObjectWhere-Object 使用参数,所以会激活绑定。

当您不使用内部包含 process 脚本块的匿名脚本块时,将忽略所有的参数绑定并显著加速 PowerShell 管道的执行速度。