빌드노트
PowerShell -match가 실패했는데 $Matches에 이전 값이 남는 이유
PowerShell의 -match가 False를 반환해도 $Matches에는 이전 성공의 캡처가 남는다. 성공 분기에서만 값을 복사하는 수정과 배열 입력의 반환값 차이를 실제 실행 결과로 비교한다.
PowerShell에서 정규식 매칭은 실패했는데 $Matches['id']에서 앞서 읽은 숫자가 나온다면, 이전 캡처를 현재 입력의 결과로 사용하고 있는 것이다. 문자열 하나에 대한 -match의 성공 분기 안에서만 캡처를 읽고, 실패 입력은 별도로 처리해야 한다. $Matches에 값이 있다는 사실만으로 방금 매칭이 성공했다고 판단할 수는 없다.
$Matches는 매칭할 때마다 비워지는 변수가 아니다. PowerShell의 자동 변수 문서 (새 탭에서 열림)는 한 번 채워진 값이 다음 매칭 성공으로 덮어써질 때까지 남으며, -match 실패로 $null이 되지 않는다고 설명한다.
False가 나와도 id는 42였다
아래는 업무 로그가 아닌 합성 문자열로 확인한 결과다. 실행 환경은 Windows의 PowerShell 7.6.6, .NET 10.0.12였다. PowerShell 7이 설치되어 있다면 터미널에서 pwsh -NoLogo -NoProfile로 새 세션을 열고, 이어지는 예제를 순서대로 실행한다. NoProfile 옵션 (새 탭에서 열림)은 프로필을 불러오지 않는다. 예제를 마치고 exit로 나오면 기존 세션의 $Matches를 건드리지 않는다.
먼저 숫자를 담은 item=42와 숫자 자리에 문자가 온 item=oops를 비교한다. 패턴의 (?<id>[0-9]+)는 한 자리 이상의 숫자를 id라는 이름으로 캡처한다. 각 문자열의 매칭 결과와 그 직후 남은 캡처를 함께 출력한다.
$pattern = '^item=(?<id>[0-9]+)$'
foreach ($line in 'item=42', 'item=oops') {
$ok = $line -match $pattern
'{0} -> matched={1}; id={2}; whole={3}' -f $line, $ok, $Matches['id'], $Matches[0]
}
item=42 -> matched=True; id=42; whole=item=42
item=oops -> matched=False; id=42; whole=item=42
두 번째 줄에서 볼 것은 matched=False와 그대로 남은 id=42다. 전체 일치 문자열을 담는 $Matches[0]도 item=42였다. item=oops에서 숫자를 뽑아낸 것이 아니라, 실패한 호출이 이전 해시 테이블을 갱신하지 않은 것이다. 별도의 새 프로세스에서 실패 입력만 먼저 넣었을 때는 $Matches가 $null이었다. 따라서 이 문제는 앞선 매칭 성공 여부에 따라 드러나기도 하고 숨기도 한다.
자기 코드에서는 $Matches를 읽는 줄 바로 앞의 -match 결과를 확인한다. 위 입력에서 두 번째 결과가 True라면 실제 패턴이 같은지부터 대조한다. 반대로 결과가 False이고 값만 이전 입력과 같다면, 그 값으로 현재 항목을 처리하는 경로를 고쳐야 한다.
성공한 입력에서 곧바로 복사한다
같은 패턴에 item=57을 더해 성공 뒤 실패, 다시 성공하는 흐름을 처리한다. 캡처를 사용하는 코드를 성공 분기 안으로 옮기고, 실패 분기에서는 해당 입력을 잘못된 형식으로 기록한다.
$lines = 'item=42', 'item=oops', 'item=57'
foreach ($line in $lines) {
if ($line -match $pattern) {
$id = $Matches['id']
"$line -> id=$id"
} else {
"$line -> invalid"
}
}
item=42 -> id=42
item=oops -> invalid
item=57 -> id=57
실패 입력에서도 $Matches 자체에는 앞선 값이 남지만, 이제 그 값을 읽는 경로가 없다. 실제 스크립트에서는 invalid 출력 자리에 건너뛰기·오류 수집 등 필요한 실패 처리를 넣는다. $Matches를 수동으로 비우는 일에 의존하기보다 이번 입력의 성공 여부와 캡처 사용을 같은 분기에 둔다. 비교 연산자 문서 (새 탭에서 열림)도 현재 호출이 변수를 채웠는지 조건문으로 확인하도록 안내한다.
$id에는 성공 직후 필요한 문자열을 복사했다. 같은 범위에서 다른 정규식이 성공하면 $Matches가 다시 바뀔 수 있기 때문이다. 별도 비교에서는 42를 복사한 뒤 tag=blue를 다른 패턴으로 매칭하자 $Matches의 id 키는 없어졌지만, 복사한 $id는 42로 남았다. 다른 매칭을 호출하는 코드를 거치기 전에 필요한 값을 옮겨 두는 이유다.
다만 if가 $id를 위한 별도 스코프를 만드는 것은 아니다. 위 예제는 $id를 성공 분기에서만 사용한다. 분기 밖에서 공통 처리를 이어가야 한다면 매 반복 시작에 $id = $null로 초기화하고, 성공·실패 상태도 함께 전달해야 한다. 캡처를 일반 변수로 복사한 것만으로 실패한 반복의 이전 값까지 사라지지는 않는다. 변수의 현재 범위와 함수·스크립트 호출의 범위 구분은 PowerShell 스코프 문서 (새 탭에서 열림)에서 확인할 수 있다.
왼쪽에 배열을 놓으면 반환값의 뜻이 달라진다
앞의 foreach는 $line이라는 문자열 하나씩 매칭했다. 반복문을 줄이려고 $lines -match $pattern으로 바꾸면, 이제 왼쪽 입력은 배열이다. 이 경우 -match는 성공 Boolean이 아니라 패턴에 맞는 원소들을 반환하며 $Matches를 갱신하지 않는다. 이 구분 역시 비교 연산자 문서에 명시되어 있다.
차이를 보려면 먼저 별개의 문자열 item=99로 캡처를 채운 뒤, 앞에서 만든 $lines 배열을 매칭한다. 반환된 값의 타입·내용과 남은 id를 확인한다.
$null = 'item=99' -match $pattern
$selected = $lines -match $pattern
$selected.GetType().FullName
$selected -join ', '
"count=$($selected.Count); id=$($Matches['id'])"
System.Object[]
item=42, item=57
count=2; id=99
배열에서 골라진 문자열은 item=42와 item=57인데 캡처는 여전히 99다. 마지막 원소의 캡처가 자동으로 저장된다고 생각하면 잘못된 항목을 사용한다. 일치하는 원소가 없는 배열을 넣은 대조에서도 결과는 길이 0의 배열이었고, 기존 id=99는 남았다.
필터링한 문자열 목록이 필요하면 $selected를 사용한다. 각 항목의 id를 추출해야 한다면 앞의 foreach처럼 문자열 하나씩 매칭하고, 그 성공 분기에서 캡처를 복사한다. 배열 매칭 뒤 $Matches를 읽어서 항목별 캡처를 얻으려 해서는 안 된다.
캡처 이름이 없는 성공도 구별한다
매칭 실패와 캡처 그룹의 부재는 별개다. 같은 item=42에서 이름 캡처만 제거한 패턴을 실행해 보면 구분할 수 있다.
$ok = 'item=42' -match '^item=[0-9]+$'
"matched=$ok; whole=$($Matches[0]); hasId=$($Matches.ContainsKey('id'))"
matched=True; whole=item=42; hasId=False
매칭은 성공해 전체 일치 값이 채워졌지만, 패턴에 id 그룹을 만들지 않았으므로 그 키는 없다. 성공 여부는 -match 반환값으로, 필요한 캡처의 존재는 패턴과 키로 확인한다. 선택적 그룹이나 빈 문자열을 허용하는 그룹을 사용하는 경우에도 성공 여부와 원하는 값의 유무를 따로 판단해야 한다.
직접 확인한 범위는 위 Windows 환경의 짧은 합성 문자열과 문자열 배열이다. 선택적 그룹이 참여하지 않은 성공과 빈 문자열을 캡처한 성공도 별도로 비교했지만, Windows PowerShell 5.1·다른 운영체제·함수나 모듈 사이의 스코프 동작은 실행해 비교하지 않았다. 자기 코드에서도 먼저 실제 왼쪽 입력이 문자열 하나인지 배열인지 확인한 뒤, 해당 입력의 성공 분기에서만 캡처를 소비하는지 살펴본다.