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.