PowerShell 技能连载 - 留意副作用
PowerShell 可以使用许多底层的系统函数。例如这个,可以创建一个临时文件名:
[System.IO.Path]::GetTempFileName()
然而,它不仅只做这一件事。它还真实地创建了那个文件。所以如果您使用这个函数来创建临时文件名,您可能最终会在文件系统中创建一堆孤立的文件。请在您的确需要创建一个临时文件的时候才使用它。
PowerShell 可以使用许多底层的系统函数。例如这个,可以创建一个临时文件名:
[System.IO.Path]::GetTempFileName()
然而,它不仅只做这一件事。它还真实地创建了那个文件。所以如果您使用这个函数来创建临时文件名,您可能最终会在文件系统中创建一堆孤立的文件。请在您的确需要创建一个临时文件的时候才使用它。
假设在一个文件夹中有一大堆脚本(或照片、日志等任意文件),并且您想要重命名所有的文件。比如新文件名的格式为固定前缀 + 自增的编号。
以下是实现方法。
这个例子将重命名指定文件夹中所有扩展名为 .ps1 的 PowerShell 脚本。新文件名为 powershellscriptX.ps1,其中“X”为自增的数字。
请注意脚本禁止了真正的重命名操作。如果要真正地重命名文件,请移除 -WhatIf
参数,但必须非常小心!如果您敲错一个变量或使用了错误的文件夹路径,那么您的脚本将会十分开心地重命名成千上万个错误的文件。
$Path = 'c:\temp'
$Filter = '*.ps1'
$Prefix = 'powershellscript'
$Counter = 1
Get-ChildItem -Path $Path -Filter $Filter -Recurse |
Rename-Item -NewName {
$extension = [System.IO.Path]::GetExtension($_.Name)
'{0}{1}.{2}' -f $Prefix, $script:Counter, $extension
$script:Counter++
} -WhatIf
如果您想重新整理您的照片库,以下这段代码能帮您从照片文件中读取拍摄日期信息。
这个例子使用了一个系统函数来查找“我的照片”的路径,然后递归搜索它的子文件夹。输出的结果通过管道传递给 Get-DataTaken
,该函数返回照片的文件名、文件夹名,以及照片的拍摄时间。
function Get-DateTaken
{
param
(
[Parameter(ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true)]
[Alias('FullName')]
[String]
$Path
)
begin
{
$shell = New-Object -COMObject Shell.Application
}
process
{
$returnvalue = 1 | Select-Object -Property Name, DateTaken, Folder
$returnvalue.Name = Split-Path $path -Leaf
$returnvalue.Folder = Split-Path $path
$shellfolder = $shell.Namespace($returnvalue.Folder)
$shellfile = $shellfolder.ParseName($returnvalue.Name)
$returnvalue.DateTaken = $shellfolder.GetDetailsOf($shellfile, 12)
$returnvalue
}
}
$picturePath = [System.Environment]::GetFolderPath('MyPictures')
Get-ChildItem -Path $picturePath -Recurse -ErrorAction SilentlyContinue |
Get-DateTaken
大多数软件都会在注册表中登记自己。以下是一段从能从本地和远程的 32 位及 64 位注册表中读取已安装的软件列表的代码。它还是一个演示如何读取远程注册表的不错的例子。
# NOTE: RemoteRegistry Service needs to run on a target system!
$Hive = 'LocalMachine'
# you can specify as many keys as you want as long as they are all in the same hive
$Key = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall', 'SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall'
# you can specify as many value names as you want
$Value = 'DisplayName', 'DisplayVersion', 'UninstallString'
# you can specify a remote computer name as long as the RemoteRegistry service runs on the target machine,
# you have admin permissions on the target, and the firewall does not block you. Default is the local machine:
$ComputerName = $env:COMPUTERNAME
# add the value "RegPath" which will contain the actual Registry path the value came from (since you can specify more than one key)
$Value = @($Value) + 'RegPath'
# now for each regkey you specified...
$Key | ForEach-Object {
# ...open the hive on the appropriate machine
$RegHive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Hive, $ComputerName)
# ...open the key in that hive...
$RegKey = $RegHive.OpenSubKey($_)
# ...find names of all subkeys...
$RegKey.GetSubKeyNames() | ForEach-Object {
# ...open subkeys...
$SubKey = $RegKey.OpenSubKey($_)
# ...and read all the requested values from each subkey
# ...to store them, use Select-Object to create a simple new object
$returnValue = 1 | Select-Object -Property $Value
$Value | ForEach-Object {
$returnValue.$_ = $subkey.GetValue($_)
}
# ...add the current regkey path name
$returnValue.RegPath = $SubKey.Name
# return the values:
$returnValue
# close the subkey
$SubKey.Close()
}
# close the regkey
$RegKey.Close()
# close the hive
$RegHive.Close()
} | Out-GridView
您可以用这样的一段脚本远程执行 gpupdate.exe
:
function Start-GPUpdate
{
param
(
[String[]]
$ComputerName
)
$code = {
$rv = 1 | Select-Object -Property ComputerName, ExitCode
$null = gpupdate.exe /force
$rv.Exitcode = $LASTEXITCODE
$rv.ComputerName = $env:COMPUTERNAME
$rv
}
Invoke-Command -ScriptBlock $code -ComputerName $ComputerName |
Select-Object -Property ComputerName, ExitCode
}
Start-GPUpdate
接受一个或多个计算机名,然后对每台计算机运行 gpupdate.exe
,并返回执行结果。
这段脚本利用了 PowerShell 远程管理技术,所以它需要目标计算机启用了 PowerShell 远程管理,并且您需要这些机器的本地管理员权限。
您是否疑惑过一个数据库的连接字符串到底长什么样?当您从控制面板中创建一个数据源时,一个向导将指引您完成整个创建过程。以下是一个利用这个向导并获取生成的连接字符串的方法。
请注意该向导的选择要依赖于您机器上所安装的数据库驱动。
function Get-ConnectionString
{
$Path = Join-Path -Path $env:TEMP -ChildPath 'dummy.udl'
$null = New-Item -Path $Path -ItemType File -Force
$CommandArg = """$env:CommonProgramFiles\System\OLE DB\oledb32.dll"",OpenDSLFile " + $Path
Start-Process -FilePath Rundll32.exe -Argument $CommandArg -Wait
$ConnectionString = Get-Content -Path $Path | Select-Object -Last 1
$ConnectionString | clip.exe
Write-Warning 'Connection String is also available from clipboard'
$ConnectionString
}
当您调用 Get-ConnectionString
方法时,将会创建一个临时的 udl 文件,并且用控制面板向导打开它。您可以通过向导完成配置。配置完成之后,PowerShell 将会检测临时文件并且返回连接字符串。
它的工作原理是 Get-Process
函数带了 -Wait
参数,它能够挂起脚本的执行,直到向导退出。在向导退出以后,脚本就可以安全地访问 udl 文件了。
Splatting 是向 cmdlet 传递多个参数的好方法。以下例子演示了如何封装 WMI 调用,并且使它们支持不同的名称:
function Get-BIOSInfo
{
param
(
$ComputerName,
$Credential,
$SomethingElse
)
$null = $PSBoundParameters.Remove('SomethingElse')
Get-WmiObject -Class Win32_BIOS @PSBoundParameters
}
Get-BIOSInfo
通过 WMI 获取 BIOS 信息,并且它支持本地、远程以及通过证书的远程调用。这是因为用户向 Get-BIOSInfo
传递的实参实际上传递给了 Get-WmiObject
对应的参数。所以当一个用户没有传递 -Credential
参数,那么就不会向 Get-WmiObject
传递 -Credential
参数。
Splatting 技术通常使用一个自定义的哈希表,它的每个键代表一个形参,每个值代表一个实参。在这个例子中,使用了一个预定义的 $PSBoundParameters
哈希表。它事先插入了要传递给函数的参数。
请确保不要传给目标 cmdlet 它不知道的参数。举个例子,Get-BIOSInfo
函数定义了一个“SomethingElse”参数。而 Get-WmiObject
没有这个参数,所以您在 splat 之前,您必须先调用 Remove()
方法从哈希表中把这个键移掉。
Splatting 是 PowerShell 3.0 引入的概念,但是许多用户还没有听说这个概念。这是一种以可编程的方式将参数传给 cmdlet 的技术。请看:
$infos = @{}
$infos.Path = 'c:\Windows'
$infos.Recurse = $true
$infos.Filter = '*.log'
$infos.ErrorAction = 'SilentlyContinue'
$infos.Remove('Recurse')
dir @infos
这个例子定义了一个包含键值对的哈希表。每个键对应 dir 命令中的一个参数,并且每个值作为实参传递给对应的形参。
当您的代码需要决定哪些参数需要传给 cmdlet 时,Splatting 十分有用。您的代码可以只需要维护一个哈希表,然后选择性地将它传给 cmdlet。
如果您需要了解您的用户账户所在的 Active Directory 组,通常需要查询 Active Directory,并且还需要查找嵌套的组成员身份。
以下是一种快速获取您所在的组(包括嵌套的以及本地组)成员身份的方法。这段脚本查看您的存取令牌(它管理了您的各种权限)然后从您的令牌中读取所有 SID 并将 SID 转换为真实名称。
请注意您只能对当前用户使用这种技术。它很适合用作登录脚本,用来做一些基于组成员身份的操作。
[System.Security.Principal.WindowsIdentity]::GetCurrent().Groups.Value |
ForEach-Object {
$sid = $_
$objSID = New-Object System.Security.Principal.SecurityIdentifier($sid)
$objUser = $objSID.Translate( [System.Security.Principal.NTAccount])
$objUser.Value
}
设置注册表项的权限并不是一件小事。不过通过一些技巧,并不是一件大事。
首先,运行 REGEDIT
并创建一个测试项。然后,右击该项并且使用图形界面设置您想要的权限。
然后,运行这段脚本(请将 -Path
值设为您刚才定义的注册表项):
$path = 'HKCU:\software\prototype'
$sd = Get-Acl -Path $Path
$sd.Sddl | clip
这段代码将从您的注册表项中读取安全信息并将它复制到剪贴板中。
接下来,使用这段脚本为新创建的或已有的注册表项应用相同的安全设置。只需要将这段脚本中的 SDDL 定义替换成您刚创建的值:
# replace the content of this variable with the SDDL you just created
$sddl = 'O:BAG:S-1-5-21-1908806615-3936657230-2684137421-1001D:PAI(A;CI;KR;;;BA)(A;CI;KA;;;S-1-5-21-1907506615-3936657230-2684137421-1001)'
$Path = 'HKCU:\software\newkey'
$null = New-Item -Path $Path -ErrorAction SilentlyContinue
$sd = Get-Acl -Path $Path
$sd.SetSecurityDescriptorSddlForm($sddl)
Set-Acl -Path $Path -AclObject $sd
您可能需要以完整 Administrator 权限来运行这段脚本。如您所见,第一段脚本和您的测试注册表项只是用来生成 SDDL 文本。当您得到 SSDL 文本之后,您只需要将它粘贴入第二段脚本中。第二段脚本不再需要用到那个测试注册表项。