PowerShell 파이프라인

PowerShell 파이프라인 (about_Pipelines)

여러 명령어를 |로 이어서 연결하면, 앞 명령어의 결과가 자동으로 다음 명령어로 흘러 들어가요. 이걸 파이프라인(pipeline)이라고 해요. PowerShell에서 아주 자주 쓰는 개념이라, 이 문서에서 하나씩 차근차근 설명해 드릴게요.

출처: Microsoft Learn — about_Pipelines

본문

짧은 설명 (Short description)

PowerShell에서 명령어들을 파이프라인으로 묶어 사용하는 방법을 다뤄요.

긴 설명 (Long description)

파이프라인은 파이프 연산자(|)(ASCII 124)로 연결된 일련의 명령어예요. 파이프 연산자는 앞 명령어의 결과를 다음 명령어로 보내 줘요.

첫 번째 명령어의 출력은 두 번째 명령어의 입력으로 처리되고, 그 출력은 다시 또 다른 명령어로 보낼 수 있어요. 그 결과로, 단순한 명령어들이 모여 복잡한 명령 체인, 즉 파이프라인이 되는 거예요.

예를 들어 볼게요.

Command-1 | Command-2 | Command-3

이 예시에서 Command-1이 내보낸 개체들이 Command-2로 전달돼요. Command-2는 그 개체들을 처리해서 Command-3으로 보내고, Command-3은 개체들을 처리해 다시 파이프라인 아래로 내려 보내요. 이제 파이프라인에 남은 명령어가 없으니, 최종 결과가 콘솔에 표시돼요.

파이프라인 안에서는 명령어가 왼쪽에서 오른쪽 순서로 처리돼요. 이 처리는 하나의 작업으로 묶여서 실행되고, 출력은 만들어지는 대로 바로 표시돼요.

아주 간단한 예시를 하나 들어 볼게요. 다음 명령어는 메모장(Notepad) 프로세스를 가져와서 그다음에 그 프로세스를 중지시켜요.

예를 들어,

Get-Process notepad | Stop-Process

첫 번째 명령어는 Get-Process cmdlet으로 메모장 프로세스를 나타내는 개체를 가져와요. 그리고 파이프 연산자(|)로 그 프로세스 개체를 Stop-Process cmdlet에 보내서 메모장 프로세스를 중지시켜요. 여기서 눈여겨볼 점이 있어요. Stop-Process 명령어에는 프로세스를 지정하는 Name이나 Id 매개 변수가 없어요. 그 이유는 지정할 프로세스가 파이프라인을 통해 이미 들어왔기 때문이에요.

이번 예시는 현재 디렉터리에서 텍스트 파일을 가져오되, 10,000바이트보다 큰 파일만 골라서 길이순으로 정렬한 뒤, 각 파일의 이름과 길이를 표로 보여 줘요.

Get-ChildItem -Path *.txt |
 Where-Object {$_.Length -gt 10000} |
 Sort-Object -Property Length |
 Format-Table -Property Name, Length

이 파이프라인은 네 개의 명령어가 순서대로 연결된 구조예요. 아래 그림은 각 명령어가 처리한 결과가 다음 명령어로 전달되는 모습을 보여 줘요.

Get-ChildItem -Path *.txt
| (FileInfo objects for *.txt)
V
Where-Object {$_.Length -gt 10000}
| (FileInfo objects for *.txt)
| ( Length > 10000 )
V
Sort-Object -Property Length
| (FileInfo objects for *.txt)
| ( Length > 10000 )
| ( Sorted by length )
V
Format-Table -Property Name, Length
| (FileInfo objects for *.txt)
| ( Length > 10000 )
| ( Sorted by length )
| ( Formatted in a table )
V

Name Length
---- ------
tmp1.txt 82920
tmp2.txt 114000
tmp3.txt 114000

파이프라인 사용하기 (Using pipelines)

대부분의 PowerShell cmdlet은 파이프라인을 지원하도록 설계돼 있어요. 대부분의 경우 Get cmdlet의 결과를 같은 명사(noun)의 다른 cmdlet으로 파이프로 보낼 수 있어요. 예를 들어 Get-Service cmdlet의 출력을 Start-ServiceStop-Service cmdlet으로 파이프로 보낼 수 있죠.

이 예시는 컴퓨터에서 WMI 서비스를 시작해요.

Get-Service wmi | Start-Service

다른 예로, PowerShell 레지스트리 공급자(Registry provider) 안에서 Get-Item이나 Get-ChildItem의 출력을 New-ItemProperty cmdlet으로 파이프로 보낼 수 있어요. 이 예시는 MyCompany 레지스트리 키에 값이 8124인 NoOfEmployees라는 새 레지스트리 항목을 추가해요.

Get-Item -Path HKLM:\Software\MyCompany |
 New-ItemProperty -Name NoOfEmployees -Value 8124

Get-Member, Where-Object, Sort-Object, Group-Object, Measure-Object 같은 많은 유틸리티 cmdlet은 거의 파이프라인 안에서만 사용돼요. 이런 cmdlet에는 어떤 개체 유형이든 파이프로 보낼 수 있어요. 이 예시는 컴퓨터의 모든 프로세스를 각 프로세스가 연 열린 핸들(handle) 수를 기준으로 정렬하는 방법을 보여 줘요.

Get-Process | Sort-Object -Property Handles

개체를 서식(formatting), 내보내기(export), 출력(output) cmdlet에도 파이프로 보낼 수 있어요. 대표적으로 Format-List, Format-Table, Export-Clixml, Export-Csv, Out-File 같은 것들이죠.

이 예시는 Format-List cmdlet을 사용해 프로세스 개체의 속성 목록을 표시하는 방법을 보여 줘요.

Get-Process winlogon | Format-List -Property *

또한 네이티브 명령어(native command)의 출력을 PowerShell cmdlet으로 파이프로 보낼 수도 있어요. 예를 들어:

PS> ipconfig.exe | Select-String -Pattern 'IPv4'

 IPv4 Address. . . . . . . . . . . : 172.24.80.1
 IPv4 Address. . . . . . . . . . . : 192.168.1.45
 IPv4 Address. . . . . . . . . . . : 100.64.108.37

중요 — 성공(Success) 스트림과 오류(Error) 스트림은 다른 셸의 stdoutstderr 스트림과 비슷해요. 하지만 stdin은 입력을 위해 PowerShell 파이프라인에 연결되지 않아요. 자세한 내용은 about_Redirection을 참고해 주세요.

조금만 연습하면, 단순한 명령어들을 파이프라인으로 묶는 게 시간도 줄여 주고 타이핑도 줄여 주면서 스크립팅을 훨씬 효율적으로 만들어 준다는 걸 느끼게 될 거예요.

파이프라인은 어떻게 동작하나요? (How pipelines work)

이번 절에서는 파이프라인 실행 중에 입력 개체가 cmdlet 매개 변수에 어떻게 바인딩되고 처리되는지 설명해 드릴게요.

파이프라인 입력 받기 (Accepts pipeline input)

파이프라인을 지원하려면, 받는 쪽 cmdlet에 파이프라인 입력을 받는 매개 변수가 반드시 있어야 해요. Get-Help 명령어에 Full 또는 Parameter 옵션을 사용하면 특정 cmdlet의 어떤 매개 변수가 파이프라인 입력을 받는지 알 수 있어요.

예를 들어 Start-Service cmdlet의 매개 변수 중 어떤 것이 파이프라인 입력을 받는지 확인하려면 이렇게 입력해요.

Get-Help Start-Service -Full

또는

Get-Help Start-Service -Parameter *

Start-Service cmdlet의 도움말을 보면 InputObjectName 매개 변수만 파이프라인 입력을 받는 걸 확인할 수 있어요.

-InputObject <ServiceController[]>
Specifies ServiceController objects representing the services to be started.
Enter a variable that contains the objects, or type a command or expression
that gets the objects.

Required? true
Position? 0
Default value None
Accept pipeline input? True (ByValue)
Accept wildcard characters? false

-Name <String[]>
Specifies the service names for the service to be started.

The parameter name is optional. You can use Name or its alias, ServiceName,
or you can omit the parameter name.

Required? true
Position? 0
Default value None
Accept pipeline input? True (ByPropertyName, ByValue)
Accept wildcard characters? false

개체를 파이프라인을 통해 Start-Service로 보내면, PowerShell은 그 개체들을 InputObjectName 매개 변수에 연결하려고 시도해요.

파이프라인 입력을 받는 방법 (Methods of accepting pipeline input)

cmdlet 매개 변수는 다음 두 가지 방식 중 하나로 파이프라인 입력을 받을 수 있어요.

  • ByValue: 매개 변수가 예상하는 .NET 유형과 일치하거나 그 유형으로 변환할 수 있는 값을 받아요. 예를 들어 Start-ServiceName 매개 변수는 값을 기준(ByValue)으로 파이프라인 입력을 받아요. 문자열 개체나 문자열로 변환할 수 있는 개체를 받을 수 있죠.

  • ByPropertyName: 입력 개체에 매개 변수와 같은 이름의 속성이 있을 때만 매개 변수가 입력을 받아요. 예를 들어 Start-ServiceName 매개 변수는 Name 속성을 가진 개체를 받을 수 있어요. 개체의 속성 목록을 보려면 그 개체를 Get-Member로 파이프로 보내면 돼요.

일부 매개 변수는 값(ByValue)으로든 속성 이름(ByPropertyName)으로든 개체를 받을 수 있어서, 파이프라인에서 입력을 받기 더 쉬워져요.

매개 변수 바인딩 (Parameter binding)

한 명령어에서 다른 명령어로 개체를 파이프로 보내면, PowerShell은 파이프로 들어온 개체를 받는 쪽 cmdlet의 매개 변수에 연결하려고 해요.

PowerShell의 매개 변수 바인딩(parameter binding) 구성 요소는 다음 기준에 따라 입력 개체를 cmdlet 매개 변수에 연결해요.

  • 매개 변수는 파이프라인에서 입력을 받을 수 있어야 해요.
  • 매개 변수는 보내는 개체의 유형을 받거나, 그 유형으로 변환할 수 있어야 해요.
  • 매개 변수가 명령어에서 아직 사용되지 않았어야 해요.

예를 들어 Start-Service cmdlet에는 매개 변수가 많지만, 그중 NameInputObject 두 개만 파이프라인 입력을 받아요. Name 매개 변수는 문자열을 받고, InputObject 매개 변수는 서비스 개체를 받아요. 따라서 문자열, 서비스 개체, 그리고 문자열이나 서비스 개체로 변환할 수 있는 속성을 가진 개체를 파이프로 보낼 수 있어요.

PowerShell은 매개 변수 바인딩을 최대한 효율적으로 처리해요. 특정 매개 변수에 바인딩되도록 제안하거나 강제할 수는 없어요. PowerShell이 파이프로 들어온 개체를 바인딩하지 못하면 명령어는 실패해요.

바인딩 오류를 해결하는 방법에 대한 자세한 내용은 이 문서의 뒷부분인 파이프라인 오류 조사하기를 참고해 주세요.

한 번에 하나씩 처리하기 (One-at-a-time processing)

개체를 명령어로 파이프로 보내는 건, 개체를 명령어의 매개 변수로 넘겨주는 것과 상당히 비슷해요. 파이프라인 예시를 하나 볼게요. 이 예시에서는 파이프라인을 사용해 서비스 개체들의 표를 표시해요.

Get-Service | Format-Table -Property Name, DependentServices

기능적으로 보면, 이건 Format-TableInputObject 매개 변수를 사용해 개체 컬렉션을 제출하는 것과 같아요.

예를 들어, 서비스 컬렉션을 변수에 저장해서 InputObject 매개 변수로 전달할 수 있어요.

$services = Get-Service
Format-Table -InputObject $services -Property Name, DependentServices

아니면 명령어를 InputObject 매개 변수 안에 직접 넣을 수도 있어요.

Format-Table -InputObject (Get-Service) -Property Name, DependentServices

하지만 여기엔 중요한 차이점이 있어요. 여러 개체를 명령어로 파이프로 보내면, PowerShell은 그 개체들을 한 번에 하나씩 명령어에 보내요. 반면 명령어 매개 변수를 사용하면 개체들이 하나의 배열 개체로 한꺼번에 보내져요. 이 작은 차이가 결과적으로는 꽤 큰 영향을 줘요.

파이프라인을 실행할 때, PowerShell은 IEnumerable 인터페이스나 그 제네릭 버전을 구현하는 모든 유형을 자동으로 열거(enumerate)해요. 열거된 항목들은 한 번에 하나씩 파이프라인을 통해 보내져요. PowerShell은 System.Data.DataTable 유형도 Rows 속성을 통해 열거해요.

자동 열거에는 몇 가지 예외가 있어요.

  • 해시 테이블(hash table), IDictionary 인터페이스나 그 제네릭 버전을 구현하는 유형, 그리고 System.Xml.XmlNode 유형은 GetEnumerator() 메서드를 직접 호출해야 해요.
  • System.String 클래스는 IEnumerable을 구현하지만, PowerShell은 문자열 개체를 열거하지 않아요.

다음 예시에서는 배열과 해시 테이블을 Measure-Object cmdlet으로 파이프로 보내서 파이프라인에서 받은 개체 수를 세어 봐요. 배열은 여러 요소를 갖고 있고, 해시 테이블은 여러 키-값 쌍을 갖고 있어요. 한 번에 하나씩 열거되는 건 배열뿐이에요.

@(1,2,3) | Measure-Object
Count : 3
Average :
Sum :
Maximum :
Minimum :
Property :
@{"One"=1;"Two"=2} | Measure-Object
Count : 1
Average :
Sum :
Maximum :
Minimum :
Property :

마찬가지로 Get-Process cmdlet에서 여러 프로세스 개체를 Get-Member cmdlet으로 파이프로 보내면, PowerShell은 각 프로세스 개체를 하나씩 Get-Member로 보내요. Get-Member는 프로세스 개체의 .NET 클래스(유형)와 그 속성, 메서드를 보여 줘요.

Get-Process | Get-Member
TypeName: System.Diagnostics.Process

Name MemberType Definition
---- ---------- ----------
Handles AliasProperty Handles = Handlecount
Name AliasProperty Name = ProcessName
NPM AliasProperty NPM = NonpagedSystemMemorySize
...

참고Get-Member는 중복을 제거해요. 그래서 개체들이 모두 같은 유형이면 한 가지 개체 유형만 표시해요.

하지만 Get-MemberInputObject 매개 변수를 사용하면, Get-MemberSystem.Diagnostics.Process 개체들의 배열을 하나의 단위로 받아요. 그리고 그때는 개체 배열의 속성을 보여 줘요. (System.Object 유형 이름 뒤에 붙은 배열 기호([])를 눈여겨보세요.)

예를 들어,

Get-Member -InputObject (Get-Process)
TypeName: System.Object[]

Name MemberType Definition
---- ---------- ----------
Count AliasProperty Count = Length
Address Method System.Object& Address(Int32 )
Clone Method System.Object Clone()
...

이 결과는 처음 보면 의도한 것과 다를 수 있어요. 하지만 이해하고 나면 써먹을 수 있어요. 예를 들어 모든 배열 개체에는 Count 속성이 있어요. 그걸 이용해 컴퓨터에서 실행 중인 프로세스의 수를 셀 수 있죠.

예를 들어,

(Get-Process).Count

파이프라인을 통해 보내지는 개체들은 한 번에 하나씩 전달된다는 점을 꼭 기억해 두세요.

파이프라인에서 네이티브 명령어 사용하기 (Using native commands in the pipeline)

PowerShell에서는 네이티브 외부 명령어를 파이프라인에 포함할 수 있어요.

PowerShell 7.4 이전에는, 원시 바이트 데이터를 출력하는 네이티브 프로그램의 출력을 파이프로 보내거나 리디렉션하면 그 출력이 .NET 문자열로 변환됐어요. 이 변환 때문에 원시 데이터 출력이 손상되는 문제가 있었어요.

PowerShell 7.4 이상에서는 PSNativeCommandPreserveBytePipe 실험 기능이 정식 기능이 됐어요. 이 기능은 네이티브 명령어의 stdout 스트림을 파일로 리디렉션하거나, 바이트 스트림 데이터를 네이티브 명령어의 stdin 스트림으로 파이프로 보낼 때 바이트 스트림 데이터를 그대로 보존해 줘요.

예를 들어 네이티브 명령어인 curl을 사용하면 이진 파일을 다운로드해서 리디렉션으로 디스크에 저장할 수 있어요.

$uri = 'https://github.com/PowerShell/PowerShell/releases/download/v7.3.4/powershell-7.3.4-linux-arm64.tar.gz'

# native command redirected to a file
curl -s -L $uri > powershell.tar.gz

또한 바이트 스트림 데이터를 다른 네이티브 명령어의 stdin 스트림으로 파이프로 보낼 수도 있어요. 다음 예시는 curl로 압축된 TAR 파일을 다운로드해요. 다운로드한 파일 데이터는 tar 명령어로 스트리밍되어 아카이브의 내용이 추출돼요.

# native command output piped to a native command
curl -s -L $uri | tar -xzvf - -C .

PowerShell 명령어의 바이트 스트림 출력을 네이티브 명령어의 입력으로 파이프로 보낼 수도 있어요. 다음 예시들은 Invoke-WebRequest를 사용해 앞선 예시와 같은 TAR 파일을 다운로드해요.

# byte stream piped to a native command
(Invoke-WebRequest $uri).Content | tar -xzvf - -C .

# bytes piped to a native command (all at once as byte[])
,(Invoke-WebRequest $uri).Content | tar -xzvf - -C .

stderr 출력을 stdout으로 리디렉션할 때는 이 기능이 바이트 스트림 데이터를 지원하지 않아요. stderrstdout 스트림을 함께 결합하면, 결합된 스트림은 문자열 데이터로 취급돼요.

파이프라인 오류 조사하기 (Investigating pipeline errors)

PowerShell이 파이프로 들어온 개체를 받는 쪽 cmdlet의 매개 변수에 연결하지 못하면, 명령어는 실패해요.

다음 예시에서는 레지스트리 항목을 한 레지스트리 키에서 다른 키로 옮기려고 해요. Get-Item cmdlet이 대상 경로를 가져오고, 그 경로를 Move-ItemProperty cmdlet으로 파이프로 보내요. Move-ItemProperty 명령어에는 옮길 레지스트리 항목의 현재 경로와 이름을 지정해 줘요.

Get-Item -Path HKLM:\Software\MyCompany\sales |
Move-ItemProperty -Path HKLM:\Software\MyCompany\design -Name product

명령어가 실패하면 PowerShell이 다음과 같은 오류 메시지를 표시해요.

Move-ItemProperty : The input object can't be bound to any parameters for
the command either because the command doesn't take pipeline input or the
input and its properties do not match any of the parameters that take
pipeline input.
At line:1 char:23
+ $a | Move-ItemProperty <<<< -Path HKLM:\Software\MyCompany\design -Name p

원인을 조사하려면 Trace-Command cmdlet으로 PowerShell의 매개 변수 바인딩 구성 요소를 추적해 보면 돼요. 다음 예시는 파이프라인이 실행되는 동안 매개 변수 바인딩을 추적해요. PSHost 매개 변수는 추적 결과를 콘솔에 표시하고, FilePath 매개 변수는 추적 결과를 나중에 참고할 수 있도록 debug.txt 파일에 보내요.

Trace-Command -Name ParameterBinding -PSHost -FilePath debug.txt -Expression {
 Get-Item -Path HKLM:\Software\MyCompany\sales |
 Move-ItemProperty -Path HKLM:\Software\MyCompany\design -Name product
}

추적 결과는 길지만, Get-Item cmdlet에 바인딩되는 값들과 그다음 Move-ItemProperty cmdlet에 바인딩되는 명명된 값들을 보여 줘요.

...
BIND NAMED cmd line args [`Move-ItemProperty`]
BIND arg [HKLM:\Software\MyCompany\design] to parameter [Path]
...
BIND arg [product] to parameter [Name]
...
BIND POSITIONAL cmd line args [`Move-ItemProperty`]
...

마지막으로, 경로를 Move-ItemPropertyDestination 매개 변수에 바인딩하려는 시도가 실패한 모습을 보여 줘요.

...
BIND PIPELINE object to parameters: [`Move-ItemProperty`]
PIPELINE object TYPE = [Microsoft.Win32.RegistryKey]
RESTORING pipeline parameter's original values
Parameter [Destination] PIPELINE INPUT ValueFromPipelineByPropertyName NO
COERCION
Parameter [Credential] PIPELINE INPUT ValueFromPipelineByPropertyName NO
COERCION
...

Get-Help cmdlet을 사용해 Destination 매개 변수의 특성을 살펴볼게요.

Get-Help Move-ItemProperty -Parameter Destination

-Destination <String>
 Specifies the path to the destination location.

 Required? true
 Position? 1
 Default value None
 Accept pipeline input? True (ByPropertyName)
 Accept wildcard characters? false

결과를 보면 Destination은 "속성 이름 기준(ByPropertyName)"으로만 파이프라인 입력을 받는 걸 알 수 있어요. 그러니까 파이프로 들어오는 개체에는 Destination이라는 속성이 반드시 있어야 해요.

Get-MemberGet-Item에서 나오는 개체의 속성을 확인해 볼게요.

Get-Item -Path HKLM:\Software\MyCompany\sales | Get-Member

출력을 보면 그 항목은 Destination 속성이 없는 Microsoft.Win32.RegistryKey 개체라는 걸 알 수 있어요. 그래서 명령어가 실패한 거죠.

반면 Path 매개 변수는 이름이나 값 기준으로 파이프라인 입력을 받아요.

Get-Help Move-ItemProperty -Parameter Path

-Path <String[]>
 Specifies the path to the current location of the property. Wildcard
 characters are permitted.

 Required? true
 Position? 0
 Default value None
 Accept pipeline input? True (ByPropertyName, ByValue)
 Accept wildcard characters? true

명령어를 고치려면, 대상 경로를 Move-ItemProperty cmdlet에 지정하고 Get-Item으로 옮길 항목의 Path를 가져오면 돼요.

예를 들어,

Get-Item -Path HKLM:\Software\MyCompany\design |
Move-ItemProperty -Destination HKLM:\Software\MyCompany\sales -Name product

내장 줄 연속 (Intrinsic line continuation)

앞에서 이미 다뤘듯이, 파이프라인은 파이프 연산자(|)로 연결된 일련의 명령어로, 보통 한 줄에 작성돼요. 하지만 가독성을 위해 PowerShell은 파이프라인을 여러 줄로 나눠 쓸 수 있게 해 줘요. 줄의 마지막 토큰이 파이프 연산자이면, PowerShell 파서는 다음 줄을 현재 명령어에 이어 붙여 파이프라인 구성을 계속해요.

예를 들어, 다음 한 줄짜리 파이프라인은

Command-1 | Command-2 | Command-3

이렇게도 쓸 수 있어요.

Command-1 |
 Command-2 |
 Command-3

이어지는 줄의 앞쪽 공백은 의미가 없어요. 들여쓰기는 가독성을 높이기 위해 쓰는 것뿐이에요.

PowerShell 7에서는 줄의 시작 부분에 파이프 문자를 놓아서 파이프라인을 계속하는 것도 지원해요. 다음 예시들은 이 새로운 기능을 사용하는 방법을 보여 줘요.

# Wrapping with a pipe at the beginning of a line (no backtick required)
Get-Process | Where-Object CPU | Where-Object Path
 | Get-Item | Where-Object FullName -Match "AppData"
 | Sort-Object FullName -Unique

# Wrapping with a pipe on a line by itself
Get-Process | Where-Object CPU | Where-Object Path
 |
 Get-Item | Where-Object FullName -Match "AppData"
 |
 Sort-Object FullName -Unique

중요 — 셸에서 대화형으로 작업할 때, 줄 시작에 파이프라인이 있는 코드를 붙여넣는 건 Ctrl+V로만 붙여넣어야 해요. 마우스 오른쪽 버튼으로 붙여넣으면 줄이 한 줄씩 삽입돼요. 그 줄이 파이프 문자로 끝나지 않으므로, PowerShell은 입력이 완료된 것으로 보고 그 줄을 입력된 그대로 실행해요.

더 알아보기