diff --git a/base/cmd.jl b/base/cmd.jl index da29c732c7f26..8ee94df239f0b 100644 --- a/base/cmd.jl +++ b/base/cmd.jl @@ -504,6 +504,7 @@ Process(`echo 1`, ProcessExited(0)) ``` """ macro cmd(str) + str = escape_raw_string(str, '`') cmd_ex = shell_parse(str, special=shell_special, filename=String(__source__.file))[1] return :(cmd_gen($(esc(cmd_ex)))) end diff --git a/base/strings/io.jl b/base/strings/io.jl index 441b39693096f..c1e45775f6ca0 100644 --- a/base/strings/io.jl +++ b/base/strings/io.jl @@ -612,14 +612,14 @@ julia> println(raw"\\\\x \\\\\\"") macro raw_str(s); s; end """ - escape_raw_string(s::AbstractString) - escape_raw_string(io, s::AbstractString) + escape_raw_string(s::AbstractString, delim='"') -> AbstractString + escape_raw_string(io, s::AbstractString, delim='"') Escape a string in the manner used for parsing raw string literals. -For each double-quote (`"`) character in input string `s`, this -function counts the number _n_ of preceding backslash (`\\`) characters, -and then increases there the number of backslashes from _n_ to 2_n_+1 -(even for _n_ = 0). It also doubles a sequence of backslashes at the end +For each double-quote (`"`) character in input string `s` (or `delim` if +specified), this function counts the number _n_ of preceding backslash (`\\`) +characters, and then increases there the number of backslashes from _n_ to +2_n_+1 (even for _n_ = 0). It also doubles a sequence of backslashes at the end of the string. This escaping convention is used in raw strings and other non-standard @@ -629,36 +629,41 @@ command-line string into the argv[] array.) See also [`escape_string`](@ref). """ -function escape_raw_string(io, str::AbstractString) +function escape_raw_string(io::IO, str::AbstractString, delim::Char='"') + total = 0 escapes = 0 for c in str if c == '\\' escapes += 1 else - if c == '"' + if c == delim # if one or more backslashes are followed by # a double quote then escape all backslashes # and the double quote - escapes = escapes * 2 + 1 - end - while escapes > 0 - write(io, '\\') - escapes -= 1 + escapes += 1 + total += escapes + while escapes > 0 + write(io, '\\') + escapes -= 1 + end end escapes = 0 - write(io, c) end + write(io, c) end # also escape any trailing backslashes, # so they do not affect the closing quote + total += escapes while escapes > 0 - write(io, '\\') write(io, '\\') escapes -= 1 end + total +end +function escape_raw_string(str::AbstractString, delim::Char='"') + total = escape_raw_string(devnull, str, delim) # check whether the string even needs to be copied and how much to allocate for it + return total == 0 ? str : sprint(escape_raw_string, str, delim; sizehint = sizeof(str) + total) end -escape_raw_string(str::AbstractString) = sprint(escape_raw_string, str; - sizehint = lastindex(str) + 2) ## multiline strings ## diff --git a/doc/src/base/strings.md b/doc/src/base/strings.md index 2504f3dbd583a..2b70a4f53198e 100644 --- a/doc/src/base/strings.md +++ b/doc/src/base/strings.md @@ -95,5 +95,6 @@ Base.isspace Base.isuppercase Base.isxdigit Base.escape_string +Base.escape_raw_string Base.unescape_string ``` diff --git a/test/misc.jl b/test/misc.jl index 249175a0ed1d3..db7944740cf9c 100644 --- a/test/misc.jl +++ b/test/misc.jl @@ -1402,3 +1402,21 @@ end GC.gc(true); yield() @test in_fin[] end + +@testset "cmd literals with escaped backslashes" begin + @test `` == Cmd(String[]) + @test `\\` == Cmd(["\\"]) + @test `\\\\` == Cmd(["\\\\"]) + @test `\\\\\\` == Cmd(["\\\\\\"]) + + @test `"\\"` == Cmd(["\\"]) + @test `"\\\\"` == Cmd(["\\\\"]) + @test `"\\\\\\"` == Cmd(["\\\\\\"]) + + @test `'\\'` == Cmd(["\\\\"]) + @test `'\\\\'` == Cmd(["\\\\\\\\"]) + + @test `\`\\\`` == Cmd(["`\\`"]) + @test `\`\\\\\`` == Cmd(["`\\\\`"]) + @test `\`\\\\\\\`` == Cmd(["`\\\\\\`"]) +end