PowerShell实战指南 Chapter 18-22

万人都要将火熄灭 我一人独将此火高高举起 此火为大 开花落英于神圣的祖国 和所有以梦为马的诗人一样 我借此火得度一生的茫茫黑夜

Chapter 18 变量:一个存放资料的地方

变量名可以包含空格,但是名字必须被花括号包住。

注意单双引号的区别:

Screen Shot 2018-06-05 at 12.38.13 PM.png

双引号中的变量名会被解析,但是只是发生在形成字符串时。之后即使$comp改变,也不影响$phrase

另外,反引号会把$取消转义,即不会解析变量。反引号相当于C语言中的\,所以

Screen Shot 2018-06-05 at 12.41.59 PM.png

如上相当于\n

通过help about_escape了解更多。

单一变量可以存储多个对象,用逗号隔开:

$computers = 'Servcer-R2','Server-R1','localhost'

Screen Shot 2018-06-05 at 12.49.10 PM.png

如上图,对元素的访问倒是类似于Python。

修改元素内容:

$computers[1] = $computers[1].replace('SERVER', 'CLIENT')

如何对多个对象都调用某方法?

$computers = $computers | ForEach-Object {$_.ToLower()}

Screen Shot 2018-06-05 at 12.53.10 PM.png

在v3及后续版本中,可以直接对包含多个对象的单一变量进行属性或方法的访问:

Screen Shot 2018-06-05 at 12.55.15 PM.png

如果在字符串中要解析某个元素,则需要使用双引号和$()(即子表达式):

Screen Shot 2018-06-05 at 12.58.09 PM.png

有时我们需要指定变量类型,否则如下图:

Screen Shot 2018-06-05 at 1.00.18 PM.png

应该如下:

Screen Shot 2018-06-05 at 1.01.21 PM.png

练习

完成以下操作:

Screen Shot 2018-06-05 at 1.01.21 PM.png

Screen Shot 2018-06-05 at 1.07.31 PM.png

Chapter 19 输入和输出

PowerShell的运作方式为

Screen Shot 2018-06-06 at 3.01.47 PM.png

Read-Host

Screen Shot 2018-06-06 at 2.53.14 PM.png

注意提示信息的最后被自动加了冒号,另外,输入的信息被放入了管道(与后面的Write-Host区别)。

如果希望能够提供一个GUI让用户来输入,则需要直接调用.Net框架:

先载入组件:

[void][System.Reflection.Assembly]::LoadWithPartialName('Microsoft.VisualBasic')

再使用:

$computername = [Microsoft.VisualBasic.Interaction]::InputBox('Enter a computer name', 'Computer name', 'localhost')

效果如下:

Screen Shot 2018-06-06 at 2.58.20 PM.png

Write-Host

Write-Host的工作原理如下:

Screen Shot 2018-06-06 at 3.02.11 PM.png

它会绕过管道,直接显示。所以,它还可以控制显示的颜色:

Screen Shot 2018-06-06 at 3.03.34 PM.png

另外还有Write-Verbose/Write-Debug/Write-Warning/Write-Error,但是前两者默认不输出,需要修改变量如下:

$VerbosePreference = "Continue"
$DebugPreference = "Continue"

测试如下:

Screen Shot 2018-06-06 at 3.15.19 PM.png

注意,Write-Error会把信息写入错误流。

Write-Output

Write-Host相反,它把对象直接发送给管道。

Screen Shot 2018-06-06 at 3.12.09 PM.png

也就是说,你可以在其后加入其他命令,如:

Write-Output "Hello" | Where-Object {$_.Length -GT 10}

另外,还有一个Write-Process用于显示进度条。

Chapter 20 轻松实现远程控制

可重用会话

$ad_computer = New-PSSession -ComputerName WIN-F8E9GPVN2N1 -Credential "rambo\administrator"

Screen Shot 2018-06-07 at 4.07.08 PM.png

查看并使用会话:

Screen Shot 2018-06-07 at 4.09.31 PM.png

不过比较优雅的方式是这样:

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

Screen Shot 2018-06-07 at 4.28.30 PM.png

此时我们查看当前(关闭Shell或远程连接后就不存在了)拥有的命令:

Screen Shot 2018-06-07 at 4.33.28 PM.png

这些命令在远程计算机上运行,然后把结果(反序列化对象)返回给本地计算机,就好像你直接在远程计算机上操作一样。

断开会话

在v3及以后,你需要显式断开会话。且,断开的会话需要你自己去清理。

Disconnect-PSSession -Id 4
# re-connect
Get-PSSession -ComputerName Computer2 | Connect-PSSession

WSMan:\localhost\Shell下有管理已断开会话的设置项:

Screen Shot 2018-06-07 at 4.58.30 PM.png

练习

Get-PSSession | Remove-PSSession

Screen Shot 2018-06-07 at 5.06.30 PM.png

Screen Shot 2018-06-07 at 5.06.57 PM.png

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]}}

Screen Shot 2018-06-09 at 3.46.02 PM.png

如上,在ISE中每一行都可以以逗号或管道操作符结尾。在脚本中最好指定参数名称,方便以后查看。

如下,一个好用的debug技巧是进行部分运行,选中要运行的命令并按F8或单击上方的“部分运行”按钮(所以在写脚本的时候把不同命令分行写,方便debug):

Screen Shot 2018-06-09 at 3.49.04 PM.png

推荐按照“动词-名词”这样的格式保存脚本,如上为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()中的是包含默认值的命命、位置参数:

Screen Shot 2018-06-09 at 4.08.13 PM.png

Screen Shot 2018-06-09 at 4.08.00 PM.png

添加文档

<#
.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查看:

Screen Shot 2018-06-09 at 4.13.44 PM.png

可以通过help about_comment_based_help查看更多。

脚本与管道

首先来看一个小实验:

我们在Shell中依次执行Get-ProcessGet-Service,得到的为格式化过的结果:

Screen Shot 2018-06-09 at 8.14.02 PM.png

Screen Shot 2018-06-09 at 8.14.13 PM.png

上面的详细处理过程其实如下:

Screen Shot 2018-06-09 at 8.18.10 PM.png

但如果在脚本中像这样连续执行,结果不同:

Screen Shot 2018-06-09 at 8.15.44 PM.png

Screen Shot 2018-06-09 at 8.15.55 PM.png

可以发现,进程的展示结果与之前相同,而服务的则不同。这是因为一个脚本中的所有命令共用一个管道:脚本自身运行的管道。其处理过程如下:

Screen Shot 2018-06-09 at 8.18.17 PM.png

由于Process对象先被放入管道,所以其输出结果很正常。因此,一般来说最好在一个脚本中尽量保持输出对象属于同一类。

作用域

作用域是特定类型PowerShell元素的容器,如别名、变量和函数。Shell本身具有最高级的作用域,成为global scope,运行脚本时,会在脚本范围创建一个新的script scope,其为全局作用域的子集(父子关系)。函数有自己的private scope

Screen Shot 2018-06-09 at 8.22.51 PM.png

作用域的生命周期持续到作用域的最后一行代码。当你访问域元素时,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会提示他输入:

Screen Shot 2018-06-10 at 3.10.35 PM.png

注意,[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
)

Screen Shot 2018-06-10 at 3.16.23 PM.png

参看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"

Screen Shot 2018-06-10 at 3.20.56 PM.png

注意,[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."

Screen Shot 2018-06-10 at 3.30.58 PM.png

总结

学习PowerShell至此大概有一个月,我从这本书中学到的除了技术,还有别的东西:不急不躁的学习态度,步步为营、稳扎稳打、不贪心的学习进程。每天学一点点,就好像是吃点心一般快乐。我以前有过很多次贪心的经历:学习,只是为了证明自己学过这本书,有时根本不去理解作者在书中的思路,也没有耐心去解决那一个个巧妙设置的问题,只是为了赶快学完,让自己的水平有所提高。然而这样其实只是求得心理安慰:我研究过多少本书,我有多厉害,而事实上收获甚少。那样每天都给自己灌很多知识,而不是技术、方法或者思想,最终只能是消化不良。那样每天灌输的过程必然也是充满煎熬的,完全不似吃点心。当然,当年学习Linux和汇编的时候每天的进度也很快,但是却不觉得煎熬和难受。那是因为当时的自己是真的急切地渴望学习,因为当时自己真的为它们着迷。一旦进入那种状态,很自然地就会废寝忘食地学习。我认为,那种学习过程和这种每天一点点的过程都是可取的,主要还是看自己对于眼前学习的东西是否有那种痴迷的感觉。

总之,不要急。博学而笃志,切问而近思。但行好事,莫问前程。

静水流深吧。