PowerShell 技能连载 - 导出不带引号的CSV(和其他转换技巧)

PowerShell 附带了一堆 Export-ConvertTo- cmdlet,因此您可以将对象数据序列化为 CSV、JSON、XML和其他格式。很好,但是创建自己的导出功能并不难。

例如,Windows PowerShell 中的 Export-Csv 始终对数值添加双引号。如果你不喜欢带双引号的 CSV,就不好办了。 PowerShell 7 已解决此问题,其中包含额外的参数,但创建自己的 Export-Csv 函数根本并不难。

这是一个简单的例子:

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
function ConvertTo-MyCsv
{
param
(
[char]
$Delimiter = ','
)
begin
{
$init = $false
}
process
{
# write the headers
if ($init -eq $false)
{
$_.PSObject.Properties.Name -join $Delimiter
$init = $true
}

# write the items
$_.PSObject.Properties.Value -join $Delimiter
}
}

$a = Get-Service | ConvertTo-MyCsv

这的确是所有的内容了:ConvertTo-MyCsv 将对象转换为没有双引号的 CSV,如果需要,甚至可以选择分隔符(默认为 “,“)。

要快速检查生成的CSV的完整性,请将其转换回对象:

1
PS> $a | ConvertFrom-CSV | Out-GridView

一切都很好,转换生效了。它不比 ConvertTo-Csv 慢。

显然,如果任何数据值包含分隔符(这就是为什么 ConvertTo-Csv 为它们添加双引号的原因),则转换将失败。但这不是这里的重点。您可以轻松微调函数。更重要的是,PowerShell 中将对象自动转换为文本的艺术。

要将对象转换为 CSV(或任何其他格式),只需访问隐藏的 PSObject 属性(可用于任何 PowerShell 对象)。它描述了对象,并提供所有属性名称,值和数据类型。

PowerShell 技能连载 - 更好的 PowerShell 帮助(第 3 部分)

在上一个技能中,我们使用自定义代理功能对原始的 Get-Help cmdlet 进行了“影子”处理。该功能检查命令是否存在联机帮助,如果存在,则默认情况下会打开丰富的联机帮助。这极大地改善了 PowerShell 中的帮助体验。

但是,通过简单的调整,您可以使该功能更进一步:为什么不同时检查另一种方式呢?如果用户使用 -Online 参数,但是没有在线帮助,则 Get-Help 会抛出一个丑陋的异常。显示内置的本地帮助而不是抛出异常,不是更好吗?

当用户将 Get-Help 的默认值设置为 -Online 时,也可使该函数能适应 $PSDefaultParameterValue 的调整。

这是更新的函数:

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
function Get-Help{
\# clone the original param block taken from Get-Help [CmdletBinding(DefaultParameterSetName='AllUsersView', HelpUri='https://go.microsoft.com/fwlink/?LinkID=113316')]
param(
[Parameter(Position=0, ValueFromPipelineByPropertyName)]
[string]
$Name, [Parameter(ParameterSetName='Online', Mandatory)]
[switch]
$Online, [ValidateSet('Alias','Cmdlet','Provider','General','FAQ','Glossary','HelpFile','ScriptCommand','Function','Filter','ExternalScript','All','DefaultHelp','Workflow','DscResource','Class','Configuration')]
[string[]]
$Category, [string]
$Path, [string[]]
$Component, [string[]]
$Functionality, [string[]]
$Role, [Parameter(ParameterSetName='DetailedView', Mandatory)]
[switch]
$Detailed, [Parameter(ParameterSetName='AllUsersView')]
[switch]
$Full, [Parameter(ParameterSetName='Examples', Mandatory)]
[switch]
$Examples, [Parameter(ParameterSetName='Parameters', Mandatory)]
[string]
$Parameter, [Parameter(ParameterSetName='ShowWindow', Mandatory)]
[switch]
$ShowWindow )

begin {
\# we do the adjustments only when the user has submitted \# the -Name, -Category, and -Online parameters
if ( (@($PSBoundParameters.Keys) -ne 'Name' -ne 'Category' -ne 'Online'). Count -eq 0 )
{
\# check whether there IS online help available at all
\# retrieve the help URI $help = Microsoft.PowerShell.Core\Get-Command -Name $Name
\# reset the parameter -Online based on availability of online help $PSBoundParameters['Online']= [string]::IsNullOrWhiteSpace($help.HelpUri) -eq $false }

\# once the parameter adjustment has been processed, call the original \# Get-Help cmdlet with the parameters found in $PSBoundParameters
\# turn the original Get-Help cmdlet into a proxy command receiving the \# adjusted parameters \# with a proxy command, you can invoke its begin, process, and end \# logic separately. That's required to preserve pipeline functionality $cmd = Get-Command -Name 'Get-Help' -CommandType Cmdlet $proxy = {& $cmd @PSBoundParameters}. GetSteppablePipeline($myInvocation.CommandOrigin)

\# now, call its default begin, process, and end blocks in the appropriate \# script blocks so it integrates in real-time pipelines $proxy.Begin($PSCmdlet)
}

process { $proxy.Process($_) }

end { $proxy.End() }

\# use the original help taken from Get-Help for this function <#
.ForwardHelpTargetName Microsoft.PowerShell.Core\Get-Help .ForwardHelpCategory Cmdlet \#>}

运行这段代码之后,无论使用 Get-Helphelp 还是公共参数 -?,您的帮助系统现在都变得更加智能。

如果有可用于命令的联机帮助,则默认情况下显示:

1
2
3
PS> Get-Help Get-Service

PS> Get-Service -?

如果没有可用的联机帮助,它将始终显示本地帮助(即使您不小心指定了 -Online 或使用 $PSDefaultParameterValue 来显式使用 -Online):

1
2
3
PS> Connect-IscsiTarget -?

PS> Get-Help Connect-IscsiTarget -Online

并且,如果您指定其他参数,它们仍然可以按预期继续工作:

1
PS> Get-Help Get-Service -ShowWindow

本质上,该调整仅包含同时提供丰富在线帮助内容,并在可用时显示它。

如果您喜欢此功能,请将其添加到配置文件脚本中,以便在启动 PowerShell 时对其进行定义。路径可以在这里找到:$profile.CurrentUserAllHosts

PowerShell 技能连载 - 更好的 PowerShell 帮助(第 2 部分)

在上一个技能中,我们更改了 Get-Help 的默认参数值,以在您使用 Get-Help 或通用参数 -? 时自动显示丰富的联机帮助。但是,当 cmdlet 没有联机帮助时,此方法会产生错误。

更好的方法是首先检查给定命令是否存在联机帮助,然后才打开联机帮助。如果没有在线帮助,则应显示默认的本地帮助。

此方法无法通过默认参数实现。相反,Get-Help cmdlet 本身需要进行调整。要将逻辑添加到 Get-Help,可以使用以下代理函数:

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
function Get-Help{
\# clone the original param block taken from Get-Help [CmdletBinding(DefaultParameterSetName='AllUsersView', HelpUri='https://go.microsoft.com/fwlink/?LinkID=113316')]
param(
[Parameter(Position=0, ValueFromPipelineByPropertyName)]
[string]
$Name, [Parameter(ParameterSetName='Online', Mandatory)]
[switch]
$Online, [ValidateSet('Alias','Cmdlet','Provider','General','FAQ','Glossary','HelpFile','ScriptCommand','Function','Filter','ExternalScript','All','DefaultHelp','Workflow','DscResource','Class','Configuration')]
[string[]]
$Category, [string]
$Path, [string[]]
$Component, [string[]]
$Functionality, [string[]]
$Role, [Parameter(ParameterSetName='DetailedView', Mandatory)]
[switch]
$Detailed, [Parameter(ParameterSetName='AllUsersView')]
[switch]
$Full, [Parameter(ParameterSetName='Examples', Mandatory)]
[switch]
$Examples, [Parameter(ParameterSetName='Parameters', Mandatory)]
[string]
$Parameter, [Parameter(ParameterSetName='ShowWindow', Mandatory)]
[switch]
$ShowWindow )

begin {
\# determine whether -Online should be made a default if (
\# user submitted -Name only ( $PSBoundParameters.Count -eq 1 -and
$PSBoundParameters.ContainsKey('Name')
) -or \# system submitted -Name and -Category (when using -?) (
$PSBoundParameters.Count -eq 2 -and
$PSBoundParameters.ContainsKey('Name') -and
$PSBoundParameters.ContainsKey('Category')
)
)
{
\# prerequisites are OK, now check whether there IS online help \# available at all
\# retrieve the help URI $help = Microsoft.PowerShell.Core\Get-Command -Name $Name
\# set the -Online parameter only if there is a help URI $PSBoundParameters['Online']= [string]::IsNullOrWhiteSpace($help.HelpUri) -eq $false }

\# once the parameter adjustment has been processed, call the original \# Get-Help cmdlet with the parameters found in $PSBoundParameters
\# turn the original Get-Help cmdlet into a proxy command receiving the \# adjusted parameters \# with a proxy command, you can invoke its begin, process, and end \# logic separately. That's required to preserve pipeline functionality $cmd = Get-Command -Name 'Get-Help' -CommandType Cmdlet $proxy = {& $cmd @PSBoundParameters}. GetSteppablePipeline($myInvocation.CommandOrigin)

\# now, call its default begin, process, and end blocks in the appropriate \# script blocks so it integrates in real-time pipelines $proxy.Begin($PSCmdlet)
}

process { $proxy.Process($_) }

end { $proxy.End() }

\# use the original help taken from Get-Help for this function <#
.ForwardHelpTargetName Microsoft.PowerShell.Core\Get-Help .ForwardHelpCategory Cmdlet \#>}

运行此代码时,Get-Help 函数现在将覆盖原始的 Get-Help cmdlet。在内部,该函数调用原生的 cmdlet,但在此之前,该函数检查 -Online 是否应设为默认参数。

现在,仅当用户未提交任何有冲突的参数时才发生这种情况,并且仅当首先有可用于所请求命令的联机帮助时才发生这种情况。

现在,无论何时使用 Get-Help 或通用参数 -?,您都会获得丰富的在线帮助(如果可用)或本地默认帮助。

试试以下代码:

1
2
3
4
5
6
7
PS> Get-Service -?

PS> Connect-IscsiTarget -?

PS> Get-Help Get-Service

PS> Get-Help Get-Service -ShowWindow

由于有可用于 Get-Service 的联机帮助,因此第一个调用将打开浏览器窗口并显示帮助。第二个调用说明了没有可用的联机帮助时发生的情况:此处未将 “-Online“ 作为默认参数,而是显示了默认的本地帮助。

第三和第四次调用说明 Get-Help 仍然可以正常运行。默认情况下,该命令现在会打开联机帮助,但是如果您添加其他参数(例如 -ShowWindow),它们仍将按预期运行。

如果您喜欢此功能,则应将其添加到您的配置文件脚本中。

注意:如果您已通过 $PSDefaultParameterGet-Help 设置了任何默认参数(即,按照前面的提示进行操作时),则这些参数将生效,并且上面的功能无法为您带来任何改善。

确保您没有定义任何会影响 “Get-Help“ 的默认参数:

1
PS> $PSDefaultParameterValues.Keys.ToLower() -like 'get-help*'

PowerShell 技能连载 - 更好的 PowerShell 帮助(第 1 部分)

许多 cmdlet 提供了丰富的联机帮助,您可以使用 Get-Help 并加上 -Online 参数来自动打开 cmdlet 的网页:

1
PS> Get-Help -Name Get-Service -Online

每个 cmdlet 还支持公用参数 -?。不过,它仅显示了有限的内置本地帮助:

1
2
3
4
5
6
7
8
9
PS> Get-Service -?

NAME
Get-Service

SYNTAX
Get-Service [-ComputerName <System.String[]>] [-DependentServices] -DisplayName <System.String[]> [-Exclude
<System.String[]>] [-Include <System.String[]>] [-RequiredServices] []
...

但是,您可以通过一个简单的技巧告诉 PowerShell 也对内置参数 -? 使用丰富的联机帮助。请运行这段代码:

1
PS> $PSDefaultParameterValues['Get-Help:Online'] = $true

此命令自动为 Get-Help -Online 参数设置一个新的默认值,并且因为 -? 内部使用 Get-Help,您现在只需添加 -? 到命令名称就可以获得大多数 PowerShell cmdlet 的丰富在线帮助。

1
PS> Get-Service -?

真是太棒了,因此您可能需要将该命令添加到自动启动配置文件脚本中。该脚本的路径可以在 $profile.CurrentUserAllHosts 中找到。它可能尚不存在,所以您可能必须创建它。

但是,有一个警告:如果命令没有在线帮助,那么您现在会收到一条错误消息,提示没有在线帮助。在即将发布的技能中,我们将单独解决此问题。

PowerShell 技能连载 - 检测 Wi-Fi 信号强度(第 3 部分)

在上一个技巧中,我们介绍了免费的 PowerShell 模块 Get-WLANs ,该模块可以访问 Windows Wi-Fi 框架并返回信息,例如信号强度。这是下载并安装它的命令:

1
Install-Module -Name Get-WLANs -Scope CurrentUser -Force

尽管您可以简单地使用其新命令 Get-WLANs(如上一技巧中所述),但该模块还添加了一个新的 .NET 类型,您可以在运行该命令后使用它。首先运行此命令以初始化新的 .NET 类型:

1
2
# "initialize" the new type
$null = Get-WLANs

接下来,创建一个原生的 “WlanClient” 对象:

1
$wc = [NativeWifi.WlanClient]::New()

现在,您可以转储有关系统中所有可用的 Wi-Fi 适配器的所有技术细节:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
PS> $wc.Interfaces


Autoconf : True
BssType : Any
InterfaceState : Connected
Channel : 36
RSSI : 29
RadioState : NativeWifi.Wlan+WlanRadioState
CurrentOperationMode : ExtensibleStation
CurrentConnection : NativeWifi.Wlan+WlanConnectionAttributes
NetworkInterface : System.Net.NetworkInformation.SystemNetworkInterface
InterfaceGuid : 7d6c33b7-0354-4ad7-a72f-5a1a5cbb1a9b
InterfaceDescription : Killer(R) Wi-Fi 6 AX1650s 160MHz Wireless Network Adapter (201D2W)
InterfaceName : WLAN

Interfaces“ 属性返回一个数组,因此您的第一个 Wi-Fi 适配器由以下形式表示:

1
$wc.Interfaces[0]

它提供了很多方法,例如,检索可访问的可用 Wi-Fi 网络列表。这将扫描新的网络:

1
2
$wc.Interfaces[0].Scan()
Start-Sleep -Seconds 1

以下代码转储可使用的网络列表(包括信号强度,频率和信道):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
PS> $wc.Interfaces[0].GetAvailableNetworkList(3)


Dot11PhyTypes : {8}
profileName : internetcafe
dot11Ssid : NativeWifi.Wlan+Dot11Ssid
dot11BssType : Infrastructure
numberOfBssids : 3
networkConnectable : True
wlanNotConnectableReason : Success
morePhyTypes : False
wlanSignalQuality : 81
securityEnabled : True
dot11DefaultAuthAlgorithm : RSNA_PSK
dot11DefaultCipherAlgorithm : CCMP
flags : Connected, HasProfile

(...)

此对象模型为提供了丰富的方法和属性来控制和管理 Wi-Fi 网络适配器。例如,此代码转储可用的 SSID 列表及其信号强度:

1
2
3
4
5
6
7
8
9
10
11
$ssid = @{
N='SSID'
E={ [System.Text.Encoding]::Ascii.GetString( $_.dot11ssid.SSID,
0,
$_.dot11ssid.SSIDLength )
}
}

$wc.Interfaces[0].GetAvailableNetworkList(3) |
Select-Object -Property $ssid, wlanSignalQuality, profileName |
Where-Object SSID

结果看起来类似这样:

SSID                       wlanSignalQuality profileName
----                       ----------------- -----------
internetcafe                              81 internetcafe
internetcafe                              87 internetcafe 2
internetcafe                              87
DIRECT-fb-HP M477 LaserJet                31
Guest                                     67

PowerShell 技能连载 - 检测 Wi-Fi 信号强度(第 2 部分)

在上一个技能中,我们使用 netsh.exe 来确定 Wi-Fi 信号强度。由于 netsh.exe 返回的是原始格式的文本,因此需要大量的文本运算符技巧来提取实际的信号强度。通过调用结构化的 API 和对象成员直接访问信息始终是更优的方法。

坦白地说,没有内置的 PowerShell 方法可以以面向对象和结构化的方式访问 Wi-Fi 信息。但是,借助 PowerShell Gallery 中的免费 PowerShell 模块,您可以检索大量有用的 Wi-Fi 信息:

1
Install-Module -Name Get-WLANs -Scope CurrentUser -Force

该模块基本上随附访问任何最新 Windows 操作系统中存在的内置 Windows “Managed Wi-Fi” 框架所需的 C# 代码。

安装该模块后,新命令 Get-WLANs 将返回有关所有可访问的 Wi-Fi 网络的面向对象的信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
PS> Get-WLANs

SSID : internetcafe
BSSID : 38:96:ED:0E:31:AD
RSSI : -63
QUALITY : 81
FREQ : 5180
CHANNEL : 36
PHY : VHT
CAPABILITY : 0x1511
IESIZE : 393

SSID : guests
BSSID : 3E:96:ED:0E:31:AD
RSSI : -69
QUALITY : 70
FREQ : 5180
CHANNEL : 36
PHY : VHT
CAPABILITY : 0x1511
IESIZE : 286
(...)

这也包括有关信号强度的信息(在 Quality 属性中)。

PowerShell 技能连载 - 检测 Wi-Fi 信号强度(第 1 部分)

如果您已连接到无线网络,则以下这行代码可提供当前信号强度:

1
2
PS> @(netsh wlan show interfaces) -match '^\s+Signal' -replace '^\s+Signal\s+:\s+',''
80%

信号强度来自 netsh.exe 提供的文本输出。将其包含在 @() 中可确保它始终返回一个数组。然后,-match 运算符使用正则表达式来标识在行首包含单词 “Signal” 的行。后面的 -replace 运算符使用正则表达式删除信号强度之前的文本。

如果您不喜欢正则表达式,则可以使用其他方法来实现,例如:

1
2
PS> (@(netsh wlan show interfaces).Trim() -like 'Signal*').Split(':')[-1].Trim()
80%

这段代码中,将首先修剪 netsh.exe 返回的每一行(删除两端的空白)。接下来,经典的 -like 运算符选择以 “Signal” 开头的行。然后用 “:” 分隔该行,并使用最后一部分(索引为 -1)。再次清除信号强度两端的空白。

如果两个命令均未返回任何内容,请检查您的计算机是否已连接到无线网络。您可能还希望仅使用 netsh.exe 命令的参数来运行它,而没有任何文本运算符来查看命令的原始输出。

PowerShell 技能连载 - Linux 如何保护安全字符串(实际未保护)

当您在非 Windows 系统上的 PowerShell 中将对象序列化为 XML 时,即通过使用 Export-CliXml,所有 SecureString 数据仅被编码,而不被加密。这是根本的区别,也是安全代码一旦移植到非 Windows 操作系统后可能变得不安全的原因。

例如,如果序列化凭据,则结果仅在 Windows 操作系统上是安全的:

1
Get-Credential | Export-Clixml -Path $env:temp\secret.xml

在 XML 文件中,您可以看到 Export-CliXml 如何更改 SecureString 的表示形式并将其转换为十六进制值的列表。在 Windows 上,SecureString 数据已安全加密,并且只有加密数据的人员(并且正在使用该计算机时)才能读取。在 Linux 和 macOS 上并非如此。由于此处缺少 Windows 加密API,因此仅对 SecureString 进行编码而不是加密。

要在非 Windows 操作系统上解码序列化 XML 内容中的纯文本内容,请尝试如下代码:

1
2
3
$secret = '53007500700065007200530065006300720065007400' $bytes = $secret -split '(?<=\G.{2})(?=.)' |ForEach-Object {
[Convert]::ToByte($_, 16)}
$plain = [Text.Encoding]::Unicode.GetString($bytes)$plain

它采用编码字符串,将其分成两对,将十六进制值转换为十进制值,并通过相应的 Unicode 解码器运行结果字节。结果就是原始的密文。

简而言之,请记住,仅在 Windows 操作系统上对序列化的 SecureString 进行安全加密。在 Linux 和 macOS 上,将敏感数据发送到 Export-CliXml 不会对其进行保护。

PowerShell 技能连载 - 快速初始化多个PowerShell控制台

假设您是许多领域的管理员,例如,Azure、SharePoint、SQL、Microsoft 365。对于每种环境,您可能需要运行一些先决条件,登录某些系统并运行一些命令,直到您的 PowerShell 环境准备好为止。

这是一个简单的策略,可以帮助您自动启动不同的PowerShell控制台。在新文件夹中,放置一个具有以下内容的脚本:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# safely serialize a credential
$credPath = "$PSScriptRoot\secret.xml"
$exists = Test-Path -Path $credPath
if (!$exists)
{
$cred = Get-Credential
$cred | Export-Clixml -Path $credPath
}

# define your different environments
$action = @{
'Azure' = "$PSScriptRoot\azure.ps1"
'Teams' = "$PSScriptRoot\teams.ps1"
'Office' = "$PSScriptRoot\office.ps1"
}

# run a new PowerShell for each environment, and run the
# associated "spin-up" script:
$action.Keys | ForEach-Object {
$path = $action[$_]
Start-Process -FilePath powershell -ArgumentList "-noexit -noprofile -executionpolicy bypass -file ""$path"""
}

该示例启动了三个 PowerShell 控制台,并为每个控制台运行一个单独的启动脚本。通过将脚本 azure.ps1,teams.ps1 和 office.ps1 添加到您的文件夹,您现在可以定义初始化和准备每个控制台所需的代码。您还可以通过从任何其他脚本中读取主凭据来使用公共凭据。这是一个例子:

1
2
3
4
# set the console title bar
$host.UI.RawUI.WindowTitle = 'Administering Office'
# read the common credential from file
$cred = Import-Clixml -Path "$PSScriptRoot\secret.xml"

PowerShell 技能连载 - 识别组成员身份

如果您的脚本需要知道当前用户是否是给定组的成员,那么最快且耗费最少资源的方法是使用如下代码:

1
2
3
4
5
6
$token = [System.Security.Principal.WindowsIdentity]::GetCurrent()
$xy = 'S-1-5-64-36'
if ($token.Groups -contains $xy)
{
"You're in this group."
}

当当前用户是由 SID S-1-5-64-36 标识的组的直接或间接成员时,该示例将执行代码(将该 SID 替换为所需的组的 SID)。

这段代码访问用户已经存在并且始终有权访问的访问令牌。无需额外耗时进行 AD 查询,并且嵌套组成员身份也没有问题。访问令牌具有当前用户所属的直接和间接组的完整列表。

所有组均按 SID 列出,这很有意义。再次解析 SID 名称既耗时又毫无意义。如果您只想知道用户是否是组的成员,则只需找出该组的 SID(即使用 Get-AdGroup),然后在上述方法中使用此 SID 即可。