Skip to content

Commit 0662f62

Browse files
simeonschaubvtjnash
authored andcommitted
fix command literals with trailing backslashes
`` `\\` `` 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.
1 parent c019132 commit 0662f62

File tree

4 files changed

+42
-17
lines changed

4 files changed

+42
-17
lines changed

base/cmd.jl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -504,6 +504,7 @@ Process(`echo 1`, ProcessExited(0))
504504
```
505505
"""
506506
macro cmd(str)
507+
str = escape_raw_string(str, '`')
507508
cmd_ex = shell_parse(str, special=shell_special, filename=String(__source__.file))[1]
508509
return :(cmd_gen($(esc(cmd_ex))))
509510
end

base/strings/io.jl

Lines changed: 22 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -612,14 +612,14 @@ julia> println(raw"\\\\x \\\\\\"")
612612
macro raw_str(s); s; end
613613

614614
"""
615-
escape_raw_string(s::AbstractString)
616-
escape_raw_string(io, s::AbstractString)
615+
escape_raw_string(s::AbstractString, delim='"') -> AbstractString
616+
escape_raw_string(io, s::AbstractString, delim='"')
617617
618618
Escape a string in the manner used for parsing raw string literals.
619-
For each double-quote (`"`) character in input string `s`, this
620-
function counts the number _n_ of preceding backslash (`\\`) characters,
621-
and then increases there the number of backslashes from _n_ to 2_n_+1
622-
(even for _n_ = 0). It also doubles a sequence of backslashes at the end
619+
For each double-quote (`"`) character in input string `s` (or `delim` if
620+
specified), this function counts the number _n_ of preceding backslash (`\\`)
621+
characters, and then increases there the number of backslashes from _n_ to
622+
2_n_+1 (even for _n_ = 0). It also doubles a sequence of backslashes at the end
623623
of the string.
624624
625625
This escaping convention is used in raw strings and other non-standard
@@ -629,36 +629,41 @@ command-line string into the argv[] array.)
629629
630630
See also [`escape_string`](@ref).
631631
"""
632-
function escape_raw_string(io, str::AbstractString)
632+
function escape_raw_string(io::IO, str::AbstractString, delim::Char='"')
633+
total = 0
633634
escapes = 0
634635
for c in str
635636
if c == '\\'
636637
escapes += 1
637638
else
638-
if c == '"'
639+
if c == delim
639640
# if one or more backslashes are followed by
640641
# a double quote then escape all backslashes
641642
# and the double quote
642-
escapes = escapes * 2 + 1
643-
end
644-
while escapes > 0
645-
write(io, '\\')
646-
escapes -= 1
643+
escapes += 1
644+
total += escapes
645+
while escapes > 0
646+
write(io, '\\')
647+
escapes -= 1
648+
end
647649
end
648650
escapes = 0
649-
write(io, c)
650651
end
652+
write(io, c)
651653
end
652654
# also escape any trailing backslashes,
653655
# so they do not affect the closing quote
656+
total += escapes
654657
while escapes > 0
655-
write(io, '\\')
656658
write(io, '\\')
657659
escapes -= 1
658660
end
661+
total
662+
end
663+
function escape_raw_string(str::AbstractString, delim::Char='"')
664+
total = escape_raw_string(devnull, str, delim) # check whether the string even needs to be copied and how much to allocate for it
665+
return total == 0 ? str : sprint(escape_raw_string, str, delim; sizehint = sizeof(str) + total)
659666
end
660-
escape_raw_string(str::AbstractString) = sprint(escape_raw_string, str;
661-
sizehint = lastindex(str) + 2)
662667

663668
## multiline strings ##
664669

doc/src/base/strings.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,5 +95,6 @@ Base.isspace
9595
Base.isuppercase
9696
Base.isxdigit
9797
Base.escape_string
98+
Base.escape_raw_string
9899
Base.unescape_string
99100
```

test/misc.jl

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1402,3 +1402,21 @@ end
14021402
GC.gc(true); yield()
14031403
@test in_fin[]
14041404
end
1405+
1406+
@testset "cmd literals with escaped backslashes" begin
1407+
@test `` == Cmd(String[])
1408+
@test `\\` == Cmd(["\\"])
1409+
@test `\\\\` == Cmd(["\\\\"])
1410+
@test `\\\\\\` == Cmd(["\\\\\\"])
1411+
1412+
@test `"\\"` == Cmd(["\\"])
1413+
@test `"\\\\"` == Cmd(["\\\\"])
1414+
@test `"\\\\\\"` == Cmd(["\\\\\\"])
1415+
1416+
@test `'\\'` == Cmd(["\\\\"])
1417+
@test `'\\\\'` == Cmd(["\\\\\\\\"])
1418+
1419+
@test `\`\\\`` == Cmd(["`\\`"])
1420+
@test `\`\\\\\`` == Cmd(["`\\\\`"])
1421+
@test `\`\\\\\\\`` == Cmd(["`\\\\\\`"])
1422+
end

0 commit comments

Comments
 (0)