PowerShell 해시 테이블

PowerShell 해시 테이블 (about_Hash_Tables)

해시 테이블은 PowerShell에서 가장 자주 마주치게 되는 자료 구조 중 하나예요. 이름표(키)와 값이 짝을 이룬 상자들이 한 번에 묶여 있는 모양이라고 생각하면 돼요. key로 값을 찾고, 값으로 이름을 찾는 식으로 데이터를 빠르게 주고받을 수 있거든요. 이번 글에서는 해시 테이블을 만들고, 값을 읽고, 다루는 방법을 하나씩 풀어볼게요.

출처: PowerShell 공식 문서 — about_Hash_Tables

본문

해시 테이블이 뭘까요

해시 테이블(hashtable)은 딕셔너리(dictionary)나 연관 배열(associative array)이라고도 불리는 컴팩트한 자료 구조로, 하나 이상의 key-value 쌍을 담아요. 예를 들어 IP 주소와 컴퓨터 이름의 짝을 담고 싶다면 IP 주소를 key로, 컴퓨터 이름을 value로 묶으면 되고, 반대로 뒤집어도 돼요.

PowerShell에서 해시 테이블 하나하나는 [System.Collections.Hashtable] 객체예요. 그래서 Hashtable 객체의 속성과 메서드를 그대로 쓸 수 있어요.

PowerShell 3.0부터는 [ordered] 형식 가속기(type accelerator)를 써서 [System.Collections.Specialized.OrderedDictionary] 객체를 만들 수도 있어요.

순서 있는 딕셔너리(ordered dictionary)는 key가 항상 나열한 순서대로 나타난다는 점에서 해시 테이블과 달라요. 해시 테이블의 key 순서는 비결정적이라서 섞여 보이는 게 정상이에요.

해시 테이블의 key와 value도 역시 .NET 객체예요. 대부분 문자열이나 정수지만, 아무 객체 타입이 돼도 좋고, value가 또 다른 해시 테이블인 중첩 해시 테이블도 만들 수 있어요.

해시 테이블이 자주 쓰이는 이유는 데이터를 찾고 꺼내는 데 효율적이기 때문이에요. 목록을 저장하거나 PowerShell에서 계산된 속성(calculated property)을 만들 때도 쓰고요. 또 ConvertFrom-StringData cmdlet은 구조화된 문자열 데이터를 해시 테이블로 바꿔 줘요.

문법 살펴보기

해시 테이블의 문법은 이렇게 생겼어요.

@{ <name> = <value>; [<name> = <value> ] ...}

순서 있는 딕셔너리의 문법은 이렇고요.

[ordered]@{ <name> = <value>; [<name> = <value> ] ...}

[ordered] 형식 가속기는 PowerShell 3.0에서 처음 등장했어요.

해시 테이블을 만들 때는 다음 규칙을 따라주시면 돼요.

  • 해시 테이블은 골뱅이(@)로 시작해요.
  • 중괄호({})로 감싸요.
  • 내용으로는 key-value 쌍을 하나 이상 넣어요.
  • 각 key와 value는 등호(=)로 구분해요.
  • key-value 쌍끼리는 세미콜론(;)이나 줄바꿈으로 구분해요.
  • 공백이 들어 있는 key는 반드시 따옴표로 감싸야 해요. value는 유효한 PowerShell 식이어야 하고, 문자열은 공백이 없어도 따옴표로 묶어야 해요.
  • 해시 테이블을 관리하려면 변수에 저장해 두는 게 편해요.
  • 순서 있는 해시 테이블을 변수에 할당할 때는 [ordered] 자료형을 @ 기호 앞에 둬요. 변수 이름 앞에 두면 명령이 실패해요.

순서 있는 딕셔너리는 해시 테이블과 똑같은 방식으로 쓸 수 있어요. 해시 테이블이나 딕셔너리(IDictionary) 타입 객체를 받는 매개변수의 값으로 어느 쪽이든 사용할 수 있어요.

해시 테이블과 순서 있는 딕셔너리 만들기

해시 테이블 예시를 먼저 볼게요.

$hash = @{
    1       = 'one'
    2       = 'two'
    'three' = 3
}
$hash
Name                           Value
----                           -----
three                          3
2                              two
1                              one

보시다시피 해시 테이블은 key-value 쌍을 정의한 순서대로 보여 주지 않아요.

순서 있는 딕셔너리를 만드는 가장 쉬운 방법은 [ordered] 특성을 쓰는 거예요. @ 기호 바로 앞에 두면 돼요.

$dictionary = [ordered]@{
    1       = 'one'
    2       = 'two'
    'three' = 3
}
$dictionary
Name                           Value
----                           -----
1                              one
2                              two
three                          3

해시 테이블과 달리 순서 있는 딕셔너리는 key-value의 순서를 그대로 유지해요.

변환하기

[ordered] 형식 가속기로 해시 테이블을 변환하거나 캐스팅할 수는 없어요. ordered 특성을 변수 이름 앞에 두면 다음 오류 메시지와 함께 명령이 실패해요.

[ordered]$orderedHash = @{}
ParserError:
Line |
   1 |  [ordered]$orderedHash = @{}
     |  ~~~~~~~~~~~~~~
     | The ordered attribute can be specified only on a hash literal node.

식을 고치려면 [ordered] 특성을 옮겨 주면 돼요.

$orderedHash = [ordered]@{}

순서 있는 딕셔너리를 해시 테이블로 캐스팅할 수는 있지만, 멤버의 순서는 보장할 수 없어요.

[hashtable]$newHash = [ordered]@{
    Number = 1
    Shape = "Square"
    Color = "Blue"
}
$newHash
Name                           Value
----                           -----
Color                          Blue
Shape                          Square
Number                         1

해시 테이블과 딕셔너리의 속성

해시 테이블과 순서 있는 딕셔너리는 몇 가지 속성을 공유해요. 아까 만든 $hash$dictionary 변수를 예로 볼게요.

$hash | Get-Member -MemberType Properties, ParameterizedProperty
   TypeName: System.Collections.Hashtable

Name           MemberType            Definition
----           ----------            ----------
Item           ParameterizedProperty System.Object Item(System.Object key) {get;set;}
Count          Property              int Count {get;}
IsFixedSize    Property              bool IsFixedSize {get;}
IsReadOnly     Property              bool IsReadOnly {get;}
IsSynchronized Property              bool IsSynchronized {get;}
Keys           Property              System.Collections.ICollection Keys {get;}
SyncRoot       Property              System.Object SyncRoot {get;}
Values         Property              System.Collections.ICollection Values {get;}
$dictionary | Get-Member -MemberType Properties, ParameterizedProperty
   TypeName: System.Collections.Specialized.OrderedDictionary

Name           MemberType            Definition
----           ----------            ----------
Item           ParameterizedProperty System.Object Item(int index) {get;set;},
                                     System.Object Item(System.Object key) {get;set;}
Count          Property              int Count {get;}
IsFixedSize    Property              bool IsFixedSize {get;}
IsReadOnly     Property              bool IsReadOnly {get;}
IsSynchronized Property              bool IsSynchronized {get;}
Keys           Property              System.Collections.ICollection Keys {get;}
SyncRoot       Property              System.Object SyncRoot {get;}
Values         Property              System.Collections.ICollection Values {get;}

가장 자주 쓰는 속성은 Count, Keys, Values, Item 네 가지예요.

  • Count 속성은 객체에 들어 있는 key-value 쌍의 개수를 알려 줘요.
  • Keys 속성은 해시 테이블이나 딕셔너리에 있는 key 이름들의 컬렉션이에요.
PS> $hash.Keys
three
2
1

PS> $dictionary.Keys
1
2
three
  • Values 속성은 해시 테이블이나 딕셔너리에 있는 value들의 컬렉션이에요.
PS> $hash.Values
3
two
one

PS> $dictionary.Values
one
two
3
  • Item 속성은 지정한 항목의 값을 돌려주는 매개변수화 속성(parameterized property)이에요. 해시 테이블은 key를 이 속성의 매개변수로 쓰고, 딕셔너리는 기본적으로 인덱스를 써요. 이 차이가 각 타입에서 값을 접근하는 방식을 가른답니다.

값 접근하기

해시 테이블이나 딕셔너리의 값에 접근하는 방법은 크게 두 가지예요. 멤버 표기법(member notation)과 배열 인덱스 표기법(array index notation)이에요.

  • 멤버 표기법 — key 이름을 객체의 멤버 속성처럼 써서 값을 접근해요.
PS> $hash.1
one

PS> $dictionary.2
two
  • 배열 인덱스 표기법 — 인덱스 표기법으로 값을 접근해요. PowerShell은 이 표기법을 객체의 Item 매개변수화 속성 호출로 변환해요.

해시 테이블에서 인덱스 표기법을 쓰면 대괄호 안의 값이 key 이름이 돼요. key가 문자열 값이라면 key 이름을 따옴표로 감싸면 돼요.

PS> $hash['three']
3

PS> $hash[2]
two

이 예시에서 key 값 2는 값 컬렉션의 인덱스가 아니라 key-value 쌍에서 key의 값이에요. 값 컬렉션에 인덱싱해 보면 이 사실을 확인할 수 있어요.

PS> ([array]$hash.Values)[2]
one

딕셔너리에서 인덱스 표기법을 쓰면 대괄호 안의 값이 그 타입에 따라 해석돼요. 값이 정수라면 값 컬렉션의 인덱스로 취급하고, 정수가 아니면 key 이름으로 취급해요.

PS> $dictionary[1]
two
PS> ([array]$dictionary.Values)[1]
two
PS> $dictionary[[Object]1]
one
PS> $dictionary['three']
3

이 예시에서 배열 값 [1]Item(int index) 매개변수화 속성 오버로드를 써서 값 컬렉션을 인덱싱한 거예요. 배열 값 [[Object]1]은 인덱스가 아니라 Item(System.Object key) 오버로드를 쓰는 key 값이에요.

⚠️ 주의 — key 값이 정수일 때 이 동작이 헷갈릴 수 있어요. 가능하면 딕셔너리에서 정수 key 값은 피하는 게 좋아요.

속성 이름 충돌 다루기

key 이름이 HashTable 타입의 속성 이름과 겹친다면 psbase 고유 멤버(intrinsic member)를 써서 그 속성에 접근할 수 있어요. 예를 들어 key 이름이 keys인데 HashTable의 key 컬렉션을 돌려받고 싶다면 이렇게 쓰세요.

$hashtable.psbase.Keys

이 요구 사항은 System.Collections.IDictionary 인터페이스를 구현하는 다른 타입(예: OrderedDictionary)에도 그대로 적용돼요.

key와 value 둘러보기(반복)

해시 테이블의 key를 여러 가지 방법으로 순회하면서 값을 처리할 수 있어요. 이 섹션의 예시는 모두 같은 출력을 내요. 여기 정의한 $hash 변수를 기준으로 반복할 거예요.

$hash = [ordered]@{Number = 1; Shape = "Square"; Color = "Blue"}

참고 — 이 예시들에서 $hash는 항상 같은 순서로 출력되도록 순서 있는 딕셔너리로 정의했어요. 표준 해시 테이블에서도 똑같이 동작하지만 출력 순서는 예측할 수 없답니다.

각 예시는 모든 key와 그 value에 대해 하나씩 메시지를 만들어 내요.

The value of 'Number' is: 1
The value of 'Shape' is: Square
The value of 'Color' is: Blue

foreach 블록으로 key를 순회하는 방법이에요.

foreach ($Key in $hash.Keys) {
    "The value of '$Key' is: $($hash[$Key])"
}

ForEach-Object로 key를 순회할 수도 있어요.

$hash.Keys | ForEach-Object {
    "The value of '$_' is: $($hash[$_])"
}

GetEnumerator() 메서드로 각 key-value 쌍을 파이프라인에 흘려 ForEach-Object로 보내는 방법도 있어요.

$hash.GetEnumerator() | ForEach-Object {
    "The value of '$($_.Key)' is: $($_.Value)"
}

마지막으로 GetEnumerator()ForEach() 메서드를 함께 써서 각 key-value 쌍을 순회할 수도 있어요.

$hash.GetEnumerator().ForEach({"The value of '$($_.Key)' is: $($_.Value)"})

key와 value 추가 및 제거

보통 해시 테이블을 만들 때 정의 안에 key-value 쌍을 함께 넣지만, 언제든 추가하거나 제거할 수도 있어요. 먼저 빈 해시 테이블을 만들어 볼게요.

$hash = @{}

배열 표기법으로 key-value 쌍을 추가할 수 있어요. 다음 예시는 Time이라는 key에 Now 값을 넣어요.

$hash["Time"] = "Now"

System.Collections.Hashtable 객체의 Add() 메서드로도 추가할 수 있어요. Add() 메서드 문법은 이렇게 생겼어요.

Add(Key, Value)

예를 들어 Time key에 Now 값을 넣으려면 다음과 같은 문장 형식을 쓰면 돼요.

$hash.Add("Time", "Now")

덧셈 연산자(+)를 써서 기존 해시 테이블에 해시 테이블을 더하는 방법도 있어요. 다음 문장은 $hash 변수의 해시 테이블에 Time key와 Now 값을 추가해요.

$hash = $hash + @{Time="Now"}

변수에 저장된 값을 추가하는 것도 가능해요.

$t = "Today"
$now = (Get-Date)

$hash.Add($t, $now)

key-value 쌍을 지울 때 뺄셈 연산자는 쓸 수 없어요. 대신 해시 테이블 객체의 Remove() 메서드를 쓰면 돼요. Remove 메서드 문법은 이렇습니다.

$object.Remove(<key>)

다음 예시는 $hash에서 Time key-value 쌍을 제거해요.

$hash.Remove("Time")

해시 테이블의 객체 타입

해시 테이블의 key와 value는 어떤 .NET 객체 타입이든 될 수 있고, 한 해시 테이블에 여러 타입의 key와 value가 섞여 있어도 돼요.

다음 문장은 프로세스 이름 문자열과 프로세스 객체 값을 담은 해시 테이블을 만들어 $p 변수에 저장해요.

$p = @{
    "PowerShell" = (Get-Process powershell)
    "Notepad" = (Get-Process notepad)
}

$p의 해시 테이블을 표시하고, key 이름 속성을 써서 값을 보여 줄 수 있어요.

PS> $p

Name                           Value
----                           -----
PowerShell                     System.Diagnostics.Process (PowerShell)
Notepad                        System.Diagnostics.Process (notepad)

PS> $p.PowerShell

Handles  NPM(K)    PM(K)      WS(K) VM(M)   CPU(s)     Id ProcessName
-------  ------    -----      ----- -----   ------     -- -----------
    441      24    54196      54012   571     5.10   1788 PowerShell

PS> $p.Keys | ForEach-Object {$p.$_.Handles}
441
251

해시 테이블의 key는 어떤 .NET 타입이든 될 수 있어요. 다음 문장은 $p 변수의 해시 테이블에 key-value 쌍을 하나 더해요. key는 WinRM 서비스를 나타내는 Service 객체이고, value는 그 서비스의 현재 상태예요.

$p = $p + @{
    (Get-Service WinRM) = ((Get-Service WinRM).Status)
}

새로 추가한 key-value 쌍도 다른 쌍과 같은 방법으로 표시하고 접근할 수 있어요.

PS> $p

Name                           Value
----                           -----
PowerShell                     System.Diagnostics.Process (PowerShell)
Notepad                        System.Diagnostics.Process (notepad)
System.ServiceProcess.Servi... Running

PS> $p.Keys
PowerShell
Notepad

Status   Name               DisplayName
------   ----              -----------
Running  winrm              Windows Remote Management (WS-Manag...

PS> $p.Keys | ForEach-Object {$_.Name}
WinRM

해시 테이블의 key와 value는 해시 테이블 객체일 수도 있어요. 다음 문장은 $p 변수의 해시 테이블에 key-value 쌍을 더하는데, key는 Hash2라는 문자열이고 value는 key-value 쌍을 세 개 가진 해시 테이블이에요.

$p = $p + @{
    "Hash2"= @{a=1; b=2; c=3}
}

새 값도 같은 방법으로 표시하고 접근하면 돼요.

PS> $p

Name                           Value
----                           -----
PowerShell                     System.Diagnostics.Process (pwsh)
Hash2                          {[a, 1], [b, 2], [c, 3]}
Notepad                        System.Diagnostics.Process (Notepad)
WinRM                          Running

PS> $p.Hash2

Name                           Value
----                           -----
a                              1
b                              2
c                              3

PS> $p.Hash2.b
2

key와 value 정렬하기

해시 테이블의 항목은 본질적으로 순서가 없어요. 그래서 표시할 때마다 key-value 쌍이 다른 순서로 나타날 수 있어요.

해시 테이블 자체를 정렬할 수는 없지만, 해시 테이블의 GetEnumerator() 메서드로 key와 value를 열거한 뒤 Sort-Object cmdlet으로 열거된 값을 정렬해 표시할 수 있어요.

예를 들어 다음 명령은 $p 변수의 해시 테이블에서 key와 value를 열거한 다음 key를 알파벳순으로 정렬해요.

PS> $p.GetEnumerator() | Sort-Object -Property Key

Name                           Value
----                           -----
Hash2                          {[a, 1], [b, 2], [c, 3]}
Notepad                        System.Diagnostics.Process (Notepad)
PowerShell                     System.Diagnostics.Process (pwsh)
WinRM                          Running

다음 명령은 같은 방법으로 해시 값을 내림차순으로 정렬해요.

PS> $p.GetEnumerator() | Sort-Object -Property Value -Descending

Name                           Value
----                           -----
PowerShell                     System.Diagnostics.Process (pwsh)
Notepad                        System.Diagnostics.Process (Notepad)
Hash2                          {[a, 1], [b, 2], [c, 3]}
WinRM                          Running

해시 테이블로 객체 만들기

PowerShell 3.0부터는 속성과 속성 값으로 이루어진 해시 테이블로 객체를 만들 수 있어요.

문법은 이렇게 생겼어요.

[<class-name>]@{
  <property-name>=<property-value>
  <property-name>=<property-value>
}

이 방법은 매개변수가 없는 생성자(constructor)를 가진 클래스에서만 동작해요. 객체 속성은 public이고 settable이어야 해요.

자세한 내용은 about_Object_Creation 문서를 참고하세요.

ConvertFrom-StringData

ConvertFrom-StringData cmdlet은 key-value 쌍으로 된 문자열이나 here-string을 해시 테이블로 변환해요. 이 cmdlet은 스크립트의 Data 섹션에서 안전하게 쓸 수 있고, Import-LocalizedData cmdlet과 함께 쓰면 현재 사용자의 UI 문화권에 맞춰 사용자 메시지를 표시할 수도 있어요.

here-string은 해시 테이블의 value에 따옴표가 들어 있을 때 특히 유용해요. here-string에 대한 자세한 내용은 about_Quoting_Rules 문서를 참고하세요.

다음 예시는 앞 예시의 사용자 메시지로 here-string을 만들고, ConvertFrom-StringData로 문자열에서 해시 테이블로 변환하는 방법을 보여 줘요.

다음 명령은 key-value 쌍의 here-string을 만들어 $string 변수에 저장해요.

$string = @"
Msg1 = Type "Windows".
Msg2 = She said, "Hello, World."
Msg3 = Enter an alias (or "nickname").
"@

이 명령은 ConvertFrom-StringData cmdlet으로 here-string을 해시 테이블로 변환해요.

ConvertFrom-StringData $string
Name                           Value
----                           -----
Msg3                           Enter an alias (or "nickname").
Msg2                           She said, "Hello, World."
Msg1                           Type "Windows".

여기서도 here-string에 대한 더 자세한 내용은 about_Quoting_Rules 문서를 참고하세요.

더 알아보기