43 comments

[ 3.4 ms ] story [ 85.5 ms ] thread
Parsing ? On windows even when programs accept command line arguments, good luck finding who they are.
The Wild West of Trying to Shove UTF-8 onto a UTF-16 Operating System
The issue there is that the Windows kernel does not validate Unicode. Linux has the same essential problem. Or more so, in that it does not specify an encoding at all.
They're essentially identical problems. In Linux these strings are arbitrary bytes with a rule forbidding byte 0x00, in Windows they're arbitrary unsigned 16-bit integers, I don't know if 0x0000 is forbidden. Not a significant difference.

Rust's OsString provides a structure that has the potentially nonsensical raw data inside it, and offers ways to ask for:

* The Unicode text if that's what is actually encoded (or else None)

* The text after applying Unicode decoding rules and substituting U+FFFD (Unicode's "Replacement Character" �) where errors occur

* The actual raw bytes / 16-bit unsigned integers

If all your program cared about was a filename, this is not coincidentally also how these operating systems spell filenames, so you can just hand the OsString to an OS-level file API, to open it, rename it or whatever without caring if it is Unicode or not.

The U+FFFD option does expose one potential surprise on Unix flavour systems which doesn't exist on Windows because U+FFFD is one UTF-16 code unit, but it is three UTF-8 code units, so such a "decoded" input could exceed the size of some internal storage you'd assumed would never need to be larger than the command line maximum length.

(comment deleted)
> Today, processes are nearly always launched by other programs, but despite this, must still serialize the argument array into a string as though a human had typed it out.

Author seems to make assumption that an argument array is the fundamental form of a command-line, leading to this weird thinking that on Windows you need extra layer of serialization. But I'm not convinced that is actually the case, I'd view it more like you have a command-line expression and on unixy systems you serialize it as an array and on windows as a string, but neither is really more fundamental form.

More fundamental or not, an array of arguments is much more practical than a string, hopefully well-formed, thrown at you. I can't think of any case where you would not want to split up that string in separate arguments.

(I'm not talking about the case where you control both parent and child program; then you can do whatever you want and even use e.g. JSON. But in the general case, parents expect children to accept argument lists and children expect to be passes argument lists.)

> I can't think of any case where you would not want to split up that string in separate arguments

Random example, but jq takes its input expression non-tokenized (i.e. as a single argument) while in contrast find takes its expression as a sequence of tokens (i.e. an array of arguments).

jq actually has a bunch of flags but they're not commonly used.
True in principle: On Windows, the fundamental data structure for command line args is an array, while on linux it's an array of strings.

I think you could argue that an array is a better-suited data structure than a single string though, as most programs take more than a single argument on the command line.

Also note than on unixy systems, the command line is only "serialized as an array" when you run a command from the shell. (And then the shell does the serialisation, not the OS).

If a program is launched by another process, the command line is always an array, never a string.

> Also note than on unixy systems, the command line is only "serialized as an array" when you run a command from the shell. (And then the shell does the serialisation, not the OS).

> If a program is launched by another process, the command line is always an array, never a string.

Maybe an example illustrates what I meant by serializing the command-line expression. Many programs take key-value arguments in the form `--key value`, so conceptually that is not an array but a serialization of key-value mapping into an array. You could imagine that an higher level wrapper to call that program could be represented as `exec_foo({"key": "value"})`, and that argument map is then serialized into the string array `--key value`.

Of course that sort of serialization of higher-level concepts (like key-value mappings or subcommands or whatev) is very much target program specific. Which leads to the main argument that in any non-trivial cases you need to serialize the higher-level "api" (which might be key-value mapping, or something more complex) to the "wire format" which can be either array of strings or single string; conceptually that is fairly minor difference.

While there's no good reason for the Platonic ideal of a command-line to have one particular shape or another (a string! a list! a tree! a graph!) a vast number of Windows and Unix apps are written in C or C++, which demand the standard main() entry point be given an array of strings. Even completely Windows-native apps that use WinMain() instead of main() still often need to take a filename on the command-line, or want to handle command-line switches, for which they need to lex the command-line string into an array of tokens.
Even C#, a formerly Windows-only language, has Main(string[] args)
When talking about command line programs, we talk about discrete arguments. "The first argument of copy is the source, the second is the destination".
Hum. A command line, the thing you write on shells, is a command followed by a series of parameters. That's its form on Unix, and DOS/Windows didn't deviate from it in any way. It could have created a different interface, but it didn't.
What always confused me on Windows is how shells (e.g. CMD) fit into all of this. You can use pipes, variables, semicolon delimiters etc inside a line, which have to be parsed by the shell before launching any process - and those interact with quoting!

So does that mean the shell first parses a line using its own quoting rules, then launches the processes which parse the line again, possibly using a different set of quoting rules?

This sounds plainly insane.

IIRC Batch interprets ^ escapes unless it's double quoted - so, yes.
It also has a different idea of what’s actually double-quoted: it ignores backslashes completely. (Well, the Microsoft C runtime and SHELL32.DLL also have different ideas in that regard, as mentioned in the links in the post, but CMD.EXE’s different different idea is ... more different. I still don’t understand whetehr SHELL32.DLL and pre-2008 C runtime are the same or not. An MSDN blog post discussing this is—concisely if not in detail—here: https://docs.microsoft.com/en-gb/archive/blogs/twistylittlep... I think I came upon it by following links from the head post but I can’t seem to recover the path.)
Yes the shell interprets it first. `cmd.exe` does very little processing and mostly passes on the literal string. But what it does do can be confusing (e.g. `^` is an escape character which is annoying if you were trying to pass in a regex).

In powershell you can use `--%` to pass a literal string as the command line. This is useful for applications that don't use the standard C/C++ parsing rules.

See: https://docs.microsoft.com/en-us/powershell/module/microsoft...

Thank you very much for pointing out --%. I often find it incredibly hard to find what I'm looking for from Microsoft's voluminous tutorial-style Powershell docs.
Yep, which leads to things like the following not creating out.txt (in cmd.exe)

  prog.exe "abc \" def" > out.txt
> Many runtimes, including Microsoft’s own CRTs, don’t call GetCommandLineW and instead do their own parsing.

I think that's supposed to be CommandLineToArgv here.

As many others, I started on dos, then Windows and finally Linux. When I switched to Linux I found it really odd that the parsing is done by the shell, and how you can't know whether the user typed in a lot of jpg files or the shell expanded *.jpg. but the more I used it, the more reasonable it appeared. You almost never really need to know whether the shell expanded something or not. There is no ambiguity between programs, as the article also mentions. This has bitten me in Windows before, but was somehow just a fact of life. Quoting on Linux is consistent. On Windows, the first time you want to enter a command line that contains multiple literal " and spaces, you want to jump off the nearest cliff. Except you don't if you never used anything else, because it's just normal that every program uses somewhat different parsing rules.

I don’t think that’s a strong argument. Counterarguments:

- it’s easy to hit a Unix command line length limit when a single * argument expands into many arguments. xargs solves that, but it shouldn’t be necessary.

- Unix tools cannot discriminate between * and the list of arguments it expands to. Consequently, rm cannot know whether you typed a single * or the specific list it expands into, so it cannot silently accept the latter and ask ‘are you sure’ for the former.

- “cat foo* > foos” or “grep -r foo * > foos” fill up your disk.

- tools that don’t want to accept file names as input have to do all kinds of escaping to prevent it. For example, you can’t have a calculator that can be invoked with

   calc 3 * 4 + 5
(sed, perl or python one-liners suffer the same fate)

I think a system library function to do the globbing, etc. would have been the better choice.

I also guess Unix doesn’t have that only because, back in the time globbing was invented, it would make all the executables so much larger.

Good points, some more or less relevant.

> - it’s easy to hit a Unix command line length limit when a single * argument expands into many arguments. xargs solves that, but it shouldn’t be necessary.

Not on Linux. It used to be a single page (so in practice 4k), but on my system for example, it's 128k. Never ran into this problem.

> Unix tools cannot discriminate between * and the list of arguments it expands to. Consequently, rm cannot know whether you typed a single * or the specific list it expands into, so it cannot ask silently accept the latter and ask ‘are you sure’ for the former.

Fair point. I like to alias rm to rm -i for that reason, which is sometimes inconvenient too of you actually want to delete a lot of files, so I have to pass -f again to undo it...

> “cat paper* > paper.all” or “grep foo > foos” fill up your disk

Hmmm, does it create the output file first and then expand the arguments? Otherwise the output file has to already exist, which would create the same problem on Windows, or Linux with a glob syscall, unless you explicitly check if stdout is redirected to a file, and that file is one of your input files.

I guess in windows this cannot happen in practice as you cannot open a file twice for reading and writing, but then this has nothing to do with command line handling anymore.

> calc 3 * 4 + 5

You'd probably make it so that it has its own parsing too in that case, so you can do calc '3 * 4 + 5'.

There are ugly situations with the Linux or really shell way, and after having switched to Linux a long time ago I might be biased towards it, but I really think I had greater wtf moments with command line handling on Windows.

> It used to be a single page (so in practice 4k), but on my system for example, it's 128k. Never ran into this problem.

that's fairly reachable with some complicated compiler command lines where things are 8-levels deep in some random embedded SDK

but that's not relevant to the original question of *
> but on my system for example, it's 128k. Never ran into this problem.

That may be because of decades of defence against it. For example, the naive way to run foo on all your .c files in one go is:

  find . -type f -name "*.c" -exec foo {} \+
(aside: also note that I had to escape the + at the end of the command. RANT: of course, man find doesn’t mention that, as that’s a shell issue that has nothing to do with find. Technically correct, but beginning users will be baffled)

In my home directory, that’s about a megabyte of arguments, so I have to fall back to

  find . -type f -name "*.c" -exec foo {} \;
which calls foo thousands of times, to invoking xargs, or to some other workaround. Either way, foo is called more than once, so the result may not be what I expected.

For real-world examples, see

- https://stackoverflow.com/questions/55453993/argument-list-t...

- https://www.linuxquestions.org/questions/linux-software-2/ar...

- https://www.baeldung.com/linux/argument-list-too-long-error

> You'd probably make it so that it has its own parsing too in that case, so you can do calc '3 * 4 + 5’

That’s what you would do, yes, but that’s not ideal, and won’t work well if you want to write a one-liner for a tool whose language can contain single quotes, such as sed/awk/perl/….

> Hmmm, does it create the output file first and then expand the arguments?

Yes, it does.

It all works, but isn’t ideal, and I think using a library to do argument parsing would help here (variable expansion may be a problem when doing that, though, as postponing that till after the tool runs may introduce a race condition when running a tool in the background. If that is a problem at all, I think that’s solvable, though)

> For example, the naive way to run foo on all your .c files in one go is:

  find . -type f -name "*.c" -exec foo {} \+
That would break on Windows much earlier, the limit there is 32k as the article mentions. This example has nothing to do with globbing, you're recursively enumerating all .c files in the find process and building the command's arguments. On Windows, likewise, find would build a huge command line that exceeds 32k chars and fail. For this example to make sense, you'd need a recursive glob (which can be enabled in bash) but on Windows yet again the actual tool you're invoking would need to implement that. I wouldn't bet too many do that.

> It all works, but isn’t ideal, and I think using a library to do argument parsing would help here (variable expansion may be a problem

I mean, we basically have that on Windows since NT, but as the article shows, people/compilers do whatever. It wouldn't be any better on Linux I can tell you. :)

Also, variable Expansion would be impossible in a library that's used by the invoked tool, as they never leave the shell for security reasons if you don't make them part of the environment.

EDIT: copied the wrong find example earlier

Fascinating to read through this, I feel sorry for people who can't use powershell.
I think that rm in particular is usually a shell built-in, not a separate executable, so it does have access to the exact arguments. But your larger point still stands.
> - it’s easy to hit a Unix command line length limit when a single * argument expands into many arguments. xargs solves that, but it shouldn’t be necessary.

this isn't true in the vast majority of cases on Linux and most other Unix-like operating systems; Linux has ARG_MAX=2097152, macOS =262144, and Solaris 10 =1048320. this problem does often arise with globstar, but! that is actually an argument towards having the shell expand arguments, not each command; if each command had to implement globstar, it would be effectively useless.

> - Unix tools cannot discriminate between * and the list of arguments it expands to. Consequently, rm cannot know whether you typed a single * or the specific list it expands into, so it cannot silently accept the latter and ask ‘are you sure’ for the former.

zsh comes with a function to check "rm" arguments and issue a warning if rm * is supplied. this doesn't work for arbitrary commands, but few commands actually require such treatment. a comparison could be made to tab completion functions, which are also shell-specific.

> - “cat foo* > foos” or “grep -r foo * > foos” fill up your disk.

I am almost certain that the implied premise is incorrect. According to https://pubs.opengroup.org/onlinepubs/9699919799/utilities/V..., the shell performs word expansion before redirection.

> - tools that don’t want to accept file names as input have to do all kinds of escaping to prevent it. For example, you can’t have a calculator that can be invoked with

this is a bad example; it is simple to write "calc '3 * 4 + 5'", or of course more reasonably "calc '3*4+5'". in fact, this program already exists, and is called expr.

> I think a system library function to do the globbing, etc. would have been the better choice.

that would be terrible. it would be impossible to innovate globbing extensions like ksh or zsh; the system administrator would dictate for all users a major component of their experience. in modern terms, it would be like only being able to install Gnome or KDE, not both at the same time.

> zsh comes with a function to check "rm" arguments and issue a warning if rm * is supplied.

For those of us following along at home that is the RM_STAR_SILENT option, and there is also a RM_STAR_WAIT option should you wish to just have a little wait first.

> it is simple to write "calc '3 * 4 + 5'"

You can use the GP's syntax without quoting on zsh by using the noglob builtin: `noglob calc 3 * 4 + 5`. There are a few commands I like to alias with `noglob =$cmd` for nicer interactive usage.

--

GNU coreutils' cat and GNU grep will also error out in the basic case of IO loops with `{cat,grep}: foos: input file is output file`.

I largely agree with the premise that there are a heap of problems though, and having some environments that fix the some of the problems isn't all that useful.

> I also guess Unix doesn’t have that only because, back in the time globbing was invented, it would make all the executables so much larger.

I think it was more that, doing it in the shell meant it would be possible to write another shell with totally different syntax and globbing rules. That way, all the commands would still work, no matter which shell any particular user chose to use.

The bourne shell (and its descendants) won out as a "standard" in the end, and the system shell at `/bin/sh` is now required by POSIX to be bourne-compatible, but Unix was designed to have pluggable shells, and it might not have turned out that way.

I don't think programs should be aware of globbing at all except in special circumstances, like with the `find` command. It could be helpful in a few rare circumstances I guess, like with 'rm' as you showed, but you can have the shell itself handle those cases, which is what some shells such as zsh do today.

With regards to trying to pass a literal '*' to commands like calc, I think the user is responsible for escaping the star and not calc itself. Since the shell is what handles glob expansion, calc has no way of even doing so that I can think of. Escaping a glob just requires putting it in single quotes like so

  calc '3 * 4 + 5'
> Quoting on Linux is consistent.

Out of curiosity, what do you use to do this? Gnu getopt and genopt are brilliant in my experience and handle all of the edge cases really well. The fact that they are consistent just means that it works, as long as you don't hit the stack limit.

I mean, from the apps perspective, everything has already been either parsed properly by the shell, or passed directly as intended in an array by another program.

From the user's perspective, the shell does it the same for every program you invoke. Not all shells have exactly the same quoting rules necessarily, but it's the component that's deciding how to do it. If you're in doubt you can even ask it to print how it parses the command line you're currently writing.

On Windows I can compile the same C program with two different compilers and get two different comment line parsing behaviors, which I'd say, is much less obvious to the user.

> Rust (LLVM) pragmatically call[s] CommandLineToArgv

This is not correct. It calls `GetCommandLine` to get the raw command line string and then attempts to parse it in the same way as modern C/C++. https://github.com/rust-lang/rust/blob/3b186511f62b0ce20e72e...

The rules themselves are officially documented here: https://docs.microsoft.com/en-us/cpp/cpp/main-function-comma...

Though the linked David Deley investigation is more in-depth: https://daviddeley.com/autohotkey/parameters/parameters.htm#...

Wow. I would have never known. For the last couple years, I have been running Windows every day on all of my computers. I used Debian Testing for many years before that, though- that's where I learned to code.

When I'm not using WSL2 (which is rare), it's almost always little CLI tools using Python, Rust, Nim, or C with MinGW. I'm a professional C programmer, I use Windows every day at home and at work, and I couldn't tell you how to use the Windows API to get command line args.

All I can say is that I'm all the more grateful for the MinGW people for letting me pretend this insanity isn't there.

Quote: "From Windows XP through Windows 11, this returns exactly the same address as GetCommandLineW. There’s little reason to do it this way other than to annoy Raymond Chen, but it’s still neat and maybe has some super niche use"

Hihi, this made me lol. I bet plenty enough will start to hardcode the address, leading to Ray having an entry saying "this is not in the contract" and after many will do it regardless, in about 10 years and 2 Windows versions later Ray will write another grump-ish entry in his blog of how he had to patch old software that was failing due to Microsoft finally making that address dynamic.

The way CreateProcess takes a string as an argument instead of argv is what made me lose patience with Windows overall.

I wasn't a big fan of Windows to start with but I had to support it as a potential platform in a build syste. We wrote a kind of miniature shell that worked on Windows and Linux to capture and keep output from being interleaved (something GNU make does itself now).

It was a great misery to try to quote various bits of a string so that windows cmdline parsing would interpret internationalised filenames with spaces, single or double quotes etc as separate arguments. The command was potentially a call to exec bash or some other shell with its own quoting rules going on in the arguments....

This may seem like a small thing but it made me really reject the idea of wasting time on the platform. It's really the most crucial API in the OS and it's awful and there was a very simple pre-existing example of how to do it right (char *argv[]) - what excuse was there for it?