PowerShell 技能连载 - 向字符串添加数字(第 1 部分)

双引号括起来的字符串可以方便地扩展变量,但是这个概念并不是万无一失的:

1
2
3
4
5
6
$id = 123

# this is the desired output:
# Number is 123:
# this DOES NOT WORK:
"Number is $id:"

如您所见的上述例子中,当您在双引号中放置变量时,PowerShell 自动判断变量的起止位置。而 : 被当成变量的一部分。要修复这个问题,您需要某种方法来明确地标记变量的起止位置。以下是一些修复这类问题的方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
$id = 123

# PowerShell escape character ends the variable
"Number is $id`:"
# braces "embrace" the variable name
"Number is ${id}:"
# subexpressions execute the code in the parenthesis
"Number is $($id):"
# the format operator inserts the array on the right into the
# placeholders in the template on the left
'Number is {0}:' -f $id
# which is essentially this:
'Number is ' + @($id)[0] + ':'

# careful with "addition": this requires the first
# element to be a string. So this works:
'Number is ' + $id + ':'
# this won't:
$id + " is the number"
# whereas this will again:
'' + $id + " is the number"

PowerShell 技能连载 - Get-PSCallStack 和调试

在前一个技能中我们使用 Get-PSCallStack 来确定代码的“调用深度”。今天我们来看看如何使用这个 cmdlet 来帮助调试。要演示这个功能,请将以下代码保存为一个脚本文件。将它保存为一个 ps1 文件十分重要。请在 PowerShell ISE 中执行它。

1
2
3
4
5
6
7
8
9
10
11
12
13
function test1
{
test2
}

function test2
{
Wait-Debugger
Get-Process

}

test1

test1 调用了 test2,并且在 test2 中,有一个 Wait-Debugger 调用。这个 cmdlet 是从 PowerShell 5 开始引入的。它会导致代码暂停并调用调试器。如果您使用的是一个早版本的 PowerShell,那么可以通过 F9 键设置一个断点。当您运行这段代码时,调试器会在 Get-Process 正要执行之前暂停,并且该行代码会以黄色高亮(如果没有效果,请检查是否已将代码保存为文件?)。

在交互式的 PowerShell 控制台中,您现在可以键入 Get-PSCallStack 来检查在代码块中停在哪里:

[DBG]: PS C:\>> Get-PSCallStack

Command Arguments Location
------- --------- --------
test2   {}        a1.ps1: Line 11
test1   {}        a1.ps1: Line 5
a1.ps1  {}        a1.ps1: Line 14

输出结果显示您当前位于函数 test2 中,它是被 test1 调用,而 test1 是被 a1.ps1 调用。

您还可以通过 InvocationInfo 属性看到更多的信息:

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
[DBG]: PS C:\>> Get-PSCallStack | Select-Object -ExpandProperty InvocationInfo


MyCommand : test2
BoundParameters : {}
UnboundArguments : {}
ScriptLineNumber : 5
OffsetInLine : 5
HistoryId : 17
ScriptName : C:\Users\tobwe\Documents\PowerShell\a1.ps1
Line : test2

PositionMessage : In C:\Users\tobwe\Documents\PowerShell\a1.ps1:5 Line:5
+ test2
+ ~~~~~
PSScriptRoot : C:\Users\tobwe\Documents\PowerShell
PSCommandPath : C:\Users\tobwe\Documents\PowerShell\a1.ps1
InvocationName : test2
PipelineLength : 1
PipelinePosition : 1
ExpectingInput : False
CommandOrigin : Internal
DisplayScriptPosition :

MyCommand : test1
BoundParameters : {}
UnboundArguments : {}
ScriptLineNumber : 14
OffsetInLine : 1
HistoryId : 17
ScriptName : C:\Users\tobwe\Documents\PowerShell\a1.ps1
Line : test1

PositionMessage : In C:\Users\tobwe\Documents\PowerShell\a1.ps1:14 Line:1
+ test1
+ ~~~~~
PSScriptRoot : C:\Users\tobwe\Documents\PowerShell
PSCommandPath : C:\Users\tobwe\Documents\PowerShell\a1.ps1
InvocationName : test1
PipelineLength : 1
PipelinePosition : 1
ExpectingInput : False
CommandOrigin : Internal
DisplayScriptPosition :

MyCommand : a1.ps1
BoundParameters : {}
UnboundArguments : {}
ScriptLineNumber : 0
OffsetInLine : 0
HistoryId : 17
ScriptName :
Line :
PositionMessage :
PSScriptRoot :
PSCommandPath :
InvocationName : C:\Users\tobwe\Documents\PowerShell\a1.ps1
PipelineLength : 2
PipelinePosition : 1
ExpectingInput : False
CommandOrigin : Internal
DisplayScriptPosition :

PowerShell 技能连载 - 发现嵌套层数

Get-PSCallStack 返回所谓的“调用堆栈”——它最基本的功能是告诉您代码的嵌套深度:每次进入一个脚本块,就将向堆栈加入一个新的对象。让我们看一看:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function test1
{
$callstack = Get-PSCallStack
$nestLevel = $callstack.Count - 1
"TEST1: Nest Level: $nestLevel"
test2
}

function test2
{
$callstack = Get-PSCallStack
$nestLevel = $callstack.Count - 1
"TEST2: Nest Level: $nestLevel"
}

# calls test1 which in turn calls test2
test1
# calls test2 directly
test2

在这个例子中,您会看到两个函数。它们使用 Get-PSCallStack 来确定它们的“嵌套深度”。当运行 test1 时,它内部调用 test2,所以 test2 的嵌套深度为 2。而当您直接调用 test2,它的嵌套深度为 1:

TEST1: Nest Level: 1
TEST2: Nest Level: 2
TEST2: Nest Level: 1

还有一个使用相同技术的,更有用的示例:一个递归的函数调用,当嵌套深度为 10 层时停止递归:

1
2
3
4
5
6
7
8
9
10
11
12
13
function testRecursion
{
$callstack = Get-PSCallStack
$nestLevel = $callstack.Count - 1
"TEST3: Nest Level: $nestLevel"

# function calls itself if nest level is below 10
if ($nestLevel -lt 10) { testRecursion }

}

# call the function
testRecursion

以下是执行结果:

TEST3: Nest Level: 1
TEST3: Nest Level: 2
TEST3: Nest Level: 3
TEST3: Nest Level: 4
TEST3: Nest Level: 5
TEST3: Nest Level: 6
TEST3: Nest Level: 7
TEST3: Nest Level: 8
TEST3: Nest Level: 9
TEST3: Nest Level: 10

PowerShell 技能连载 - 超级简单的密码生成器

以下是一个超级简单的生成随机密码的方法。这个方法确保不会使用有歧义的字符,但不会关心其它规则,例如指定字符的最小个数。

1
2
3
4
5
6
$Length = 12
$characters = 'abcdefghkmnprstuvwxyz23456789§$%&?*+#'
$password = -join ($characters.ToCharArray() |
Get-Random -Count $Length)

$password

PowerShell 技能连载 - 优先使用 WLAN 连接

当您同时连接到 LAN 和 WLAN,并且希望指定一个优先的连接,那么您可以调整网络跃点。网络跃点值越小,网卡的优先级越高。

要列出当前首选项,请使用 Get-NetIPInterface。例如要将所有 WLAN 网卡的跃点值设成 10,请使用这行代码:

1
2
Get-NetIPInterface -InterfaceAlias WLAN |
Set-NetIPInterface -InterfaceMetric 10

改变网络跃点值需要管理员权限。相关 cmdlet 在 Windows 10 和 Windows Server 2016/2019 中可用。

PowerShell 技能连载 - 向上下文菜单添加个人 PowerShell 命令

您可以针对文件类型,例如 PowerShell 文件,添加个人的上下文菜单。当您右键单击一个 .ps1 文件时,将显示这些上下文菜单命令。它们关联到个人账户,并且不需要管理员权限就可以设置。

以下是一个实现的脚本。只需要调整头两个变量:指定上下文菜单中需要出现的命令,以及需要执行的命令行。在这个命令中,使用 “%1“ 作为右键单击时 PowerShell 脚本路径的占位符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# specify your command name
$ContextCommand = "Open Script with Notepad"
# specify the command to execute. "%1" represents the file path to your
# PowerShell script
$command = 'notepad "%1"'


$baseKey = 'Registry::HKEY_CLASSES_ROOT\.ps1'
$id = (Get-ItemProperty $baseKey).'(Default)'
$ownId = $ContextCommand.Replace(' ','')
$contextKey = "HKCU:\Software\Classes\$id\Shell\$ownId"
$commandKey = "$ContextKey\Command"

New-Item -Path $commandKey -Value $command -Force
Set-Item -Path $contextKey -Value $ContextCommand

当您运行这段脚本时,将生成一个名为 “Open Script with Notepad” 的新的上下文菜单命令。您可以利用这个钩子并且设计任何命令,包括 GitHub 或备份脚本。

请注意:当您对 OpenWith 打开方式选择了一个非缺省的命令,那么自定义命令将不会在上下文菜单中显示。这个命令仅当记事本为缺省的 OpenWith 打开方式应用时才出现。

要移除所有上下文菜单扩展,请运行以下代码:

1
2
3
4
$baseKey = 'Registry::HKEY_CLASSES_ROOT\.ps1'
$id = (Get-ItemProperty $baseKey).'(Default)'
$contextKey = "HKCU:\Software\Classes\$id"
Remove-Item -Path $contextKey -Recurse -Force

PowerShell 技能连载 - 用 PowerShell 锁定屏幕

以下是一个名为 Lock-Screen 的 PowerShell 函数,它可以锁定屏幕,禁止用户操作。可以指定一个自定义消息,并且可以在锁定时将屏幕调暗。

以下是一个调用示例:

1
PS> Lock-Screen -LockSeconds 4 -DimScreen -Title 'Go away and come back in {0} seconds.'

以下是 Lock-Screen 的源码:

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
Function Lock-Screen
{
[CmdletBinding()]
param
(
# number of seconds to lock
[int]
$LockSeconds = 10,

# message shown. Use {0} to insert remaining seconds
# do not use {0} for a static message
[string]
$Title = 'wait for {0} more seconds...',

# dim screen
[Switch]
$DimScreen
)

# when run without administrator privileges, the keyboard will not be blocked!

# get access to API functions that block user input
# blocking of keyboard input requires admin privileges
$code = @'
[DllImport("user32.dll")]
public static extern int ShowCursor(bool bShow);

[DllImport("user32.dll")]
public static extern bool BlockInput(bool fBlockIt);
'@

$userInput = Add-Type -MemberDefinition $code -Name Blocker -Namespace UserInput -PassThru

# get access to UI functionality
Add-Type -AssemblyName PresentationFramework
Add-Type -AssemblyName PresentationCore

# set window opacity
$opacity = 1
if ($DimScreen) { $opacity = 200 }

# create a message label
$label = New-Object -TypeName Windows.Controls.Label
$label.FontSize = 60
$label.FontFamily = 'Consolas'
$label.FontWeight = 'Bold'
$label.Background = 'Transparent'
$label.Foreground = 'Blue'
$label.VerticalAlignment = 'Center'
$label.HorizontalAlignment = 'Center'


# create a window
$window = New-Object -TypeName Windows.Window
$window.WindowStyle = 'None'
$window.AllowsTransparency = $true
$color = [Windows.Media.Color]::FromArgb($opacity, 0,0,0)
$window.Background = [Windows.Media.SolidColorBrush]::new($color)
$window.Opacity = 0.8
$window.Left = $window.Top = 0
$window.WindowState = 'Maximized'
$window.Topmost = $true
$window.Content = $label

# block user input
$null = $userInput::BlockInput($true)
$null = $userInput::ShowCursor($false)

# show window and display message
$null = $window.Dispatcher.Invoke{
$window.Show()
$LockSeconds..1 | ForEach-Object {
$label.Content = ($title -f $_)
$label.Dispatcher.Invoke([Action]{}, 'Background')
Start-Sleep -Seconds 1
}
$window.Close()
}

# unblock user input
$null = $userInput::ShowCursor($true)
$null = $userInput::BlockInput($false)
}

请注意 Lock-Screen 需要管理员权限才能完全禁止用户输入。

PowerShell 技能连载 - 禁止用户输入

如果一个 PowerShell 脚本需要进行危险的操作,而且用户操作必须被禁止,那么您可以使用 API 来临时禁止所有键盘输入。锁定键盘输入不需要管理员权限。

以下是一个演示如何阻止所有键盘输入 4 秒钟的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#requires -RunAsAdministrator
# when run without administrator privileges, the keyboard will not be blocked!

# get access to API functions that block user input
# blocking of keyboard input requires administrator privileges
$code = @'
[DllImport("user32.dll")]
public static extern bool BlockInput(bool fBlockIt);
'@

$userInput = Add-Type -MemberDefinition $code -Name Blocker -Namespace UserInput -PassThru

# block user input
$null = $userInput::BlockInput($true)

Write-Warning "Your input has been disabled for 4 seconds..."
Start-Sleep -Seconds 4

# unblock user input
$null = $userInput::BlockInput($false)

PowerShell 技能连载 - 向编码的命令传递参数

对 PowerShell 代码编码是一种在 PowerShell 环境之外运行 PowerShell 代码的方法,例如在批处理文件中。以下是一些读取 PowerShell 代码,对它编码,并且通过命令行执行它的示例代码:

1
2
3
4
5
6
7
8
9
10
$command = {
Get-Service |
Where-Object Status -eq Running |
Out-GridView -Title 'Pick a service that you want to stop' -PassThru |
Stop-Service
}

$bytes = [System.Text.Encoding]::Unicode.GetBytes($command)
$encodedCommand = [Convert]::ToBase64String($bytes)
"powershell.exe -noprofile -encodedcommand $encodedCommand" | clip

当执行这段代码之后,您会发现剪贴板里有 PowerShell 命令。代码类似这样:

1
powershell.exe -noprofile -encodedcommand DQAKAEcAZQB0AC0AUwBlAHIAdgBpAGMAZQAgAHwAIAANAAoAIAAgACAAIABXAGgAZQByAGUALQBPAGIAagBlAGMAdAAgAFMAdABhAHQAdQBzACAALQBlAHEAIABSAHUAbgBuAGkAbgBnACAAfAAgAA0ACgAgACAAIAAgAE8AdQB0AC0ARwByAGkAZABWAGkAZQB3ACAALQBUAGkAdABsAGUAIAAnAFAAaQBjAGsAIABhACAAcwBlAHIAdgBpAGMAZQAgAHQAaABhAHQAIAB5AG8AdQAgAHcAYQBuAHQAIAB0AG8AIABzAHQAbwBwACcAIAAtAFAAYQBzAHMAVABoAHIAdQAgAHwAIAANAAoAIAAgACAAIABTAHQAbwBwAC0AUwBlAHIAdgBpAGMAZQANAAoA

当您打开一个新的 cmd.exe 窗口,您可以将这段代码粘贴到控制台并且执行纯的 PowerShell 代码。您可以在任何有足够空间容纳整行代码的地方执行编码过的命令。因为长度限制,编码过的命令在快捷方式文件(.lnk 文件)以及开始菜单中的运行对话框中工作不正常。

还有一个额外的限制:无法传递参数到编码过的命令。除非使用一个很酷的技能。首先,在代码中加入一个 param() 块,然后使该参数成为必选。然后,从一个外部的 PowerShell 通过管道将参数传递进去。

以下是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
$command = {
param
(
[Parameter(Mandatory)]
[string]
$FirstName,

[Parameter(Mandatory)]
[string]
$LastName
)
"Hello, your first name is $FirstName and your last name is $lastname!"
}

$bytes = [System.Text.Encoding]::Unicode.GetBytes($command)
$encodedCommand = [Convert]::ToBase64String($bytes)
"powershell.exe -noprofile -command 'Tobias', 'Weltner' | powershell -noprofile -encodedcommand $encodedCommand" | clip

命令看起来类似这样:

1
powershell.exe -noprofile -command 'Tom', 'Tester' | powershell -noprofile -encodedcommand DQAKAHAAYQByAGEAbQANAAoAKAANAAoAIAAgACAAIABbAFAAYQByAGEAbQBlAHQAZQByACgATQBhAG4AZABhAHQAbwByAHkAKQBdAA0ACgAgACAAIAAgAFsAcwB0AHIAaQBuAGcAXQANAAoAIAAgACAAIAAkAEYAaQByAHMAdABOAGEAbQBlACwADQAKAA0ACgAgACAAIAAgAFsAUABhAHIAYQBtAGUAdABlAHIAKABNAGEAbgBkAGEAdABvAHIAeQApAF0ADQAKACAAIAAgACAAWwBzAHQAcgBpAG4AZwBdAA0ACgAgACAAIAAgACQATABhAHMAdABOAGEAbQBlAA0ACgApAA0ACgAiAEgAZQBsAGwAbwAsACAAeQBvAHUAcgAgAGYAaQByAHMAdAAgAG4AYQBtAGUAIABpAHMAIAAkAEYAaQByAHMAdABOAGEAbQBlACAAYQBuAGQAIAB5AG8AdQByACAAbABhAHMAdAAgAG4AYQBtAGUAIABpAHMAIAAkAGwAYQBzAHQAbgBhAG0AZQAhACIADQAKAA==

当您运行这段代码,参数 “Tom” 和 “Tester” 将通过管道传递给执行编码过命令的 PowerShell。由于参数是必选的,所以管道的元素会传递给提示符,并且被编码的命令处理。

PowerShell 技能连载 - 删除无法删除的注册表键

删除注册表键通常很简单,用 Remove-Item 就可以了。然而,有时你会遇到一些无法删除的注册表键。在这个技能中我们将演示一个例子,并且提供一个解决方案。

在前一个技能中我们解释了当定义了一个非缺省的打开方式之后,PowerShell 文件的“使用 PowerShell 运行”上下文命令可能会丢失,而且出现了这个注册表键:

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

当您在文件管理器中右键点击一个 PowerShell 脚本,然后通过“选择其他应用”,选择一个 ISE 或 VSCode 之外的非缺省应用,并选中“始终使用此应用打开 .ps1 文件”复选框,来使用其他应用来打开 PowerShell 文件,那么注册表中就会创建上述注册表键。

当上述注册表键存在时,上下文菜单中缺省的 PowerShell 命令,例如“使用 PowerShell 运行”将不可见。用注册表删除这个键修复它很容易,但出于某种未知原因用 PowerShell 命令来删除会失败。所有 .NET 的方法也会失败。

以下命令会执行失败:

1
Remove-Item -Path HKCU:\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1\UserChoice

以下命令也会执行失败(这是 .NET 的等价代码):

1
2
3
$parent = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1', $true)
$parent.DeleteSubKeyTree('UserChoice',$true)
$parent.Close()

要删除这个键,您需要显示地调用 DeleteSubKey() 方法来代替 DeleteSubKeyTree()。明显地,在那个键中有一些不可见的异常子键导致该键无法删除。

当您只是删除该键(不包含它的子键,虽然子键并不存在),该键可以正常删除,并且 PowerShell 的“使用 PowerShell 运行“命令就恢复了:

1
2
3
$parent = [Microsoft.Win32.Registry]::CurrentUser.OpenSubKey('Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.ps1', $true)
$parent.DeleteSubKey('UserChoice', $true)
$parent.Close()

另一方面:由于这段代码只操作了用户配置单元,所以不需要任何特权,然而在 regedit.exe 中修复则需要管理员特权。