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를 친다. 안쪽 예외와 스택까지 보여 준다.
다음 글: 원격 접속