Powershell:关于hashtable你想知道的一切

译者语:本篇为一篇译文,详细介绍了在powershell中如何使用hashtable这种数据类型.本文为本人2018年最后一篇博文(哈哈,一年内写没写几篇),也是本人的第一次译文,有不足之处还请指教.祝大家元旦快乐!

我在工作中经常用到它,现在想停下来讨论一下它(hashtable).昨天夜里小组会议后我教了一些同事如何使用hashtable,我很快意识到初识hashtable时我也曾经有与他们相同的困惑.Hashtable在powershell里着实非常重要因此我们需要对它有充会的理解.

hashtable是元素的集合

首先,我想让你们把hastable看作传统意义上定义的集合.这将会使你对后面它在高级应用中是如何工作的有一些基本的了解.通常很多困惑都源自于对此没有认知.

什么是数组

在讨论什么是hashtable之前,我想要先提一下数组.就本讨论而言,数组是一系列元素或者对象的集合(或列表)

$array = @(1,2,3,5,7,11)

一旦数组中有了对象,你就可以使用foreach来迭代它们或者索引来访问某元素

foreach($item in $array)
{
    Write-Output $item
}

Write-Output $array[3]

同样,你可以使用索引来更新值

$array[2] = 13

这里我仅仅介绍了数组的一些皮毛,但是把我们下面将要讨论的hashtable引到了正确的话题上.

什么是hashtable

我们首先从一般的,被许多编程语言都采用的技术性描述开始,然后再转到powershell里对它的定义

hashtable是一个类似于数组的数据结构,除了你使用键来存储一个值(或者对象),它是一个基本的键/值对存储.首先,我们来创建一个空的hashtable

$ageList = @{}

注意这里用的是花括号,而上面定义数组用的是小括号.
然后我们使得键来添加一些值:

$key = 'Kevin'
$value = 36
$ageList.add( $key, $value )

$ageList.add( 'Alex', 9 )

以上人名是键,而值是我们想存储的东西.

使用中括号访问元素

一旦你把值添加到hashtable,你可以使用添加时使用的键把值取出来(不像上面数组使得的是数字索引)

$ageList['Kevin']
$ageList['Alex']

当我想要获取Kevin的年龄时,我使用他的名字来获取,我们也可以使用这种方法来添加或者更新hashtable里的值.这和上面使用add方法效果是一样的

$ageList = @{}

$key = 'Kevin'
$value = 36
$ageList[$key] = $value

$ageList['Alex'] = 9

还有一种另外的语法来添加元素,我们后面会讲到,如果你是从其它语言转到powershell你会看到以上使用和你在其它语言的使用方法类似.

创建包含值的hashtable

截至目前我们创建的hashtable初始状态是一个空集合,我们在创建hashtable时也可以预先添加一些键值:

$ageList = @{
    Kevin = 36
    Alex  = 9
}

作为一个查找表(原文为lookup table)

下面是一个例子:

$environments = @{
    Prod = 'SrvProd05'
    QA   = 'SrvQA02'
    Dev  = 'SrvDev12'
}

$server = $environments[$env]

在这个例子中,你可以指定一个环境变量的值,然后就可以获取到正确的服务器(示例中的hashtable值是服务器名),当然你也可以使用switch($env){...}这种方法,不过hashtable一种很好的替换

译者注,这个示例仅仅是伪代码,并不能运行通过,作者实际想要表述的是你可以把服务器的键存到环境变量量,然后就可以通过键索引取出来.比如说你定义了一个名为ServerEnv的环境变量,并赋值为Prod,则可以通过
$server = $environments[$env:ServerEnv]来获取到hashtable对应的服务器名

获取多项

一般情况下,你认为hashtable是一个键值对集合,你提供一个键,获取一个值,实际上powershell允许提供一系列的键来获取多个值.

$environments[@('QA','DEV')]
$environments[('QA','DEV')]
$environments['QA','DEV']

在这个例子中我们使用上上面的查找表并且提供了三种不同风格的数组来获取匹配的项.这是powershell隐藏的未被多数人发掘的宝贝.

译者注,以上三种确为powerhsell数组的三种定义方式,是等价的,一般情况下数组定义使用@()这种形式,对于泛型数组类型或者几个离散元素通常使用第三种形式,例如$num=1..5或者$num=1,2,3,5,5,效果是一样的

遍历hashtable

由于hashtable是一系列的键值对,因此你不能像遍历数组或者普通列表一样来遍历它.
首先需要注意的是如果你使用管道来操作hashtable,则hashtable被视为一个整体对象

PS:\> $ageList | Measure-Object
count : 1

即便它的count属性告诉了你它其实包含了多少个元素

PS:\> $ageList.count
2

译者注 以上就是说hashtable被视为一个整体对象,因此你使用Measure-Object来获取它的元素个数的时候,你将得不到正确结果.很多初识powershell的童鞋不知道如何获取集合元素的个数,其实一般是通过在管道下一级使用 Measure-Object来实现的
measure-object 展示信息中count属性即为集合元素的个数.如果你想要通过程序化的方式来拿到这个count属性也很简单,就是把以上操再加一级Select Count管道
measuremeasure-object命令的另名.命令的别名和命令本身是等效的.

如果你需要的仅仅是值,则可以通过.values属性来获取值,这个问题就解决了.(实际上values获取的是一个数组,数组使用measure-object是可以正确获取到元素个数的)

PS:\> $ageList.values | Measure-Object -Average
Count   : 2
Average : 22.5

译者注 Meausre-object是获取元素个数信息的好帮手,如果没有任何参数只有count属性,这里指定的Average,实际上还可以指定Max,Min,Sum等参数来获取集合元素的统计信息

通常情况下遍历hashtable的键集合,然后通过键来获取值是很有用的.

PS:\> $ageList.keys | ForEach-Object{
        $message = '{0} is {1} years old!' -f $_, $ageList[$_]
        Write-Output $message
}
Kevin is 36 years old
Alex is 9 years old

以下代码使用foreach(){...}循环,效果是一样的

foreach($key in $ageList.keys)
{
    $message = '{0} is {1} years old' -f $key, $ageList[$key]
    Write-Output $message
}

译者注 由于这是一篇全面讲解hashtable的文章,因此很多地方超出了基本范围,很多没有powershell基础的童鞋可能看一些语法感觉很费劲,上面ForEach-Object管理可能看起来不如下面foreach循环更为直观,更符合编程语言语法

我们遍历hashtable里的每一个key,然后使用key来索引元素的值,这是我们遍历hashtable的惯用方法.

使用GetEnumerator()方法

我们也可以使用GetEnumerator()来遍历hashtable

$ageList.GetEnumerator() | ForEach-Object{
        $message = '{0} is {1} years old!' -f $_.key, $_.value
        Write-Output $message
}

迭代器迭代出一个个的键值对,它是专为这种场景使用设计的.谢谢Mark Kraus提醒我这种方法

译者注: 使用迭代器可能在一些语言中很常用,但是在powershell里并不是很常用,一般使用foreach循环就好了

遍历异常

一个非常重要的细节是在遍历hashtable的时候,你不能修改它.我们仍然使用上面的$environments来举例

$environments = @{
    Prod = 'SrvProd05'
    QA   = 'SrvQA02'
    Dev  = 'SrvDev12'
}

如果我们试图把所有的值都设置成一样,则会运行失败

$environments.Keys | ForEach-Object {
    $environments[$_] = 'SrvDev03'
} 

An error occurred while enumerating through a collection: Collection was modified; enumeration operation may not execute.
+ CategoryInfo          : InvalidOperation: tableEnumerator:HashtableEnumerator) [], RuntimeException
+ FullyQualifiedErrorId : BadEnumeration

以下看起来没问题,实际上同样运行时会出错

foreach($key in $environments.keys) {
    $environments[$key] = 'SrvDev03'
} 

Collection was modified; enumeration operation may not execute.
    + CategoryInfo          : OperationStopped: (:) [], InvalidOperationException
    + FullyQualifiedErrorId : System.InvalidOperationException

这里有个小伎俩我们在遍历前先把keys复制一份

$environments.Keys.Clone() | ForEach-Object {
    $environments[$_] = 'SrvDev03'
} 

hashtable作为属性集合

截至目前我们往hashtable里添加的都是同一种类型元素.在powershell里hashtable一个常用的用法主是用它来存储一系列的属性集合,把属性名作为键,把属性值作为值.

基于属性的访问

基于属性的访问改变了hashtable的动态属性和使用poweshell的使用方式.下面示例使用上面的例子使用键来作为属性

$ageList = @{}
$ageList.Kevin = 35
$ageList.Alex = 9

和前面的示例一样,如果key不在集合里则会被添加进去.

$person = @{
    name = 'Kevin'
    age  = 36
}

我们也可以使用下面方法来访问$person的属性

$person.city = 'Austin'
$person.state = 'TX'

突然之间hashtable看起来像个对象,实际上它仍然是一个集合,对以上所有示例仍然是有效的.我们仅仅是从不同的视角来看待.

检测键和值

通常情况下,你可能使用如下方法来检测值

if( $person.age ){...}

它非常简单但是却是产生很多bug的源头因为以上忽略了一个重要的逻辑.我们使用它来检测一个key是否存在,但是如果正好值是$false或者0,以上检测将返回意外地返回false

if( $person.age -ne $null ){...}

以上解决了前面的问题但是仍然解决不了值正好是$null的情况.大多数情况下你不需要区分这些这么严格但是确立有方法解决这一问题:

if( $person.ContainsKey('age') ){...}

hashtable里还有一个ContainsValue在你不知道key的时候判断一个value是否存在,而不用遍历整个集合.

删除和清空键

你可以使用Remove方法来删除一个键

$person.remove('age')

把键的值设置为$null仅仅留下一个空key(值为$null,并不能把键删除掉)

一个常用的把集合清空的方法是把它的值设置为一个空集合

$person = @{}

也可以使用clear()方法

$person.clear()

在这些示例中,使用方法使得代码整洁,不言自明.

关于有序hashtable

默认情况下,hashtable是无序的.在传统的上下文中,你使用键来获取值顺序往往无关紧要.但是你使用它来存储属性的时候,你可能希望属性存储的顺序和你定义它们时候的顺序是一致的.幸好,可以使用ordered关键字来定义.

$person = [ordered]@{
    name = 'Kevin'
    age  = 36
}

这时候你再遍历它,它将保持顺序

行内hashtable

你可以把hashtable定义在一行,不同的键值之间用分号;隔开

$person = @{ name = 'kevin'; age = 36; }

如果你是在管道中创建hashtable这将非常有用

在常见的管道命令里使用自定义表达式

有少数命令支持使用hashtable来创建自定义或者计算属性,最为常见的是Select-ObjectFormat-Table,hashtable有一种特殊的形式展开形式如下:

$property = @{
    name = 'totalSpaceGB'
    expression = { ($_.used + $_.free) / 1GB }
}

名字是命令用来标识列的,expression 是一个代码块可以被执行,$_是从管道传过来的值,以下脚本使用了上面定义:

PS:\> $drives = Get-PSDrive | Where Used 
PS:\> $drives | Select-Object -Properties name, $property

Name     totalSpaceGB
----     ------------
C    238.472652435303

我把它放在了一个变量量,实际上可以使用内联的方式定义,你也可以使用n作为name的缩写,使用'e'作为expression的缩写

$drives | Select-Object -properties name, @{n='totalSpaceGB';e={($_.used + $_.free) / 1GB}}

自定义排序表达式

如果集合中有你想要按照它来排序的字段,排序是非常容易的,你可以把数据添加到对象里或者使用Sort-Object来创建一个自定义排序表达式

Get-ADUser | Sort-Object -Parameter @{ e={ Get-TotalSales $_.Name } }

在这个示例中,我取出了用户集合并使用自定义命令获取额外信息用来排序.

排序hashtable列表

如果你有一个hashtable的列表(list)想来排序,你会发现Sort-Object 并不会把键作为属性.我们可以使用自定义排序表达式来解决这个问题

$data = @(
    @{name='a'}
    @{name='c'}
    @{name='e'}
    @{name='f'}
    @{name='d'}
    @{name='b'}
)

$data | Sort-Object -Property @{e={$_.name}}

在命令中把hashtable打散

这是我最为喜欢的一个功能,早些时候很多人并不知道.除了把所有的属性都在一行内提供,你可以首先把它们打包成一个hashtable.然后你可以通过特殊方式把hashtable传入函数作为参数.以下是使用普通方法来创建DHCP

Add-DhcpServerv4Scope -Name 'TestNetwork' -StartRange'10.0.0.2' -EndRange '10.0.0.254' -SubnetMask '255.255.255.0' -Description 'Network for testlab A' -LeaseDuration (New-TimeSpan -Days 8) -Type "Both"

在不使用打散方法前,所有的参数都在一行甚至会超出屏幕视觉范围或者折行,下面我们比较一下使用hashtable打散的方法传入参数

$DHCPScope = @{
    Name        = 'TestNetwork'
    StartRange  = '10.0.0.2'
    EndRange    = '10.0.0.254'
    SubnetMask  = '255.255.255.0'
    Description = 'Network for testlab A'
    LeaseDuration = (New-TimeSpan -Days 8)
    Type = "Both"
}
Add-DhcpServerv4Scope @DHCPScope

使用@符而不是$来调用hashtable的打散功能.

我们停下来欣赏这下这段代码可读性是多么的高!同样的命令,同样的值.下面的代码比上面可读性和可维护性都提高很多.

当命令参数变得很长的 时候,我就地考虑使用hashtable打散.

参数打散在可选参数中使用

在日常工作中,一个最为常见的情况是用打散来处理可选参数,比如说我有一个封闭好的名字叫作Get-CIMInstance的方法,方法调用时有一个可选参数叫作$Credential

$CIMParams = @{
    ClassName = 'Win32_Bios'
    ComputerName = $ComputerName
}

if($Credential)
{
    $CIMParams.Credential = $Credential
}

Get-CIMInstance @CIMParams

从最基本的参数开始,然后我添加了$Credential参数如果它存在的话,因为这里使用了参数打散,在代码里仅需要调用Get-CIMInstance一次,这种模式非常简洁并且可以很容易地处理很多常见的可选参数

多项打散

你可以把多个hashtable打散到一个cmdlet命令里,我们来看下其初的一个示例

$Common = @{        
    SubnetMask  = '255.255.255.0'
    LeaseDuration = (New-TimeSpan -Days 8)
    Type = "Both"
}

$DHCPScope = @{
    Name        = 'TestNetwork'
    StartRange  = '10.0.0.2'
    EndRange    = '10.0.0.254'
    Description = 'Network for testlab A'        
}

Add-DhcpServerv4Scope @DHCPScope @Common

如果有一些共同的参数需要传入到许多不同的命令,我经常使用这种方法

为了代码的整洁使用打散

如果为了代码整洁,即便只打散一个参数也是不为过的

$log = @{Path = '.\logfile.log'}

Add-Content "logging this command" @log

hashtable相加

hashtable支持额外的操作签把两个hashtable相加

$person += @{Zip = '78701'}

嵌套hashtable

我们可以把hashtable作为另一个hashtable的值

$person = @{
    name = 'Kevin'
    age  = 36
}
$person.location = @{}
$person.location.city = 'Austin'
$person.location.state = 'TX'

这个hashtable初始包含两个键,然后添加了一个键为location值为一个空hatable的条目.我们也可以以内联的方式声明:

$person = @{
    name = 'Kevin'
    age  = 36
    location = @{
        city  = 'Austin'
        state = 'TX'
    }
}

这和上面创建的hashtable是等效的,我们也可以使用同样的方法来访问它的属性:

PS:> $person.location.city
Austin

有许多方法可以获取到对象的结构,下面是另一种视角来看待这个问题:

$people = @{
    Kevin = @{
        age  = 36
        city = 'Austin'
    }
    Alex = @{
        age  = 9
        city = 'Austin'
    }
}

这里混合使用了hashtable是对象集合和属性集合的概念.不管你使用什么方法,获取嵌套对象仍然是很简单的.

PS:\> $people.kevin.age
36
PS:\> $people.kevin['city']
Austin
PS:\> $people['Alex'].age
9
PS:\> $people['Alex']['City']
Austin

对我而言,当我把它们看作属性时我倾向于使用点号方法.它们都是我在代码里静态定义的我很容易知道它们.如果我想遍历集合或者从程序中获取键时,我会使用中括号加上键名

foreach($name in $people.keys)
{
    $person = $people[$name]
    '{0}, age {1}, is in {2}' -f $name, $person.age, $person.city
}

hashtable可以嵌套带来了更多编程灵活性选择

查看嵌套hashtable

当你开始使用hashtable的时候,你需要一个简单的方式从控制台查看它们.如果我们在控制台查看上面的hashtable,它像下面一样层次非常深:

ps:\> $people
Name                           Value
----                           -----
Kevin                          {age, city}
Alex                           {age, city}

这里可以使用ConvertTo-JSON因为它非常简洁并且我经常使用它来做其它事情

PS:\> $people | ConvertTo-JSON
{
    "Kevin":  {
                "age":  36,
                "city":  "Austin"
            },
    "Alex":  {
                "age":  9,
                "city":  "Austin"
            }
}

即便你不知道json,你仍然可以看明白它.powershell有一个Format-Custom命令来查看结构化的数据但是我仍然更倾向使用json查看

创建对象

有些时候你需要创建一个对象来完成仅仅使用hashtable来保存一些属性无法完成的工作.最为常见的是你想把键作为列名.使用pscustomobject可以使这项工作变得非常简单

$person = [pscustomobject]@{
    name = 'Kevin'
    age  = 36
year=2014
}

 PS:\> $person
   
name  age
----  ---
Kevin  36

译者注,很多童鞋如果仅仅是看文章而没动手的话可能一下子看不明白加了pscustomobject之后和之前有什么区别,其它自动动手看一下主知道.hashtable展示的是键作为一列,值作为一列.一共两列,而对象有多少个属性就会有多少列.

即便在初始的时候你没有创建pscustomobject,你仍然可以在需要使用的时候转换成它

$person = @{
    name = 'Kevin'
    age  = 36
}

 PS:\> [pscustomobject]$person
   
name  age
----  ---
Kevin  36

保存为csv

尝试着把上面提到的hashtable转为csv是一件非常挣扎的事.把hashtable转为pscustomobject可以正确地保存csv.如果你在创建的时候就使用了pscustomobject则可以保存创建的顺序.但是你也可以使用内联的方式转化为pscustomobject

$person | ForEach-Object{ [pscustomobject]$_ } | Export-CSV -Path $path

把嵌套对象保存到文件

如果我想要把嵌套hashtable保存到文件并且可以还原回来,我会使用json命令来做

$people | ConvertTo-JSON | Set-Content -Path $path
$people = Get-Content -Path $path -Raw | ConvertFrom-JSON

这里有两个重要的点.首先写入文件时json文件是多行的,因此我需要使用Raw选项把它读到到单行.第二点是导入的对象不再是一个hashtable,而是一个pscustomobject,并非可能会产生非预期的结果

如果在导入的时候你想要它是一个hashtable,这时候你需要使用Export-CliXmlImport-CliXml命令

键仅仅是字符串

键仅仅是字符串,我们可以给任何对象加上引号把它变成键

$person = @{
    'full name' = 'Kevin Marquette'
    '#' = 3978
}
$person['full name']

你甚至可以做一些你认为为可能的奇怪的事:

$person.'full name'

$key = 'full name'
$person.$key

虽然你可以这样做,但并不意味着你应该这样做.最后一行很容易产生bug和误解.

从技术上讲,键并不仅仅是字符串(可以是字符串外其它对象).但是如果你仅仅使用字符串你可以把它当作字符串.

PSBoundParameters

PSBoundParameters是一个仅仅存在于函数上下文的自动对象.它包含了函数需要调用的所有参数,准确地讲它并不是一个hashtable但是你可以把它当作hashtable.

它包括的移除键和打散到其它函数.想进一步了解它,请查看微软官方文档以了解更多细节.

PSBoundParameters 问题

一件很重要的事是它仅仅包含了传入的参数.如果你不家其它默认参数但是没有被调用者传入进来,PSBoundParameters 不会包含这些值,这一点经常被忽略.

PSDefaultParameterValues

这个自动变量让你可以在不改变命令情况下给它指定默认值:

$PSDefaultParameterValues["Out-File:Encoding"] = "UTF8"

这添加了一个把Out-File-Encoding参数值默认设置为UTF8 hashtable.这仅仅是对特定session有效的因此你需要把它添加到$profile

当需要经常键入一些命令的时候我经常预先赋一些值

$PSDefaultParameterValues[ "Connect-VIServer:Server" ] = 'VCENTER01.contoso.local'

它还接收通配符,因此你可以批量设置

$PSDefaultParameterValues[ "Get-*:Verbose" ] = $true
$PSDefaultParameterValues[ "*:Credential" ] = Get-Credental

正则匹配

当你使用-match操作符,一个叫作$matches的变量被创建作来保存结果,如果你的正则中有子表达式,这些子表达式的结果也被列出

$message = 'My SSN is 123-45-6789.'

$message -match 'My SSN is (.+)\.'
$Matches[0]
$Matches[1]

命名匹配

这是一个我非常喜欢的,很多人都不知道的特性.如果你使用的命名匹配,你可以使用名字来访问它

$message = 'My Name is Kevin and my SSN is 123-45-6789.'

if($message -match 'My Name is (?<Name>.+) and my SSN is (?<SSN>.+)\.')
{
    $Matches.Name
    $Matches.SSN
}

以上示例中,`(?<Name>.*)`是子表达式的名称.值被存储在`$Matches.Name`属性里

## Group-Object -AsHashtable

关于`Group-Object`一个鲜为人知的特性是:它可以把一些数据集转化为hashtable

Import-CSV $Path | Group-Object -AsHashtable -Property email

这会把每一行都添加到hashtable,使用指定的键来访问它.

## 拷贝hashtable

很重要的一点是hashtable是对象的集合.每一个变量都是对一个对象的引用,这就意味着想要拷贝一个hashtable会有更多的工作要做

### 赋值引用类型

当你有一个hashtable并且你把它赋值给一个变量,它们都批向同一个hashtable

PS> $orig = @{name='orig'}
PS> $copy = $orig
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name

Copy: [copy]
Orig: [copy]


这个示例表名它们是同一个对象因为任意的一个改变都会影响到另一个.把hashtable传入其它的函数也是如此,如果这个函数改变了hashtable的值,原始的hashtable也被改变

### 一级浅拷贝

如果我们的hashtable像上面示中那样非常简单,我们可以使用`.Clone()`做个浅拷贝

PS> $orig = @{name='orig'}
PS> $copy = $orig.Clone()
PS> $copy.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.name
PS> 'Orig: [{0}]' -f $orig.name

Copy: [copy]
Orig: [orig]


这样我们改变其中一个对象就不会改变另一个.

### 嵌套浅拷贝

我之所以叫它浅拷贝因为它仅仅拷贝了基层属性.如果其中 一个属性是引用对象(比如说是一个hashtable),这时候嵌套对象之间仍然有互相引用关系.

PS> $orig = @{
person=@{
name='orig'
}
}
PS> $copy = $orig.Clone()
PS> $copy.person.name = 'copy'
PS> 'Copy: [{0}]' -f $copy.person.name
PS> 'Orig: [{0}]' -f $orig.person.name

Copy: [copy]
Orig: [copy]


你会 看到即便我克隆了hashtable,对person对象的引用并没有克隆,我们需要一个正真的深拷贝来使它们之间不再有关联

### 深拷贝

在写此篇文章的时候,我还没有找到一个更聪明的方法来对hashtable做深拷贝.下面是一个快速方法来实现深拷贝

function Get-DeepClone
{
[cmdletbinding()]
param(
$InputObject
)
process
{
if($InputObject -is [hashtable]) {
$clone = @{}
foreach($key in $InputObject.keys)
{
$clone[$key] = Get-DeepClone $InputObject[$key]
}
return $clone
} else {
return $InputObject
}
}
}


这并不能处理数组或其它的引用类型,但是它是一个好的开端

## 结语

我快速的讲述了一些基础知识.每当你看这篇文章的时候我希望你能从别处学到一些新的知识以便可以理好的理解.因为我涵盖了所有的范围,很多特征并不能马上对你有用.

原文链接:https://kevinmarquette.github.io/2016-11-06-powershell-hashtable-everything-you-wanted-to-know-about/#adding-hashtables
posted @ 2019-01-01 09:59  周国通  阅读(5145)  评论(0编辑  收藏  举报