Throwing from a `transform` closure

If an argument property supplies a transform closure, like so,

@Option(help: "Some string input please.", transform: { try convert($0) })
var input: String

...and the function convert(...) throws an error should this error be printed to the standard error in the same way throwing form run() or validate() do?

All I get is "Error: Internal error. Invalid state while parsing command-line arguments.", no matter what type of error I throw (ExitCode, ValidationError, CleanExit).

Have I misunderstood something?

No, that sounds like a bug — it should really treat anything thrown at that point as a validation error.

I've created a PR: https://github.com/apple/swift-argument-parser/pull/110

I wasn't sure if I was going in the right directions so its very basic, but I'm happy to flesh it out of it passes muster.

I wonder if any kind of error thrown from a transform closure should be wrapped in a userValidationError instead of just ValidationErrors. It would probably be more useful to see something along the lines of "Error: <specific transform error>" if the argument wasn't transformable.

1 Like

After feedback I have updated the implementation and created a new PR: https://github.com/apple/swift-argument-parser/pull/115

Any error thrown from a transform closure is now wrapped in a ParserError.unableToParseValue error.

I also added a customMessage associated value to ParserError.unableToParseValue. When this is set the default error message is suppressed and customMessage is used.

Therefore it is possible to use ValidationErrors with a message to return a custom message.

It is also possible to throw any error your like from the transform closure and the default error message is significant improved, no more "Invalid state while parsing command-line arguments". Additionally, custom error types can conform to the new protocol CustomParserErrorConvertible to participate in the custom error mechanism.

After a few rounds of code review I think we are at a solution for this. See the PR here.

The main change was removing the CustomParserErrorConvertible protocol and using LocalizedError or CustomStringConvertible.

I have updated the documentation too, below is the section I added, feedback welcome.

Handling Transform Errors

During argument and option parsing you can use a closure to transform the command line stings to custom types. If this transformation fails you can throw a ValidationError its message property will be displayed to the user.

In addition, you can throw your own errors. Errors that conform to CustomStringConvertible or LocalizedError provide the best experience for users.

struct ExampleTransformError: Error, CustomStringConvertible {
    var description: String
}

struct ExampleDataModel: Codable {
  let identifier: UUID
  let tokens: [String]
  let tokenCount: Int
}

struct Example: ParsableCommand {

  // Reads in the argument string and attempts to transform it to
  // a `ExampleDataModel` object using the JSONDecoder. If the
  // string is not valid JSON `decode` will throw an error and
  // parsing will halt.
  @Argument(transform: {
    guard let data = $0.data(using: .utf8) else { throw ValidationError("Badly encoded string, should be UTF-8") }
    return try JSONDecoder().decode(ExampleDataModel.self, from: data) })
  var inputJSON: ExampleDataModel
  
  // Specifiying this option will always cause the parser to exit
  // and print the cistom error.
  @Option(transform: { throw ExampleTransformError(description: "Trying to write to failOption always produces an error. Input: \($0)") })
  var failOption: String?
}
Terms of Service

Privacy Policy

Cookie Policy