rg -rn에서 n은 줄 번호가 아니었다

검색 결과에 줄 번호를 붙이고 싶었다.

손은 익숙하게 움직였다.

rg -rn 'PATTERN' file

출력도 나왔다.

에러도 없었다.

검색한 식별자는 살짝 달라져 있었다.

n으로 시작했다.

처음에는 원본이 그런 줄 알았다.

두 번째 결과도 n으로 시작했다.

그제야 플래그를 읽었다.

-rn은 -r -n이 아니었다.

-r의 replacement 값이 n이었다.

grep의 근육 기억이 문제였다

내가 가져온 grep 명령에서는 -r을 recursive로 읽었다.

-n은 line number다.

두 글자가 붙어 있으니 -r -n이라는 묶음으로 읽었다.

grep -rn 'PATTERN' directory

그래서 같은 모양을 ripgrep에 가져왔다.

하지만 ripgrep은 directory를 기본으로 재귀 검색한다.

현재 rg --help의 첫 설명부터 “recursively searches”라고 적혀 있다.

그리고 -r은 재귀 플래그가 아니다.

-r REPLACEMENT, --replace=REPLACEMENT

값을 하나 받는 옵션이다.

-rn을 만나면 parser는 이렇게 읽는다.

-r n

n은 별도의 -n 플래그가 아니다.

replacement 문자열이다.

출력이 너무 그럴듯했다

작은 fixture에 두 줄을 넣었다.

android_ad_unit=ca-app-pub-3940256099942544/6300978111
second=ca-app-pub-1111222233334444/5555666677

그리고 오해한 명령을 실행했다.

rg -rn 'ca-app-pub-' fixture.txt

결과는 이랬다.

android_ad_unit=n3940256099942544/6300978111
second=n1111222233334444/5555666677

원래 prefix가 n으로 바뀌었다.

줄 번호도 없다.

하지만 나머지 숫자는 멀쩡하다.

결과 줄의 key도 그대로다.

그래서 망가진 출력이 오히려 실제 설정처럼 보인다.

완전히 깨졌다면 빨리 의심했을 것이다.

한 글자만 그럴듯하게 바뀌었기 때문에 위험했다.

파일을 수정한 것은 아니다

여기서 경계를 정확히 해야 한다.

ripgrep의 --replace는 검색 결과를 출력할 때 match를 바꾼다.

원본 파일을 inplace 수정하지 않는다.

현재 help도 명시한다.

Neither this flag nor any other ripgrep flag will modify your files.

그러므로 사건의 정확한 상태는 이렇다.

원본 파일 → 정상
터미널 출력 → replacement 적용
사람이 출력에서 읽은 값 → 잘못될 수 있음

위험은 검색 명령이 파일을 망가뜨리는 데 있지 않았다.

사람이나 다음 스크립트가 변형된 출력을 원본 데이터로 믿는 데 있었다.

clipboard는 provenance를 기억하지 않는다.

의도한 명령은 훨씬 짧았다

줄 번호가 필요했다면 이것이면 된다.

rg -n 'ca-app-pub-' fixture.txt

실제 출력은 원문을 보존한다.

1:android_ad_unit=ca-app-pub-3940256099942544/6300978111
2:second=ca-app-pub-1111222233334444/5555666677

더 읽기 쉽게 만들고 싶다면 long option을 쓸 수 있다.

rg --line-number 'ca-app-pub-' fixture.txt

긴 이름은 타이핑이 조금 늘어난다.

대신 review 때 의미가 보인다.

식별자, token, URL처럼 복사될 가능성이 높은 값을 찾을 때에는 이 몇 글자가 싸다.

값 옵션은 남은 글자를 먹을 수 있다

짧은 플래그를 합칠 수 있다는 사실과, 아무 플래그나 합쳐도 같은 뜻이라는 주장은 다르다.

값을 받는 옵션이 끼면 나머지 문자가 그 값으로 소비될 수 있다.

-abc

이 모양만 보고 다음처럼 해석하면 안 된다.

-a -b -c

이번 rg -rn처럼 앞 옵션이 값을 요구하면 남은 글자가 그 값으로 소비될 수 있다.

그래서 익숙한 모양만 보고 뜻을 옮기지 말고, 지금 쓰는 도구의 help를 읽어야 한다.

tidy output은 에러보다 위험하다

명령이 실패했다면 shell이 알려준다.

exit code가 non-zero라면 자동화도 멈출 수 있다.

이번 명령은 정상 실행됐다.

정상 기능인 replacement를 정확히 수행했다.

그래서 출력이 tidy했다.

이런 실패는 syntax error가 아니다.

의도와 parser가 서로 다른 계약을 실행한 semantic error다.

내 의도 → recursive search + line numbers
실제 parser → replace matches with n
실제 프로그램 → 요청받은 대로 정상 종료

모든 계층이 자기 기준으로 정상이다.

사용자만 다른 프로그램을 실행한 셈이 된다.

출력을 복사하기 전에 원본 한 줄을 대조한다

식별자를 여러 개 찾을 때에는 결과 개수만 보지 않는다.

원본 파일 한 줄과 검색 출력 한 줄을 나란히 본다.

원본 prefix가 유지됐는가?
separator가 유지됐는가?
line number 외에 추가된 문자가 있는가?
검색 결과를 다시 검색했을 때 같은 match가 나오는가?

이번 fixture에서는 마지막 질문이 특히 잘 실패한다.

변형된 출력의 n394...에는 원래 pattern인 ca-app-pub-가 없다.

출력을 다시 같은 pattern으로 검색하면 match가 사라진다.

positive control 하나가 replacement를 드러낸다.

명령을 줄이면서 검증한다

이상한 결과가 나오면 flag를 더 붙이지 않는다.

가장 작은 명령으로 돌아간다.

rg 'ca-app-pub-' fixture.txt

그다음 한 번에 하나씩 추가한다.

rg -n 'ca-app-pub-' fixture.txt

두 출력의 차이가 line number뿐인지 확인한다.

rg -rn과 rg -n을 바로 비교하면 오류 위치도 좁아진다.

pattern이나 file이 아니라 option parsing이 달라졌다는 사실이 보인다.

실제 프로젝트에서는 더 위험했다

이 flag 오독은 개인 모바일 프로젝트의 한 세션에서 두 번 반복됐다.

광고 unit identifier를 찾는 출력이 n... 형태로 바뀌었다.

그 출력을 코드에 복사했다면 원본과 다른 문자열이 들어갈 수 있었다.

검색 결과가 원본인지 replacement preview인지 확인하지 않은 채 옮기는 순간, 잘못된 출력이 다음 입력이 된다.

그러니 배포 identifier를 옮기는 작업에서는 검색 명령 자체가 evidence chain의 일부다.

실전 규칙

ripgrep은 기본으로 directory를 재귀 검색한다.

줄 번호는 -n 또는 --line-number로 요청한다.

-r은 recursive가 아니라 replacement 값을 받는 옵션이다.

rg -rn은 -r -n으로 읽히지 않을 수 있다.

이 경우 n이 replacement 값이 된다.

--replace는 파일을 수정하지 않지만 출력의 match를 바꾼다.

식별자 검색 결과를 복사하기 전에는 원본 한 줄과 대조한다.

이상하면 long option으로 다시 실행한다.

그리고 ad hoc check는 known input에서 한 번 틀리게 만들어 본다.

에러 메시지가 없는 출력이 안전한 출력은 아니다.

가끔은 프로그램이 너무 정확하게 오해를 실행한다.