PowerShell 技能连载 - 在计划任务中运行 PowerShell 脚本
如果您需要以固定的频率运行一段 PowerShell 脚本,何不以计划任务的方式运行它呢?以下是一段帮您新建一个每天上午 6 点执行一个 PowerShell 脚本的计划任务的代码:
1 | #requires -Modules ScheduledTasks |
如果您需要以固定的频率运行一段 PowerShell 脚本,何不以计划任务的方式运行它呢?以下是一段帮您新建一个每天上午 6 点执行一个 PowerShell 脚本的计划任务的代码:
1 | #requires -Modules ScheduledTasks |
以下是一种快速查看 PowerShell 函数源码的方法:
1 | ${function:Clear-Host} | clip |
这将会把 Clear-Host
的源代码复制到剪贴板中,并且当您粘贴它时,您可以看到 Clear-Host
是如何工作的:
1 | $RawUI = $Host.UI.RawUI |
通常可以从这里学到很多东西。如果您想用非空格的字符填充 PowerShell 控制台,例如绿底白字的 ‘X’,请试试这段代码:
1 | $host.UI.RawUI.SetBufferContents( |
请注意这只能在真正的 PowerShell 控制台宿主中起作用。
在 PowerShell 控制台中,您仍然可以用 more 管道,就像在 cmd.exe 中一样一页一页查看结果。然而,more 不支持实时管道,所以所有数据需要首先收集好。这将占用很长时间和内存:
1 | dir c:\windows -Recurse -ea 0 | more |
一个更好的方法是使用 PowerShell 自带的分页功能:
1 | dir c:\windows -Recurse -ea 0 | Out-Host -Paging |
请注意这些都需要一个真正的控制台窗口,而在图形界面的宿主中不能工作。
以下几行代码能在远程服务器上创建一个 SMB 共享:
1 | #requires -Version 3.0 -Modules CimCmdlets, SmbShare -RunAsAdministrator |
您可以在客户端将该共享映射为一个网络驱动器。请注意这个网络共享是单用户的,所以如果您使用 Administrator 账户做了映射,那么无法在 Windows Explorer 中存取。
1 | $computername = 'Server12' |
以下是一个重要的 PowerShell 变量的列表:$pshome
表示 PowerShell 所在的位置。$home
是个人用户配置文件夹的路径。$PSVersionTable
返回 PowerShell 的版本和重要的子组件的版本:
1 | PS> $pshome |
$profile
是您个人的自启动脚本所在的位置。每当您当前的 PowerShell 宿主启动时,自启动脚本就会自动加载(假设文件存在)。$profile.CurrentUserAllHosts
是任何宿主都会加载的配置文件脚本。并且 $env:PSModulePath
列出 PowerShell 可以自动发现的存放 PowerShell module 的文件夹:
1 | PS> $profile |
使用 Read-Host 向用户提示输入信息可能会造成问题,因为它影响了脚本的自动化运行。一个更好的方法是将 Read-Host
包装在 param()
代码块中。通过这种方式,该信息可以通过无人值守操作的参数传入,也可以通过交互式提示传入:
1 | param |
当您运行以上脚本时,它像 Read-Host
一模一样地显示提示信息。您也可以通过参数执行该脚本:
1 | PS> C:\myscript.ps1 –Name test –Id 12 |
If you do not need custom prompting, you can go even simpler, and declare parameters as mandatory by adding [Parameter(Mandatory)] above each parameter variable.
如果您不需要自定义提示信息,您还可以更加简单,只需要在每个参数变量上加上 [Parameter(Mandatory)]
使它们变为必需参数。
如果您的客户端没有和您的域控制器正常地同步时间,请使用以下代码。这段代码需要管理员特权:
1 | w32tm.exe /resync /force |
PowerShell 对 XML 的支持非常酷。这篇文章整理了所有日常的 XML 任务,甚至非常复杂的任务,都可以轻松搞定。
我们从一个简单的例子开始,以免把脑子搞乱。
我们先创建一个 XML 文档,然后加入数据集合,改变一些信息,增加新数据,删除数据,最后将修改好的版本保存为一个格式化过的 XML 文件。
从头开始创建新的 XML 文档是一件很乏味的事。许多写脚本的人干脆以纯文本的方式来创建 XML 文件。虽然这样也可以,但是容易出错。这样很有可能拼写错误导致问题,而且您会发现自己身处一个和 XML 很不友好的世界里。
其实不然,只要用 XMLTextWriter
对象就可以创建 XML 文档了。这个对象屏蔽了处理原生 XML 对象模型的细节,并且帮您将信息写入 XML 文件。
开始之前,我们用下面的代码创建一个 TMD 复杂的 XML 文档来玩。这段代码的目的是创建一个包含所有典型内容的 XML 文档,包括节点、属性、数据区、注释。
# 这是文档存储的路径:
$Path = "$env:temp\inventory.xml"
# 新建一个 XMLTextWriter 来创建 XML:
$XmlWriter = New-Object System.XMl.XmlTextWriter($Path,$Null)
# 设置排版参数:
$xmlWriter.Formatting = 'Indented'
$xmlWriter.Indentation = 1
$XmlWriter.IndentChar = "`t"
# 写入头部:
$xmlWriter.WriteStartDocument()
# 声明 XSL
$xmlWriter.WriteProcessingInstruction("xml-stylesheet", "type='text/xsl' href='style.xsl'")
# 创建根对象“machines”,并且添加一些属性
$XmlWriter.WriteComment('List of machines')
$xmlWriter.WriteStartElement('Machines')
$XmlWriter.WriteAttributeString('current', $true)
$XmlWriter.WriteAttributeString('manager', 'Tobias')
# 加入一些随机的节点
for($x=1; $x -le 10; $x++)
{
$server = 'Server{0:0000}' -f $x
$ip = '{0}.{1}.{2}.{3}' -f (0..256 | Get-Random -Count 4)
$guid = [System.GUID]::NewGuid().ToString()
# 每个数据集的名字都是“machine”,并且增加一个随机的属性:
$XmlWriter.WriteComment("$x. machine details")
$xmlWriter.WriteStartElement('Machine')
$XmlWriter.WriteAttributeString('test', (Get-Random))
# 增加三条信息:
$xmlWriter.WriteElementString('Name',$server)
$xmlWriter.WriteElementString('IP',$ip)
$xmlWriter.WriteElementString('GUID',$guid)
# 增加一个含有属性和正文的节点:
$XmlWriter.WriteStartElement('Information')
$XmlWriter.WriteAttributeString('info1', 'some info')
$XmlWriter.WriteAttributeString('info2', 'more info')
$XmlWriter.WriteRaw('RawContent')
$xmlWriter.WriteEndElement()
# 增加一个含有 CDATA 段的节点:
$XmlWriter.WriteStartElement('CodeSegment')
$XmlWriter.WriteAttributeString('info3', 'another attribute')
$XmlWriter.WriteCData('this is untouched code and can contain special characters /\@<>')
$xmlWriter.WriteEndElement()
# 关闭“machine”节点:
$xmlWriter.WriteEndElement()
}
# 关闭“machines”节点:
$xmlWriter.WriteEndElement()
# 完成整个文档:
$xmlWriter.WriteEndDocument()
$xmlWriter.Flush()
$xmlWriter.Close()
notepad $path
这段脚本用随机数据生成了一个虚拟的服务器进货单。结果自动用记事本,看起来如下:
<?xml version="1.0"?>
<?xml-stylesheet type='text/xsl' href='style.xsl'?>
<!--List of machines-->
<Machines current="True" manager="Tobias">
<!--1. machine details-->
<Machine test="578163632">
<Name>Server0001</Name>
<IP>31.248.95.170</IP>
<GUID>51cb0dfb-75ed-4967-8392-47d87596c73c</GUID>
<Information info1="some info" info2="more info">RawContent</Information>
<CodeSegment info3="another attribute"><![CDATA[this is untouched code and can contain special characters /\@<>]]></CodeSegment>
</Machine>
<!--2. machine details-->
<Machine test="124214010">
<Name>Server0002</Name>
<IP>33.60.233.89</IP>
<GUID>9618b8bc-c200-46ce-b423-ee030555242d</GUID>
<Information info1="some info" info2="more info">RawContent</Information>
<CodeSegment info3="another attribute"><![CDATA[this is untouched code and can contain special characters /\@<>]]></CodeSegment>
</Machine>
(...)
</Machines>
这个 XML 文档有两个目的:一是提供了一个创建 XML 文件的代码模板二是为接下来的练习提供一个基础数据。
我们假想这个 XML 文件有重要的信息在里面,接下来要用各种方法来操作这个规范的 XML 文件。
注意:XMLTextWriter
做了很多智能的事情,不过你需要确保你创建的内容没问题。比如说一个常见的问题是节点名称非法。节点名称不得含有空格。
所以写成 CodeSegment
是正确的,而 Code Segment
是错误的。XML 将会试着将节点命名为 Code
,然后增加一个名为 Segment
的属性,最后因为没有为属性设置值而出错。
一个常见的任务是从 XML 文件中提取信息。我们假设您需要获得一个机器和 IP 地址的列表。加入您已经生成了上述的 XML 文件,那么以下是创建一个报告要做的所有事情:
# 这是 XML 例子文件的存储路径:
$Path = "$env:temp\inventory.xml"
# 将它加载入 XML 对象:
$xml = New-Object -TypeName XML
$xml.Load($Path)
# 注意:如果 XML 格式是非法的,这里会报异常
# 一定要注意节点名不能包含空格
# 只需要在节点中自由遍历,就能 select 获得您要的信息:
$Xml.Machines.Machine | Select-Object -Property Name, IP
结果大概是这样:
Name IP
---- --
Server0001 31.248.95.170
Server0002 33.60.233.89
Server0003 226.6.1.30
Server0004 139.30.8.110
Server0005 94.104.253.8
Server0006 202.80.178.61
Server0007 22.217.227.159
Server0008 253.72.25.212
Server0009 233.147.116.60
Server0010 41.173.220.129
注意:有些人奇怪我为什么不直接用 XML 对象。这段是你们常见的代码:
# 这是 XML 例子文件的存储路径:
$Path = "$env:temp\inventory.xml"
# 将它读入一个 XML 对象:
[XML]$xml = Get-Content $Path
原因是性能问题。通过 Get-Content
将 XML 以纯文本文件的方式读入,第二步再将它转换为 XML 是一个非常消耗性能的过程。虽然我们的 XML 文件不算大,后面的方式也要比第一种方式消耗多大约 7 倍的时间,而且随着 XML 文件的增大,性能差异会更明显。
所以建议在任何读取 XML 文件的时候,都先创建 XML 对象,并调用它的 Load()
方法。这个方法能够智能地接受 URL,所以您也可以用 RSS feed 的 URL 地址(假设有现成的 Internet 连接,并且不需要设置代理服务器)。
假设您不想要整个服务器列表,而只是想要列表中某台服务器的 IP 地址以及 info1 属性。你可以用类似这样的代码:
$Xml.Machines.Machine |
Where-Object { $_.Name -eq 'Server0009' } |
Select-Object -Property IP, {$_.Information.info1}
这段代码将获取 server0009 的 IP 地址以及 info1 属性。您也可以不用在客户端过滤所有的元素,可以用 XPath(一种 XML 查询语言)来做:
$item = Select-XML -Xml $xml -XPath '//Machine[Name="Server0009"]'
$item.Node | Select-Object -Property IP, {$_.Information.Info1}
这段 XPath 查询语句 //Machine[Name="Server0009"]
在所有的 Machine 节点中查找含有 Name 子节点,且它的值为 _Server009_。
强调: XPath 是大小写敏感的,所以如果节点名称是 _Machine_,那么您不能用 machine 来查询。
另外说一句,在这两种写法里,您都会用到脚本块来读写属性,这是因为 “info1” 属性是 “Information” 子节点的一部分。在这类场景中,您可以用哈希表来更好地呈现它的名字:
$info1 = @{Name='AdditionalInfo'; Expression={$_.Information.Info1}}
$item = Select-XML -Xml $xml -XPath '//Machine[Name="Server0009"]'
$item.Node | Select-Object -Property IP, $info1
结果看起来如下:
IP AdditionalInfo
-- --------------
97.196.140.12 some info
XPath 是非常强大的 XML 查询语言。您在网上到处都可以找到它的语法介绍(比如这些链接:http://www.w3schools.com/xpath/ 和 http://go.microsoft.com/fwlink/?LinkId=143609)。当您阅读这些文档的时候,您会发现 XPath 可以使用一些称为“用户定义函数”的东西,比如 last()
和 lowercase()
。这里不支持这些函数。
您常常需要更改 XML 文档的内容。与其手工解析 XML 文档,不如用上刚刚学到的技术。
假设您想修改 Server0006 并且为它赋值一个新的名称和一个新的 IP 地址,需要做以下事情:
$item = Select-XML -Xml $xml -XPath '//Machine[Name="Server0006"]'
$item.node.Name = "NewServer0006"
$item.node.IP = "10.10.10.12"
$item.node.Information.Info1 = 'new attribute info'
$NewPath = "$env:temp\inventory2.xml"
$xml.Save($NewPath)
notepad $NewPath
如您所见,修改信息十分简单,所有做出的改变会自动反映到相应的 XML 对象中。您所需要做的只是将修改后的 XML 对象保存到文件,将修改的地方持久化起来。结果将显示在记事本中,看起来类似这样:
<!--6. machine details-->
<Machine test="559669990">
<Name>NewServer0006</Name>
<IP>10.10.10.12</IP>
<GUID>cca8df99-78e1-48e0-8c4d-193c6d4acbd2</GUID>
<Information info1="new attribute info" info2="more info">RawContent</Information>
<CodeSegment info3="another attribute"><![CDATA[this is untouched code and can contain special characters /\@<>]]></CodeSegment>
</Machine>
您不用任何解析工作就瞬间完成了对已有 XML 文档的修改,并且不会破坏 XML 的结构。
用同样的方式,您可以进行大量的修改。假设所有的服务器都要赋予一个新的名称。旧名称是 _“ServerXXXX”_,新名称是 _“Prod_ServerXXXX”_。以下是解决方法:
Foreach ($item in (Select-XML -Xml $xml -XPath '//Machine'))
{
$item.node.Name = 'Prod_' + $item.node.Name
}
$NewPath = "$env:temp\inventory2.xml"
$xml.Save($NewPath)
notepad $NewPath
请注意 XML 文档中的所有服务器名称都更新了。Select-XML
这回不仅返回一个对象,而是返回多个,每个对象都是一个服务器。这是因为这回的 XPath 选择所有的“Machine”节点,并没有做特别的过滤。所以 foreach 循环里对所有节点都进行了操作。
在循环内部,Name 节点被赋予了一个新的值,当所有“Machine”节点都更新完以后,XML 文档被保存到文件并用记事本打开。
您可以能对这个例子有意见,为服务器名添加_“Prod_”_前缀,这点小改动太弱智了。不过我们在这里主要是向您介绍如何改变 XML 数据,而不是关注怎么做字符串操作。
不过,如果您坚持想知道如何实现例如将 “ServerXXXX” 替换成 _“PCXX”_(包括将四位数字转换为二位数字,这不算一个弱智的需求了吧),以下是解决方法:
foreach($item in (Select-XML -Xml $xml -XPath '//Machine'))
{
if ($item.node.Name -match 'Server(\d{4})')
{
$item.node.Name = 'PC{0:00}' -f [Int]$matches[1]
}
}
$NewPath = "$env:temp\inventory2.xml"
$xml.Save($NewPath)
notepad $NewPath
这次,我们用正则表达式以数字块的方式提取原先服务器的名称,然后用 -f
操作符重新格式化数字,并加上新的前缀。
我们这篇文章不关注正则表达式,也不关注数字的格式化。重要的是您能理解可以用任何技术来构造新的服务器名称。在剩下的部分,我们也遵循这一原则。
有些时候,改变数据还不够。您可能会需要向列表添加新的服务器。这也是十分简单的。您只需要选择一个已有的节点,把它克隆一份,然后更新它的内容,再将它附加到父节点上即可。通过这种方式,您无须自己创建复杂的节点结构,并且可以确保新节点的结构和已有的节点完全一致。
这段代码将向服务器列表添加一台新的机器:
# 克隆已有的节点结构
$item = Select-XML -Xml $xml -XPath '//Machine[1]'
$newnode = $item.Node.CloneNode($true)
# 根据需要更新信息
# 所有其它信息都和原始节点中的一致
$newnode.Name = 'NewServer'
$newnode.IP = '1.2.3.4'
# 获取您希望新节点所附加到的父节点:
$machines = Select-XML -Xml $xml -XPath '//Machines'
$machines.Node.AppendChild($newnode)
$NewPath = "$env:temp\inventory2.xml"
$xml.Save($NewPath)
notepad $NewPath
由于您新加的节点是从已有的节点克隆的,所以旧节点的所有信息都拷贝到了新的节点。您不想更新的信息可以保持原有的值。
那么如何向列表的顶部插入新的节点呢?只需要用 InsertBefore()
代替 AppendChild()
:
# 向列表的顶部添加节点:
$machines.Node.InsertBefore($newnode, $item.node)
类似地,您可以在任意处插入新的节点。以下代码将在 Server0007 之后插入:
# 在“Server0007”之后插入:
$parent = Select-XML -Xml $xml -XPath '//Machine[Name="Server0007"]'
$machines.Node.InsertAfter($newnode, $parent.node)
从 XML 文件中删除数据也同样很简单。如果您想从列表中删掉 _Server0007_,以下是实现方法:
# 删除“Server0007”:
$item = Select-XML -Xml $xml -XPath '//Machine[Name="Server0007"]'
$null = $item.Node.ParentNode.RemoveChild($item.node)
通过以上展现的例子,您可以通过几行代码实现常见的 XML 操作需求。值得投入一些时间来提高 XML 和 XPath 的熟练度,这样您可以通过它们实现令人惊叹的功能。
对于一路读到这儿的朋友,我为你们准备了一点小礼物:一个我常用的很棒的小工具,我相信应该对你们也十分有用。
_ConvertTo-XML
可以将所有的对象转换为 XML,并且由于 XML 是一个分层的数据格式,所以通过控制深度,能够很好地展现嵌套的对象属性。所以您可以“展开”一个对象的结构并且查看它的所有属性,甚至递归地查看嵌套的属性。
不用 XML 和 XPath 的话,您只能查看原始的 XML 并且靠自己查找信息。例如,如果您想查看 PowerShell 的颜色信息到底存储在 $host 对象的什么地方,您可以这么做(也许不是一个好方法,因为您可能会被原始的 XML 信息淹没):
$host | ConvertTo-XML -Depth 5 | Select-Object -ExpandProperty outerXML
通过刚才演示的知识,您现在可以读取原始的 XML ,然后解析并过滤对象的属性。
以下是称为 Get-ObjectProperty
的辅助函数,有点类似 Get-Member
的意思。它能告诉您对象中的哪个属性存放了您想要的值。让我们来看看:
PS> $host | Get-ObjectProperty -Depth 2 -Name *color*
Name Value Path Type
---- ----- ---- ----
TokenColors $obj1.PrivateData.To... Microsoft.PowerShel...
ConsoleTokenColors $obj1.PrivateData.Co... Microsoft.PowerShel...
XmlTokenColors $obj1.PrivateData.Xm... Microsoft.PowerShel...
ErrorForegroundColor #FFFF0000 $obj1.PrivateData.Er... System.Windows.Medi...
ErrorBackgroundColor #FFFFFFFF $obj1.PrivateData.Er... System.Windows.Medi...
WarningForegroundColor #FFFF8C00 $obj1.PrivateData.Wa... System.Windows.Medi...
WarningBackgroundColor #00FFFFFF $obj1.PrivateData.Wa... System.Windows.Medi...
VerboseForegroundColor #FF00FFFF $obj1.PrivateData.Ve... System.Windows.Medi...
VerboseBackgroundColor #00FFFFFF $obj1.PrivateData.Ve... System.Windows.Medi...
DebugForegroundColor #FF00FFFF $obj1.PrivateData.De... System.Windows.Medi...
DebugBackgroundColor #00FFFFFF $obj1.PrivateData.De... System.Windows.Medi...
ConsolePaneBackgroun... #FF012456 $obj1.PrivateData.Co... System.Windows.Medi...
ConsolePaneTextBackg... #FF012456 $obj1.PrivateData.Co... System.Windows.Medi...
ConsolePaneForegroun... #FFF5F5F5 $obj1.PrivateData.Co... System.Windows.Medi...
ScriptPaneBackground... #FFFFFFFF $obj1.PrivateData.Sc... System.Windows.Medi...
ScriptPaneForeground... #FF000000 $obj1.PrivateData.Sc... System.Windows.Medi...
这将返回 $host 中所有名字包含 “Color” 的嵌套属性。控制台输出很可能被截断,所以您最好将结果输出到 grid view 窗口:
$host | Get-ObjectProperty -Depth 2 -Name *color* | Out-GridView
请注意 “Path” 列:这个属性精确指示了您如何存取一个指定的嵌套属性。在这个例子里,Get-ObjectProperty
在对象层次中遍历两层。如果指定更深的便利层次,将会展开更多的信息,不过也会导致结果中含有更多的垃圾信息。
虽然您可以通过管道输入多个对象,但是最好一次只导入一个,以免产生大量的结果数据。这行代码将列出进程对象所有嵌套的属性,递归层次为 5,将产生大量的结果:
PS> Get-Process -id $pid | Get-ObjectProperty -Depth 5 -IsNumeric
Name Value Path Type
---- ----- ---- ----
Handles 684 $obj1.Handles System.Int32
VM 1010708480 $obj1.VM System.Int32
WS 291446784 $obj1.WS System.Int32
PM 251645952 $obj1.PM System.Int32
NPM 71468 $obj1.NPM System.Int32
CPU 161,0398323 $obj1.CPU System.Double
BasePriority 8 $obj1.BasePriority System.Int32
HandleCount 684 $obj1.HandleCount System.Int32
Id 4560 $obj1.Id System.Int32
Size 264 $obj1.MainModule.Size System.Int32
ModuleMemorySize 270336 $obj1.MainModule.Mod... System.Int32
FileBuildPart 9421 $obj1.MainModule.Fil... System.Int32
FileMajorPart 6 $obj1.MainModule.Fil... System.Int32
FileMinorPart 3 $obj1.MainModule.Fil... System.Int32
ProductBuildPart 9421 $obj1.MainModule.Fil... System.Int32
ProductMajorPart 6 $obj1.MainModule.Fil... System.Int32
ProductMinorPart 3 $obj1.MainModule.Fil... System.Int32
Size 264 $obj1.Modules[0].Size System.Int32
ModuleMemorySize 270336 $obj1.Modules[0].Mod... System.Int32
(...)
这行代码将返回 spooler 服务对象中所有“String”类型的嵌套属性:
PS> Get-Service -Name spooler | Get-ObjectProperty -Type System.String
Name Value Path Type
---- ----- ---- ----
Name spooler $obj1.Name System.String
Name RPCSS $obj1.RequiredServic... System.String
Name DcomLaunch $obj1.RequiredServic... System.String
DisplayName DCOM Server Process ... $obj1.RequiredServic... System.String
MachineName . $obj1.RequiredServic... System.String
ServiceName DcomLaunch $obj1.RequiredServic... System.String
Name RpcEptMapper $obj1.RequiredServic... System.String
DisplayName RPC Endpoint Mapper $obj1.RequiredServic... System.String
(...)
以下是 Get-ObjectProperty
的源代码。它虽然不只是几行代码,但仍然相当短小精悍。
它完全使用了刚才介绍的技术,所以如果您对以上的例子感到满意,您也可以尝试并消化它的代码,或只是把它当做一个工具,而不用关心它对 XML 做的魔法。
Function Get-ObjectProperty
{
param
(
$Name = '*',
$Value = '*',
$Type = '*',
[Switch]$IsNumeric,
[Parameter(Mandatory=$true,ValueFromPipeline=$true)]
[Object[]]$InputObject,
$Depth = 4,
$Prefix = '$obj'
)
Begin
{
$x = 0
Function Get-Property
{
param
(
$Node,
[String[]]$Prefix
)
$Value = @{Name='Value'; Expression={$_.'#text' }}
Select-Xml -Xml $Node -XPath 'Property' | ForEach-Object {$i=0} {
$rv = $_.Node | Select-Object -Property Name, $Value, Path, Type
$isCollection = $rv.Name -eq 'Property'
if ($isCollection)
{
$CollectionItem = "[$i]"
$i++
$rv.Path = (($Prefix) -join '.') + $CollectionItem
}
else
{
$rv.Path = ($Prefix + $rv.Name) -join '.'
}
$rv
if (Select-Xml -Xml $_.Node -XPath 'Property')
{
if ($isCollection)
{
$PrefixNew = $Prefix.Clone()
$PrefixNew[-1] += $CollectionItem
Get-Property -Node $_.Node -Prefix ($PrefixNew )
}
else
{
Get-Property -Node $_.Node -Prefix ($Prefix + $_.Node.Name )
}
}
}
}
}
Process
{
$x++
$InputObject |
ConvertTo-Xml -Depth $Depth |
ForEach-Object { $_.Objects } |
ForEach-Object { Get-Property $_.Object -Prefix $Prefix$x } |
Where-Object { $_.Name -like "$Name" } |
Where-Object { $_.Value -like $Value } |
Where-Object { $_.Type -like $Type } |
Where-Object { $IsNumeric.IsPresent -eq $false -or $_.Value -as [Double] }
}
}
初学者的第一个问题总是“请告诉我,PowerShell 是什么?”
PowerShell 不仅仅是一个新的 shell(外壳)。PowerShell 是一个面向对象的分布式自动化引擎、脚本语言,以及命令行 shell。我不指望一个初学者能完全理解我刚才说的,不过至少,我希望您能理解 PowerShell 天生是面向对象的。如果说“因为 PowerShell 是基于 .NET 框架的,所以它是面向对象的”,这句话并不太准确。实际上,我们说 PowerShell 是面向对象的,是因为它处理的是对象而不是文本。让我们看一个例子。
这是我在 DOS 的批处理脚本中获取一个文件夹(包括子文件夹等)大小的方法:
@echo off
For /F "tokens=*" %%a IN ('"dir /s /-c /a | find "bytes" | find /v "free""') do Set xsummary=%%a
For /f "tokens=1,2 delims=)" %%a in ("%xsummary%") do set xfiles=%%a&set xsize=%%b
Set xsize=%xsize:bytes=%
Set xsize=%xsize: =%
Echo Size is: %xsize% Bytes
您看到这有多痛苦了吗?有多少人能理解这段批处理脚本到底在做什么?
好吧,让我们来看看用 PowerShell 如何实现。
Get-ChildItem –Recurse | Measure-Object -Property Length -Sum
很简单吧?至少,它看上去很清爽。这是因为 PowerShell 能处理对象——那些能自我描述的东西。这些对象拥有各种属性。文件对象有一个 Length
属性是代表文件的大小有多少字节的。所以,我们把文件夹下所有文件的大小加起来,就能得到文件夹的大小。如何你把这段 PowerShell 脚本和刚才的 DOS 批处理脚本做一个比较,可以看出我们用不着处理任何临时变量和解析文本。我们只需要将 Get-ChildItem
cmdlet 的结果通过管道输出到 Measure-Object
,并且将管道中传过来的每个对象的 Length
属性求和即可。
好了,这是一个解释 PowerShell 天生是面向对象的一个小例子。当您开始学习 PowerShell 之后,将可以举出更多类似的例子。
我们再假设一个问题“如何获得某个进程的 CPU 相关性?”,您会怎么实现?
有些人会这么做:
$process = Get-Process -Name notepad
目前这么操作 OK。然后他们接下来在键盘上敲:
$process.<Tab> <Tab> <Tab> … <Tab>
直到找到所需要的属性为止。
虽然这么做也没错,可以获得您要的属性,不过万一你要的属性是 100 个属性中的第 99 个呢?显然不太明智,是吧?现在学习 shell 的使用就很有帮助了。在 shell 中有更好的办法实现这个。
如果您知道精确的属性名:
Get-Process -Name Notepad | Select-Object ProcessorAffinity
或者
$process = Get-Process -Name Notepad
$process.ProcessorAffinity
如果您不知道精确的属性名,不要一直按 Tab
!请用 PowerShell 的自我探索功能。这是接下来我们要讨论的问题。
初学者最重要的事情是了解如何使用以下的 cmdlet:
Get-Command
返回一个 PowerShell 会话中可用的所有命令。据我观察,某些初学者在输入一个不存在的或者错误的 cmdlet 名字以后,一直在纠结为什么不能用。如果我们知道如何探索 PowerShell,我们就可以用 Get-Command
cmdlet 来验证我们想要用的命令是否存在。例如:
PS C:\> Get-Command -Name Get-ComputerName
Get-Command : The term 'Get-ComputerName' is not recognized as the name of a cmdlet, function, script file, or operable program. Check
the spelling of the name, or if a path was included, verify that the path is correct and try again.
At line:1 char:1
+ Get-Command -Name Get-ComputerName
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (Get-ComputerName:String) [Get-Command], CommandNotFoundException
+ FullyQualifiedErrorId : CommandNotFoundException,Microsoft.PowerShell.Commands.GetCommandCommand
见到了吗?您还可以用 Get-Command
针对特定的动词或者名词来获取命令,甚至可以使用通配符来过滤。
Get-Command -Noun Host
Get-Command -Verb Write
Get-Command N*
在前面的段落中,我们学习了如何获得一个对象的某个属性。但是,只有您精确地知道属性名才能用。那么我们如何探索某个对象中有哪些属性和方法可用呢?Get-Member
这时候派上用场了。
Get-Process | Get-Member
以上命令可以获取某个对象类型的所有属性、方法和事件。您可以对它进一步地筛选来找到您感兴趣的成员。
Get-Process | Get-Member -MemberType Method
Get-Process | Get-Member -MemberType Property
Get-Process | Get-Member -MemberType Event
另外一个初学者常见的问题是对内置的帮助系统没什么概念。PowerShell 内置的 cmdlet 都带有如何使用的详细说明。当然,在 PowerShell 3.0 及更高的版本,您首先需要用 Update-Help
cmdlet 来更新帮助内容。当您想了解某个 PowerShell cmdlet 的详细语法时,您就可以使用用 Get-Help
cmdlet。它能告诉您 cmdlet 参数的信息、如何使用每个参数、以及提供一些例子。当不明白怎么使用时,您第一件事就应该是查看本地的帮助系统。
Get-Help Get-Command
Get-Help Get-Member -Detailed
Get-Help Get-Process –Examples
Get-Help Get-Service –Parameter InputObject
我不指望一个初学者能掌握编写脚本和模块的知识。但至少要掌握使用 shell 的技能。您必须亲自动手实践一下内置的 cmdlet。这包括了一些简单的操作,比如说列出服务、进程、文件和文件夹。至少要会用 Get-ChildItem
来递归地搜索文件,才算是接触到了 PowerShell 的表层,才算是一名初学者。
在学习 PowerShell 的过程中,您应该从 shell 开始学起。基本上在 PowerShell 命令行中能运行的一切命令,都能在脚本中运行。所以一开始可以在 shell 中用 PowerShell 语言和内置的 cmdlet 来做一些简单的任务。最终,您将会学习如何编写脚本。这是初学 PowerShell 很重要的一个方向。
某些人对我说,他们只了解一些特定产品专用的 PowerShell cmdlet,并且他们只要用哪些 cmdlet 就够了。那么能告诉我只用特定产品专用的 cmdlet 而不用任何内置的 cmdlet、管道以及 PowerShell 语言要怎么完成一个自动化任务呢?这是不可能做到的。如果有些人认为可能,那么很可能他们根本不了解基础只是或者从来没有用过 shell。
在用 PowerShell 的过程中,如果要有效地使用它,您需要了解什么是管道,以及您可以将多个命令用管道连接起来。我在前面段落的例子中已经用到了管道,只是没有深入讲解它是什么。运行一个简单的命令不是什么难事。不过当您把多个命令用管道连接起来变成一个更大的任务时,您就会意识到管道的强大之处。要完整地讨论管道的知识,需要 50 - 75 页书才能讲完。让我们保持这篇文章简单易懂一些。
我们假想 PowerShell 的管道是一个制造单元的一条组装线。在一个制造单元中,部件从一个站点传递到另一个站点以及输出端,一路装配过来。我们可以在组装线的最末端看到装配完成的产品。通过类似这样的方式,当我们将多个 PowerShell cmdlet 用管道连接起来时,一个命令的输出结果会作为下一个命令的输入参数。例如:
Get-Process -Name s* | Where-Object { $_.HandleCount -lt 100 }
在上述命令中,Get-Process
命令输出的一个或多个对象会作为输入送给 Where-Object
cmdlet。Where-Object
命令过滤出输入对象数组中 HandleCount
属性小于 100 的对象。您当然也可以不用管道来完成这个任务。让我们看看做起来是怎么样。
$process = Get-Process -name s*
foreach ($proc in $process) {
if ($proc.HandleCount -lt 100) {
$proc
}
}
如您所见,要写更多的代码。这还不是大问题,您会看到生成输出时的区别。在这个例子里,许多人认为第一个命令执行完以后,所有的输出结果送入第二个命令。这样描述并不精确。在管道中,第一个命令每生成一个对象,就立即向第二个命令传递。
前一个例子只合并了两个命令,所以看起来很微不足道。让我们看看下一个例子:
Get-ChildItem -Recurse -Force |
Where-Object {$_.Length -gt 10MB} |
Sort-Object -Property Length -Descending |
Select-Object Name, @{name='Size (MB)'; expression={$_.Length/1MB}}, Extension |
Group-Object -Property extension
我不指望一名初学者能理解这段代码,或是写出这样的代码。不过,这段代码显示出管道的强大之处。上述的命令(或者说用管道连接的命令)获取所有大于 10MB 的文件,将它们按文件的大小降序排列,然后将它们按文件的扩展名分组。您敢不敢不用管道将这段代码的功能实现一遍?
PowerShell 往往提供多种方法来实现同一件事情。当然,这些方法各有差别。有效率上的差别,有简单和复杂的差别。
所以,当我提出“请告诉我计算机名”的需求时,我不希望您开始写一段 WMI 查询语句:
Get-WmiObject -Class Win32_ComputerSystem | Select-Object -Property Name
或
Get-WmiObject -Class Win32_OperatingSystem | Select-Object -Property CSName
您可能坚称这些命令确实可以获取本地计算机名。但是,请您理解有更好的方法来实现:
$env:ComputerName
噢,用传统的 hostname
命令也得到了相同的结果。一切正确。我常常使用它。不过,我们现在关注的是 PowerShell,对吧?
这是另一个常见的问题。您也许知道如何运行别人写的脚本。不过,这样能使您成为一个脚本编写者吗?不可能。阅读别人写的脚本确实能帮您理解最佳和最差实践。但是,当您是一个初学者时,这不会有助于您的学习。只有您自己动手编写自己的脚本才能学到知识。
还有,不要轻易说您会编写高级的函数。除非您知道如何描述通用参数、参数类型、cmdletbinding
,以及 Begin
、Process
和 End
代码块是如何工作和为什么需要它们。如果您不了解这些概念,就不要觉得自己写过 PowerShell 的高级函数。高级函数和我们平时在 PowerShell 中写的普通函数是不同的。当您在函数定义中增加了 CmdletBinding()
以后,函数的基本行为就改变了。我们来看一个例子吧?
以下是一个普通函数,接受两个数字输入参数,并且返回它们的和。
function sum {
param (
$number1,
$number2
)
$number1 + $number2
}
PS C:\> sum 10 30
40
现在,用不同个数的参数来调用这个函数。
PS C:\> sum 10 30 40
40
见到了吗?虽然我们在函数定义中只有两个参数,但它也可以接受三个参数,并且只是把第三个参数忽略掉。现在,加入 CmdletBinding()
属性并看看行为发生什么变化。
function sum {
[CmdletBinding()]
param (
$number1,
$number2
)
$number1 + $number2
}
用先前一样的参数再次测试!
PS C:\> sum 10 30
40
PS C:\> sum 10 30 40
sum : A positional parameter cannot be found that accepts argument '40'.
At line:1 char:1
+ sum 10 30 40
+ ~~~~~~~~~~~~
+ CategoryInfo : InvalidArgument: (:) [sum], ParameterBindingException
+ FullyQualifiedErrorId : PositionalParameterNotFound,sum
见到了吗?加入了 CmdletBionding()
属性以后,处理输入参数的基本行为发生了变化。这是一个高级的 PowerShell 函数。但是,这只是开头。我们在这里不再深入下去,我希望您在告诉我写过高级函数之前知道这些。
我听很多人说他们写脚本的时候要依赖 Google。他们只是使用搜索引擎,查找问题现成的解决方案,并且直接拿来用。他们常常是开始编写脚本或者尝试做一个任务几分钟就放弃了。我只有在我彻底没有思路,实在继续不下去的时候才用搜索引擎。或者当我知道某些人已经开发了一个脚本并且我不想重复发明轮子的时候。但是,当我想学习 PowerShell 的时候,我不会用这种方式。搜索引擎是寻找解决方案的最简单方法,但是您从中学不到任何东西,除了怎么用搜索引擎。特别地,当您还是一个初学者时,直接使用现成的脚本对学习没有任何帮助。
祝您好运!
PowerShell ISE 是编写 PowerShell 脚本最重要的环境。熟练掌握 ISE 的快捷键有以下好处:
其实,所有快捷键的定义都在 ISE 的 Microsoft.PowerShell.GPowerShell
(DLL)中。我们首先需要获取这个 DLL 的引用。
1 | PS> $gps = $psISE.GetType().Assembly |
然后我们可以获取这个程序集的资源列表:
1 | PS> $gps.GetManifestResourceNames() |
然后我们创建一个 ResourceManager
对象来存取程序集中的资源。在构造函数中将需要打开的资源名(去掉 .resources 扩展名)以及包含资源的程序集对象传给它。
1 | $rm = New-Object System.Resources.ResourceManager GuiStrings,$gps |
剩下只要调用 GetResourceSet()
方法根据特定的文化信息获取资源。
1 | $rs = $rm.GetResourceSet((Get-Culture),$true,$true) |
查看输出结果,我们可以发现包含“>”的几行类似按键组合信息。如果您仔细查看输出结果,将会发现规律是 Name
以 Shortcut
结尾(有可能包含数字),以及以 F
开头加 1 至 2 位数字并带有 Keyboard
关键字的。通过下面一行代码,我们可以过滤出所有和键盘有关系的项目并对它们进行排序。
1 | $rs | where Name -match 'Shortcut\d?$|^F\d+Keyboard' | Sort-Object Value |
以下是完整的代码片段和完整的结果:
1 | $gps = $psISE.GetType().Assembly |