swift-argument-parser icon indicating copy to clipboard operation
swift-argument-parser copied to clipboard

[WIP] Add DOS-/Windows-style command line parsing

Open grynspan opened this issue 2 years ago • 3 comments

⚠️ Not ready for merging yet!

Description

This change introduces support for DOS-/Windows-style command line parsing. Historically, DOS and Windows have used a different set of rules for parsing their command-line arguments than POSIX-like or UNIX-like platforms like Linux, BSD, macOS, etc. Wikipedia has a decent write-up here.

Detailed Design

There are two commits:

  • The first commit refactors snake case/camel case conversions to be more flexible. This is necessary in order to get the correct parsing behaviour using the DOS convention.
  • The second commit actually adds the new parsing convention and plumbs it through.

An open problem is that this is a runtime configuration flag, but there's no good way to make it per-command state (e.g. a field on CommandConfiguration) without tendrils getting eeeeeverywhere. Thoughts/feedback/advice here would be good. For now, you control the parsing convention by setting ParsingConvention.current = .dos before calling Command.main().

Documentation Plan

TODO

Test Plan

Existing tests continue to pass despite the refactor (a good thing.) I've added new tests confirm the basic assumptions of the code (i.e. that it can handle both conventions.) There's always room for more tests.

Source Impact

There are a few symbol names including the phrase withSingleDash which, because DOS and Windows don't use dashes, is misleading. I've introduced replacement API named withShortPrefix instead and deprecated the existing symbols.

Because existing codebases expect POSIX-style parsing, DOS-style parsing is off by default even on Windows.

Checklist

  • [x] I've added at least one test that validates that my change is working, if appropriate
  • [x] I've followed the code style of the rest of the project
  • [x] I've read the Contribution Guidelines
  • [ ] I've updated the documentation if necessary

grynspan avatar Feb 03 '22 23:02 grynspan

@swift-ci please test

grynspan avatar Feb 03 '22 23:02 grynspan

My overarching question is whether it makes sense to always couple the UNIX interface and the Windows interface, given that there are different idioms across the platforms. That is, if I define @Flag var version = false with names -v and --version, should a Windows version of my executable always have +v and /Version, or should we have a way to specify those names separately? I don't think I know enough about the way command-line tools are usually built to know which is the right approach. Do you have examples of command-line tools that you find idiomatic? @compnerd, do you have any insight you can share here?

I don't think there's a 1:1 correspondence between the POSIX convention and Windows convention here. Windows commands don't consistently use one syntax or another.

Additionally, the short/long dichotomy (e.g. -v / --verbose) is pretty clear on the Unix side, but it doesn't seem so apparently clear on Windows, nor do / and + seem to match up to the short/long terminology very well. Do individual tools support both? What's the difference between them?

Broadly speaking, a leading slash is used where both "--" and "-" might be used on POSIX, while a leading plus is used for a flag that can be set or cleared. But it's not a universal rule. So maybe we could say "if a @Flag has a .short name, use +, otherwise use /."


Perhaps instead of a distinct "parsing convention", we could add new cases to Name/NameSpecification:

enum Name {
  ...
  case slashName(String) // e.g. /Foo, /A, /FooBar
  case plusFlag(Character) // e.g. +a, +A, maybe also supporting -a for negation?
}

And then when deciding which names are valid, we pre-flight it: if any "DOS" names are present and we're on Windows, use them and ignore the POSIX names, otherwise use the POSIX names:

extension NameSpecification {
  var filteredNames: [Name] {
#if os(Windows)
    let windowsNames = self.names.filter(\.isWindowsConvention)
    if !windowsNames.isEmpty {
      return windowsNames
    }
#endif
    return self.names.filter(\.isPOSIXConvention)
  }

Or however that's spelled.

The downside being that each individual argument/flag/option would need to be annotated for Windows, and I can't see many devs bothering.

grynspan avatar Feb 08 '22 18:02 grynspan

So, I think that help is one case that is odd (-?, /? and --help are common, -h less so on Windows). In general, the really interesting thing is that the Windows command line is more of a mess than the Unix-y ones. Unixy ones have the original unix style arguments (e.g. x, f in tar), GNU style arguments (e.g. -h, --h, --help in GNU tools), and SUN style (e.g. -lto_path in ld64).

Windows on the other hand generally accepts - and / as the argument specifier. Help is the only one that is generally available as -?. Most others are less formally defined. One thing that is generally interesting is that they are also somewhat more lax about case sensitivity (that is everything should be case normalized before matching).

As examples: link -dump -exports ... is equivalent to link /dump /exports but I tend to use the former (years of practice is difficult to break). certutil -hashfile ... md5 is equivalent to certutil -HashFile ... MD5

Command line processing is complicated 😭

compnerd avatar Feb 08 '22 18:02 compnerd