Change:
#[test] fn official_examples() { chk(r#"EXE "abc" d e"#, &["EXE", "abc", "d", "e"]); <del> chk(r#"EXE ab d"e f"g h"#, &["EXE", r#"ab"#, "de fg", "h"]); </del> <ins> chk(r#"EXE ab d"e f"g h"#, &["EXE", r"ab", "de fg", "h"]); </ins> chk(r#"EXE a"b c d"#, &["EXE", r#"a"b"#, "c", "d"]); <del> chk(r#"EXE a"b c" d e"#, &["EXE", r#"ab c"#, "d", "e"]); </del> <ins> chk(r#"EXE a"b c" d e"#, &["EXE", r"ab c", "d", "e"]); </ins> } #[test] fn whitespace_behavior() { <del> chk(r#" test"#, &["", "test"]); chk(r#"  test"#, &["", "test"]); chk(r#" test test2"#, &["", "test", "test2"]); chk(r#" test  test2"#, &["", "test", "test2"]); chk(r#"test test2 "#, &["test", "test2"]); chk(r#"test  test2 "#, &["test", "test2"]); chk(r#"test "#, &["test"]); </del> <ins> chk(" test", &["", "test"]); chk("  test", &["", "test"]); chk(" test test2", &["", "test", "test2"]); chk(" test  test2", &["", "test", "test2"]); chk("test test2 ", &["test", "test2"]); chk("test  test2 ", &["test", "test2"]); chk("test ", &["test"]); </ins> } #[test] fn genius_quotes() { chk(r#"EXE "" """#, &["EXE", "", ""]); <del> chk(r#"EXE "" """"#, &["EXE", "", """]); </del> <ins> chk(r#"EXE "" """"#, &["EXE", "", r#"""#]); </ins> chk( r#"EXE "this is """all""" in the same argument""#, <del> &["EXE", "this is "all" in the same argument"], </del> <ins> &["EXE", r#"this is "all" in the same argument"#], </ins> ); <del> chk(r#"EXE "a"""#, &["EXE", "a""]); chk(r#"EXE "a"" a"#, &["EXE", "a"", "a"]); </del> <ins> chk(r#"EXE "a"""#, &["EXE", r#"a""#]); chk(r#"EXE "a"" a"#, &["EXE", r#"a" a"#]); </ins> // quotes cannot be escaped in command names chk(r#""EXE" check"#, &["EXE", "check"]); chk(r#""EXE check""#, &["EXE check"]); <del> chk(r#""EXE """for""" check"#, &["EXE ", r#"for""#, "check"]); chk(r#""EXE "for" check"#, &[r#"EXE "#, r#"for""#, "check"]); </del> <ins> chk(r#""EXE """for""" check"#, &["EXE for check"]); chk(r#""EXE "for" check"#, &[r"EXE for check"]); chk(r#""EXE " for " check"#, &[r"EXE ", "for", r#"""#, "check"]); chk(r#"E"X"E test"#, &["EXE", "test"]); chk(r#"EX""E test"#, &["EXE", "test"]); } // from https://daviddeley.com/autohotkey/parameters/parameters.htm#WINCRULESEX #[test] fn post_2008() { chk("EXE CallMeIshmael", &["EXE", "CallMeIshmael"]); chk(r#"EXE "Call Me Ishmael""#, &["EXE", "Call Me Ishmael"]); chk(r#"EXE Cal"l Me I"shmael"#, &["EXE", "Call Me Ishmael"]); chk(r#"EXE CallMe"Ishmael"#, &["EXE", r#"CallMe"Ishmael"#]); chk(r#"EXE "CallMe"Ishmael""#, &["EXE", r#"CallMe"Ishmael"#]); chk(r#"EXE "Call Me Ishmael""#, &["EXE", r"Call Me Ishmael"]); chk(r#"EXE "CallMe"Ishmael""#, &["EXE", r#"CallMe"Ishmael"#]); chk(r#"EXE ab"#, &["EXE", r"ab"]); chk(r#"EXE "ab""#, &["EXE", r"ab"]); chk(r#"EXE ""Call Me Ishmael"""#, &["EXE", r#""Call Me Ishmael""#]); chk(r#"EXE "C:TEST A""#, &["EXE", r"C:TEST A"]); chk(r#"EXE ""C:TEST A"""#, &["EXE", r#""C:TEST A""#]); chk(r#"EXE "a b c"  d  e"#, &["EXE", "a b c", "d", "e"]); chk(r#"EXE "ab"c"  ""  d"#, &["EXE", r#"ab"c"#, r"", "d"]); chk(r#"EXE ab d"e f"g h"#, &["EXE", r"ab", "de fg", "h"]); chk(r#"EXE a"b c d"#, &["EXE", r#"a"b"#, "c", "d"]); chk(r#"EXE a"b c" d e"#, &["EXE", r"ab c", "d", "e"]); // Double Double Quotes chk(r#"EXE "a b c"""#, &["EXE", r#"a b c""#]); chk(r#"EXE """CallMeIshmael"""  b  c"#, &["EXE", r#""CallMeIshmael""#, "b", "c"]); chk(r#"EXE """Call Me Ishmael""""#, &["EXE", r#""Call Me Ishmael""#]); chk(r#"EXE """"Call Me Ishmael"" b c"#, &["EXE", r#""Call"#, "Me", "Ishmael", "b", "c"]); </ins> }

Discussion:
For a command-line of the form , where does indeed exist, produces different results than and in a C++ program. Specifically, is returning troublesome results. It looks like the CRT and CommandLineToArgvW . Invocation in Command Prompt: Expected arguments: [] C++ main() arguments: [] Result of GetCommandLineW: Result of CommandLineToArgvW: [, ] Result of : [, ] Obviously, this interpretation makes no sense. does not seem to be affected. A Rust program which demonstrates the mismatch: A C++ program which demonstrates the mismatch:
This probably won't surprise you, but stdlib uses CommandLineToArgvW:
That's fine. It wouldn't be too terrible to write a test case to match with the newer CRT, as long as the libs team was fine with it. This sounds like it's going to end with "you broke backwards compat."
Fundamentally this is a big decision of which standard to follow for argument parsing. I'm not particularly concerned with which standard we follow as long as it is Microsoft's standard and we follow it accurately. Decide between: Maintain status quo of behavior. Change behavior to match what newer CRT versions do.
I personally feel that we should follow the newer standard since that's what all Windows apps are using these days. I'm concerned about the possibility of this being a breaking change but this seems to only affect some rare edge cases in quote handling.
I'm legitimately not sure what "all Windows apps are using these days." Let's try Now let's try PowerShell: . From the way variables behave, it looks like putting two quotes in a row creates an escaped quote, but they don't get properly re-escaped when you pass them to a command line app: Microsoft is making it really difficult to figure out what actually counts as "correct" command line parsing, when there doesn't even seem to be a "right" way that actually works with their stuff. Let's see what WSL does: Bash still acts like you expect it, and apparently the WSL-to-Windows bridge generates backslash escapes when it tries to convert WSL's arguments list into a Windows GetCommandLineW string. Nuts that Bash on Ubuntu on Windows does a better job than PowerShell does (at least, it generates command strings that are capable of being unambiguously parsed).

My next thought was to create a file name with quotes in it, and see what kind of parameters Explorer generates for me, but apparently Windows doesn't actually allow quotes in file names. And, finally, let's see what happens if one Rust program calls another one: Apparently, everybody seems to prefer backslashes when generating CLIs for Windows, except for PowerShell which passes quotes unescaped, and which just passes the raw CLI unchanged. So the current approach already seems to have interoperability as good as we would need it to be, but changing it to mimic the current CRT version probably won't break anything, either.
I think it's safe to say that "all Windows apps" are using Microsoft's C runtime for parsing commandline arguments. If "all" means "the majority of software built since 2008". In powershell you can use backticks to escape quotes: .targetdebug "a " c" Or you can surround the argument with single quotes: .targetdebug 'a "b" c' Or you can pass the entire line verbatim, without interpretation by the shell: .targetdebug --% "a "b" c" Or, if you're feeling particularly adventurous: .targetdebug you can write a multi-line "argument" '@ I'm not sure how you reached your last conclusion. It seems odd to use Rust's argument parsing to prove how most Windows programs handle arguments, no?
No I can't. Here's what happens when I try: PS C:Usersmichatestexe.targetdebug "a " c" env::args() = ["C:Usersmichatestexetargetdebug", "a b c"] GetCommandLineW = "C:Usersmichatestexetargetdebug" "a "b" c" It's exactly the same as when I did it with two quotes in a row. It creates a string object with quotes in it (like we want), but then it turns around and passes it verbatim without re-escaping it.

PS C:Usersmichatestexe.targetdebug 'a "b" c' env::args() = ["C:Usersmichatestexetargetdebug", "a b c"] GetCommandLineW = "C:Usersmichatestexetargetdebug" "a "b" c" It's not PowerShell's syntax that I'm questioning here. I can easily create a string object with quotes in it. What I want to know is what syntax PowerShell expects us to have. What syntax do they use when re-serializing the command line? Apparently, they don't. Rust and Cargo communicate through command-line parameters, so we probably want to make sure that and can accurately round-trip their data.
The behaviour I was hoping for, by the way, is something like one of these: It's the same behaviour that you were getting when you demoed the python interpreter's argument parsing: it's a single argument with spaces and quotes within it. It's "how do I escape a quote?"
Just to be absolute clear about how Windows works, it might help to completely forget how *nix works. The Windows kernel doesn't know at all about arguments. As far as it's concerned arguments are just a single string that it can pass on to a new process. No more, no less. It's not an array of strings. It's just one string. This string doesn't have to contain the or anything else. The kernel does not care. This string is what gets. It's up to individual applications to send the kernel a string when creating a process. It's up to each individual application to parse it however it likes (or not at all). So what happens is relatively simple: shell sets the string to the being invoked and appends the arguments as the user input them (subject to how the shell itself interprets strings). invoked application parses the string according to its own rules (although in reality it usually defers to the C runtime's interpretation). Step 1 in the above could be replaced by an application. In which case it's entirely up to the application what it sets to. There are no rules. Of course you usually want to make an arguments string that will be correctly interpreted by other applications.

So for compatibility reasons double quotes should be escaped if they are actually wanted in the parsed arguments. Or not if they're unwanted. They all work the same, except for Rust which is special. In the following table I've trimmed the enclosing for onwards, just to make it clearer but you can imagine they're still there.
In short the powershell user is constructing the string more or less directly, the particulars of string types aside. But I would add that the intricacies of powershell should have nothing to do with Rust std's handling of command line arguments. So long as the std behaves like other Windows applications it doesn't matter. If powershell makes a nicer way to construct a string then great! Rust will benefit too, assuming it acts the same as other Windows applications. So to sum up, as you say, Rust has two jobs: Parse the incoming command line string (the were linked above) Construct an outgoing command line string (for ) Of course it's easy for Rust to be consistent with itself no matter what rules it uses. It just has to test that construction and parsing match up as expected. However, it's obviously beneficial to be consistent with other applications on the platform. All that said, there's nothing massively wrong with how Rust currently parses arguments. It's just that 12 years ago Microsoft tweaked their CRT's argument parsing rules slightly. Rust uses the old rules so, in some situations, Rust applications may exhibit surprising behaviours to users more accustomed to modern applications.
Yeah, I agree with everything you said there. I'm fine with changing to the new CRT behaviour, as long as we don't break anything. And it looks like we won't.
PowerShell is not a good example, because the rules are still screwed up. . It's very uncomfortable to call apps from PowerShell. I sometimes ended up with generating a bat file and then running that from PowerShell.
I'd highly recommend installing if you can. The current stable version is 7.0.1 which has slightly better command line handling, and the upcoming 7.1 release should improve it further. Otherwise, using (or manually escaping double-quotes using backticks) to set the command string directly produces the most consistent results.