파워쉘 파이프라인 알아보기 및 함수 생성하기

PowerShell 파이프라인은 PowerShell 쉘 및 스크립팅 언어의 가장 중요하고 유용한 기능 중 하나입니다. 작동 방식과 가능한 기능에 대한 기본적인 이해를 한다면 해당 기능을 사용하여 자신의 함수에서 그 힘을 발휘할 수 있습니다. 이 튜토리얼에서는 바로 그것을 해보겠습니다!

PowerShell 파이프라인을 사용하면 명령을 연결하여 하나의 ‘파이프라인’을 구성할 수 있으며, 코드를 간소화하고 병렬 처리 등을 가능하게 합니다. 파이프라인을 이해하고 파이프라인을 활용하기 위해 자신의 함수를 만들어보려면 시작해봅시다!

사전 요구 사항

이 게시물은 튜토리얼이며, 실습 위주로 진행됩니다. 함께 따라하려면 PowerShell v3+가 필요합니다. 이 튜토리얼은 Windows PowerShell v5.1을 사용합니다.

PowerShell 파이프라인 이해하기

대부분의 PowerShell 명령은 매개 변수를 통해 일부 입력을 받습니다. 명령은 일부 객체를 입력으로 받아 내부에서 작업을 수행한 다음 선택적으로 일부 객체를 출력으로 반환합니다.

파이프라인의 명령은 릴레이 경주에서 인간 러너처럼 작동합니다. 첫 번째와 마지막 러너를 제외한 모든 러너는 이전 러너로부터 배턴(객체)을 받아 다음 러너로 전달합니다.

예를 들어, Stop-Service cmdlet에는 InputObject라는 매개변수가 있습니다. 이 매개변수를 사용하면 Stop-Service에 특정 유형의 개체를 전달하여 중지하려는 Windows 서비스를 나타낼 수 있습니다.

InputObject 매개변수를 사용하려면 Get-Service를 통해 서비스 개체를 검색한 다음 아래에 표시된대로 개체를 InputObject 매개변수에 전달할 수 있습니다. InputObject 매개변수를 통해 입력을 제공하는이 Stop-Service cmdlet의 방법은 훌륭하게 작동하며 작업을 완료합니다.

$service = Get-Service -Name 'wuauserv'
Stop-Service -InputObject $service

Stop-Service 명령에 입력을 전달하는이 방법은 두 가지 다른 단계가 필요합니다. PowerShell은 먼저 Get-Service를 실행하고 출력을 변수에 저장한 다음 그 값을 InputObject 매개변수를 통해 Stop-Service에 전달해야 합니다.

이제 위의 코드 일부를 아래의 코드 일부와 비교하면 같은 작업을 수행합니다. $services 변수를 만들거나 InputObject 매개변수를 사용할 필요가 없기 때문에 훨씬 간단합니다. 대신 PowerShell은 InputObject 매개변수를 사용할 것으로 인식합니다. 이것은 매개변수 바인딩이라는 개념을 통해 수행됩니다.

이제 | 연산자를 사용하여 명령을 연결했습니다. 여기에 파이프라인을 만들었습니다.

Get-Service -Name 'wuauserv' | Stop-Service

하지만 파이프라인을 만들려면 두 개의 명령만 사용할 필요는 없으며 (명령 매개변수가 지원하는 경우) 원하는 만큼 많이 연결할 수 있습니다. 예를 들어, 아래 코드 스니펫을 참조하십시오:

  1. 모든 객체를 Get-Service cmdlet이 반환하는 Where-Object cmdlet에 전달합니다.
  2. Where-Object cmdlet은 각 객체의 Status 속성을 확인한 다음 값이 Running인 객체만 반환합니다.
  3. 그런 다음 해당 객체들은 Select-Object로 전송되며 해당 객체의 NameDisplayName 속성만 반환합니다.

Select-Object이 출력하는 객체를 수용하는 다른 cmdlet이 없으므로 해당 명령은 객체를 직접 콘솔로 반환합니다.Where-ObjectSelect-Object cmdlet은 매개변수 바인딩이라는 개념을 통해 파이프라인 입력을 처리하는 방법을 이해합니다.

Get-Service | Where-Object Status -eq Running | Select-Object Name, DisplayName

파이프라인에 대한 추가 정보는 명령을 실행하십시오 Get-Help about_pipelines.

파이프라인 매개변수 바인딩

처음 보면 파이프라인은 사소한 것처럼 보일 수 있습니다. 어쨌든 단지 명령에서 객체를 다른 명령으로 전달하는 것뿐입니다. 그러나 실제로는 파이프라인이 훨씬 더 복잡합니다. 명령은 매개변수를 통해서만 입력을 수용합니다. 파이프라인은 명시적으로 정의하지 않아도 사용할 매개변수를 어떻게 찾아야 할지 어떻게 알아야 할지 결정해야 합니다.

파라미터 바인딩은 파이프라인을 통해 입력을 받을 때 어떤 매개변수를 사용해야 하는지 결정하는 작업입니다. 파이프라인으로 들어오는 객체를 매개변수에 바인딩하려면, 들어오는 명령의 매개변수가 이를 지원해야 합니다. 명령 매개변수는 두 가지 방법 중 하나로 파이프라인 매개변수 바인딩을 지원합니다. ByValue와/또는 ByPropertyName.

ByValue

명령 매개변수는 전체 들어오는 객체를 매개변수 값으로 받습니다. ByValue 매개변수는 들어오는 객체 중 특정 유형의 객체를 찾습니다. 해당 객체 유형이 일치하는 경우, PowerShell은 해당 매개변수에 바인딩되어야 한다고 가정하고 수락합니다.

Get-ChildItem cmdlet에는 Path라는 매개변수가 있으며, string 객체 유형과 ByValue를 통해 파이프라인 입력을 받습니다. 따라서 'C:\Windows' | Get-ChildItem과 같은 명령을 실행하면 C:\Windows 디렉토리의 모든 파일이 반환됩니다. 왜냐하면 C:\Windows는 문자열이기 때문입니다.

ByPropertyName

명령 매개변수는 객체 전체를 받는 것이 아니라 해당 객체의 단일 속성을 받습니다. 이는 객체 유형이 아닌 속성 이름을 보고 수행됩니다.

Get-Process cmdlet에는 Name 매개변수가 있으며, 이는 파이프라인 입력 ByPropertyName을 수락하도록 설정되어 있습니다. [pscustomobject]@{Name='firefox'} | Get-Process와 같이 Name 속성이 있는 개체를 Get-Process cmdlet에 전달할 때 PowerShell은 들어오는 개체의 Name 속성을 Name 매개변수에 매핑하고 해당 값을 사용합니다.

파이프라인 입력을 지원하는 명령 매개변수 찾기

앞서 언급했듯이 모든 명령이 파이프라인 입력을 지원하는 것은 아닙니다. 명령의 작성자는 이 기능을 개발해야 합니다. 명령은 파이프라인을 지원하는 매개변수가 하나 이상 있어야 합니다. ByValue 또는 ByPropertyName을 사용합니다.

어떻게 하면 어떤 명령과 해당 매개변수가 파이프라인 입력을 지원하는지 알 수 있을까요? 시행착오로 시도할 수 있지만 Get-Help 명령을 사용하여 PowerShell 도움말 시스템을 사용하는 것이 더 나은 방법입니다.

Get-Help <COMMAND> -Parameter <PARAMETER>

Get-ChildItem cmdlet의 Path 매개변수를 살펴보겠습니다. 이것이 두 종류의 파이프라인 입력을 지원하는 것을 볼 수 있습니다.

PowerShell pipeline input is allowed

한 번 파이프라인 입력을 지원하는 명령 매개변수를 알게 되면 아래에 표시된 것처럼 해당 기능을 활용할 수 있습니다.

# 파이프라인 호출 없음
Get-ChildItem -Path 'C:\\Program Files', 'C:\\Windows'

# 파이프라인 호출
'C:\\Program Files', 'C:\\Windows' | Get-ChildItem

그러나 반면에, Get-Service에서의 DisplayName 매개변수는 파이프라인 입력을 지원하지 않습니다.

PowerShell pipeline input is not allowed

자체 파이프라인 기능 만들기

표준 PowerShell cmdlet이 파이프라인 입력을 지원한다고 해서 해당 기능을 활용할 수 없는 것은 아닙니다. 다행히도, 파이프라인 입력을 수용하는 함수를 만들 수 있습니다.

이를 설명하기 위해, 기존 함수인 Get-ConnectionStatus로 시작해 보겠습니다.

  • 이 함수에는 단일 매개변수([Parameter()]로 정의되지 않은)인 ComputerName이 있습니다. 이를 통해 하나 이상의 문자열을 전달할 수 있습니다. ComputerName 매개변수가 파이프라인 입력을 수용하지 않는다는 것을 알 수 있습니다.
  • 그런 다음 함수는 각 문자열을 읽고 각각에 대해 Test-Connection cmdlet을 실행합니다.
  • 각 문자열 컴퓨터 이름이 전달되면 ComputerNameStatus 속성이 있는 객체를 반환합니다.
function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter()] ## no pipeline input
        [string[]]$ComputerName
    )

    foreach($c in $ComputerName)
    {
        if(Test-Connection -ComputerName $c -Quiet -Count 1)
        {
            $status = 'Ok'
        }
        else
        {
            $status = 'No Connection'
        }

        [pscustomobject]@{
            ComputerName = $c
            Status = $status
        }
    }
}

그런 다음 다음과 같이 하나 이상의 호스트 이름 또는 IP 주소를 ComputerName 매개변수로 전달하여 Get-ConnectionStatus를 호출합니다.

Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'

단계 1: 파이프라인 입력 허용

이 함수가 파이프라인 입력을 허용하려면 먼저 적절한 매개변수 속성을 정의해야 합니다. 이 속성은 ValueFromPipeline으로 파이프라인 입력 ByValue를 허용하거나 ValueFromPipelineByPropertyName로 파이프라인 입력 ByPropertyName을 허용할 수 있습니다.

이 예에서는 다음과 같이 [Parameter()] 정의의 괄호 안에 ValueFromPipeline 매개변수 속성을 추가합니다.

 [Parameter(ValueFromPipeline)]
 [string[]]$ComputerName

이 시점에서 기술적으로 이것이 전부입니다. Get-ConnectionStatus 함수는 이제 전달된 모든 문자열 객체를 ComputerName 매개변수에 바인딩합니다. 그러나 매개변수 바인딩이 진행 중이라고 해서 함수가 의미 있는 작업을 수행한다는 것은 아닙니다.

단계 2: 프로세스 블록 추가

파이프라인에서 들어오는 모든 객체를 처리하려면 다음으로 Process 블록을 추가해야 합니다. 이 블록은 PowerShell에게 파이프라인에서 들어오는 각 객체를 처리하라고 알려줍니다.

Process 블록이 없으면 PowerShell은 파이프라인에서 오는 첫 번째 객체만 처리합니다. Process 블록은 PowerShell에게 계속해서 객체를 처리하도록 알려줍니다.

다음과 같이 Process 블록을 추가하여 함수의 모든 기능을 묶어주세요.

function Get-ConnectionStatus
{
    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline)]
        [string[]]$ComputerName
    )

    Process ## 새로운 Process 블록
    {
        foreach($c in $ComputerName)
        {
            if(Test-Connection -ComputerName $c -Quiet -Count 1)
            {
                $status = 'Ok'
            }
            else
            {
                $status = 'No Connection'
            }

            [pscustomobject]@{
                ComputerName = $c
                Status = $status
            }
        }
    } ## end Process 블록
}

Process 블록 내에서 항상 출력을 보내세요. Process 블록 내에서 출력을 보내면 객체가 파이프라인을 통해 스트리밍되어 다른 명령에서 해당 객체를 파이프라인에서 받을 수 있게 됩니다.

PowerShell 파이프라인으로 객체 전달

위의 함수에서 Process 블록을 정의한 후, 아래에 표시된 대로 파이프라인을 통해 ComputerName 매개변수에 값 전달하여 함수를 호출할 수 있습니다.

Get-ConnectionStatus -ComputerName '127.0.0.1', '192.168.1.100'
## 또는
'127.0.0.1', '192.168.1.100' | Get-ConnectionStatus

이 시점에서 파이프라인의 실제 장점을 활용하여 더 많은 명령을 조합할 수 있습니다. 예를 들어, 아래와 같이 새 줄로 구분된 IP 주소의 텍스트 파일인 C:\Test\computers.txt가 있다고 가정해보겠습니다.

127.0.0.1
192.168.1.100

그런 다음 Get-Content cmdlet을 사용하여 텍스트 파일에서 각 IP 주소를 읽고 이를 Get-ConnectionStatus 함수에 직접 전달할 수 있습니다.

Get-Content -Path C:\Test\computers.txt | Get-ConnectionStatus 

이 설정을 한 단계 더 나아가서 Get-ConnectionStatus가 반환하는 객체를 직접 ForEach-Object cmdlet에 파이프하여 사용할 수 있습니다.

다음 코드:

  • 컴퓨터 이름을 모두 텍스트 파일에서 읽어와 Get-ConnectionStatus 함수에 전달합니다.
  • Get-ConnectionStatus는 각 컴퓨터 이름을 처리하고 ComputerNameStatus 속성이 있는 객체를 반환합니다.
  • Get-ConnectionStatus는 이후에 각 객체를 ForEach-Object cmdlet에 전달하며 이는 사람이 읽을 수 있는 Cyan으로 색상이 지정된 단일 문자열을 반환합니다.
Get-Content -Path C:\Test\computers.txt |
Get-ConnectionStatus |
ForEach-Object { Write-Host "$($_.ComputerName) connection status is: $($_.Status)" -ForegroundColor Cyan }

ComputerName 매개변수에서 파이프라인 입력이 활성화되지 않았거나 Get-ConnectionStatusProcess 블록 내에서 객체를 반환하지 않았다면 PowerShell은 모든 객체(IP 주소)가 처리될 때까지 콘솔에 어떤 상태도 반환하지 않을 것입니다.

속성 이름에 의한 파이프라인 바인딩

Get-ConnectionStatus cmdlet은 지금까지 '127.0.0.1', '192.168.1.100'과 같은 문자열 배열을 허용하여 파이프라인 입력 ByValue(ValueFromPipeline)을 수락하도록 설정되어 있습니다. 입력이 텍스트 파일 대신 CSV 파일에서 받아진 경우 이 기능도 예상대로 작동할까요?

아마도 C:\Test\pc-list.csv에서 아래와 같이 보이는 CSV 파일이 있을 것입니다.

ComputerName,Location
127.0.0.1,London
192.168.1.100,Paris

CSV 파일의 ComputerName 필드가 Get-ConnnectionStatus ComputerName 매개변수와 동일한 이름임을 주의하세요.

CSV를 가져와 파이프라인을 통해 Get-ConnectionStatus에 전달하려고 시도하면 함수가 ComputerName 열에서 예상치 못한 결과를 반환합니다.

Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus

ComputerName                                  Status       
------------                                  ------       
@{ComputerName=127.0.0.1; Location=London}    No Connection
@{ComputerName=192.168.1.100; Location=Paris} No Connection

무엇이 잘못되었는지 추측할 수 있나요? 결국 매개변수 이름은 일치하기 때문에 PowerShell 파이프라인이 Import-CSV에서 반환한 출력을 Get-ConnectionStatusComputerName 매개변수에 바인딩하지 않은 이유는 무엇일까요? 왜냐하면 매개변수 속성 ValueFromPipelineByPropertyName이 필요하기 때문입니다.

현재 함수의 ComputerName 매개변수는 다음과 같은 매개변수 정의를 가지고 있습니다: [Parameter(ValueFromPipeline)]. 따라서 아래와 같이 ValueFromPipelineByPropertyName을 추가하여 ComputerName 매개변수를 ByPropertyName 입력을 지원하도록 설정해야 합니다.

    [CmdletBinding()]
    param
    (
        [Parameter(ValueFromPipeline,ValueFromPipelineByPropertyName)]
        [string[]]$ComputerName
    )

ByPropertyName을 파이프라인 지원으로 추가하면 PowerShell에게 객체 속성 이름과 객체 유형을 살펴보라고 알립니다. 이 변경 사항을 수행한 후에는 예상한 출력이 표시됩니다.

PS> Import-Csv -Path 'C:\Test\pc-list.csv' | Get-ConnectionStatus

ComputerName  Status       
------------  ------       
127.0.0.1     Ok           
192.168.1.100 No Connection

요약

이 튜토리얼에서는 PowerShell 파이프라인의 작동 방식, 매개변수 바인딩 방법 및 PowerShell 파이프라인을 지원하는 자체 함수를 생성하는 방법을 배웠습니다.

함수는 파이프라인 없이 작동할 수 있지만, 객체를 한 명령에서 다른 명령으로 “스트리밍”하고 코드를 단순화할 수는 없습니다.

파이프라인을 준비할 수 있는 함수에 대해 작성한 것이나 작성할 예정인 함수에 대해 생각해 볼 수 있나요?

Source:
https://adamtheautomator.com/ppowershell-pipeline/