Skip to content

Commit 49abb10

Browse files
committed
build: the GNU response file doubles backslashes, because the tokenizer escapes them inside quotes too
The first form single-quoted each argument, which a POSIX shell would take literally and this tokenizer does not: LLVM's GNU tokenizer escapes a backslash inside quotes as well as outside, so the Windows paths still arrived with their separators eaten. Measured with clang 22.1.8 -- a response file holding `'-DX=a\b'` yields `X=ab`, one holding `-DX=a\\b` yields `X=a\b` -- so every backslash is doubled, every quote escaped, and whitespace handled by quoting the whole argument. The case states the measurement.
1 parent 77b632f commit 49abb10

2 files changed

Lines changed: 29 additions & 19 deletions

File tree

‎src/build/build_program.cppm‎

Lines changed: 15 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -430,19 +430,24 @@ std::string response_file_body(std::span<const std::string> args, bool gnuQuotin
430430
std::string body;
431431
for (auto const& a : args) {
432432
if (gnuQuoting) {
433-
// GNU TOKENIZATION TREATS A BACKSLASH AS AN ESCAPE, everywhere but
434-
// inside single quotes. A Windows path written plainly therefore
435-
// arrives with its separators eaten -- clang read
433+
// GNU TOKENIZATION TREATS A BACKSLASH AS AN ESCAPE, INCLUDING INSIDE
434+
// QUOTES -- which is where this differs from a POSIX shell, and where
435+
// the first attempt at this function was wrong. A Windows path
436+
// written plainly arrives with its separators eaten: clang read
436437
// `D:\a\mcpp-plugins\...` back as `D:amcpp-plugins...` and
437-
// reported `no such file or directory`. Inside single quotes every
438-
// character is literal, so each argument is wrapped, and an embedded
439-
// single quote is closed, escaped and reopened.
440-
body.push_back('\'');
438+
// reported `no such file or directory`, and single-quoting it changed
439+
// nothing. Measured with clang 22.1.8: a response file holding
440+
// `'-DX=a\b'` yields `X=ab`, and one holding `-DX=a\\b` yields
441+
// `X=a\b`. So every backslash is doubled and every quote escaped,
442+
// and whitespace is handled by quoting the whole argument.
443+
const bool quote = a.find_first_of(" \t") != std::string::npos;
444+
if (quote) body.push_back('"');
441445
for (char c : a) {
442-
if (c == '\'') body += "'\\''";
443-
else body.push_back(c);
446+
if (c == '\\' || c == '"') body.push_back('\\');
447+
body.push_back(c);
444448
}
445-
body += "'\n";
449+
if (quote) body.push_back('"');
450+
body.push_back('\n');
446451
continue;
447452
}
448453
// Windows tokenization (cl, clang-cl): a backslash is literal except

‎tests/unit/test_sources.cpp‎

Lines changed: 14 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -253,20 +253,25 @@ TEST(Sources, ResponseFileQuotesForTheWindowsTokenizer) {
253253
}
254254

255255
TEST(Sources, ResponseFileKeepsBackslashesLiteralForTheGnuTokenizer) {
256-
// clang and GCC read a backslash as an escape, so a Windows path written
257-
// plainly comes back with its separators eaten; inside single quotes
258-
// nothing is special.
256+
// clang and GCC read a backslash as an escape, inside quotes as well as
257+
// outside, so a Windows path survives only when every backslash is doubled.
258+
// Measured with clang 22.1.8: `'-DX=a\b'` in a response file yields `X=ab`,
259+
// and `-DX=a\\b` yields `X=a\b`.
259260
const std::vector<std::string> args{
260261
"-fmodule-file=mcpp=D:\\a\\p\\mcpp.pcm",
261262
"D:\\a\\obj\\x.o",
262-
"-DNAME=it's",
263+
"/Tp D:\\a\\b c\\build.mcpp",
264+
R"(-DNAME="v")",
263265
};
264266
const auto body = mcpp::build::response_file_body(args, /*gnuQuoting=*/true);
265-
EXPECT_EQ(std::ranges::count(body, '\n'), 3);
266-
EXPECT_NE(body.find("'-fmodule-file=mcpp=D:\\a\\p\\mcpp.pcm'\n"), std::string::npos) << body;
267-
EXPECT_NE(body.find("'D:\\a\\obj\\x.o'\n"), std::string::npos) << body;
268-
// A single quote in the argument closes, escapes and reopens.
269-
EXPECT_NE(body.find("'-DNAME=it'\\''s'\n"), std::string::npos) << body;
267+
EXPECT_EQ(std::ranges::count(body, '\n'), 4);
268+
EXPECT_NE(body.find("-fmodule-file=mcpp=D:\\\\a\\\\p\\\\mcpp.pcm\n"), std::string::npos) << body;
269+
EXPECT_NE(body.find("D:\\\\a\\\\obj\\\\x.o\n"), std::string::npos) << body;
270+
// Whitespace is handled by quoting the whole argument; the doubling holds
271+
// inside the quotes too.
272+
EXPECT_NE(body.find("\"/Tp D:\\\\a\\\\b c\\\\build.mcpp\"\n"), std::string::npos) << body;
273+
// A quote of the argument's own is escaped.
274+
EXPECT_NE(body.find("-DNAME=\\\"v\\\"\n"), std::string::npos) << body;
270275
}
271276

272277
// ── [xlings.overrides] in config.toml ───────────────────────────────────────

0 commit comments

Comments
 (0)