Skip to content

ArgumentParser spuriously exits with a zero status on certain errors #269

@lorentey

Description

@lorentey

This package uses the errorCode property of CustomNSError errors as the process exit status.

      // MessageInfo.swift:109
      case let error as CustomNSError:
         self = .other(message: error.localizedDescription, exitCode: Int32(error.errorCode))

The purpose of errorCode is merely to differentiate error cases within the same error domain; it is not appropriate to use it as an exit status. The process exit status has a limited range (either 8 or 32 bits depending on the syscall used to retrieve it); additional bits get silently truncated. As a result, when the classic wait/waitpid syscalls are used, ArgumentParser processes may appear to exit with a zero status (EXIT_SUCCESS) even if they failed.

Additionally, an errorCode that doesn't fit in an Int32 leads to a fatal runtime error.

The broken behavior was introduced in #244. One easy way to fix this is to revert that change.

One way to correctly implement #230 is to define a custom mix-in error protocol with an explicit property for exit codes that is kept distinct from errorCode.

ArgumentParser version: 0.3.2
Swift version: Latest stable

Checklist

  • If possible, I've reproduced the issue using the main branch of this package
  • I've searched for existing GitHub issues

Steps to Reproduce

import Foundation
import ArgumentParser

enum MyError: Int, CustomNSError {
  case foo = 256
  case bar = 10_000_000_000

  static var errorDomain: String { "MyError" }
  var errorCode: Int { self.rawValue }
  var errorUserInfo: [String : Any] { [:] }
}

struct Command: ParsableCommand {
  @Flag
  var trap: Bool = false

  func run() throws {
    throw trap ? MyError.bar : MyError.foo
  }
}

Command.main()

Expected behavior

I expect ArgumentParser.main to exit normally with a non-zero status whenever run throws an error it doesn't otherwise handle.

On the bash/fish/zsh prompt, I expect to see:

$ swift run test
$ echo $?
1   # Or some other non-zero value

$ swift run test --trap
$ echo $?
1   # Or some other non-zero value

Actual behavior

Depending on the errorCode value, the subprocess appears to exit with a zero status, or exits with a signal instead.

With the bash/zsh installations on default macOS, and fish installed from homebrew, I get:

$ swift run test
$ echo $?
0
$ swift run test --trap
Swift/Integers.swift:3550: Fatal error: Not enough bits to represent the passed value
Illegal instruction: 4

Exiting with (what appears to be) a zero exit code when the process did not succeed is a very serious problem.

Shells (and other processes) that use the newer waitid syscall can get access to the full 32 bits of the exit code, so they'd report a nonzero code here. However, all shells I tried so far are still using the older wait/waitpid syscalls; we ought to be very conservative about what values we pass to exit.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions