PowerShell 技能连载 - 将 PowerShell 脚本转换为批处理

以下是一个有趣的 PowerShell 脚本,名为 Convert-PowerShellToBatch。将 PowerShell 脚本的路径作为参数传给它,或者将 Get-ChildItem 的执行结果用管道传给它,来批量执行多个脚本。

该函数为每个脚本创建一个批处理文件。当您双击批处理文件时,将执行 PowerShell 代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function Convert-PowerShellToBatch
{
param
(
[Parameter(Mandatory,ValueFromPipeline,ValueFromPipelineByPropertyName)]
[string]
[Alias("FullName")]
$Path
)

process
{
$encoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes((Get-Content -Path $Path -Raw -Encoding UTF8)))
$newPath = [Io.Path]::ChangeExtension($Path, ".bat")
"@echo off`npowershell.exe -NoExit -encodedCommand $encoded" | Set-Content -Path $newPath -Encoding Ascii
}
}

Get-ChildItem -Path C:\path\to\powershell\scripts -Filter *.ps1 |
Convert-PowerShellToBatch

当您查看某个生成的脚本文件时,您会发现 PowerShell 代码被转换为 BASE64 编码的字符串。所以这种转换适用于许多真实世界的需求:

  • 双击执行一个批处理文件来运行 PowerShell 代码更方便。
  • 没有经验的用户更不容易受诱惑去改动脚本,因为它经过 BASE64 编码。

申明:BASE64 并不是加密。将 BASE64 编码的文本转换为可读的明文是很简单的事。所以这里用的技术不适合用来隐藏秘密,例如密码。

PowerShell 技能连载 - 用管道传递文件和文件夹

假设您想创建一个函数,接受一个文件路径参数。文件可以进行许多操作。您可能希望拷贝文件,压缩文件,将它们设置为隐藏文件,或其它各种操作。我们在这里并不关注具体需要做什么操作。我们希望关注 PowerShell 函数如何接受文件参数。

您应该遇到过这样的函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
function Process-File
{
param
(
$Path
)

# do something with the file
$file = Get-Item -Path $Path
'File {0} is of size {1} bytes.' -f $file.FullName, $file.Length
}

Process-File -Path C:\windows\explorer.exe

结果看起来类似这样:

1
2
3
4
5
PS> Process-File -Path C:\windows\explorer.exe

File C:\windows\explorer.exe is of size 3903784 bytes.

PS>

这个函数每次只处理一个路径。如果希望传入多个路径,您需要这样做:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Process-File
{
param
(
[string[]]
$Path
)

foreach($SinglePath in $Path)
{
# do something with the file
$file = Get-Item -Path $SinglePath
'File {0} is of size {1} bytes.' -f $file.FullName, $file.Length
}
}

Process-File -Path C:\windows\explorer.exe, C:\windows\notepad.exe

现在,您的函数可以接受任意多个逗号分隔的路径。如果您希望也能从管道输入路径呢?需要增加这些代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function Process-File
{
param
(
[Parameter(ValueFromPipeline)]
[string[]]
$Path
)

process
{
foreach($SinglePath in $Path)
{
# do something with the file
$file = Get-Item -Path $SinglePath
'File {0} is of size {1} bytes.' -f $file.FullName, $file.Length
}
}
}

Process-File -Path C:\windows\explorer.exe, C:\windows\notepad.exe
'C:\windows\explorer.exe', 'C:\windows\notepad.exe' | Process-File

基本上,您现在有两个嵌套的循环:process {} 是管道对象使用的循环,而其中的 foreach 循环处理用户传入的的字符串数组。

如果您希望 Get-ChildItem 提供路径给函数呢?它并不是返回字符串。它返回的是文件系统对象,而且在对象之内,有一个名为 “FullName” 的属性,存储对象的路径。以下是您要做的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function Process-File
{
param
(
[Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
[string[]]
[Alias("FullName")]
$Path
)

process
{
foreach($SinglePath in $Path)
{
# do something with the file
$file = Get-Item -Path $SinglePath
'File {0} is of size {1} bytes.' -f $file.FullName, $file.Length
}
}
}

Process-File -Path C:\windows\explorer.exe, C:\windows\notepad.exe
'C:\windows\explorer.exe', 'C:\windows\notepad.exe' | Process-File
Get-ChildItem -Path c:\windows -Filter *.exe | Process-File

现在,这个函数不止能接受管道传来的字符串 (ValueFromPipeline),而且还能接受有某个属性名或别名与参数 (Path) 相似的对象 (ValueFromPipelineByPropertyName)。任务完成了。您的函数现在能够为用户提供最大的灵活性,这基本上也是 cmdlet 所做的事。

PowerShell 技能连载 - 简易的 WMI 浏览器

WMI 是一个丰富的信息库——如果您知道 WMI 的类名:

1
2
3
Get-CimInstance -ClassName Win32_BIOS
Get-CimInstance -ClassName Win32_Share
Get-CimInstance -ClassName Win32_OperatingSystem

如果您想探索 WMI 的内容,那么以下代码会十分便利。Find-WmiClass 接受一个简单的关键字,例如 “video”、”network”、”ipaddress”。接下来它可以获取所有类名、某个属性名或方法名包含该关键字的 WMI 类。

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
function Find-WmiClass
{
param([Parameter(Mandatory)]$Keyword)

Write-Progress -Activity "Finding WMI Classes" -Status "Searching"

# find all WMI classes...
Get-WmiObject -Class * -List |
# that contain the search keyword
Where-Object {
# is there a property or method with the keyword?
$containsMember = ((@($_.Properties.Name) -like "*$Keyword*").Count -gt 0) -or ((@($_.Methods.Name) -like "*$Keyword*").Count -gt 0)
# is the keyword in the class name, and is it an interesting type of class?
$containsClassName = $_.Name -like "*$Keyword*" -and $_.Properties.Count -gt 2 -and $_.Name -notlike 'Win32_Perf*'
$containsMember -or $containsClassName
}
Write-Progress -Activity "Find WMI Classes" -Completed
}

$classes = Find-WmiClass

$classes |
# let the user select one of the found classes
Out-GridView -Title "Select WMI Class" -OutputMode Single |
ForEach-Object {
# get all instances of the selected class
Get-CimInstance -Class $_.Name |
# show all properties
Select-Object -Property * |
Out-GridView -Title "Instances"
}

接下来用户可以选择某个找到的类,该代码将显示这个类的实际实例。

声明:有部分类有几千个实例,例如 CIM_File。当选择了一个有这么多实例的 WMI 类时,该脚本将执行很长时间才能完成。

PowerShell 技能连载 - 查找嵌套的 AD 组成员

以下代码查找某个 Active Directory 用户属于哪些组(包括嵌套的组成员)。该代码需要 ActiveDirectory 模块。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#requires -Module ActiveDirectory

function Get-NestedGroupMember
{
param
(
[Parameter(Mandatory,ValueFromPipeline)]
[string]
$Identity
)

process
{
$user = Get-ADUser -Identity $Identity
$userdn = $user.DistinguishedName
$strFilter = "(member:1.2.840.113556.1.4.1941:=$userdn)"
Get-ADGroup -LDAPFilter $strFilter -ResultPageSize 1000
}
}

要查找组成员,只需要执行 Get-NestedGroupMember,跟上用户名即可。该函数和 Get-ADUser 接受同样的身份信息,所以您可以传入 SamAccountName、SID、GUID,或 distinguishedName。

PowerShell 技能连载 - 检查网络连接

如果您的机器通过不同网络连接,连到了 internet(或 VPN),以下两个函数可能对您有用。

Get-ActiveConnection 列出当前所有获取到 IP 地址的网络连接。Test-ActiveConnection 接受一个关键字并检查是否有一个名字中包含该关键字的活动连接。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function Get-ActiveConnection
{
Get-WmiObject -Class Win32_NetworkAdapterConfiguration |
Where-Object { $_.IPAddress } |
Select-Object -ExpandProperty Description
}

function Test-ActiveConnection
{
param([Parameter(Mandatory)]$Keyword)


@(Get-WmiObject -Class Win32_NetworkAdapterConfiguration |
Where-Object { $_.IPAddress } |
Where-Object { $_.Description -like "*$Keyword*" }).Count -gt 0

}

以下是一个快速的演示输出:

1
2
3
4
5
6
7
8
9
10
PS> Get-ActiveConnection
Dell Wireless 1820A 802.11ac

PS> Test-ActiveConnection dell
True

PS> if ( (Test-ActiveConnection dell) ) { Write-Warning "Connected via DELL network card" }
WARNING: Connected via DELL network card

PS>

PowerShell 技能连载 - 从 Windows 10 中移除臃肿的软件

Windows 10 附带了各种预装的应用程序和其它特性,这些内容可能是某些人反感。Richard Newton 创建了一个 PowerShell 脚本,用来识别和移除许多预装的特性,并且加强了隐私设置。他在 https://github.com/Sycnex/Windows10Debloater 描述了他的项目。

源码可以在这里找到:https://github.com/Sycnex/Windows10Debloater/blob/master/Windows10Debloater.ps1

我们向您推荐这个脚本因为它包含了许多有用的技术,演示了在 Windows 10 中如何移除应用和注册表键。当然,不建议在不了解的情况下运行这个脚本。请确保您了解将会移除什么内容,以及您是否真的希望移除它。

PowerShell 技能连载 - 通过 PowerShell 管理 FTP

在 PowerShell 中没有内置 FTP 命令,但是您可以方便地下载和安装一个免费的扩展,该扩展提供了您想要的 FTP 管理功能。只需要运行这行代码:

1
PS> Install-Module -Name Posh-SSH -Scope CurrentUser

如果 PowerShell 无法找到 Install-Module 命令,那么您很有可能没有运行最新版本的 PowerShell (5.1)。请升级您的 PowerShell,或者添加 Microsoft 的 “PowerShellGet” 模块,该模块提供了 Install-Module 命令。

该命令会会从公共的 PowerShell Gallery 下载 Posh-SSH 模块。当您同意下载内容之后,便新增了以下指令:

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
PS> Get-Command -Module Posh-SSH

CommandType Name Version Source
----------- ---- ------- ------
Function Get-PoshSSHModVersion 2.0.2 Posh-SSH
Function Get-SFTPChildItem 2.0.2 Posh-SSH
Function Get-SFTPContent 2.0.2 Posh-SSH
Function Get-SFTPLocation 2.0.2 Posh-SSH
Function Get-SFTPPathAttribute 2.0.2 Posh-SSH
Function Get-SFTPSession 2.0.2 Posh-SSH
Function Get-SSHPortForward 2.0.2 Posh-SSH
Function Get-SSHSession 2.0.2 Posh-SSH
Function Get-SSHTrustedHost 2.0.2 Posh-SSH
Function Invoke-SSHCommand 2.0.2 Posh-SSH
Function Invoke-SSHCommandStream 2.0.2 Posh-SSH
Function Invoke-SSHStreamExpectAction 2.0.2 Posh-SSH
Function Invoke-SSHStreamExpectSecureAction 2.0.2 Posh-SSH
Function New-SFTPFileStream 2.0.2 Posh-SSH
Function New-SFTPItem 2.0.2 Posh-SSH
Function New-SFTPSymlink 2.0.2 Posh-SSH
Function New-SSHDynamicPortForward 2.0.2 Posh-SSH
Function New-SSHLocalPortForward 2.0.2 Posh-SSH
Function New-SSHRemotePortForward 2.0.2 Posh-SSH
Function New-SSHShellStream 2.0.2 Posh-SSH
Function New-SSHTrustedHost 2.0.2 Posh-SSH
Function Remove-SFTPItem 2.0.2 Posh-SSH
Function Remove-SFTPSession 2.0.2 Posh-SSH
Function Remove-SSHSession 2.0.2 Posh-SSH
Function Remove-SSHTrustedHost 2.0.2 Posh-SSH
Function Rename-SFTPFile 2.0.2 Posh-SSH
Function Set-SFTPContent 2.0.2 Posh-SSH
Function Set-SFTPLocation 2.0.2 Posh-SSH
Function Set-SFTPPathAttribute 2.0.2 Posh-SSH
Function Start-SSHPortForward 2.0.2 Posh-SSH
Function Stop-SSHPortForward 2.0.2 Posh-SSH
Function Test-SFTPPath 2.0.2 Posh-SSH
Cmdlet Get-SCPFile 2.0.2 Posh-SSH
Cmdlet Get-SCPFolder 2.0.2 Posh-SSH
Cmdlet Get-SFTPFile 2.0.2 Posh-SSH
Cmdlet New-SFTPSession 2.0.2 Posh-SSH
Cmdlet New-SSHSession 2.0.2 Posh-SSH
Cmdlet Set-SCPFile 2.0.2 Posh-SSH
Cmdlet Set-SCPFolder 2.0.2 Posh-SSH
Cmdlet Set-SFTPFile 2.0.2 Posh-SSH

powershellmagazine.com 有一篇延伸的文章介绍如何使用这些命令:

PowerShell 技能连载 - 格式化数字(第 2 部分)

在前一个技能中我们介绍了 Get-DisplayFileSize 函数,它可以自动将字节数转换成容易阅读的带单位的数字,例如 “KB” 和 “MB”。

使用 Select-Object 指令,您可以创建一个带有易读的文件尺寸的文件夹列表:

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
$Length = @{
Name = "Length"
Expression = {
if ($_.PSIsContainer) { return }
$Number = $_.Length
$newNumber = $Number
$unit = 'Bytes,KB,MB,GB,TB,PB,EB,ZB' -split ','
$i = 0
while ($newNumber -ge 1KB -and $i -lt $unit.Length)
{
$newNumber /= 1KB
$i++
}

if ($i -eq $null) { $decimals = 0 } else { $decimals = 2 }
$displayText = "'{0,10:N$decimals} {1}'" -f $newNumber, $unit[$i]
$Number = $Number | Add-Member -MemberType ScriptMethod -Name ToString -Value ([Scriptblock]::Create($displayText)) -Force -PassThru
return $Number
}
}


# pretty file sizes
dir $env:windir |
Select-Object -Property Mode, LastWriteTime, $Length, Name |
Sort-Object -Property Length

请注意计算属性 Length 仍然可以用于排序。它仍是字节数据,只是显示的方式改变了。

PowerShell 技能连载 - 格式化数字(第 1 部分)

以下 Get-DisplayFileSize 函数接受任何字节数值,并且返回一个以 “MB”、”GB” 或 “PB” 为单位的,格式良好的大小值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function Get-DisplayFileSize
{
param([Double]$Number)

$newNumber = $Number

$unit = ',KB,MB,GB,TB,PB,EB,ZB' -split ','
$i = $null
while ($newNumber -ge 1KB -and $i -lt $unit.Length)
{
$newNumber /= 1KB
$i++
}

if ($i -eq $null) { return $number }
$displayText = "'{0:N2} {1}'" -f $newNumber, $unit[$i]
$Number = $Number | Add-Member -MemberType ScriptMethod -Name ToString -Value ([Scriptblock]::Create($displayText)) -Force -PassThru
return $Number
}

以下是一些例子:

1
2
3
4
5
6
7
8
PS> Get-DisplayFileSize -Number 800
800

PS> Get-DisplayFileSize -Number 678678674345
632,07 GB

PS> Get-DisplayFileSize -Number 6.23GB
6,23 GB

真正有趣的地方是这个函数返回的并不是字符串。它返回的是原始的数值,而只是覆盖了 ToString() 方法。您仍然可以对它进行排序、计算和对比:

1
2
3
4
5
6
7
8
9
10
11
12
13
PS> $n = 1245646233213
PS> $formatted = Get-DisplayFileSize -Number $n
PS> $formatted
1,13 TB

PS> $formatted -eq $n
True

PS> $formatted * 2
2491292466426

PS> Get-DisplayFileSize ($formatted * 2)
2,27 TB

PowerShell 技能连载 - 过滤文件

您可能还没有注意到,Get-ChildItem(也叫做 dir 或者 ls) 的 -Filter 参数并不是像您所想的方式工作。以下代码的本意是只查找 PowerShell 脚本,但实际上找到的结果更多:

1
2
Get-ChildItem -Path $env:windir -Filter *.ps1 -Recurse -ErrorAction Silent |
Group-Object -Property Extension -NoElement

以下是查找结果:

1
2
3
4
Count Name
----- ----
800 .ps1
372 .ps1xml

-Filter 参数的作用和传统的 dir 命令的行为类似。要真正起到您想要的效果,您应该使用以下代码:

1
2
Get-ChildItem -Path $env:windir -Filter *.ps1 -Include *.ps1 -Recurse -ErrorAction SilentlyContinue |
Group-Object -Property Extension -NoElement

以下是正确的结果:

1
2
3
Count Name
----- ----
800 .ps1

虽然您可以省略 -Filter 参数,但强烈建议保留着它。首先,-Include 只能和 -Recurse 配合使用其次,-Include 速度很慢。先用一个粗略(但是快速)的 -Filter 过滤,然后用 Include 是最佳实践。