///|
fn normalized_args(args : ArrayView[String]) -> Array[String] {
  let result : Array[String] = []
  let mut options = true
  let mut value_next = false
  for arg in args {
    if value_next {
      result.push(arg)
      value_next = false
      continue
    }
    if options && arg == "--" {
      options = false
    }
    result.push(
      if options && arg == "--check=quiet" {
        "--check-quiet"
      } else {
        arg
      },
    )
    if options {
      if [
          "--key", "--field-separator", "--output", "--random-source", "--sort",
          "--buffer-size", "--temporary-directory", "--batch-size",
        ].contains(arg) {
        value_next = true
      } else if arg.has_prefix("-") && !arg.has_prefix("--") {
        for offset in 1.. @cli.ParsedArgs raise @cli.CliError {
  @cli.parse(normalized_args(args), [
    @cli.flag("reverse", short='r'),
    @cli.flag("numeric-sort", short='n'),
    @cli.flag("general-numeric-sort", short='g'),
    @cli.flag("human-numeric-sort", short='h'),
    @cli.flag("month-sort", short='M'),
    @cli.flag("version-sort", short='V'),
    @cli.flag("random-sort", short='R'),
    @cli.option("sort"),
    @cli.option("random-source"),
    @cli.flag("stable", short='s'),
    @cli.flag("merge", short='m'),
    @cli.option("output", short='o'),
    @cli.option("buffer-size", short='S'),
    @cli.option("temporary-directory", short='T'),
    @cli.option("batch-size"),
    @cli.flag("unique", short='u'),
    @cli.flag("ignore-case", short='f'),
    @cli.flag("ignore-leading-blanks", short='b'),
    @cli.flag("dictionary-order", short='d'),
    @cli.flag("ignore-nonprinting", short='i'),
    @cli.flag("zero-terminated", short='z'),
    @cli.flag("check", short='c'),
    @cli.flag("check-quiet", short='C'),
    @cli.option("key", short='k'),
    @cli.option("field-separator", short='t'),
    @cli.flag("help"),
  ])
}

///|
fn one_path(
  parsed : @cli.ParsedArgs,
  name : String,
  label : String,
) -> String? raise CliError {
  let values = parsed.values(name)
  guard values.get(0) is Some(first) else { return None }
  if values.any(value => value != first) {
    raise CliError("sort: multiple \{label} specified")
  }
  Some(first)
}

///|
async fn preflight_sources(sources : Array[String]) -> Unit {
  for source in sources {
    if source != "-" {
      let file = @fs.open(source, mode=ReadOnly) catch {
        @os_error.OSError(_) as error =>
          raise CliError("sort: cannot read: \{source}: \{os_message(error)}")
        error => raise error
      }
      file.close()
    }
  }
}

///|
async fn prepare_output(path : String, merge : Bool) -> Unit {
  let file = @fs.open(
    path,
    mode=WriteOnly,
    create_mode=if merge { CreateNew } else { OpenOrCreate },
  ) catch {
    @os_error.OSError(_) as error if merge && error.is_EEXIST() =>
      raise CliError("sort: unsupported capability: merge into existing output")
    @os_error.OSError(_) as error =>
      raise CliError("sort: open failed: \{path}: \{os_message(error)}")
    error => raise error
  }
  file.close()
}

///|
async fn execute_sort(parsed : @cli.ParsedArgs) -> Int32 {
  let (budget, fan_in) = resource_controls(parsed)
  let output_path = one_path(parsed, "output", "output files")
  let source = one_path(parsed, "random-source", "random sources")
  if parsed.contains("check") && parsed.contains("check-quiet") {
    raise CliError("sort: options '-cC' are incompatible")
  }
  let global = default_modifiers(parsed)
  let separator = match parsed.last_value("field-separator") {
    Some(text) => {
      let bytes = @utf8.encode(text)
      if bytes.length() != 1 {
        raise CliError("sort: the field separator must be a single byte")
      }
      Some(bytes[0])
    }
    None => None
  }
  let specs = parsed.values("key").map(key => parse_key(key, global))
  if specs.is_empty() {
    validate_ordering(global)
  } else {
    for spec in specs {
      validate_ordering(spec.modifiers)
    }
  }
  let need_random = if specs.is_empty() {
    global.mode == Random
  } else {
    specs.any(spec => spec.modifiers.mode == Random)
  }
  let seed = if need_random { random_seed(source) } else { b"" }
  let check = parsed.contains("check") || parsed.contains("check-quiet")
  let check_letter = if parsed.contains("check-quiet") { "C" } else { "c" }
  if check && output_path is Some(_) {
    raise CliError("sort: options '-\{check_letter}o' are incompatible")
  }
  let sources = if parsed.operands.is_empty() { ["-"] } else { parsed.operands }
  if check && sources.length() > 1 {
    raise CliError(
      "sort: extra operand '\{sources[1]}' not allowed with -\{check_letter}",
    )
  }
  preflight_sources(sources)
  if output_path is Some(path) {
    prepare_output(path, parsed.contains("merge"))
  }
  sort_sources(
    sources,
    @stdio.stdout,
    if parsed.contains("zero-terminated") {
      b'\x00'
    } else {
      b'\n'
    },
    specs,
    global,
    separator,
    unique=parsed.contains("unique"),
    stable=parsed.contains("stable"),
    merge=parsed.contains("merge"),
    check~,
    diagnose=parsed.contains("check"),
    seed~,
    output_path~,
    budget~,
    fan_in~,
    temporary_directories=parsed.values("temporary-directory"),
  )
}