Regex 모듈

Regex 모듈

Elixir를 위한 정규 표현식(regular expression)을 제공하는 모듈이에요. Regex는 PCRE(Perl Compatible Regular Expressions)를 기반으로 하며 Erlang의 :re 모듈 위에 구축되어 있습니다. 더 자세한 정보는 :re 모듈 문서에서 찾을 수 있어요.

출처: Regex

본문

Elixir의 정규 표현식은 ~r 시그널(sigil, sigil_r/2 참고)로 만들 수 있습니다.

# A simple regular expression that matches foo anywhere in the string
~r/foo/

# A regular expression with case insensitive and Unicode options
~r/foo/iu

Regex는 내부적으로 Regex 구조체로 표현됩니다. 따라서 %Regex{}는 매칭이 필요할 때마다 쓸 수 있어요. 구조체의 모든 필드는 비공개임을 명심하세요. 정규식은 컴파일되므로 같은 소스에서 만든 두 정규식이 같다는 보장이 없어요. 예를 들면:

~r/(?<foo>.)(?<bar>.)/ == ~r/(?<foo>.)(?<bar>.)/

이는 머신, 엔디언(endianness), 사용 가능한 최적화 등에 따라 truefalse를 반환할 수 있습니다. 하지만 컴파일된 정규식의 소스는 source 필드에 접근해 가져오고, 그것들을 직접 비교할 수는 있어요.

~r/(?<foo>.)(?<bar>.)/.source == ~r/(?<foo>.)(?<bar>.)/.source

이스케이프(Escapes)

이스케이프 시퀀스는 두 범주로 나뉩니다.

인쇄되지 않는 문자(Non-printing characters)

  • \a - Alarm, 즉 BEL 문자 (hex 07)
  • \e - Escape (hex 1B)
  • \f - Form feed (hex 0C)
  • \n - Line feed (hex 0A)
  • \r - Carriage return (hex 0D)
  • \t - Tab (hex 09)
  • \xhh - hex 코드 hh의 문자
  • \x{hhh..} - hex 코드 hhh..의 문자

\u\U는 지원되지 않습니다. 8진수용 \ddd 같은 다른 이스케이프 시퀀스는 지원되지만 권장되지는 않아요.

일반 문자 타입(Generic character types)

  • \d - 모든 십진 숫자
  • \D - 십진 숫자가 아닌 모든 문자
  • \h - 모든 가로 공백 문자
  • \H - 가로 공백 문자가 아닌 모든 문자
  • \s - 모든 공백 문자
  • \S - 공백 문자가 아닌 모든 문자
  • \v - 모든 세로 공백 문자
  • \V - 세로 공백 문자가 아닌 모든 문자
  • \w - 모든 "단어(word)" 문자
  • \W - 모든 "비-단어" 문자

수정자(Modifiers)

Regex를 만들 때 사용 가능한 수정자는 다음과 같습니다.

  • :unicode (u) — \p 같은 Unicode 특화 패턴을 활성화하고 \w, \W, \s 같은 문자 클래스가 Unicode와도 매치되게 합니다(아래 "Character classes"의 예시 참고). 매치 시 유효한 Unicode 문자열이 주어질 것을 기대합니다
  • :caseless (i) — 대소문자 무시 추가
  • :dotall (s) — dot이 newline에 매치되게 하고 newline을 (*ANYCRLF)로 설정합니다. :re 문서에 설명된 newline 설정은 정규식 패턴을 다음으로 시작해서 덮어쓸 수 있어요:
    • (*CR) - carriage return
    • (*LF) - line feed
    • (*CRLF) - carriage return, 그다음 line feed
    • (*ANYCRLF) - 위 셋 중 아무거나
    • (*ANY) - 모든 Unicode newline 시퀀스
    • (*NUL) - NUL 문자(바이너리 0) (Erlang/OTP 28부터)
  • :multiline (m) — ^$가 각 줄의 시작과 끝을 나타내게 함. 문자열의 끝이나 시작을 매치하려면 \A\z를 쓰세요
  • :extended (x) — 이스케이프되거나 [..] 안에 있지 않는 한 공백 문자를 무시하고, #이 주석을 구분하게 함
  • :firstline (f) — 앵커되지 않은(un-anchored) 패턴이 첫 newline 전이나 그 위치에서 매치되게 하지만, 매치된 텍스트는 newline을 넘어 계속될 수 있음
  • :ungreedy (U) — 정규식의 "greediness"를 뒤집음 (이전 r 옵션은 U를 위해 deprecated)
  • :export (E) (Elixir 1.19.3부터) — 노드 간 공유하거나 config로 전달할 수 있는 export된 패턴을 사용하지만, 실행할 때마다 다시 import하는 런타임 오버헤드가 있음. 이 수정자는 Erlang/OTP 28부터 효과가 있고, 이전 버전에서는 무시됩니다(즉 ~r/foo/E == ~r/foo/). 그 버전들에서는 패턴이 공유되기 위해 export될 필요도 없고 그럴 수도 없기 때문이에요.

캡처(Captures)

이 모듈의 많은 함수는 :capture 옵션으로 regex 매치에서 무엇을 캡처할지 처리합니다. 지원되는 값은 다음과 같습니다.

  • :all — 완전한 매칭 문자열을 포함한 모든 캡처 하위패턴 (기본값)
  • :first — 첫 번째 캡처 하위패턴만, 항상 문자열의 완전한 매칭 부분. 명시적으로 캡처된 하위패턴은 모두 버려짐
  • :all_but_first — 첫 번째를 제외한 모든 매칭 하위패턴, 즉 명시적으로 캡처된 모든 하위패턴이지만 문자열의 완전한 매칭 부분은 아님
  • :none — 매칭 하위패턴을 전혀 반환하지 않음
  • :all_names — 하위패턴의 이름 순서대로 알파벳순으로 정렬된 리스트로 Regex의 모든 이름 있는 하위패턴 매치를 캡처
  • list(binary | atom) — 캡처할 이름 있는 캡처 리스트

문자 클래스(Character classes)

Regex는 몇 가지 내장 이름 있는 문자 클래스를 지원합니다. 이것들은 그룹 안에서 [: :]로 클래스 이름을 감싸서 사용해요. 예를 들면:

iex> String.match?("123", ~r/^[[:alnum:]]+$/)
true
iex> String.match?("123 456", ~r/^[[:alnum:][:blank:]]+$/)
true

지원되는 클래스 이름은 다음과 같습니다.

  • alnum - 문자와 숫자
  • alpha - 문자
  • blank - 공백 또는 탭만
  • cntrl - 제어 문자
  • digit - 십진 숫자 (\d와 같음)
  • graph - 공백을 제외한 인쇄 문자
  • lower - 소문자
  • print - 공백을 포함한 인쇄 문자
  • punct - 문자, 숫자, 공백을 제외한 인쇄 문자
  • space - 공백 (\s와 같음)
  • upper - 대문자
  • word - "단어" 문자 (\w와 같음)
  • xdigit - 16진수 숫자

ascii라는 다른 문자 클래스도 있는데, POSIX가 지정한 0-127 범위 대신 라틴-1 문자를 잘못 매치합니다. 다른 클래스의 동작을 바꾸지 않고는 고칠 수 없으므로, 그 범위는 [\0-\x7f]로 매치할 것을 권장합니다.

그 클래스들의 동작은 Unicode 및 다른 수정자에 따라 바뀔 수 있음에 유의하세요.

iex> String.match?("josé", ~r/^[[:lower:]]+$/)
false
iex> String.match?("josé", ~r/^[[:lower:]]+$/u)
true
iex> Regex.replace(~r/\s/, "Unicode\u00A0spaces", "-")
"Unicode spaces"
iex> Regex.replace(~r/\s/u, "Unicode\u00A0spaces", "-")
"Unicode-spaces"

타입(Types)

@type capture_opts() :: [
  return: :binary | :index,
  capture:
    :all | :first | :all_but_first | :none | :all_names | [binary() | atom()],
  offset: non_neg_integer()
]
@type named_captures_opts() :: [return: :binary | :index, offset: non_neg_integer()]
@type split_opts() :: [
  parts: pos_integer() | :infinity,
  trim: boolean(),
  on:
    :first | :all | :all_but_first | :none | :all_names | [atom() | integer()],
  include_captures: boolean()
]
@type t() :: %Regex{opts: [term()], re_pattern: term(), source: binary()}
  • capture_opts() — 매치를 캡처하는 regex 함수의 옵션
  • split_opts()split/3의 옵션

함수(Functions)

compile(source, opts \ "")

@spec compile(binary(), binary() | [term()]) :: {:ok, t()} | {:error, term()}

정규 표현식을 컴파일합니다. 주어진 옵션은 ~r(시그널 sigil_r/2 참고) 시그널에 주어지는 것과 같은 정규식 옵션을 나타내는 문자들이 담긴 바이너리이거나, Erlang :re 모듈이 기대하는 옵션 리스트일 수 있어요. 성공하면 {:ok, regex}를, 아니면 {:error, reason}을 반환합니다.

Regex.compile("foo")
#=> {:ok, ~r/foo/}

Regex.compile("foo", "i")
#=> {:ok, ~r/foo/i}

Regex.compile("*foo")
#=> {:error, {~c"quantifier does not follow a repeatable item", 0}}

compile!(source, options \ "")

@spec compile!(binary(), binary() | [term()]) :: t()

정규 표현식을 컴파일하고, 오류가 있으면 Regex.CompileError를 던집니다.

escape(string)

@spec escape(String.t()) :: String.t()

regex에서 리터럴로 매치되도록 문자열을 이스케이프합니다.

iex> Regex.escape(".")
"\\."

iex> Regex.escape("\\what if")
"\\\\what\\ if"

import(regex) (1.20.0부터)

@spec import(t()) :: t()

export된 regex를 import하며, 아니면 regex를 그대로 반환합니다. 즉 노드 간에 보내거나 config로 전달하는 능력은 잃지만, 실행할 때마다 즉석에서 import할 필요가 없으므로 더 빨라집니다. Export된 regex는 OTP 28에만 존재하므로 이전 버전에서는 효과가 없어요.

Regex.import(~r/foo/E)
~r/foo/

Regex.import(~r/foo/)
~r/foo/

match?(regex, string)

@spec match?(t(), String.t()) :: boolean()

매치가 있었는지 여부를 나타내는 불리언을 반환합니다.

iex> Regex.match?(~r/foo/, "foo")
true

iex> Regex.match?(~r/foo/, "bar")
false

Elixir는 또한 정규식과 문자열을 테스트하는 대안으로 텍스트 기반 매치 연산자 =~/2와 함수 String.match?/2를 제공합니다.

named_captures(regex, string, options \ [])

@spec named_captures(t(), String.t(), named_captures_opts()) :: map() | nil

주어진 캡처를 맵으로 반환하거나, 캡처가 없으면 nil을 반환합니다.

옵션:

  • :return:index로 설정하면 바이트 인덱스와 매치 길이를 반환. 기본값 :binary.
  • :offset — (v1.12.0부터) 주어진 문자열에서 매치를 시작할 시작 오프셋 지정. 기본값 0.
iex> Regex.named_captures(~r/c(?<foo>d)/, "abcd")
%{"foo" => "d"}

iex> Regex.named_captures(~r/a(?<foo>b)c(?<bar>d)/, "abcd")
%{"bar" => "d", "foo" => "b"}

iex> Regex.named_captures(~r/a(?<foo>b)c(?<bar>d)/, "efgh")
nil

이름 있는 캡처에서 인덱스를 가져올 수도 있어요. 이름 있는 캡처가 매치됐는지 알고 싶을 때 특히 유용합니다.

iex> Regex.named_captures(~r/a(?<foo>b)c(?<bar>d)?/, "abc", return: :index)
%{"bar" => {-1, 0}, "foo" => {1, 1}}

그런 다음 binary_part/3을 써서 주어진 문자열에서 관련 부분을 가져올 수 있어요.

names(regex)

@spec names(t()) :: [String.t()]

regex 안의 이름 리스트를 반환합니다.

iex> Regex.names(~r/(?<foo>bar)/)
["foo"]

opts(regex)

@spec opts(t()) :: [term()]

regex 옵션을 반환합니다. Regex.compile/2의 문서를 참고하세요.

iex> Regex.opts(~r/foo/m)
[:multiline]

iex> Regex.opts(Regex.compile!("foo", [:caseless]))
[:caseless]

re_pattern(regex)

@spec re_pattern(t()) :: term()

정규 표현식의 기저 re_pattern을 반환합니다.

recompile(regex) (1.4.0부터, deprecated)

이 함수는 deprecated입니다. 제거될 수 있으며 효과가 없습니다. 정규 표현식의 버전을 확인하고 버전 불일치 시 다시 컴파일합니다.

recompile!(regex) (1.4.0부터, deprecated)

이 함수는 deprecated입니다. 제거될 수 있으며 효과가 없습니다. 기존 정규 표현식을 다시 컴파일하고, 오류가 있으면 Regex.CompileError를 던집니다.

replace(regex, string, replacement, options \ [])

@spec replace(t(), String.t(), String.t() | (... -> String.t()), [
  {:global, boolean()}
]) :: String.t()

regex, 바이너리, replacement(replacement 문자열 또는 문자열을 반환하는 함수)를 받아, 모든 매치가 replacement로 교체된 새 바이너리를 반환합니다. replacement가 문자열이면 \N 또는 \g{N}으로 특정 캡처에 접근할 수 있는데, N은 캡처 번호입니다. \0을 쓰면 전체 매치가 삽입됩니다. regex에서 백슬래시는 이스케이프되어야 하므로 실제로는 \\N\\g{N}을 써야 합니다. replacement가 함수이면 특정 캡처도 허용합니다. 함수는 arity N을 가질 수 있고 각 인자는 캡처에 매핑되며, 첫 인자가 전체 매치입니다. 함수가 발견된 캡처보다 많은 인자를 기대하면 나머지 인자는 ""를 받습니다.

옵션:

  • :globalfalse이면 첫 번째 발생만 교체 (기본값 true)
iex> Regex.replace(~r/d/, "abc", "d")
"abc"

iex> Regex.replace(~r/b/, "abc", "d")
"adc"

iex> Regex.replace(~r/b/, "abc", "[\\0]")
"a[b]c"

iex> Regex.replace(~r/a(b|d)c/, "abcadc", "[\\1]")
"[b][d]"

iex> Regex.replace(~r/\.(\d)$/, "500.5", ".\\g{1}0")
"500.50"

iex> Regex.replace(~r/a(b|d)c/, "abcadc", fn _, x -> "[#{x}]" end)
"[b][d]"

iex> Regex.replace(~r/(\w+)@(\w+).(\w+)/, "[email protected]", fn _full, _c1, _c2, c3 -> "TLD: #{c3}" end)
"TLD: com"

iex> Regex.replace(~r/a/, "abcadc", "A", global: false)
"Abcadc"

run(regex, string, options \ [])

@spec run(t(), binary(), capture_opts()) ::
  nil | [binary()] | [{integer(), integer()}]

주어진 문자열에 대해 첫 매치까지 정규 표현식을 실행합니다. 모든 캡처가 담긴 리스트를 반환하거나, 매치가 없으면 nil을 반환합니다.

옵션:

  • :return:index로 설정하면 바이트 인덱스와 매치 길이 반환. 기본값 :binary.
  • :capture — 결과에서 무엇을 캡처할지. 가능한 캡처 값은 "Captures" 섹션 참고.
  • :offset — (v1.12.0부터) 주어진 문자열에서 매치를 시작할 시작 오프셋 지정. 기본값 0.
iex> Regex.run(~r/c(d)/, "abcd")
["cd", "d"]

iex> Regex.run(~r/e/, "abcd")
nil

iex> Regex.run(~r/c(d)/, "abcd", return: :index)
[{2, 2}, {3, 1}]

iex> Regex.run(~r/c(d)/, "abcd", capture: :first)
["cd"]

iex> Regex.run(~r/c(?<foo>d)/, "abcd", capture: ["foo", "bar"])
["d", ""]

scan(regex, string, options \ [])

@spec scan(t(), String.t(), capture_opts()) ::
  [[String.t()]] | [[{integer(), integer()}]]

run/3와 같지만 정규 표현식의 모든 겹치지 않는 매치를 반환합니다. 리스트의 리스트가 반환되는데, 바깥 리스트의 각 항목은 매치를, 안쪽 리스트의 각 항목은 캡처된 내용을 나타냅니다.

옵션:

  • :return:index로 설정하면 바이트 인덱스와 매치 길이 반환. 기본값 :binary.
  • :capture — 결과에서 무엇을 캡처할지. "Captures" 섹션 참고.
  • :offset — (v1.12.0부터) 주어진 문자열에서 매치를 시작할 시작 오프셋 지정. 기본값 0.
iex> Regex.scan(~r/c(d|e)/, "abcd abce")
[["cd", "d"], ["ce", "e"]]

iex> Regex.scan(~r/c(?:d|e)/, "abcd abce")
[["cd"], ["ce"]]

iex> Regex.scan(~r/e/, "abcd")
[]

iex> Regex.scan(~r/ab|bc|cd/, "abcd")
[["ab"], ["cd"]]

iex> Regex.scan(~r/ab|bc|cd/, "abbccd")
[["ab"], ["bc"], ["cd"]]

iex> Regex.scan(~r/\p{Sc}/u, "$, £, and €")
[["$"], ["£"], ["€"]]

iex> Regex.scan(~r/=+/, "=ü†ƒ8===", return: :index)
[[{0, 1}], [{9, 3}]]

iex> Regex.scan(~r/c(d|e)/, "abcd abce", capture: :first)
[["cd"], ["ce"]]

source(regex)

@spec source(t()) :: String.t()

regex 소스를 바이너리로 반환합니다.

iex> Regex.source(~r/foo/)
"foo"

split(regex, string, options \ [])

@spec split(t(), String.t(), split_opts()) :: [String.t()]

주어진 대상을 주어진 패턴과 주어진 부분 수에 따라 나눕니다.

옵션:

  • :parts — 지정하면 문자열을 주어진 부분 수로 나눔. 지정하지 않으면 :parts 기본값은 :infinity로, 주어진 패턴에 기반해 최대한 많은 부분으로 문자열을 나눔.
  • :trimtrue이면 결과에서 빈 문자열("")을 제거. 기본값 false.
  • :on — 문자열을 어떤 캡처로 나눌지, 어떤 순서로 할지 지정. 기본값 :first는 regex 안의 캡처가 분할 과정에 영향을 주지 않는다는 뜻. 가능한 캡처 값은 "Captures" 섹션 참고.
  • :include_capturestrue이면 결과에 정규 표현식의 매치를 포함. :parts 옵션과 결합하면 그 매치들은 최대 부분 수로 세지 않음. 기본값 false.
iex> Regex.split(~r/-/, "a-b-c")
["a", "b", "c"]

iex> Regex.split(~r/-/, "a-b-c", parts: 2)
["a", "b-c"]

iex> Regex.split(~r/-/, "abc")
["abc"]

iex> Regex.split(~r//, "abc")
["", "a", "b", "c", ""]

iex> Regex.split(~r/a(?<second>b)c/, "abc")
["", ""]

iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second])
["a", "c"]

iex> Regex.split(~r/(x)/, "Elixir", include_captures: true)
["Eli", "x", "ir"]

iex> Regex.split(~r/a(?<second>b)c/, "abc", on: [:second], include_captures: true)
["a", "b", "c"]

iex> Regex.split(~r/-/, "-a-b--c", trim: true)
["a", "b", "c"]

to_embed(regex, embed_opts \ []) (1.19.0부터)

@spec to_embed(t(), [{:strict, boolean()}]) :: String.t()

패턴을 임베드할 수 있는 문자열로 반환합니다. 패턴이 현재 PCRE 버전에서 임베드 가능한 수정자로 표현될 수 없는 옵션으로 컴파일되었고 strict가 true(기본값)이면 ArgumentError 예외가 발생합니다. :strict 옵션이 false이면 문제가 되는 옵션이 없었던 것처럼 패턴이 반환되고 예외를 던지지 않습니다.

임베드 가능한 수정자/옵션은 현재:

  • 'i' — :caseless
  • 'm' — :multiline
  • 's' — :dotall, {:newline, :anycrlf}
  • 'x' — :extended

임베드 불가능한 수정자는:

  • 'f' — :firstline
  • 'U' — :ungreedy
  • 'u' — :unicode, :ucp

여기 목록에 없는 다른 regex 컴파일 옵션은 임베드 불가능한 것으로 간주되어, :strict 옵션이 false가 아니면 예외를 던집니다.

iex> Regex.to_embed(~r/foo/)
"(?-imsx:foo)"

iex> Regex.to_embed(~r/^foo/m)
"(?m-isx:^foo)"

iex> Regex.to_embed(~r/foo # comment/ix)
"(?ix-ms:foo # comment\n)"

iex> Regex.to_embed(~r/foo/iu)
** (ArgumentError) regex compiled with options [:ucp, :unicode] which cannot be represented as an embedded pattern in this version of PCRE

iex> Regex.to_embed(~r/foo/imsxu, strict: false)
"(?imsx:foo\n)"

version() (1.4.0부터, deprecated)

이 함수는 deprecated입니다. :re.version()을 사용하세요. 기저 Regex 엔진의 버전을 반환합니다.

더 알아보기