使得命令可重复执行

PowerShell脚本背后的理念,首先是使得重复执行特定命令变得简单,而无须每次手动重复输入命令。我们希望该示例有合适的复杂度,所以从WMI开始,随后添加一些筛选条件、排序规则以及其他内容。

下面是要执行的命令:

  1. Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName localhost -Filter "drivetype=3" |
  2. Sort-Object -Property DeviceID |
  3. Format-Table -Property DeviceID,
  4. @{label='FreeSpace(MB)'; e={$_.FreeSpace / 1MB -as [int]}},
  5. @{label='Size(GB)'; expression={$_.Size / 1GB -as [int]}},
  6. @{name='%Free'; expre={$_.FreeSpace / $_.Size * 100 -as [int]}}

image.png
通过单击在工具栏的绿色运行按钮(快捷键F5)运行命令,对命令进行测试,输出结果显示命令正常工作。也可以选中命令的一部分并按F8键,从而只运行选中部分的命令
image.png

使用变量优化脚本

目前脚本仍然处于测试阶段,可暂时将计算机名称变量设置为静态值。
image.png
说明:
1)添加了一个变量$computername,将其值设置为localhost❶。

  • 大多数PowerShell命令,使用名称为-computerName的参数接受计算机名称。此处我们保留这种传统。

2)将-computerName参数值替换为定义的变量❸。
3)在-computerName参数和其值后面添加了反撇号❷。

  • 这是转义符号,该符号用于告诉PowerShell下一个物理行是前面命令的一部分。
  • 当行以管道操作符或逗号结尾时无须使用转义符号,这里需要在管道操作符之前分隔行,因此只能在行末尾使用反撇号。

    创建一个带参数的脚本

    上例中,-computerName参数可能是随时变化的部分。因此,可以将被赋予常量的$computername变量转变为一个输入参数。 ```powershell param(

    此处定义参数块

    $computername = ‘localhost’ )

Get-CimInstance -ClassName Win32LogicalDisk -ComputerName $computername -Filter “drivetype=3” | Sort-Object -Property DeviceID | Format-Table -Property DeviceID, @{label=’FreeSpace(MB)’; e={$.FreeSpace / 1MB -as [int]}}, @{label=’Size(GB)’; expression={$.Size / 1GB -as [int]}}, @{name=’%Free’; expre={$.FreeSpace / $_.Size * 100 -as [int]}}

只需要在变量声明代码附近添加一个`param()`块,这会将$computerName定义为一个参数,并在未对该参数赋值时指定localhost作为默认值。所有以这种方式定义的参数是命名参数,也是**位置参数**。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/635565/1655540934047-23843cc4-d049-45f4-a337-5d9127738424.png#clientId=ua8708a82-9af6-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=82&id=ufbe387cb&margin=%5Bobject%20Object%5D&name=image.png&originHeight=102&originWidth=631&originalType=binary&ratio=1&rotation=0&showTitle=false&size=19344&status=done&style=none&taskId=u14b1889e-0c27-4dcf-9b53-49e2825867d&title=&width=504.8)<br />在第一个实例中,以位置参数的形式调用该脚本,只提供参数值而不指定参数名称。在第2、3个实例中,指定参数名称,但在第3个实例中,将参数名称简化为符合PowerShell的参数名称简化规则的形式。注意,在上面3个示例中,都需要为脚本指定路径(.\,也就是当前目录),这是**由于Shell并不会搜索当前目录去找到脚本**。

**可以通过逗号作为分隔符,指定任意数量的参数**。<br />![image.png](https://cdn.nlark.com/yuque/0/2022/png/635565/1655541054252-5093054c-2444-4158-ad8f-a5fb7622a66b.png#clientId=ua8708a82-9af6-4&crop=0&crop=0&crop=1&crop=1&from=paste&height=181&id=ucda0efb7&margin=%5Bobject%20Object%5D&name=image.png&originHeight=274&originWidth=1002&originalType=binary&ratio=1&rotation=0&showTitle=false&size=39306&status=done&style=none&taskId=u7d772bd2-5da6-40b8-96f9-6d06aa12e38&title=&width=661)

**范例:**假如还希望将过滤条件设置为参数。当前脚本仅获取类型为3的驱动器,即硬盘。可以将该值变为参数。
```powershell
param(
    # 此处定义参数块
    $computername = 'localhost',
    $drivetype = 3
)

Get-CimInstance -ClassName Win32_LogicalDisk -ComputerName $computername `
-Filter "drivetype=$drivetype" |
Sort-Object -Property DeviceID |
Format-Table -Property DeviceID,
    @{label='FreeSpace(MB)'; e={$_.FreeSpace / 1MB -as [int]}},
    @{label='Size(GB)'; expression={$_.Size / 1GB -as [int]}},
    @{name='%Free'; expre={$_.FreeSpace / $_.Size * 100 -as [int]}}

此处利用了PowerShell在双引号中的文本,可以自动将变量替换为变量值的功能。

可以以最开始的3种方式运行该脚本:

PS D:\1.Work\1.脚本\0.powershell> .\Get-DiskInventory.ps1 printer 3
PS D:\1.Work\1.脚本\0.powershell> .\Get-DiskInventory.ps1 -computername printer 3
PS D:\1.Work\1.脚本\0.powershell> .\Get-DiskInventory.ps1 -computername printer -drive 3
PS D:\1.Work\1.脚本\0.powershell> .\Get-DiskInventory.ps1 -drive 3
DeviceID FreeSpace(MB) Size(GB) %Free
-------- ------------- -------- -----
C:              448535      477    92
D:             1767129     1863    93


PS D:\1.Work\1.脚本\0.powershell> .\Get-DiskInventory.ps1 -computername printer, vpn-nps
DeviceID FreeSpace(MB) Size(GB) %Free
-------- ------------- -------- -----
C:               91672      120    75
C:              448534      477    92
D:              346805      346    98
D:             1767129     1863    93

为脚本添加帮助文档

PowerShell提供了简单的方式为脚本添加帮助,也就是通过注释。通过使用特殊的注释语法,你可以提供模仿PowerShell本身帮助文件的帮助信息。

<#
.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-CimInstance -ClassName Win32_LogicalDisk -ComputerName $computername `
-Filter "drivetype=$drivetype" |
Sort-Object -Property DeviceID |
Format-Table -Property DeviceID,
    @{label='FreeSpace(MB)'; e={$_.FreeSpace / 1MB -as [int]}},
    @{label='Size(GB)'; expression={$_.Size / 1GB -as [int]}},
    @{name='%Free'; expre={$_.FreeSpace / $_.Size * 100 -as [int]}}

正常情况下,PowerShell都会忽略以#开头的代码行,意味着#用于标识某一行是注释。此处使用<# #>块注释语法,来注释多行。
这些特殊的注释被称为基于注释的帮助,必须置于脚本文件的开始部分。除了使用的.DESCRIPTION.SYNOPSIS关键字之外,还有一些关键字。在PowerShell中,可以运行help about_comment_based_help来查看完整的列表。

查看帮助信息

接下来,就可以通过运行Help .\Get-DiskInventory命令获取帮助信息。甚至还可以运行help .\Get-DiskInventory –full获取完整的帮助,其中包括了参数信息和示例。
image.png
image.png

一个脚本,一个管道

当在Shell中分别运行命令时,PowerShell会为每一个命令创建一个新的管道。在每一个管道末尾,PowerShell会查看哪一列需要被格式化并创建一个你可以看到的表格。这里的重点是“不同命令运行在不同管道中”。
下图阐述了这一点:两个完全分开的命令,两个独立的管道,两个格式化进程,两个不同界面的结果集。
image.png
下面是分别运行这两个命令经历的步骤:
1)运行Get-Process;
2)该命令将Process对象放入管道;
3)管道以Out-Default结束,该命令会接收对象;
4)Out-Default将对象传递给Out-Host,该命令会调用格式化系统产生文本输出结果。
5)文本输出结果显示在屏幕上。
6)运行Get-Service;
7)该命令将Service对象放入管道;
8)管道以Out-Default结束,该命令会接收对象。
9)Out-Default将对象传递给Out-Host,该命令会调用格式化系统产生文本输出结果。
10)文本输出结果显示在屏幕上。

在PowerShell中,所有的命令都在一个管道中执行,在脚本中也是同样。在脚本中,任何产生管道输出结果的命令都会被写入同一个管道中:脚本自身运行的管道。
image.png
将上述两个命令放入脚本文件,相关步骤如下:
1)脚本运行Get-Process;
2)该命令将Process对象放入管道;
3)脚本运行Get-Service;
4)该命令将Service对象放入管道;
5)管道以Out-Default结束,该命令会接收上面两类对象。
6)Out-Default将对象传递给Out-Host,该命令会调用格式化系统产生文本输出结果。
7)由于Process对象首先被放入管道,Shell的格式化系统会为Process对象选择合适的格式化方式。

  • 这也是为什么Process对象的输出结果看起来很正常。当Shell碰到Service对象后,它会生成一个全新的表,所以会最终生成一个列表。

8)屏幕显示文本输出结果。

两种不同的输出是由于将两种类别的对象放入一个管道中。这是将命令存入脚本和手动执行之间的重要区别:在脚本中,只能够使用一个管道。正常来讲,你的脚本应该努力保持只输出一类对象,以便PowerShell能产生合理的文本输出格式。

初探作用域

作用域(scope)是特定类型PowerShell元素的容器,这些元素主要是别名、变量和函数
Shell本身具有最高级的作用域,称为全局域(global scope)。函数还有其特有的私有作用域(private scope)。

  • 当运行一个脚本时,会在脚本范围内创建一个新的作用域,也就是所谓的脚本作用域(script scope)。
  • 脚本作用域是全局作用域的子集,也就是全局作用域的子作用域(child)。
  • 全局作用域是脚本作用域的父作用域(parent)。

作用域之间的关系:
image.png
作用域的生命周期,只持续到作用域所需执行的最后一行代码之前。这意味着全局作用域只有在PowerShell运行时有效,脚本作用域只在脚本运行时有效,以此类推。一旦停止运行,作用域和其包含的内容同时消失。

PowerShell对于别名、变量和函数之类的元素有着非常详细的规则。主要规则如下:

  • 如果尝试访问一个作用域元素,PowerShell在当前作用域内查找;
  • 如果不存在于当前作用域,会查找其父作用域,依此类推,直到找到树形关系的顶端——全局作用域。

实战步骤:
1)打开一个新的PowerShell ISE窗口。
2)在ISE中,创建一个包含一行命令的脚本,该命令为Write $x
3)将脚本保存到c:\scope.ps1
4)在一个标准的PowerShell窗口,运行脚本.\scope.ps1,此时没有任何输出结果。

  • 当脚本运行时,会自动为其创建一个新的作用域。
  • 而$x变量在该作用域内并不存在,因此PowerShell转向其父作用域,即全局作用域检查变量$x是否存在。
  • 该变量在父作用域也不存在,因此PowerShell认为$x为空,并打印出空作为输出结果。

5)在一个标准的PowerShell窗口,运行$x=6,然后再次运行.\scope.ps1

  • 这次,你会看到输出结果为6。虽然变量$x在脚本范围内未定义,但PowerShell可以在全局作用域内找到该变量,因此脚本可以使用全局作用域内的值。

6)在ISE中,在脚本的write命令之前添加$x=10,并保存脚本。
7)在标准的PowerShell窗口中,再次运行.\scope.ps1

  • 这次,你会看到输出结果为10。这是由于$x在脚本作用域内定义,因此Shell无须查看全局作用域。
  • 在Shell中运行$x,输出结果为6,这意味着在脚本作用域内的变量值,不会影响全局作用域内的变量值。

image.png

总结

当在作用域内定义一个变量、别名或函数时,当前作用域就无法访问父作用域内的任何同名变量、别名或函数。
例如,如果将New-Alias Dir Get-Service命令放入一个脚本,那么在当前脚本中,别名Dir总是运行Get-Service而不是Get-ChildItem(实际上,Shell不允许这么做,这是由于其需要保护内置别名不会重新被定义)。
通过在脚本作用域内定义别名,可以防止Shell去父作用域查找标准和默认的Dir。当然,对于Dir别名的重定义只能持续到脚本执行结束之前,而全局作用域默认的Dir将不受影响。

为了避免这种混淆,若在脚本中访问一个变量时,请确保已经在同一个作用域内对其赋值。在Param()块内的参数可以实现这一点,还有很多其他方式可以将值或对象赋予一个变量。

如果想深入研究作用域,请查阅About_Scope帮助文档中的详细内容。