3P by GN⁺ | ★ favorite | 댓글 1개
  • Python re$는 멀티라인 모드가 꺼져 있어도 문자열 끝뿐 아니라 끝에 있는 마지막 개행 앞에도 매치될 수 있음
  • ^가 “문자열 시작”처럼 보인다고 해서 $도 완전히 대칭적으로 동작한다고 보면 안 되며, 실제 의미는 정규 표현식 구현별로 달라짐
  • "cat\n"에 대한 $, \z, \Z의 결과는 PHP, ECMAScript, Python, Go, Java 8,.NET 7.0, Rust에서 서로 다르고, Python의 \zPython 3.14에서 새로 추가됨
  • 끝의 개행까지 허용한다면 멀티라인 모드의 $가 표의 모든 플랫폼에서 "cat\n"에 매치되지만, 개행을 제외한 끝만 매치하려면 문법 선택이 달라짐
  • 마지막 개행을 매치하지 않아야 한다면 대부분의 플랫폼에서는 \z를 쓰고, Python 3.14 이전과 ECMAScript에서는 각각 다른 대안을 고려해야 함

Python re에서 $가 매치하는 위치

  • Python 정규 표현식 모듈 re에서 $는 멀티라인 모드가 꺼져 있어도 문자열 끝 또는 문자열 끝의 마지막 개행 바로 앞에 매치될 수 있음
  • cat$"lolcat"에는 매치되고 "internet cat video"에는 매치되지 않아 단순해 보이지만, "cat\n"처럼 끝에 개행이 있으면 예상과 달라질 수 있음
  • re.MULTILINE이 지정되면 $는 문자열 끝과 각 줄 끝, 즉 각 개행 바로 앞에 매치됨
  • 기본값에서도 $는 문자열 끝에 매치되며, 문자열 끝에 개행이 있다면 그 개행 바로 앞에도 매치됨

마지막 개행을 제외하고 매치하기

  • 문자열 끝만 엄격히 매치하려면 $만으로는 부족할 수 있고, \z\Z끝 앵커 후보가 됨
  • Python 정규 표현식 문서다른 정규 표현식 문법 설명을 기준으로 보면, 구현별로 \z\Z의 지원 여부와 의미가 다름
  • "cat\n"에 대한 차이는 다음과 같음
    • PHP: "cat$"는 멀티라인 여부와 관계없이 매치되고, "cat\z"는 매치되지 않으며, "cat\Z"는 매치됨
    • ECMAScript: 멀티라인 "cat$"는 매치되고, 멀티라인이 아닌 "cat$"는 매치되지 않으며, \z\Z는 지원되지 않음
    • Python: "cat$"는 멀티라인 여부와 관계없이 매치되고, "cat\z""cat\Z""cat\n"에 매치되지 않음
    • Go와 Rust: 멀티라인 "cat$"는 매치되고, 멀티라인이 아닌 "cat$""cat\z"는 매치되지 않으며, \Z는 지원되지 않음
    • Java 8과 .NET 7.0: "cat$"는 멀티라인 여부와 관계없이 매치되고, "cat\z"는 매치되지 않으며, "cat\Z"는 매치됨
  • Python의 \zPython 3.14에서 새로 추가됐으며, 이전 버전에서는 지원되지 않았음
  • 끝의 개행을 허용한다면 멀티라인 모드의 $는 표의 모든 플랫폼에서 일관되게 "cat\n"에 매치됨
  • 끝의 개행을 매치하지 않으려면 대부분의 플랫폼에서는 \z를 쓰고, Python 3.14 이전에는 \Z, ECMAScript에서는 멀티라인이 아닌 $를 써야 함
  • 표의 데이터는 regex101.com에서 수집됐고, 실제 런타임으로 테스트한 것은 아님

댓글과 토론

Hacker News 의견들
  • 예전부터 ^는 “줄의 시작”, $는 “줄의 끝”으로 생각해 왔음
    정규식을 다룰 때 텍스트를 줄 단위로 처리하는 일이 많아서 결과는 같을 때가 많지만, 그 연산자를 떠올리는 방식은 여전히 “문자열”보다 “줄”에 가까움
    아마 grep으로 정규식을 접한 영향이 커서 입력을 문자열이 아니라 줄로 보는 습관이 생긴 듯함

    • 나도 제목을 보고 “당연히 아니지, 어디서 그런 얘기를 들었지?”라고 생각했음
      거의 20년 동안 정규식을 써 왔지만 $문자열의 끝이라는 얘기는 처음 듣는 것 같고, 늘 줄의 끝으로 여겨 왔음
    • 글에서 ^를 “문자열의 시작”이라고 하는 게 신경 쓰임
      실제로는 $가 “줄의 끝”인 것처럼 ^도 “줄의 시작”이고, 문자열의 시작은 \A, 문자열의 끝은 \Z에 가까워 보임
    • 나도 그렇게 생각했는데 Perl에서 직접 해 보니 $는 기본적으로 문자열 끝에 대한 긍정형 전방탐색 단언처럼 동작함
      줄바꿈 문자를 매치하고 소비하지는 않음
      여러 줄 모드에서만 줄바꿈 위치와 매치되지만, 그때도 소비하지 않는 듯함
      실제로 $를 쓰면서 한 줄의 마지막 문자를 캡처하고 줄바꿈을 소비한 뒤 다음 줄의 첫 문자를 캡처하는 정규식을 만들 수 없었고, 캡처 그룹은 그냥 $에서 끝남
    • grep보다 Vim이 나한테 그런 인식을 심어 줬음
  • POSIX 정규식과 Python 정규식은 다름
    일반적으로 정규식 문법은 보편적이지 않으므로, 사용하는 구현의 문서를 봐야 함
    POSIX 9장에 따르면 정규식은 문자열 위에서 동작하지만, 일부 유틸리티는 처리를 줄 단위로 제한함
    또한 $는 매치 대상 문자열의 끝에 고정되는 앵커라고 되어 있어서, 결국 $가 문자열 끝을 의미할지 줄 끝을 의미할지는 유틸리티나 모드가 정함
    grep, sed, awk, Python 같은 흔한 도구는 기본적으로 줄 단위로 동작하므로 보통 줄 끝으로 다룸
    단 하나의 보편적인 정규식 문법은 없음
    어떤 언어와 옵션을 쓰는지 모르면 정규식을 안정적으로 읽거나 쓸 수 없음
    https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1...

  • 이 주제라면 Robert Elder를 모르는 사람들에게 소개하기 딱 좋음
    YouTube와 블로그에서 좋은 콘텐츠를 만들고, 정규식 시리즈에서는 여러 도구가 구현한 정규식 동작 차이를 꽤 깊게 파고듦
    최근 영상도 좋음: https://www.youtube.com/watch?v=ys7yUyyQA-Y
    HN 독자들이 관심 가질 만한 콘텐츠도 많고, 컨설팅의 현실과 고충 같은 주제도 다룸
    https://www.youtube.com/@RobertElderSoftware
    https://blog.robertelder.org/
    https://blog.robertelder.org/regular-expressions/
    https://www.youtube.com/watch?v=cK87ktENPrI

  • Perl을 배울 때 정규식은 처음으로 정말 몸에 익힌 것 중 하나였고, 지금도 “Camel” 책 덕분에 Perl은 마음 한구석에 편하게 자리 잡고 있음
    지금 가장 중요한 지식은 구현마다 다르다는 점이라서, 무언가 작업할 때마다 해당 참조표를 꺼내 보는 습관이 생김
    예를 들어 Emacs 정규식은 \w 형태의 단어 문자를 지원하지 않고 \s_- 비슷한 문자 클래스를 써야 해서 짜증 나지만, Emacs는 문서화와 발견 가능성이 최고라고 봄
    어떤 유틸리티는 괄호 이스케이프가 필요하고 어떤 것은 필요 없으며, 이 동작이 설정 가능한 경우도 있고 아닌 경우도 있음
    혼란, 짜증, 부정의 단계를 다 지나고 이제는 그냥 받아들임
    개념은 어디서나 같지만 방언이 달라짐

    • 내 머리는 Perl 정규식으로 생각하고, 그다음 쓰고 있는 언어의 일관성 없는 부분에 맞춰 번역함
      특히 셸에서는 sed/grep/awk가 GNU인지 BSD인지까지 떠올리느니, 파이프라인에 perl을 끼워 넣는 쪽이 훨씬 잦음
    • 어떻게 몸에 익혔는지 궁금함
      Perl은 고양이가 키보드를 밟은 것처럼 보임
  • 수많은 별로인 채용 관리자가 “정규식에서 문자열의 끝은 어떻게 매치하나요?”를 함정 질문 목록에 추가하는 소리가 들리는 듯함

  • 정규식 관련 목록에서 Perl을 빼는 건 이상함
    perlre 문서에서 $는 이렇게 설명됨: 문자열 끝과 매치하거나, 문자열 끝의 줄바꿈 앞과 매치하거나, /m을 쓰면 임의의 줄바꿈 앞과 매치함

    • 정규식과 가장 강하게 연결된 언어라고 할 수 있는 Perl을 빠뜨린 건 꽤 큰 누락으로 보임
      그만큼 요즘 Perl이 관심 밖으로 밀려났다는 뜻 같기도 함
  • Raku, 예전 Perl 6는 ^$문자열 시작/끝으로 정하고, ^^$$를 줄 시작/끝으로 도입했음
    여러 줄 모드는 없고 필요하지도 않음
    \h는 가로 공백, \v는 세로 공백도 있음
    완전히 다시 생각하고 다시 쓴 덕분에, 예전 동작이 사람들을 놀라게 했다는 사실에서 배울 수 있었던 장점임

    • 그래서 이 고집 센 사람은 Perl 6를 못 쓰겠음
      수십 년 동안 익힌 라인 노이즈 같은 문법을 무작위로 섞어 놓은 느낌임
      기본값은 반대였어야 명확해 보임
      ^$는 줄에, ^^$$는 문자열에 쓰는 편이 자연스러웠을 것 같음
      ^^line1$\n^line2$\n^line3$\n$처럼 보이기 때문임
      게다가 Perl 6는 어디에나 있지 않지만 Perl 5는 어디에나 있음
    • 나였다면 정확히 반대로 골랐을 것 같음
      ^^^보다 더 “시작스럽게” 보임
    • 내가 작성한 정규식은 거의 전부 문자열 시작/끝을 전제로 했음
      보통 줄을 정규식에 넣어 처리하기 때문이라, 단일 ^$를 전체 문자열에 쓰는 선택은 어느 정도 하위 호환성을 유지해 줌
  • 정규식이 표준화됐다고 보는 사람이 있나 싶음
    새 환경으로 옮길 때마다 늘 다시 배워야 했음

    • 어느 순간에는 내가 모든 방언을 안다고 느꼈음
      더 많은 정규식 방언이 있겠지만 마주치지 않고, 내가 아는 범위로 대부분 해결됨
      렌터카 운전과 비슷함
      내 차와 조금 다르게 움직이고, 빠진 기능과 추가된 기능이 있지만, 전반적으로는 대부분 꽤 비슷함
    • ISO/IEC 14882 C++ 표준 라이브러리는 여섯 가지 사실상의 법정 표준 정규식 문법 구현을 요구함: IEEE Std 1003.1-2008, 즉 POSIX의 BRE, ERE, awk, grep, egrep과 ECMA-262 EcmaScript 3
      그래서 적어도 나는 정규식이 여러 공개된 공식 표준으로 표준화되어 있다고 봄
      https://open-std.org/jtc1/sc22/…
      https://pubs.opengroup.org/onlinepubs/9699919799/…
      https://262.ecma-international.org/14.0/…
    • 내가 아는 큰 갈래는 POSIX, Perl/PCRE, 그리고 Go 쪽에서 쓰는 RE2 정도임
      JavaScript를 포함해 많은 시스템이 PCRE를 구현했는데, Perl이 POSIX 체계에 유용한 확장을 많이 더했기 때문임
      기억하기로 RE2는 기존 체계의 성능 문제와 특이 동작을 억제하려는 쪽이고, 전체를 Go로 구현한 것으로 알고 있었음
      나중에 보니 RE2가 Go보다 먼저 나왔다는 건 몰랐음
    • Perl 이후에 나온 언어들은 대체로 Perl 정규식 문법의 어떤 변형을 쓰지만, 늘 사소한 차이가 있음
      그래도 $의 의미와 여러 줄 모드로 바꾸는 방식은 보통 일관적인 편임
    • 흥미롭게도 RFC 9485 https://datatracker.ietf.org/doc/rfc9485/ “I-Regexp: An Interoperable Regular Expression Format”이 작년 10월에 막 출판됐음
  • 사람들이 문자열과 줄을 혼동하고 있음
    문자열은 문자들의 시퀀스이고, 줄은 두 가지로 볼 수 있음
    줄바꿈을 줄 종결자로 보면 줄은 줄바꿈이 아닌 문자 0개 이상에 줄바꿈이 붙은 것이며, 끝에 줄바꿈이 없으면 완전한 줄이 아님
    POSIX가 이 관점을 씀
    줄바꿈을 줄 구분자로 보면 줄은 줄바꿈이 아닌 문자 0개 이상의 시퀀스임
    어느 쪽이든 줄의 내용은 줄바꿈 앞에서 끝남
    ^$의 의미론은 단일 줄 모드든 여러 줄 모드든 줄 기반
    문자열 기반 의미론, 파일을 다룬다면 전체 파일 의미론이라고 봐도 되는 경우에는 \A\Z 또는 그에 해당하는 것을 써야 함
    두 해석 모두 장점이 있음
    직렬 연결로 텍스트를 전송할 때는 줄바꿈을 줄 종결자로 두면 완전한 줄을 받았는지 알기 좋음
    텍스트 파일에서는 줄바꿈을 줄 구분자로 보는 편이 마지막 줄이 잘못된 상태가 되지 않아 편할 수 있지만, 줄 종결자를 두면 불완전하게 쓰인 줄을 감지할 수 있음

  • 이 때문에 Ruby 기반 앱에서 심각한 버그가 몇 번 났음
    항상 \A\z 를 써야 함
    https://homakov.blogspot.com/2012/05/saferweb-injects-in-var...
    https://sakurity.com/blog/2015/02/28/openuri.html
    https://sakurity.com/blog/2015/06/04/mongo_ruby_regexp.html