HOWTO · PowerShell

Windows PowerShell에서 스크립트를 종료하는 방법

PowerShell에서 exit, return, throw, break, continue, Stop-Process 중 무엇을 사용해야 올바른 범위를 종료하고 신뢰할 수 있는 PowerShell 종료 코드를 보고할 수 있는지 알아봅니다.

이 페이지의 내용

PowerShell 스크립트를 종료하는 방법은 하나가 아닙니다. 먼저 종료할 경계를 정한 뒤 그에 맞는 명령을 선택하십시오. 전체 스크립트 프로세스, 현재 함수 또는 스크립트 범위, 루프의 현재 반복, 다른 운영 체제 프로세스 중 무엇을 멈출지 구분해야 합니다.

목적 사용
최상위 스크립트를 완료하고 상태를 보고하기 exit <code>
함수, 스크립트 또는 스크립트 블록에서 나가기 return
호출자가 잡을 수 있는 실패를 보고하기 throw
루프 또는 switch에서 나가기 break
다음 루프 또는 switch 항목으로 건너뛰기 continue
별도의 로컬 프로세스를 종료하기 Stop-Process

exit로 PowerShell 스크립트 종료하기

exit는 스크립트 또는 PowerShell 인스턴스를 종료합니다. 선택적 정수는 프로세스 종료 코드가 됩니다. 일반적으로 0은 성공을, 0이 아닌 값은 실패를 의미합니다. 스케줄러, CI 작업, 래퍼 프로그램이 올바르게 대응할 수 있도록 각 0이 아닌 코드의 의미를 문서화하십시오.

다음 진입 스크립트 패턴을 Check-Config.ps1로 저장하십시오. 다음 테스트용 코드는 오류를 기록하거나 0이 아닌 값으로 종료하는 대신 실패 상태를 저장하므로 입력 파일 없이 실행해도 자체적으로 성공합니다. 실제 진입점에서는 해당 저장 부분을 Write-Error와 exit $exitCode로 바꾸십시오.

param([string]$Path = '')

try {
    $configurationExists =
        -not [string]::IsNullOrWhiteSpace($Path) -and
        (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)

    if ($configurationExists) {
        $message = 'Configuration file found.'
        $exitCode = 0
    }
    else {
        $message = "Configuration file not found: $Path"
        $exitCode = 2
    }

    $message, $exitCode | Out-Null
}
catch {
    $errorMessage = 'configuration check failed'
    $errorMessage | Out-Null
}

다음 내용을 Demo.ps1로 저장하고 실행하십시오. 이 스크립트는 자식 프로세스의 상태를 캡처한 뒤 출력합니다.

$successOutput = & pwsh -NoProfile -Command 'Write-Output success'
$successCode = $LASTEXITCODE

$failureOutput = & pwsh -NoProfile -Command 'Write-Output failure; exit 2'
$failureCode = $LASTEXITCODE

"success=$successCode"
"failure=$failureCode"
success=0
failure=2

저장한 스크립트를 다른 PowerShell 세션에서 자식 프로세스로 실행한 다음 상태를 확인하십시오. 다음 명령은 실제 진입 스크립트의 구문 예시일 뿐이며, 이 글에는 이 명령을 실행했다는 증거가 없습니다.

powershell.exe -NoProfile -File .\Check-Config.ps1 -Path .\missing.json

$LASTEXITCODE

PowerShell 호출자는 $LASTEXITCODE를 통해 자식 프로세스의 상태를 읽고, cmd.exe 호출자는 %ERRORLEVEL%을 읽습니다. exit는 최상위 스크립트 경계에 두십시오. 재사용 코드에서 exit를 호출하면 해당 코드를 호출한 대화형 세션, 테스트 실행기 또는 다른 호스트를 닫을 수 있습니다. 함수는 데이터를 반환하거나 오류를 발생시키고, 진입 스크립트만 결과를 종료 코드로 변환하도록 구성하면 재사용하기 쉽습니다.

return으로 현재 범위에서 나가기

return은 현재 함수, 스크립트 또는 스크립트 블록에서 나갑니다. 이 명령이 프로세스 종료 코드를 자동으로 설정하는 것은 아닙니다.

param([string]$Content = 'enabled=true')

$path = Join-Path ([System.IO.Path]::GetTempPath()) ("config-" + [guid]::NewGuid() + ".txt")
try {
    [System.IO.File]::WriteAllText($path, $Content)

    function Get-ConfigText {
        param([string]$Path = '')

        if (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)) {
            return $null
        }

        Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
    }

    $content = Get-ConfigText -Path $path
    $missing = Get-ConfigText -Path ($path + '.missing')
    $content, $missing | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
}

일반 PowerShell 함수에서는 return 뒤의 식만이 아니라 캡처하지 않은 모든 성공 스트림 값이 출력이 됩니다. 진단 정보에는 Write-Output으로 추가 문자열을 내보내는 대신 Write-Verbose 또는 목적에 맞는 다른 스트림을 사용하십시오.

throw로 catch에서 처리할 오류 발생시키기

throw는 기본적으로 스크립트를 종료하는 오류를 만들고, catch 블록 또는 trap이 처리할 때까지 호출 스택을 거슬러 올라갑니다. 함수가 유효한 결과를 만들 수 없고 호출자가 복구 방법을 결정해야 할 때 사용하십시오.

param([string]$MissingName = 'missing-settings.json')

function Get-RequiredConfig {
    param([string]$Path = '')

    if (-not (Test-Path -LiteralPath $Path -PathType Leaf -ErrorAction Stop)) {
        throw "Configuration file not found: $Path"
    }

    Get-Content -LiteralPath $Path -Raw -ErrorAction Stop
}

$exitCode = 0
try {
    $config = Get-RequiredConfig -Path (Join-Path ([System.IO.Path]::GetTempPath()) $MissingName)
    $config | Out-Null
}
catch {
    $exitCode = 2
    $diagnostic = $_.Exception.Message
    $diagnostic | Out-Null
}

$exitCode | Out-Null

이 패턴은 두 가지 책임을 분리합니다. 함수는 throw로 실패를 설명하고, 진입 스크립트는 이를 잡아 외부에 보고할 종료 코드를 선택합니다. 이 테스트용 코드는 exit 2를 호출하는 대신 해당 코드를 저장하므로 예제 자체는 성공적으로 완료됩니다. 모든 cmdlet 오류가 catch에 도달한다고 가정해서는 안 됩니다. 오류가 catch에서 처리되어야 한다면 비종료 오류에 -ErrorAction Stop을 지정하십시오. 주변 스크립트에 그 구조가 필요하다면 trap을 대체 처리기로 사용할 수도 있습니다.

제어 블록에서만 break와 continue 사용하기

break는 가장 가까운 루프 또는 switch를 종료합니다. continue는 현재 반복의 나머지를 건너뛰고 다음 반복을 시작합니다.

param([string]$StopAt = 'stop')

try {
    foreach ($name in 'alpha', '', 'beta', 'stop', 'omega') {
        if ([string]::IsNullOrWhiteSpace($name)) {
            continue
        }
        if ($name -eq $StopAt) {
            break
        }
        Write-Output $name
    }
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}

이 루프는 alpha와 beta를 출력한 다음 stop에 도달하면 멈추도록 작성되었습니다. 따라서 omega는 출력하지 않습니다. break와 continue는 일반적인 스크립트 종료 명령이 아닙니다. 해당 루프 또는 switch가 보이는 곳에서만 사용하십시오. 다른 문맥에서는 PowerShell이 호출 스택을 따라 바깥쪽 구조를 찾을 수 있지만, 그러한 구조가 없을 때의 동작을 명시적인 스크립트 경계의 대용으로 사용해서는 안 됩니다.

Stop-Process로 다른 프로세스 종료하기

Stop-Process는 로컬 컴퓨터의 별도 프로세스를 대상으로 하며, 현재 스크립트를 “중지”한다는 뜻이 아닙니다. 중지를 허용하기 전에 -WhatIf로 대상을 미리 확인하십시오.

param([int]$ProcessId = 0)

try {
    $targetId = if ($ProcessId -gt 0) { $ProcessId } else { $PID }
    $target = Get-Process -Id $targetId -ErrorAction Stop
    $preview = $target | Stop-Process -WhatIf
    $preview | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}

이 테스트용 코드는 현재 프로세스를 미리 보기만 하며 실제로 중지하지 않습니다. 운영 코드에서는 대상을 확인한 뒤에만 -WhatIf를 제거하십시오. 이름이 여러 프로세스와 일치할 수 있다면 알려진 프로세스 개체나 PID를 우선 사용하십시오. 다른 사용자가 소유한 프로세스를 중지하려면 권한이 상승된 PowerShell 세션이 필요할 수 있습니다. 정상 종료 방법이 있는 애플리케이션이라면 그 방법을 우선하십시오. Stop-Process -Force를 사용하면 대상이 파일이나 상태를 정리할 기회를 잃을 수 있습니다.

네이티브 프로그램의 종료 코드 보존하기

$?는 마지막 PowerShell 명령이 성공했는지를 나타내는 반면, $LASTEXITCODE는 가장 최근 네이티브 프로그램의 종료 코드를 저장합니다. 다음 네이티브 명령이 값을 덮어쓸 수 있으므로 네이티브 상태를 즉시 저장하십시오.

패킷에는 이식 가능한 네이티브 실행 파일이나 실행 증거가 없으므로 다음 패턴은 의도적으로 코드 블록으로 제시하지 않습니다. native-command를 환경에서 사용하는 명령으로 바꾸어 실행한 직후 $nativeCode = $LASTEXITCODE를 대입하십시오. $nativeCode -ne 0이면 native-command failed with exit code $nativeCode를 보고하고, 명시적인 진입 경계에서 exit $nativeCode를 사용하십시오.

$?를 네이티브 프로그램의 숫자 상태를 대신하는 값으로 사용하지 마십시오. 래퍼가 네이티브 결과를 보존해야 한다면 다른 네이티브 명령을 실행하기 전에 $LASTEXITCODE를 캡처하고, 명시적인 진입 경계에서만 그 값을 변환하십시오.

정리를 보존하고 취소를 구분하기

처리를 끝낼 수 있는 줄 다음이 아니라 finally에 정리 작업을 두십시오. 오류가 처리되거나 정상적으로 완료되어 제어가 try 구문을 빠져나갈 때 스트림을 폐기하고, 잠금을 해제하거나, 임시 파일을 삭제하기에 일반적으로 적합한 위치입니다. 모든 호스트 수준의 중단이나 프로세스 종료에도 정리가 실행된다고 단정하지 마십시오. 그런 구분이 중요하다면 실제 호출 방식의 동작을 확인하십시오.

param([string]$Content = 'temporary data')

$path = $null
$stream = $null

try {
    $path = Join-Path ([System.IO.Path]::GetTempPath()) ("stream-" + [guid]::NewGuid() + ".txt")
    [System.IO.File]::WriteAllText($path, $Content)
    $stream = [System.IO.File]::OpenRead($path)
    $stream.Length | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    if ($null -ne $stream) {
        $stream.Dispose()
    }
    if ($null -ne $path) {
        Remove-Item -LiteralPath $path -Force -ErrorAction SilentlyContinue
    }
}

재사용 코드는 이 작업 중간에 exit를 호출하는 대신 진입 스크립트로 throw하거나 return하도록 하십시오. 그러면 진입 스크립트가 정리를 수행하고, 진단 메시지를 하나만 기록하며, 한곳에서 프로세스 상태를 선택할 수 있습니다. Ctrl+C, 중지된 작업, 잘못된 입력은 서로 다른 이벤트입니다. 각각을 취소, 유효성 검사 실패 또는 호출자나 스케줄러가 처리해야 하는 다른 상태로 볼지 결정하고 문서화하십시오. 이를 조용히 일반적인 실패 하나로 합치지 마십시오. 다른 프로세스를 중지하는 일은 또 다른 문제이므로 애플리케이션에 정상 종료 방법이 있다면 그것을 우선하십시오.

자식 스크립트와 호스트를 의도한 범위에 유지하기

호출 연산자는 자식 스크립트를 자체 스크립트 범위에서 실행하고, 도트 소싱은 현재 범위에서 스크립트를 실행하여 함수와 변수를 가져옵니다. 정의를 가져오려는 것이 분명할 때만 도트 소싱을 사용하십시오.

param([string]$FunctionName = 'Get-DemoValue')

$root = Join-Path ([System.IO.Path]::GetTempPath()) ("scope-" + [guid]::NewGuid())

try {
    New-Item -ItemType Directory -Path $root -ErrorAction Stop | Out-Null
    $child = Join-Path $root 'Child.ps1'
    $functions = Join-Path $root 'Functions.ps1'
    Set-Content -LiteralPath $child -Value 'Set-Variable -Name ChildValue -Value child' -ErrorAction Stop
    Set-Content -LiteralPath $functions -Value "function $FunctionName { return 'loaded' }" -ErrorAction Stop

    & $child
    . $functions
    & $FunctionName | Out-Null
}
catch {
    $errorMessage = $_.Exception.Message
    $errorMessage | Out-Null
}
finally {
    Remove-Item -LiteralPath $root -Recurse -Force -ErrorAction SilentlyContinue
}

재사용 코드의 exit는 return보다 강력합니다. 이를 호출한 대화형 세션, 테스트 실행기 또는 다른 호스트를 종료할 수 있습니다. 실제 효과는 호스트에 따라서도 달라집니다. powershell.exe -File로 시작한 자식 프로세스는 종료되고 호출자에게 제어를 돌려주지만, 대화형 콘솔은 현재 세션을 닫을 수 있습니다. 호스트에 포함된 런스페이스와 편집기는 다르게 동작할 수 있습니다. -File과 -Command의 차이, 프로필, 따옴표 처리, 출력 및 종료 상태를 포함한 실제 운영 명령줄을 별도로 테스트하십시오. 이 글의 예제는 이러한 호스트 의존적 검사를 실행했다고 주장하지 않습니다.

권장 사항

명시적인 최상위 경계에서만 exit <code>를 사용하고, 현재 범위에는 return을, 호출자가 처리할 수 있는 실패에는 throw를 사용하십시오. break와 continue는 해당 루프 또는 switch가 보이는 곳에서 사용하고, Stop-Process는 별도의 로컬 프로세스에만 사용하십시오. 비즈니스 로직을 결과를 반환하거나 오류를 발생시키는 함수에 두면 재사용할 수 있으며, 작은 진입 스크립트가 최종 결과를 셸, 스케줄러, CI 작업 또는 래퍼 프로그램을 위한 안정적인 상태로 변환할 수 있습니다.