customLong(withSingleDash:) should be enabled by default

i’m not going to expound on this too much, except to say that the way Swift ArgumentParser handles single dash-prefixed arguments differently from double dash-prefixed arguments is incredibly confusing.

sample command:

$ archer crush .build.wasm/release/ECMAScriptTest.wasm \
    -o Bundle/main.wasm \
    -Xwasm-opt --enable-threads

Error: Missing value for '-o <output>'
Help:  -o <output>  Where to write the optimized WebAssembly (wasm) binary
Usage: archer crush [<wasm-file>] [--output <output>] [--Xwasm-opt <Xwasm-opt> ...] [--preserve-debug-info]
  See 'archer crush --help' for more information.

i think the expansion of -abc into -a -b -c is a GNU-ism that really ought to be opt-in rather than opt-out.

it’s really non-obvious that the spelling error in that example is -Xwasm-opt instead of --Xwasm-opt, and even less obvious that this is caused by a customLong(withSingleDash:) configuration error in the command line application itself.

related: Why does Swift treat double dash command line arguments differently?

1 Like

It really depends on where you’re coming from. You could just as well argue that single-dash-multiple-characters are something that’s very confusing.

It is similar to getopt_long, though.

Packed short labels

Every argument parser that I've looked at expands packed short labels. Almost every unix command assumes this, so it should not be confusing to most people.

IMHO however, one should never mix "old-style" options (e.g., -foo) with long options (e.g. --foo) in a command definition. Also, I would reserve old-style options for use with compilers, etc. Finally, if you have old-style options, then the expansion of packed short options should be disabled to avoid ambiguity.

In order to learn more about Swift, from time to time I update an argument parser that I wrote many years ago. Recently, I gave this some serious thought and came finally settled on the following parser behavior options:

public struct ParserOptions: Sendable {
    public let restrictExtraOccurances: Bool
    public let permuteStrandedValues: Bool
    public let defaultLabelIsOldStyle: Bool
    public let doNotParsePackedShortLabels: Bool

    /// - Parameters:
    ///   - restrictExtraOccurances: Respect each parameter's maxNumberOfOccurances property
    ///   - permutePostionalArguments: Add stranded postional arguments to head of postional argument list
    ///   - defaultLabelIsOldStyle: Use single hyphen insteade of double hyphen for default label
    ///   - doNotParsePackedShortLabels: Do treat "-ab" to be the same as "-a -b" and
    public init(
        restrictExtraOccurances: Bool = false,
        permuteStrandedValues: Bool = false,
        defaultLabelIsOldStyle: Bool = false,
        doNotParsePackedShortLabels: Bool = false
    ) {
        self.restrictExtraOccurances = restrictExtraOccurances
        self.permuteStrandedValues = permuteStrandedValues
        self.defaultLabelIsOldStyle = defaultLabelIsOldStyle
        self.doNotParsePackedShortLabels = doNotParsePackedShortLabels
    }
}

Should ArgumentParser do something similar?

Error messages

That said, I think that the real problem here is the error message itself. In general, it is less confusing if as many errors as possible are reported. Here is an example.

import APLess
import APLessMacros
import Foundation

typealias Number = Double
typealias File = String

enum Operation: String, RawRepresentable, APLessType {
    case add, mult
    var description: String { self.rawValue }
}

@main
struct Main03 {
    /// Add or multiply elements of a list of numbers, and optionally
    /// print contents of a text file, if specified.
    /// - Parameters:
    ///   - uppercase: Uppercase text before printing
    ///   - lowercase: Lowercase text before printing
    ///   - operation: Either "add" or "mult"
    ///   - input: The path, relative to current directory, of the text file, if any
    ///   - values: A non-empty list numbers, none of which exceeds 100
    /// The -u and -l switches override each other; the last one specified determines the formatting used.
    @MainFunction(shadowGroups: ["uppercase lowercase"])
    static func main03(
        u uppercase: Flag,
        l lowercase: Flag,
        o__op operation: Operation = .add,
        input: File?,
        values: Variadic<Number>  // if "values: Number..." then argument value cannot be an [Number]!
    ) async throws {
        let validationErrors = values.compactMap{ $0 > 100 ? "$L{values} has a $V{values} greater than 100: \($0)" : nil }
        if !validationErrors.isEmpty {
            throw APLessError(handledErrors: validationErrors)
        }
        if let input {
            var text = try String(contentsOfFile: input)
            if uppercase { text = text.uppercased() }
            if lowercase { text = text.lowercased() }
            print(text)
        }
        try await Task.sleep(nanoseconds: 1_000_000)
        switch operation {
        case .add: print("add: \(values.reduce(0, +))")
        case .mult: print("mult: \(values.reduce(1, *))")
        }
    }
}

Invoke the help screen with "--help" anywhere in the argument list.

release> ./main03 --help

DESCRIPTION: Add or multiply elements of a list of numbers, and optionally
             print contents of a text file, if specified.

USAGE: main03 -[ul] [--op OPERATION] [--input FILE] --values NUMBER...

PARAMETERS:
  -u                   Uppercase text before printing.
  -l                   Lowercase text before printing.
  -o/--op OPERATION    Either "add" or "mult" (default: add).
  --input FILE         The path, relative to current directory, of the text
                       file, if any.
  --values NUMBER...   A non-empty list numbers, none of which exceeds 100.

NOTE:
  The -u and -l switches override each other; the last one specified determines
  the formatting used.

A good run:

release> ./main03 --values -23 -19
add: -42.0

release> cat text.txt 
Thanks for reading this!     
                                     
release> ./main03 -ul -lu --input text.txt --op mult --values -23 -19
THANKS FOR READING THIS!
mult: 437.0

Some input with a lot of parse errors:

release> ./main03 -uzlxuxl --in text.txt --op multiply --values one two
Errors:
  unrecognized short labels, '-z' and '-x', in '-uzlxuxl'
  unrecognized label: '--in'
  stranded positional value: 'text.txt'
  'multiply' is not a valid OPERATION
  none of 'one' and 'two' is a valid 'NUMBER'
See 'main03 --help' for more information.

The value "text.txt" is "stranded" because it cannot be associated with a label, and by default,
this parser does not permute such values to the the head of the list of positional values.

Here is there is bad data that is reported as a "validation" error:

release> ./main03 --values -23 -19 1 2 330 200
Errors:
  --values has a NUMBER greater than 100: 330.0
  --values has a NUMBER greater than 100: 200.0
See 'main03 --help' for more information.

Here is an "unhandled" error, generated by the system:

release> ./main03 --values -23 -19 --input missing.txt
Error:
  The file “missing.txt” couldn’t be opened because there is no such file.
See 'main03 --help' for more information.

The point is that it is not that hard to report multiple errors. Also, parse errors, validation errors,
and system generated errors can be reported using a common "error screen".

IMHO ArgumentParser could improve its error reporting.

1 Like

I completely agree. From my perspective, I fully expect to use double dash for long options and single dash for short options, and not being able to combine multiple short options with a single dash would be very confusing.

I have also been thinking about this a lot, and I agree that in most cases the parsing options should be consistent within a command. While I think the flexibility of customising each argument is useful and shouldn’t be removed (SwiftPM itself is an example where this is necessary), perhaps we should have a way to define default options for all the arguments in a command.

Definitely want to be able to specify up to three "labels" for any parameter as long as they mean the same thing. e.g., some people like -h, -help, --help. Thus, in the example, o__op means that operation arguments can be, say, "-o add" or "--op add".
And "o_op_op" would add "-op add". (This is the same as Fish's argparse with "_" instead of "/"). ArgumentParser, of course, provides all of this. The question is what happens when there is no customization (say input in the example). I think long is universal, but for some cases the default should be old-style.