PowerShell实战指南 Chapter 18-22
万人都要将火熄灭 我一人独将此火高高举起 此火为大 开花落英于神圣的祖国 和所有以梦为马的诗人一样 我借此火得度一生的茫茫黑夜
Chapter 18 变量:一个存放资料的地方
变量名可以包含空格,但是名字必须被花括号包住。
注意单双引号的区别:
双引号中的变量名会被解析,但是只是发生在形成字符串时。之后即使$comp
改变,也不影响$phrase
。
另外,反引号会把$
取消转义,即不会解析变量。反引号相当于C语言中的\
,所以
如上相当于\n
。
通过help about_escape
了解更多。
单一变量可以存储多个对象,用逗号隔开:
$computers = 'Servcer-R2','Server-R1','localhost'
如上图,对元素的访问倒是类似于Python。
修改元素内容:
$computers[1] = $computers[1].replace('SERVER', 'CLIENT')
如何对多个对象都调用某方法?
$computers = $computers | ForEach-Object {$_.ToLower()}
在v3及后续版本中,可以直接对包含多个对象的单一变量进行属性或方法的访问:
如果在字符串中要解析某个元素,则需要使用双引号和$()
(即子表达式):
有时我们需要指定变量类型,否则如下图:
应该如下:
练习
完成以下操作:
- 创建后台作业,从两台计算机中查询
Win32_BIOS
信息 - 作业运行完毕后,将结果存入变量
- 展示变量内容
- 把内容导出到CliXML
Chapter 19 输入和输出
PowerShell的运作方式为
Read-Host
注意提示信息的最后被自动加了冒号,另外,输入的信息被放入了管道(与后面的Write-Host
区别)。
如果希望能够提供一个GUI让用户来输入,则需要直接调用.Net
框架:
先载入组件:
[void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')
再使用:
$computername = [Microsoft.VisualBasic.Interaction]::InputBox('Enter a computer name', 'Computer name', 'localhost')
效果如下:
Write-Host
Write-Host
的工作原理如下:
它会绕过管道,直接显示。所以,它还可以控制显示的颜色:
另外还有Write-Verbose
/Write-Debug
/Write-Warning
/Write-Error
,但是前两者默认不输出,需要修改变量如下:
$VerbosePreference = "Continue"
$DebugPreference = "Continue"
测试如下:
注意,Write-Error
会把信息写入错误流。
Write-Output
与Write-Host
相反,它把对象直接发送给管道。
也就是说,你可以在其后加入其他命令,如:
Write-Output "Hello" | Where-Object {$_.Length -GT 10}
另外,还有一个Write-Process
用于显示进度条。
Chapter 20 轻松实现远程控制
可重用会话
$ad_computer = New-PSSession -ComputerName WIN-F8E9GPVN2N1 -Credential "rambo\administrator"
查看并使用会话:
不过比较优雅的方式是这样:
Get-PSSession -ComputerName WIN-F8E9GPVN2N1 | Enter-PSSession
注意,会话会消耗计算机资源。如果不用可以关闭会话:
Get-PSSession | Remove-PSSession
# or
$ad_computer | Remove-PSSession
会话变量的优势体现在一次性处理多个会话的场景:
Invoke-Command -Command {Get-WmiObject -Class Win32_Process} -Session $sessions
隐式远程控制
它的适用场景是:你需要一些管理模块对远程计算机进行管理。远程计算机上有这些模块,你的本地系统没有且不支持这些模块的安装(如XP或Vista),那么可以通过从远程会话导入命令的手段来达到“在本地添加管理命令”的目的(事实上并未添加,用的还是远程的)。
Invoke-Command -Command {Import-Module ActiveDirectory} -Session $ad_computer
Import-PSSession -Session $ad_computer -Module ActiveDirectory -prefix rem
此时我们查看当前(关闭Shell或远程连接后就不存在了)拥有的命令:
这些命令在远程计算机上运行,然后把结果(反序列化对象)返回给本地计算机,就好像你直接在远程计算机上操作一样。
断开会话
在v3及以后,你需要显式断开会话。且,断开的会话需要你自己去清理。
Disconnect-PSSession -Id 4
# re-connect
Get-PSSession -ComputerName Computer2 | Connect-PSSession
在WSMan:\localhost\Shell
下有管理已断开会话的设置项:
练习
- 在Shell中关闭所有已打开连接
Get-PSSession | Remove-PSSession
- 建立一个到远程计算机的会话存入变量,并利用
Invoke-Command
与Get-PSSession
命令从远程计算机上获取最近20条安全事件日志条目 - 将
ServerManager
模块的命令由远程计算机导入本地计算机,并使用rem
作为名词部分前缀
- 运行刚刚导入的
Get-WindowsFeature
命令
- 关闭会话
Remove-PSSession -Session $ad_computer
Chapter 21 你把这叫做脚本
Get-WmiObject -Class Win32_LogicalDisk -ComputerName localhost -Filter "drivetype=3" |
Sort-Object -Property DeviceID |
Format-Table -Property DeviceID,
@{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
@{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
@{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}
如上,在ISE中每一行都可以以逗号或管道操作符结尾。在脚本中最好指定参数名称,方便以后查看。
如下,一个好用的debug技巧是进行部分运行,选中要运行的命令并按F8
或单击上方的“部分运行”按钮(所以在写脚本的时候把不同命令分行写,方便debug):
推荐按照“动词-名词”这样的格式保存脚本,如上为Get-DiskInventory.ps1
。
硬编码往往是需要避免的。另外,可以用`符号把参数每行一个分开:
$computername = 'localhost'
Get-WmiObject -Class Win32_LogicalDisk `
-ComputerName $computername `
-Filter "drivetype=3" |
Sort-Object -Property DeviceID |
Format-Table -Property DeviceID,
@{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
@{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
@{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}
带参数运行
param(
$computername = 'localhost',
$drivetype = 3
)
Get-WmiObject -Class Win32_LogicalDisk `
-ComputerName $computername `
-Filter "drivetype=$drivetype" |
Sort-Object -Property DeviceID |
Format-Table -Property DeviceID,
@{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
@{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
@{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}
如上。param()
中的是包含默认值的命命、位置参数:
添加文档
<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses WMI to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -computername SERVER-R2 -drivetype 3
#>
param(
$computername = 'localhost',
$drivetype = 3
)
Get-WmiObject -Class Win32_LogicalDisk `
-ComputerName $computername `
-Filter "drivetype=$drivetype" |
Sort-Object -Property DeviceID |
Format-Table -Property DeviceID,
@{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
@{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
@{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}
如上添加文档,这样别人可以用help
查看:
可以通过help about_comment_based_help
查看更多。
脚本与管道
首先来看一个小实验:
我们在Shell中依次执行Get-Process
和Get-Service
,得到的为格式化过的结果:
上面的详细处理过程其实如下:
但如果在脚本中像这样连续执行,结果不同:
可以发现,进程的展示结果与之前相同,而服务的则不同。这是因为一个脚本中的所有命令共用一个管道:脚本自身运行的管道。其处理过程如下:
由于Process
对象先被放入管道,所以其输出结果很正常。因此,一般来说最好在一个脚本中尽量保持输出对象属于同一类。
作用域
作用域是特定类型PowerShell元素的容器,如别名、变量和函数。Shell本身具有最高级的作用域,成为global scope
,运行脚本时,会在脚本范围创建一个新的script scope
,其为全局作用域的子集(父子关系)。函数有自己的private scope
。
作用域的生命周期持续到作用域的最后一行代码。当你访问域元素时,PowerShell的查找顺序如下:
当前域 -(if not found)-> 父作用域 -(if not found)-> ... -(if not found)-> 全局域
Chapter 22 优化可传参脚本
以上一章的示例脚本为起点进行优化:
<#
.SYNOPSIS
Get-DiskInventory retrieves logical disk information from one or
more computers.
.DESCRIPTION
Get-DiskInventory uses WMI to retrieve the Win32_LogicalDisk
instances from one or more computers. It displays each disk's
drive letter, free space, total size, and percentage of free
space.
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.PARAMETER drivetype
The drive type to query. See Win32_LogicalDisk documentation
for values. 3 is a fixed disk, and is the default.
.EXAMPLE
Get-DiskInventory -computername SERVER-R2 -drivetype 3
#>
param(
$computername = 'localhost',
$drivetype = 3
)
Get-WmiObject -Class Win32_LogicalDisk `
-ComputerName $computername `
-Filter "drivetype=$drivetype" |
Sort-Object -Property DeviceID |
Select-Object -Property DeviceID,
@{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
@{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
@{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}
注意,Select-Object
替换了Format-Table
,这样一来用户可以自己决定他需要什么输出,比如CSV:
.\Get-DiskInventory.ps1 | Export-Csv disks.csv
添加高级功能
在注释后、参数前添加
<#
...
#>
[CmdletBinding()]
param(
...
)
这样一来,我们就启用了几个功能,如下:
- 将参数定义为强制参数
param(
[Parameter(Mandatory=$True, HelpMessage="Enter a computer name to query")]
[string]$computername = 'localhost',
[int]$drivetype = 3
)
如上,如果用户没有给出参数,则PowerShell会提示他输入:
注意,[Parameter(Mandatory=$True)]
只是computername
参数的修饰符,不影响drivetype
。如果你需要提示用户输入drivetype
,则需要在前面再加一行。
- 添加参数别名
param(
[Parameter(Mandatory=$True, HelpMessage="Enter a computer name to query")]
[Alias('hostname')]
[string]$computername = 'localhost',
[int]$drivetype = 3
)
- 验证输入的参数
param(
[Parameter(Mandatory=$True, HelpMessage="Enter a computer name to query")]
[Alias('hostname')]
[string]$computername = 'localhost',
[ValidateSet(2,3)]
[int]$drivetype = 3
)
参看help about_functions_advanced_parameters
获取更多信息。
- 添加详细输出
# ...
param(
[Parameter(Mandatory=$True, HelpMessage="Enter a computer name to query")]
[Alias('hostname')]
[string]$computername = 'localhost',
[ValidateSet(2,3)]
[int]$drivetype = 3
)
Write-Verbose "Connecting to $computername"
Write-Verbose "Looking for drive type $drivetype"
Get-WmiObject -Class Win32_LogicalDisk `
-ComputerName $computername `
-Filter "drivetype=$drivetype" |
Sort-Object -Property DeviceID |
Select-Object -Property DeviceID,
@{label='FreeSpace(MB)';expression={$_.FreeSpace / 1MB -as [int]}},
@{label='Size(GB';expression={$_.Size / 1GB -as [int]}},
@{label='%Free';expression={$_.FreeSpace / $_.Size * 100 -as [int]}}
Write-Verbose "Finished running command"
注意,[CmdletBinding()]
会激活脚本中所有命令的详细输出(同时,你不必修改$VerbosePreference
变量,参见实战指南 Chapter 19 输入和输出)。
练习
将
Get-WmiObject Win32_networkadapter -ComputerName localhost |
where {$_.PhysicalAdapter} |
Select-Object MACAddress,AdapterType,DeviceID,Name,Speed
改成高级脚本。结果如下:
<#
.SYNOPSIS
Get-PhysicalAdapters.ps1 returns physical adapters on the specified computer
.PARAMETER computername
The computer name, or names, to query. Default: Localhost.
.EXAMPLE
Get-PhysicalAdapters.ps1 -computername localhost
#>
[CmdletBinding()]
param(
[Parameter(Mandatory=$True, HelpMessage="Enter a computer name to query")]
[Alias('hostname')]
[string]$computername
)
Write-Verbose "[*] wait a moment..."
Get-WmiObject Win32_networkadapter -ComputerName $computername |
where {$_.PhysicalAdapter} |
Select-Object MACAddress,AdapterType,DeviceID,Name,Speed
Write-Verbose "$computername done."
总结
学习PowerShell至此大概有一个月,我从这本书中学到的除了技术,还有别的东西:不急不躁的学习态度,步步为营、稳扎稳打、不贪心的学习进程。每天学一点点,就好像是吃点心一般快乐。我以前有过很多次贪心的经历:学习,只是为了证明自己学过这本书,有时根本不去理解作者在书中的思路,也没有耐心去解决那一个个巧妙设置的问题,只是为了赶快学完,让自己的水平有所提高。然而这样其实只是求得心理安慰:我研究过多少本书,我有多厉害,而事实上收获甚少。那样每天都给自己灌很多知识,而不是技术、方法或者思想,最终只能是消化不良。那样每天灌输的过程必然也是充满煎熬的,完全不似吃点心。当然,当年学习Linux和汇编的时候每天的进度也很快,但是却不觉得煎熬和难受。那是因为当时的自己是真的急切地渴望学习,因为当时自己真的为它们着迷。一旦进入那种状态,很自然地就会废寝忘食地学习。我认为,那种学习过程和这种每天一点点的过程都是可取的,主要还是看自己对于眼前学习的东西是否有那种痴迷的感觉。
总之,不要急。博学而笃志,切问而近思。但行好事,莫问前程。
静水流深吧。