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에서 한 번 틀리게 만들어 본다.
에러 메시지가 없는 출력이 안전한 출력은 아니다.
가끔은 프로그램이 너무 정확하게 오해를 실행한다.
다음 글도 받아보세요.
글을 끝까지 읽으셨다면, 다음 글은 받은편지함이나 RSS 리더에서 만나보세요.