diff --git a/NEWS.md b/NEWS.md index 0bff77b253102d..7e90e6c7b16cfa 100644 --- a/NEWS.md +++ b/NEWS.md @@ -47,6 +47,7 @@ Standard library changes `maximum` ([#30323]). * `hasmethod` can now check for matching keyword argument names ([#30712]). * `startswith` and `endswith` now accept a `Regex` for the second argument ([#29790]). +* `Regex` can now be multiplied (`*`) and exponentiated (`^`), like strings ([#23422]). * `retry` supports arbitrary callable objects ([#30382]). * `filter` now supports `SkipMissing`-wrapped arrays ([#31235]). * A no-argument construct to `Ptr{T}` has been added which constructs a null pointer ([#30919]) diff --git a/base/regex.jl b/base/regex.jl index 3bee6dbd649478..5de832439a4c98 100644 --- a/base/regex.jl +++ b/base/regex.jl @@ -517,3 +517,49 @@ function hash(r::Regex, h::UInt) h = hash(r.compile_options, h) h = hash(r.match_options, h) end + +## String operations ## + +unwrap_string(r::Regex) = r.pattern +unwrap_string(s::Union{AbstractString,Char}) = s + +""" + *(s::Regex, t::Union{Regex,AbstractString,AbstractChar}) -> Regex + *(s::Union{Regex,AbstractString,AbstractChar}, t::Regex) -> Regex + +Concatenate regexes, strings and/or characters, producing a [`Regex`](@ref). + +!!! compat "Julia 1.2" + This method requires at least Julia 1.2. + +# Examples +```jldoctest +julia> r"Hello " * "world" +r"Hello world" + +julia> 'j' * r"ulia" +r"julia" +``` +""" +function *(r1::Union{Regex,AbstractString,AbstractChar}, rs::Union{Regex,AbstractString,AbstractChar}...) + opts = unique((r.compile_options, r.match_options) for r in (r1, rs...) if r isa Regex) + length(opts) == 1 || + throw(ArgumentError("cannot multiply regexes with incompatible options")) + Regex(string(unwrap_string(r1), unwrap_string.(rs)...), opts[1][1], opts[1][2]) +end + +""" + ^(s::Regex, n::Integer) + +Repeat a regex `n` times. + +!!! compat "Julia 1.2" + This method requires at least Julia 1.2. + +# Examples +```jldoctest +julia> r"Test "^3 +r"Test Test Test " +``` +""" +^(r::Regex, i::Integer) = Regex(r.pattern^i, r.compile_options, r.match_options) diff --git a/stdlib/REPL/src/REPL.jl b/stdlib/REPL/src/REPL.jl index f119ab3f8d7c4d..0fc71c88d285d5 100644 --- a/stdlib/REPL/src/REPL.jl +++ b/stdlib/REPL/src/REPL.jl @@ -925,6 +925,7 @@ function setup_interface( oldpos = firstindex(input) firstline = true isprompt_paste = false + jl_prompt_len = 7 # "julia> " while oldpos <= lastindex(input) # loop until all lines have been executed if JL_PROMPT_PASTE[] # Check if the next statement starts with "julia> ", in that case @@ -934,7 +935,6 @@ function setup_interface( oldpos >= sizeof(input) && return end # Check if input line starts with "julia> ", remove it if we are in prompt paste mode - jl_prompt_len = 7 if (firstline || isprompt_paste) && startswith(SubString(input, oldpos), JULIA_PROMPT) isprompt_paste = true oldpos += jl_prompt_len @@ -959,7 +959,7 @@ function setup_interface( tail = lstrip(tail) end if isprompt_paste # remove indentation spaces corresponding to the prompt - tail = replace(tail, r"^ {7}"m => "") # 7: jl_prompt_len + tail = replace(tail, r"^"m * ' '^jl_prompt_len => "") end LineEdit.replace_line(s, tail, true) LineEdit.refresh_line(s) @@ -969,7 +969,7 @@ function setup_interface( line = strip(input[oldpos:prevind(input, pos)]) if !isempty(line) if isprompt_paste # remove indentation spaces corresponding to the prompt - line = replace(line, r"^ {7}"m => "") # 7: jl_prompt_len + line = replace(line, r"^"m * ' '^jl_prompt_len => "") end # put the line on the screen and history LineEdit.replace_line(s, line) diff --git a/test/regex.jl b/test/regex.jl index cb3fa965f8a50b..f73c2028469d41 100644 --- a/test/regex.jl +++ b/test/regex.jl @@ -78,6 +78,29 @@ @test !endswith("abc", r"C") @test endswith("abc", r"C"i) + @testset "multiplication & exponentiation" begin + @test r"a" * r"b" == r"ab" + @test r"a" * "b" == r"ab" + @test r"a" * 'b' == r"ab" + @test "a" * r"b" == r"ab" + @test 'a' * r"b" == r"ab" + for a = (r"a", "a", 'a'), + b = (r"b", "b", 'b'), + c = (r"c", "c", 'c') + a isa Regex || b isa Regex || c isa Regex || continue + @test a * b * c == r"abc" + end + @test r"a"i * r"b"i == r"ab"i + @test r"a"i * "b" == r"ab"i + @test r"a"i * 'b' == r"ab"i + @test "a" * r"b"i == r"ab"i + @test 'a' * r"b"i == r"ab"i + @test_throws ArgumentError r"a"i * r"b" + @test_throws ArgumentError r"a" * r"b"i + + @test r"abc"^ 2 == r"abcabc" + end + # Test that PCRE throws the correct kind of error # TODO: Uncomment this once the corresponding change has propagated to CI #@test_throws ErrorException Base.PCRE.info(C_NULL, Base.PCRE.INFO_NAMECOUNT, UInt32)