본문 바로가기
빌드노트 검색

빌드노트

PowerShell -replace에서 큰따옴표 안의 $1이 사라질 때

PowerShell -replace의 큰따옴표 replacement 안에서 $1이 사라지는 것은 정규식 치환 전의 변수 보간 때문이다. 숫자·이름 캡처와 중간 문자열을 비교하고, 작은따옴표 또는 백틱으로 캡처 참조를 보존하면서 PowerShell 변수 값을 조합하는 방법을 설명한다.

PowerShell에서 -replace의 치환 문자열을 "id=$1"로 썼는데 결과가 id=만 남는다면, 정규식보다 먼저 따옴표를 확인한다. 캡처를 참조하려는 $1을 PowerShell이 자기 변수로 해석한 것이다. PowerShell 변수를 넣을 필요가 없는 치환 문자열은 'id=$1'처럼 작은따옴표로 감싸면 된다.

PowerShell 인용 규칙 (새 탭에서 열림)에 따르면 큰따옴표 문자열의 변수는 문자열을 사용하는 명령에 전달되기 전에 값으로 바뀐다. 반면 작은따옴표는 변수 이름을 그대로 보존한다. 그 뒤 정규식 엔진이 전달받은 문자열 안의 캡처 참조를 해석한다. 같은 $1처럼 보여도 어느 단계까지 남아 있는지가 결과를 바꾼다.

치환 전에 이미 id=가 되었다

아래는 합성 문자열 code=42를 id=42로 바꾸는 비교다. Windows의 PowerShell 7.6.6과 .NET 10.0.12에서 실행했다. PowerShell 7이 설치된 터미널에서 pwsh -NoLogo -NoProfile로 새 세션을 열고 이어지는 코드를 순서대로 입력한다. NoProfile 옵션 (새 탭에서 열림)은 프로필을 불러오지 않는다. 아래의 빈 값 비교는 $1이 정의되지 않은 기본 세션을 전제로 한다. 실습을 마치면 exit로 나온다.

패턴과 입력은 그대로 두고 replacement의 따옴표만 바꾼다. 치환 직전 문자열과 최종 결과를 함께 출력해 사라진 위치를 확인한다.

$text = 'code=42'
$pattern = '^code=([0-9]+)$'
$replacement = "id=$1"
'replacement=<{0}>; result=<{1}>' -f $replacement, ($text -replace $pattern, $replacement)
$replacement = 'id=$1'
'replacement=<{0}>; result=<{1}>' -f $replacement, ($text -replace $pattern, $replacement)
replacement=<id=>; result=<id=>
replacement=<id=$1>; result=<id=42>

첫 줄에서는 정규식 치환을 하기 전부터 $replacement에 id=만 들어 있다. PowerShell이 큰따옴표 안의 $1을 이름이 1인 변수로 읽었고, 그 값이 없어서 빈 문자열이 들어간 것이다. 정규식 엔진에 도달한 문자열에는 캡처 참조 자체가 없다.

둘째 줄의 중간 값에는 $1이 남아 있다. 패턴의 ([0-9]+)가 잡은 42를 정규식 엔진이 넣어 id=42가 된다. 작은따옴표는 PowerShell의 보간을 막지만, 이후의 정규식 캡처 치환까지 막지는 않는다. 비교 연산자 문서의 치환 설명 (새 탭에서 열림)도 두 문법에서 $의 역할이 다름을 설명한다.

기존 세션에서는 꼭 빈 값이 나오지는 않는다. 별도 비교에서 $1 = 'shell-value'를 먼저 대입하자 큰따옴표 replacement와 결과가 모두 id=shell-value였다. 캡처 값이 엉뚱한 문자로 바뀌는 경우에도 치환 직전의 문자열부터 확인할 이유다.

이름 캡처도 큰따옴표에서는 변수로 읽힌다

숫자 대신 이름을 붙여도 문자열 보간 단계는 사라지지 않는다. 같은 입력을 유지하고 캡처 이름을 digits로 바꾼다. 이름이 같은 PowerShell 변수에도 다른 값을 넣어 어느 값이 쓰이는지 확인한다.

$pattern = '^code=(?<digits>[0-9]+)$'
$digits = 'shell-value'
$replacement = "id=${digits}"
'replacement=<{0}>; result=<{1}>' -f $replacement, ($text -replace $pattern, $replacement)
$replacement = 'id=${digits}'
'replacement=<{0}>; result=<{1}>' -f $replacement, ($text -replace $pattern, $replacement)
replacement=<id=shell-value>; result=<id=shell-value>
replacement=<id=${digits}>; result=<id=42>

큰따옴표 안의 ${digits}는 PowerShell 변수 $digits다. 작은따옴표 안에서는 그 글자가 남아 정규식 엔진의 이름 캡처 참조가 된다. 패턴에서 캡처 이름을 만들었다고 같은 이름의 PowerShell 변수가 채워지는 것은 아니다.

.NET의 replacement 문법 (새 탭에서 열림)은 숫자 캡처에 $1, 이름 캡처에 ${digits} 같은 표기를 사용한다. 숫자 캡처도 'id=${1}'로 전달하면 이 예제에서 id=42가 된다. 중괄호만 추가하는 것으로 PowerShell 보간을 피할 수는 없으므로, 캡처를 넘기려는 문자열의 따옴표도 함께 고친다.

PowerShell 변수와 캡처를 함께 넣으려면

접두사만 PowerShell 변수에서 가져오고 숫자는 캡처로 넣을 수도 있다. 그때는 변수 값과 캡처 참조를 작은따옴표 문자열로 나누어 조합한다. 앞에서 만든 이름 캡처 패턴을 그대로 사용한다.

$prefix = 'order-'
$replacement = $prefix + '${digits}'
'replacement=<{0}>; result=<{1}>' -f $replacement, ($text -replace $pattern, $replacement)
replacement=<order-${digits}>; result=<order-42>

PowerShell은 $prefix의 값과 리터럴 문자열 ${digits}를 이어 붙인다. 정규식 엔진은 그 결과인 order-${digits}를 받아 캡처만 바꾼다. $digits 변수의 값인 shell-value가 들어갈 자리가 없다.

큰따옴표 보간을 유지해야 한다면 캡처 참조의 $ 앞에 PowerShell 이스케이프 문자인 백틱을 붙일 수 있다. 같은 중간 값이 만들어지는지 확인한다.

$replacement = "${prefix}`${digits}"
'replacement=<{0}>; result=<{1}>' -f $replacement, ($text -replace $pattern, $replacement)
replacement=<order-${digits}>; result=<order-42>

백틱은 ${digits}의 $가 PowerShell 변수 확장을 시작하지 않게 한다. ${prefix}는 그대로 확장된다. 앞의 인용 규칙 문서에 설명된 방식이며, 위 두 조합은 직접 실행해 같은 결과를 확인했다.

이 조합은 접두사를 일반 텍스트로 이스케이프하는 기능은 아니다. 접두사에도 $1 같은 문자가 있으면 합쳐진 문자열 전체에서 정규식 replacement 문법으로 해석된다. 별도 비교에서 접두사를 '$1-'로 바꾸고 숫자 캡처 ${1}을 붙였을 때 결과는 42-42였다. 위 코드는 $가 없는 고정 접두사 order-를 넣는 경우다.

자기 코드에서도 -replace 바로 앞에서 실제 replacement 값을 먼저 출력한다. 이미 $1이나 ${digits}가 사라졌다면 그 문자열을 만드는 줄의 인용·조합을 고친다. 참조가 그대로 남아 있는데도 원하는 값이 안 나오면 그다음에는 실제 입력과 패턴의 그룹 번호·이름을 확인한다. 같은 숫자 패턴에 code=oops를 넣은 대조에서는 치환되지 않고 입력이 그대로 남았다.

직접 확인한 범위는 짧은 합성 문자열의 치환이다. Windows PowerShell 5.1.26100.9444에서도 같은 비교 결과를 확인했다. Linux·macOS와 StrictMode를 켠 세션은 실행해 비교하지 않았고, 파일 내용의 일괄 수정은 수행하지 않았다.

← 전체 글 보기