본문으로 건너뛰기

PowerShell 다시 배우기 8 - 함수와 매개변수

PowerShell 다시 배우기 8/11

2010년의 첫 PowerShell 글은 하위 폴더의 .svn 폴더를 모두 지우는 스크립트였다. Subversion은 거의 쓰지 않게 됐지만 같은 일은 지금도 한다. 대상이 node_modules, bin, obj로 바뀌었을 뿐이다. 그 스크립트를 지금의 방식으로 다시 만들면서 함수와 매개변수를 본다.

가장 짧은 꼴

function Remove-BuildFolder {
    param($Path)

    Get-ChildItem $Path -Directory -Recurse -Force |
        Where-Object Name -EQ 'node_modules' |
        Remove-Item -Recurse -Force
}

돌기는 한다. 그러나 $Path를 빼먹으면 현재 폴더에서 조용히 지우기 시작하고, 무엇을 지울지 미리 볼 방법이 없다. 하나씩 고친다.

형식과 필수 여부

매개변수에 형식을 적으면 PowerShell이 변환하고, 안 되면 실행 전에 오류를 낸다. [Parameter()] 특성으로 필수 여부와 위치를 정한다.

param(
    [Parameter(Mandatory, Position = 0)]
    [string]$Path,

    [string[]]$Name = @('node_modules', 'bin', 'obj')
)
  • Mandatory: 빼먹으면 실행하지 않고 값을 물어본다.
  • Position = 0: 이름 없이 첫 번째 값으로 줄 수 있다.
  • [string[]]: 여러 값을 받는다. -Name bin, obj.

예전에는 Mandatory=$true라고 적었다. 지금은 =$true를 생략한다.

스위치는 [switch]로 받는다. [bool]로 받아 -Force $true라고 쓰게 만들지 않는다.

param([switch]$Force)
if ($Force) { ... }

입력 검증

검증 특성을 붙이면 함수 본문이 시작되기 전에 걸러진다. 본문에 if로 검사를 늘어놓지 않아도 된다.

특성검사하는 것
[ValidateNotNullOrEmpty()]비어 있지 않은가
[ValidateSet('Dev', 'Prod')]정해진 값 중 하나인가. 탭 자동 완성도 됨
[ValidateRange(1, 254)]범위 안인가
[ValidatePattern('^\d+$')]정규식에 맞는가
[ValidateScript({ ... })]블록이 참을 돌려주는가
[ValidateScript({ Test-Path $_ -PathType Container })]
[string]$Path

파이프라인으로 받기

ValueFromPipeline을 붙이면 파이프라인으로 들어오는 값을 받는다. 이때 본문을 process 블록에 넣어야 들어오는 값마다 실행된다. 넣지 않으면 마지막 값 하나만 처리된다.

param(
    [Parameter(Mandatory, ValueFromPipeline)]
    [string]$Path
)
process {
    # $Path 하나마다 실행된다
}

지우기 전에 보여 주기: -WhatIf

무언가를 바꾸거나 지우는 함수는 SupportsShouldProcess를 켠다. 그러면 -WhatIf(하지 않고 보여 주기만)와 -Confirm(하나씩 묻기)이 생긴다. 실제 동작은 $PSCmdlet.ShouldProcess()가 참일 때만 한다.

완성

function Remove-BuildFolder {
    <#
    .SYNOPSIS
    하위 폴더에서 빌드 산출물 폴더를 찾아 지운다.
    .EXAMPLE
    Remove-BuildFolder ~/work -WhatIf
    .EXAMPLE
    Get-ChildItem ~/work -Directory | Remove-BuildFolder -Name node_modules
    #>
    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'Medium')]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline)]
        [ValidateScript({ Test-Path $_ -PathType Container })]
        [string]$Path,

        [ValidateNotNullOrEmpty()]
        [string[]]$Name = @('node_modules', 'bin', 'obj')
    )
    process {
        Get-ChildItem -LiteralPath $Path -Directory -Recurse -Force |
            Where-Object Name -In $Name |
            ForEach-Object {
                if ($PSCmdlet.ShouldProcess($_.FullName, 'Remove')) {
                    Remove-Item -LiteralPath $_.FullName -Recurse -Force
                }
            }
    }
}
Remove-BuildFolder ./proj -WhatIf
What if: Performing the operation "Remove" on target "/home/talsu/proj/b/node_modules".
What if: Performing the operation "Remove" on target "/home/talsu/proj/a/node_modules".

[CmdletBinding()]을 붙인 함수는 -Verbose, -ErrorAction 같은 공통 매개변수를 저절로 받는다. 본문에서 Write-Verbose로 적어 둔 진행 상황은 -Verbose를 줄 때만 보인다.

맨 위의 주석은 도움말이 된다. Get-Help Remove-BuildFolder -Examples로 보인다.

돌려주는 것

함수 안에서 변수에 받지 않은 값은 모두 결과로 나간다. return은 값을 돌려주는 문이 아니라 거기서 끝내는 문이다. 그래서 이런 실수가 생긴다.

function Get-Total {
    $list = [System.Collections.ArrayList]::new()
    $list.Add(1)        # Add가 돌려주는 번호 0이 결과에 섞인다
    return $list.Count
}
Get-Total    # 0 과 1 이 나온다

필요 없는 값은 $null = ...에 받거나 [void]를 붙여 버린다.

화면에 알릴 것과 결과는 길이 다르다. 결과는 그냥 내놓고, 알림은 용도에 맞는 명령으로 보낸다.

명령용도
(그냥 값)결과. 파이프라인으로 간다
Write-Verbose진행 상황. -Verbose일 때만 보임
Write-Warning계속하지만 알아야 할 것
Write-Error실패
Write-Host사람에게만 보여 줄 것. 결과에 섞이지 않음

오류 다루기

cmdlet의 오류는 대부분 멈추지 않는 오류라 try에 잡히지 않는다. 잡으려면 -ErrorAction Stop을 붙인다.

try {
    Get-Item ./missing.txt -ErrorAction Stop
}
catch [System.Management.Automation.ItemNotFoundException] {
    Write-Warning "파일이 없다: $($_.TargetObject)"
}
catch {
    throw    # 모르는 오류는 다시 던진다
}

스크립트 전체에 걸려면 맨 위에 $ErrorActionPreference = 'Stop'을 둔다. 방금 난 오류를 자세히 보려면 Get-Error를 친다. 안쪽 예외와 스택까지 보여 준다.

다음 글: 원격 접속

이 글을 가리키는 글