about_Pipeline_Chain_Operators
about_Pipeline_Chain_Operators
명령을 이어 붙일 때 — 앞 명령이 성공했을 때만 다음 명령을 실행하고 싶다거나, 반대로 실패했을 때만 특정 명령을 처리하고 싶은 상황을 겪어보셨죠? bash나 zsh, sh 같은 POSIX 셸에서 &&와 ||를 써본 분이라면 익숙한 그 패턴을, PowerShell에서는 7 버전부터 스크립트 안에서 바로 쓸 수 있어요. 이 글에서는 파이프라인 체인 연산자(Pipeline Chain Operators)를 이용해 명령을 조건부로 연결하는 방법을 하나씩 풀어볼게요.
본문
짧은 설명
&&와 || 연산자로 파이프라인을 조건에 따라 연결하는 방법을 설명해요.
자세한 설명
PowerShell 7부터 PowerShell은 &&와 || 연산자를 통해 파이프라인을 조건부로 연결할 수 있어요. 이 두 연산자를 PowerShell에서는 파이프라인 체인 연산자라고 불러요. bash, zsh, sh 같은 POSIX 셸의 AND-OR 리스트나, Windows 명령 셸(cmd.exe)의 조건부 처리 기호와 비슷한 개념이죠.
동작 방식부터 짚어볼게요. &&는 왼쪽 파이프라인이 성공했을 때 오른쪽 파이프라인을 실행하고, 반대로 ||는 왼쪽 파이프라인이 실패했을 때 오른쪽 파이프라인을 실행해요.
이 연산자들은 파이프라인이 실패했는지를 판단할 때 $?와 $LASTEXITCODE 변수를 사용해요. 덕분에 cmdlet이나 함수뿐 아니라 네이티브 명령에도 그대로 쓸 수 있답니다. 예를 들어볼게요.
# Create an SSH key pair - if successful copy the public key to clipboard
ssh-keygen -t rsa -b 2048 && Get-Content -Raw ~\.ssh\id_rsa.pub | clip
예제
두 명령이 모두 성공하는 경우를 먼저 보죠. &&라서 왼쪽이 성공했으니 오른쪽도 실행돼요.
Write-Output 'First' && Write-Output 'Second'
First
Second
첫 번째 명령이 실패하면 두 번째는 실행되지 않아요. Write-Error는 오류를 내면서 실패한 것으로 처리되거든요.
Write-Error 'Bad' && Write-Output 'Second'
Write-Error: Bad
이번엔 ||를 볼게요. 첫 번째 명령이 성공했으니, 두 번째 명령은 실행되지 않아요.
Write-Output 'First' || Write-Output 'Second'
First
반대로 첫 번째 명령이 실패하면, 두 번째 명령이 실행돼요.
Write-Error 'Bad' || Write-Output 'Second'
Write-Error: Bad
Second
파이프라인의 성공 여부는 $? 변수의 값으로 정해져요. PowerShell은 파이프라인이 실행된 뒤 실행 상태에 따라 $?를 자동으로 설정하죠. 이를 기준으로 보면, 파이프라인 체인 연산자는 다음과 같은 의미와 같아요.
Test-Command '1' && Test-Command '2'
는 다음과 같고,
Test-Command '1'; if ($?) { Test-Command '2' }
Test-Command '1' || Test-Command '2'
는 다음과 같아요.
Test-Command '1'; if (-not $?) { Test-Command '2' }
파이프라인 체인에서의 할당
파이프라인 체인에서 변수에 값을 할당하면, 체인에 있는 모든 파이프라인의 결과가 이어 붙은 값을 받아요.
$result = Write-Output '1' && Write-Output '2'
$result
1
2
만약 파이프라인 체인에서 값을 할당하는 도중 스크립트 종료 오류(script-terminating error)가 발생하면, 할당 자체는 성공하지 않아요.
try
{
$result = Write-Output 'Value' && $(throw 'Bad')
}
catch
{
# Do nothing, just squash the error
}
"Result: $result"
Result:
연산자 구문과 우선순위
주의할 점 하나를 먼저 짚을게요. 다른 연산자와 달리 &&와 ||는 +나 -and 같은 **식(expression)**이 아니라 파이프라인을 대상으로 동작해요.
우선순위도 정리해보면, &&와 ||는 파이핑(|)이나 리다이렉션(>)보다는 낮은, 하지만 작업 연산자(&), 할당(=), 세미콜론(;)보다는 높은 우선순위를 가져요. 이 말은 곧, 파이프라인 체인 안에 있는 파이프라인을 각각 따로 리다이렉트할 수 있고, 체인 전체를 백그라운드 작업으로 돌리거나 변수에 할당하거나 하나의 문(statement)으로 구분할 수 있다는 뜻이에요.
파이프라인 체인 안에서 더 낮은 우선순위의 구문을 쓰고 싶다면 괄호 (...)를 고려해보세요. 마찬가지로 체인 안에 문을 끼워 넣고 싶을 때는 부분식 $(...)를 쓸 수 있어요. 네이티브 명령을 제어 흐름과 함께 조합할 때 특히 유용하죠.
foreach ($file in 'file1','file2','file3')
{
# When find succeeds, the loop breaks
find $file && Write-Output "Found $file" && $(break)
}
find: file1: No such file or directory
file2
Found file2
PowerShell 7부터는 이런 구문들의 동작이 바뀌어서, 괄호나 부분식 안에서 명령이 성공하거나 실패할 때 $?가 기대한 대로 설정돼요.
또한 PowerShell의 다른 대부분의 연산자처럼, &&와 ||도 왼쪽 결합(left-associative)이라 왼쪽부터 묶여요. 예를 들어 보죠.
Get-ChildItem -Path ./file.txt ||
Write-Error "file.txt doesn't exist" &&
Get-Content -Raw ./file.txt
는 다음과 같이 묶여서,
(Get-ChildItem -Path ./file.txt || Write-Error "file.txt doesn't exist") &&
Get-Content -Raw ./file.txt
결국 다음과 같은 의미가 돼요.
Get-ChildItem -Path ./file.txt
if (-not $?) { Write-Error "file.txt does not exist" }
if ($?) { Get-Content -Raw ./file.txt }
오류와의 상호작용
파이프라인 체인 연산자는 오류를 흡수하지 않아요. 체인 안의 문이 스크립트 종료 오류를 던지면, 그 순간 파이프라인 체인 전체가 종료돼요. 예를 들면 이렇죠.
$(throw 'Bad') || Write-Output '2'
Exception: Bad
심지어 오류를 catch로 잡아도, 체인 자체는 계속 종료된 상태예요.
try
{
$(throw 'Bad') || Write-Output '2'
}
catch
{
Write-Output "Caught: $_"
}
Write-Output 'Done'
Caught: Bad
Done
반면 오류가 비종료 오류(non-terminating)이거나 파이프라인 하나만 종료시키는 수준이라면, 체인은 $?의 값을 존중하며 계속 진행돼요.
function Test-NonTerminatingError
{
[CmdletBinding()]
param()
$exception = [System.Exception]::new('BAD')
$errorId = 'BAD'
$errorCategory = 'NotSpecified'
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
$exception, $errorId, $errorCategory, $null
)
$PSCmdlet.WriteError($errorRecord)
}
Test-NonTerminatingError || Write-Output 'Second'
Test-NonTerminatingError: BAD
Second
명령이 아니라 파이프라인을 연결하기
파이프라인 체인 연산자는 이름 그대로, 단순히 명령이 아니라 파이프라인을 연결할 수 있어요. 다른 셸과도 일치하는 동작이지만, 파이프라인 수준에서 보면 성공 여부를 판단하기가 조금 더 까다로워질 수 있어요.
function Test-NotTwo
{
[CmdletBinding()]
param(
[Parameter(ValueFromPipeline)]
$Input
)
process
{
if ($Input -ne 2)
{
return $Input
}
$exception = [System.Exception]::new('Input is 2')
$errorId = 'InputTwo'
$errorCategory = 'InvalidData'
$errorRecord = [System.Management.Automation.ErrorRecord]::new(
$exception, $errorId, $errorCategory, $null
)
$PSCmdlet.WriteError($errorRecord)
}
}
1,2,3 | Test-NotTwo && Write-Output 'All done!'
1
Test-NotTwo : Input is 2
3
여기서 Write-Output 'All done!'가 실행되지 않는다는 점을 눈여겨보세요. Test-NotTwo가 비종료 오류를 낸 뒤 실패한 것으로 간주되기 때문이에요.