Skip to content

Commit

Permalink
fix command literals with trailing backslashes
Browse files Browse the repository at this point in the history
`` `\\` `` currently throws an error because it is parsed as `@cmd "\\"`
due to escaping in the parser and `shell_parse` therefore thinks it's a
dangling backslash.
  • Loading branch information
simeonschaub authored and vtjnash committed Nov 1, 2023
1 parent c019132 commit 0662f62
Show file tree
Hide file tree
Showing 4 changed files with 42 additions and 17 deletions.
1 change: 1 addition & 0 deletions base/cmd.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
39 changes: 22 additions & 17 deletions base/strings/io.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 ##

Expand Down
1 change: 1 addition & 0 deletions doc/src/base/strings.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,5 +95,6 @@ Base.isspace
Base.isuppercase
Base.isxdigit
Base.escape_string
Base.escape_raw_string
Base.unescape_string
```
18 changes: 18 additions & 0 deletions test/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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

0 comments on commit 0662f62

Please sign in to comment.