///|
priv suberror CliError {
  CliError(String)
}

///|
priv enum CompareMode {
  Lexical
  Numeric
  GeneralNumeric
  HumanNumeric
  Month
  Version
  Random
} derive(Eq)

///|
priv enum GeneralNumber {
  NonNumber
  NotANumber
  NegativeInfinity
  Finite(Double)
  PositiveInfinity
}

///|
priv struct Modifiers {
  mut ignore_leading_blanks : Bool
  mut dictionary_order : Bool
  mut fold_case : Bool
  mut ignore_nonprinting : Bool
  mut mode : CompareMode
  mut ordering_modes : Int32
  mut reverse : Bool
}

///|
priv struct Position {
  field : Int32
  character : Int32
  skip_blanks : Bool
}

///|
priv struct KeySpec {
  start : Position
  end : Position?
  modifiers : Modifiers
}

///|
priv struct Entry {
  record : Bytes
  keys : Array[Bytes]
  index : Int64
}

///|
fn byte_is_blank(byte : Byte) -> Bool {
  byte is (b' ' | b'\t')
}

///|
fn byte_is_digit(byte : Byte) -> Bool {
  byte is (b'0'..<=b'9')
}

///|
fn byte_is_alphanumeric(byte : Byte) -> Bool {
  byte is (b'0'..<=b'9' | b'a'..<=b'z' | b'A'..<=b'Z')
}

///|
fn mode_from_flags(flags : Int32) -> CompareMode {
  for
    (mode, flag) in [
      (Random, 16),
      (Numeric, 1),
      (GeneralNumeric, 2),
      (HumanNumeric, 4),
      (Month, 8),
      (Version, 32),
    ] {
    if (flags & flag) != 0 {
      return mode
    }
  }
  Lexical
}

///|
fn ordering_flag(mode : CompareMode) -> Int32 {
  match mode {
    Numeric => 1
    GeneralNumeric => 2
    HumanNumeric => 4
    Month => 8
    Random => 16
    Version => 32
    Lexical => 0
  }
}

///|
fn add_ordering(modifiers : Modifiers, mode : CompareMode) -> Unit {
  modifiers.ordering_modes = modifiers.ordering_modes | ordering_flag(mode)
  if mode != Version || modifiers.mode == Lexical {
    modifiers.mode = mode
  }
}

///|
fn validate_ordering(modifiers : Modifiers) -> Unit raise CliError {
  let modes = modifiers.ordering_modes & 31
  if (modes & (modes - 1)) == 0 {
    return
  }
  let flags = StringBuilder()
  if modifiers.ignore_leading_blanks {
    flags.write_char('b')
  }
  if modifiers.dictionary_order {
    flags.write_char('d')
  }
  if modifiers.fold_case {
    flags.write_char('f')
  }
  if (modifiers.ordering_modes & 2) != 0 {
    flags.write_char('g')
  }
  if (modifiers.ordering_modes & 4) != 0 {
    flags.write_char('h')
  }
  if modifiers.ignore_nonprinting {
    flags.write_char('i')
  }
  if (modifiers.ordering_modes & 8) != 0 {
    flags.write_char('M')
  }
  if (modifiers.ordering_modes & 1) != 0 {
    flags.write_char('n')
  }
  if (modifiers.ordering_modes & 16) != 0 {
    flags.write_char('R')
  }
  if modifiers.reverse {
    flags.write_char('r')
  }
  if (modifiers.ordering_modes & 32) != 0 {
    flags.write_char('V')
  }
  raise CliError("sort: options '-\{flags.to_string()}' are incompatible")
}

///|
fn default_modifiers(parsed : @cli.ParsedArgs) -> Modifiers raise CliError {
  let mut ordering_modes = 0
  for
    (name, mode) in [
      ("numeric-sort", Numeric),
      ("general-numeric-sort", GeneralNumeric),
      ("human-numeric-sort", HumanNumeric),
      ("month-sort", Month),
      ("random-sort", Random),
      ("version-sort", Version),
    ] {
    if parsed.contains(name) {
      ordering_modes = ordering_modes | ordering_flag(mode)
    }
  }
  for value in parsed.values("sort") {
    let choices = [
      "general-numeric", "human-numeric", "month", "numeric", "random", "version",
    ]
    let selected = choices.filter(choice => choice.has_prefix(value))
    if selected.length() != 1 {
      let problem = if selected.is_empty() { "invalid" } else { "ambiguous" }
      let valid = choices.map(choice => "  - '\{choice}'\n").join("")
      raise CliError(
        "sort: \{problem} argument '\{value}' for '--sort'\nValid arguments are:\n\{valid}Try 'sort --help' for more information.",
      )
    }
    let mode = match selected[0] {
      "general-numeric" => GeneralNumeric
      "human-numeric" => HumanNumeric
      "month" => Month
      "numeric" => Numeric
      "random" => Random
      _ => Version
    }
    ordering_modes = ordering_modes | ordering_flag(mode)
  }
  {
    ignore_leading_blanks: parsed.contains("ignore-leading-blanks"),
    dictionary_order: parsed.contains("dictionary-order"),
    fold_case: parsed.contains("ignore-case"),
    ignore_nonprinting: parsed.contains("ignore-nonprinting"),
    mode: mode_from_flags(ordering_modes),
    ordering_modes,
    reverse: parsed.contains("reverse"),
  }
}

///|
fn apply_modifier(
  modifier : Char,
  modifiers : Modifiers,
  local_ordering : Ref[Bool],
) -> Unit raise CliError {
  match modifier {
    'b' => modifiers.ignore_leading_blanks = true
    'd' => {
      modifiers.dictionary_order = true
      local_ordering.val = true
    }
    'f' => {
      modifiers.fold_case = true
      local_ordering.val = true
    }
    'g' => {
      add_ordering(modifiers, GeneralNumeric)
      local_ordering.val = true
    }
    'h' => {
      add_ordering(modifiers, HumanNumeric)
      local_ordering.val = true
    }
    'i' => {
      modifiers.ignore_nonprinting = true
      local_ordering.val = true
    }
    'M' => {
      add_ordering(modifiers, Month)
      local_ordering.val = true
    }
    'n' => {
      add_ordering(modifiers, Numeric)
      local_ordering.val = true
    }
    'R' => {
      add_ordering(modifiers, Random)
      local_ordering.val = true
    }
    'r' => {
      modifiers.reverse = true
      local_ordering.val = true
    }
    'V' => {
      add_ordering(modifiers, Version)
      local_ordering.val = true
    }
    _ =>
      raise CliError(
        "sort: invalid field specification modifier: '\{modifier}'",
      )
  }
}

///|
fn parse_position(
  text : String,
  spec : String,
  inherited : Modifiers,
  end_position : Bool,
) -> (Position, Modifiers, Bool) raise CliError {
  let mut index = 0
  while index < text.length() && text[index] is ('0'..<='9') {
    index += 1
  }
  if index == 0 {
    raise CliError("sort: invalid field specification: '\{spec}'")
  }
  let field = @string.parse_int(text[0:index]) catch {
    _ => raise CliError("sort: invalid field specification: '\{spec}'")
  }
  if field < 1 {
    raise CliError("sort: field number is zero: '\{spec}'")
  }
  let mut character = if end_position { 0 } else { 1 }
  if index < text.length() && text[index] is '.' {
    index += 1
    let start = index
    while index < text.length() && text[index] is ('0'..<='9') {
      index += 1
    }
    if start == index {
      raise CliError("sort: invalid number after '.': '\{spec}'")
    }
    character = @string.parse_int(text[start:index]) catch {
      _ => raise CliError("sort: invalid field specification: '\{spec}'")
    }
    if !end_position && character == 0 {
      raise CliError("sort: character offset is zero: '\{spec}'")
    }
  }
  let modifiers : Modifiers = {
    ignore_leading_blanks: false,
    dictionary_order: false,
    fold_case: false,
    ignore_nonprinting: false,
    mode: Lexical,
    ordering_modes: 0,
    reverse: false,
  }
  let local_ordering = Ref(false)
  for modifier in text[index:] {
    apply_modifier(modifier, modifiers, local_ordering)
  }
  let skip_blanks = modifiers.ignore_leading_blanks
  if !local_ordering.val {
    modifiers.dictionary_order = inherited.dictionary_order
    modifiers.fold_case = inherited.fold_case
    modifiers.ignore_nonprinting = inherited.ignore_nonprinting
    modifiers.mode = inherited.mode
    modifiers.ordering_modes = inherited.ordering_modes
    modifiers.reverse = inherited.reverse
  }
  if inherited.ignore_leading_blanks {
    modifiers.ignore_leading_blanks = true
  }
  (
    {
      field,
      character,
      skip_blanks: skip_blanks || inherited.ignore_leading_blanks,
    },
    modifiers,
    local_ordering.val,
  )
}

///|
fn parse_key(spec : String, inherited : Modifiers) -> KeySpec raise CliError {
  let pieces : Array[String] = spec
    .split(",")
    .map(piece => piece.to_owned())
    .collect()
  if pieces.length() > 2 || pieces.is_empty() || pieces[0] == "" {
    raise CliError("sort: invalid key specification: '\{spec}'")
  }
  let (start, start_modifiers, start_local) = parse_position(
    pieces[0],
    spec,
    inherited,
    false,
  )
  let mut modifiers = start_modifiers
  let end = if pieces.length() == 2 {
    if pieces[1] == "" {
      raise CliError("sort: invalid key specification: '\{spec}'")
    }
    let (position, end_modifiers, end_local) = parse_position(
      pieces[1],
      spec,
      inherited,
      true,
    )
    if start_local || end_local {
      modifiers = {
        ignore_leading_blanks: start_modifiers.ignore_leading_blanks,
        dictionary_order: (start_local && start_modifiers.dictionary_order) ||
        (end_local && end_modifiers.dictionary_order),
        fold_case: (start_local && start_modifiers.fold_case) ||
        (end_local && end_modifiers.fold_case),
        ignore_nonprinting: (start_local && start_modifiers.ignore_nonprinting) ||
        (end_local && end_modifiers.ignore_nonprinting),
        mode: if end_local && end_modifiers.mode != Lexical {
          end_modifiers.mode
        } else if start_local {
          start_modifiers.mode
        } else {
          Lexical
        },
        ordering_modes: (if start_local {
          start_modifiers.ordering_modes
        } else {
          0
        }) |
        (if end_local { end_modifiers.ordering_modes } else { 0 }),
        reverse: (start_local && start_modifiers.reverse) ||
        (end_local && end_modifiers.reverse),
      }
    } else {
      modifiers = inherited
    }
    Some(position)
  } else {
    None
  }
  if modifiers.ordering_modes != 0 {
    modifiers.mode = mode_from_flags(modifiers.ordering_modes)
  }
  if end is Some(position) &&
    (
      position.field < start.field ||
      (
        position.field == start.field &&
        position.character != 0 &&
        position.character < start.character
      )
    ) {
    raise CliError("sort: invalid key specification: '\{spec}'")
  }
  { start, end, modifiers, }
}

///|
fn field_ranges(line : Bytes, separator : Byte?) -> Array[(Int32, Int32)] {
  let ranges : Array[(Int32, Int32)] = []
  match separator {
    Some(delimiter) => {
      let mut start = 0
      for index, byte in line {
        if byte == delimiter {
          ranges.push((start, index))
          start = index + 1
        }
      }
      ranges.push((start, line.length()))
    }
    None => {
      let mut start = 0
      let mut index = 0
      let mut seen_nonblank = false
      while index < line.length() {
        if byte_is_blank(line[index]) {
          if seen_nonblank {
            ranges.push((start, index))
            start = index
            seen_nonblank = false
          }
        } else {
          seen_nonblank = true
        }
        index += 1
      }
      if start < line.length() {
        ranges.push((start, line.length()))
      }
    }
  }
  ranges
}

///|
fn key_start(
  line : Bytes,
  position : Position,
  ranges : Array[(Int32, Int32)],
) -> Int32 {
  if position.field > ranges.length() {
    return line.length()
  }
  let (field_start, field_end) = ranges[position.field - 1]
  let mut start = field_start
  if position.skip_blanks {
    while start < field_end && byte_is_blank(line[start]) {
      start += 1
    }
  }
  Int32::min(field_end, start + Int32::max(0, position.character - 1))
}

///|
fn key_end(
  line : Bytes,
  position : Position,
  ranges : Array[(Int32, Int32)],
) -> Int32 {
  if position.field > ranges.length() {
    return line.length()
  }
  let (field_start, field_end) = ranges[position.field - 1]
  let mut start = field_start
  if position.skip_blanks {
    while start < field_end && byte_is_blank(line[start]) {
      start += 1
    }
  }
  if position.character == 0 {
    field_end
  } else {
    Int32::min(field_end, start + position.character)
  }
}

///|
fn extract_key(line : Bytes, spec : KeySpec, separator : Byte?) -> Bytes {
  let ranges = field_ranges(line, separator)
  let start = key_start(line, spec.start, ranges)
  let end = match spec.end {
    Some(position) => key_end(line, position, ranges)
    None => line.length()
  }
  if end <= start {
    b""
  } else {
    line[start:end].to_owned()
  }
}

///|
fn transforms_key(modifiers : Modifiers) -> Bool {
  modifiers.ignore_leading_blanks ||
  modifiers.ignore_nonprinting ||
  modifiers.dictionary_order ||
  modifiers.fold_case
}

///|
fn transformed_key(key : Bytes, modifiers : Modifiers) -> Bytes {
  if !transforms_key(modifiers) {
    return key
  }
  let output : Array[Byte] = []
  let mut start = 0
  if modifiers.ignore_leading_blanks {
    while start < key.length() && byte_is_blank(key[start]) {
      start += 1
    }
  }
  for byte in key[start:] {
    if modifiers.ignore_nonprinting &&
      (byte.to_int() < 0x20 || byte.to_int() > 0x7E) {
      continue
    }
    if modifiers.dictionary_order &&
      !byte_is_blank(byte) &&
      !byte_is_alphanumeric(byte) {
      continue
    }
    let value = byte.to_int()
    output.push(
      if modifiers.fold_case && value >= 0x61 && value <= 0x7A {
        (value - 0x20).to_byte()
      } else {
        byte
      },
    )
  }
  Bytes::from_array(output)
}

///|
fn numeric_slice(key : Bytes, exponent : Bool, allow_plus : Bool) -> Bytes? {
  let mut index = 0
  while index < key.length() && byte_is_blank(key[index]) {
    index += 1
  }
  let start = index
  if index < key.length() && key[index] is b'-' {
    index += 1
  } else if allow_plus && index < key.length() && key[index] is b'+' {
    index += 1
  }
  let mut digits = 0
  while index < key.length() && byte_is_digit(key[index]) {
    digits += 1
    index += 1
  }
  if index < key.length() && key[index] is b'.' {
    index += 1
    while index < key.length() && byte_is_digit(key[index]) {
      digits += 1
      index += 1
    }
  }
  if exponent &&
    digits > 0 &&
    index < key.length() &&
    (key[index] is b'e' || key[index] is b'E') {
    let exponent_start = index
    index += 1
    if index < key.length() && (key[index] is b'+' || key[index] is b'-') {
      index += 1
    }
    let exponent_digits = index
    while index < key.length() && byte_is_digit(key[index]) {
      index += 1
    }
    if exponent_digits == index {
      index = exponent_start
    }
  }
  if digits == 0 {
    return None
  }
  Some(key[start:index].to_owned())
}

///|
fn numeric_prefix(key : Bytes, exponent : Bool) -> Double {
  match numeric_slice(key, exponent, exponent) {
    Some(number) =>
      @string.parse_double(@utf8.decode(number)) catch {
        _ => 0.0
      }
    None => 0.0
  }
}

///|
fn ascii_lower_byte(byte : Byte) -> Byte {
  let value = byte.to_int()
  if value >= 0x41 && value <= 0x5A {
    (value + 0x20).to_byte()
  } else {
    byte
  }
}

///|
fn ascii_prefix_at(key : Bytes, index : Int32, prefix : Bytes) -> Bool {
  if index + prefix.length() > key.length() {
    return false
  }
  for offset, byte in prefix {
    if ascii_lower_byte(key[index + offset]) != byte {
      return false
    }
  }
  true
}

///|
fn general_number(key : Bytes) -> GeneralNumber {
  let mut index = 0
  while index < key.length() && byte_is_blank(key[index]) {
    index += 1
  }
  let mut negative = false
  if index < key.length() && key[index] is b'-' {
    negative = true
    index += 1
  } else if index < key.length() && key[index] is b'+' {
    index += 1
  }
  if ascii_prefix_at(key, index, b"nan") {
    return NotANumber
  }
  if ascii_prefix_at(key, index, b"inf") {
    return if negative { NegativeInfinity } else { PositiveInfinity }
  }
  match numeric_slice(key, true, true) {
    None => NonNumber
    Some(number) => {
      let value = @string.parse_double(@utf8.decode(number)) catch {
        _ => return NonNumber
      }
      if value.is_nan() {
        NotANumber
      } else if value.is_inf() {
        if value < 0.0 {
          NegativeInfinity
        } else {
          PositiveInfinity
        }
      } else {
        Finite(value)
      }
    }
  }
}

///|
fn general_compare(left : Bytes, right : Bytes) -> Int32 {
  match (general_number(left), general_number(right)) {
    (NonNumber, NonNumber) | (NotANumber, NotANumber) => 0
    (NonNumber, _) => -1
    (_, NonNumber) => 1
    (NotANumber, _) => -1
    (_, NotANumber) => 1
    (NegativeInfinity, NegativeInfinity) => 0
    (NegativeInfinity, _) => -1
    (_, NegativeInfinity) => 1
    (PositiveInfinity, PositiveInfinity) => 0
    (PositiveInfinity, _) => 1
    (_, PositiveInfinity) => -1
    (Finite(left), Finite(right)) => left.compare(right)
  }
}

///|
fn human_parts(key : Bytes) -> (Int32, Int32, Double) {
  let mut index = 0
  while index < key.length() && byte_is_blank(key[index]) {
    index += 1
  }
  let start = index
  if index < key.length() && key[index] is b'-' {
    index += 1
  } else if index < key.length() && key[index] is b'+' {
    return (0, 0, 0.0)
  }
  let mut digits = 0
  while index < key.length() && byte_is_digit(key[index]) {
    digits += 1
    index += 1
  }
  if index < key.length() && key[index] is b'.' {
    index += 1
    while index < key.length() && byte_is_digit(key[index]) {
      digits += 1
      index += 1
    }
  }
  if digits == 0 {
    return (0, 0, 0.0)
  }
  let number = @string.parse_double(@utf8.decode(key[start:index].to_owned())) catch {
    _ => 0.0
  }
  let power = if index < key.length() {
    match key[index] {
      b'K' | b'k' => 1
      b'M' => 2
      b'G' => 3
      b'T' => 4
      b'P' => 5
      b'E' => 6
      b'Z' => 7
      b'Y' => 8
      b'R' => 9
      b'Q' => 10
      _ => 0
    }
  } else {
    0
  }
  let sign = if number < 0.0 { -1 } else if number > 0.0 { 1 } else { 0 }
  (sign, power, number)
}

///|
fn human_compare(left : Bytes, right : Bytes) -> Int32 {
  let (left_sign, left_power, left_value) = human_parts(left)
  let (right_sign, right_power, right_value) = human_parts(right)
  if left_sign != right_sign {
    return left_sign - right_sign
  }
  if left_power != right_power {
    return if left_sign < 0 {
      right_power - left_power
    } else {
      left_power - right_power
    }
  }
  left_value.compare(right_value)
}

///|
fn month_value(key : Bytes) -> Int32 {
  let text = transformed_key(key, {
    ignore_leading_blanks: true,
    dictionary_order: false,
    fold_case: true,
    ignore_nonprinting: false,
    mode: Lexical,
    ordering_modes: 0,
    reverse: false,
  })
  let text = @utf8.decode(text) catch { _ => "" }
  if text.length() < 3 {
    return 0
  }
  match text[0:3].to_owned() {
    "JAN" => 1
    "FEB" => 2
    "MAR" => 3
    "APR" => 4
    "MAY" => 5
    "JUN" => 6
    "JUL" => 7
    "AUG" => 8
    "SEP" => 9
    "OCT" => 10
    "NOV" => 11
    "DEC" => 12
    _ => 0
  }
}

///|
fn version_compare(left : Bytes, right : Bytes) -> Int32 {
  let mut li = 0
  let mut ri = 0
  while li < left.length() || ri < right.length() {
    if li < left.length() && left[li] is b'~' {
      if ri < right.length() && right[ri] is b'~' {
        li += 1
        ri += 1
        continue
      }
      return -1
    }
    if ri < right.length() && right[ri] is b'~' {
      return 1
    }
    if li == left.length() {
      return -1
    }
    if ri == right.length() {
      return 1
    }
    if byte_is_digit(left[li]) && byte_is_digit(right[ri]) {
      let mut lzero = li
      while lzero < left.length() && left[lzero] is b'0' {
        lzero += 1
      }
      let mut rzero = ri
      while rzero < right.length() && right[rzero] is b'0' {
        rzero += 1
      }
      let mut lend = lzero
      while lend < left.length() && byte_is_digit(left[lend]) {
        lend += 1
      }
      let mut rend = rzero
      while rend < right.length() && byte_is_digit(right[rend]) {
        rend += 1
      }
      let llen = lend - lzero
      let rlen = rend - rzero
      if llen != rlen {
        return llen - rlen
      }
      let cmp = left[lzero:lend].lexical_compare(right[rzero:rend])
      if cmp != 0 {
        return cmp
      }
      li = lend
      ri = rend
    } else {
      if left[li] != right[ri] {
        return left[li].to_int() - right[ri].to_int()
      }
      li += 1
      ri += 1
    }
  }
  0
}

///|
fn compare_prepared_key(
  left : Bytes,
  right : Bytes,
  modifiers : Modifiers,
) -> Int32 {
  let result = match modifiers.mode {
    Lexical => left.lexical_compare(right)
    Numeric => numeric_prefix(left, false).compare(numeric_prefix(right, false))
    GeneralNumeric => general_compare(left, right)
    HumanNumeric => human_compare(left, right)
    Month => month_value(left) - month_value(right)
    Version => version_compare(left, right)
    Random => left.lexical_compare(right)
  }
  if modifiers.reverse {
    -result
  } else {
    result
  }
}

///|
fn compare_entries(
  left : Entry,
  right : Entry,
  specs : Array[KeySpec],
  global : Modifiers,
  last_resort : Bool,
) -> Int32 {
  if specs.is_empty() {
    let result = compare_prepared_key(
      left.keys.get(0).unwrap_or(left.record),
      right.keys.get(0).unwrap_or(right.record),
      global,
    )
    if result != 0 || !last_resort {
      return result
    }
  } else {
    for index, spec in specs {
      let result = compare_prepared_key(
        left.keys[index],
        right.keys[index],
        spec.modifiers,
      )
      if result != 0 {
        return result
      }
    }
    if !last_resort {
      return 0
    }
  }
  let result = left.record.lexical_compare(right.record)
  if global.reverse {
    -result
  } else {
    result
  }
}

///|
fn make_entry(
  record : Bytes,
  index : Int64,
  specs : Array[KeySpec],
  global : Modifiers,
  separator : Byte?,
  seed~ : Bytes = b"",
) -> Entry {
  let keys = if specs.is_empty() {
    if transforms_key(global) || global.mode == Random {
      [prepared_key(record, global, seed)]
    } else {
      []
    }
  } else {
    specs.map(spec => {
      prepared_key(extract_key(record, spec, separator), spec.modifiers, seed)
    })
  }
  { record, keys, index, }
}

///|
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"),
  )
}

///|
async fn main {
  let args = @env.args()[1:]
  if args.length() == 1 && args[0] == "--version" {
    @stdio.stdout.write("sort 0.2.3\n")
    return
  }
  let parsed = parse_args(args) catch {
    @cli.CliError(option~, message~, ..) => {
      @stdio.stderr.write("sort: \{message}: '\{option}'\n")
      @sys.exit(2)
      return
    }
  }
  if parsed.contains("help") {
    @stdio.stdout.write(
      "Usage: sort [-bcdfghinMruVz] [-s] [-m] [-o FILE] [-R|--sort=random] [--random-source=FILE] [-k KEY] [-t CHAR] [-c|-C] [-S SIZE] [-T DIR] [--batch-size=N] [FILE...]\n      --help      display this help and exit\n      --version   display version and exit\n\nLarge inputs use temporary disk runs and require filesystem write access.\n-S/--buffer-size selects an accounted byte budget (default units: KiB).\n-T/--temporary-directory may repeat; --batch-size uses at most 16 merge inputs.\nNonzero percentage buffer sizes require an unavailable physical-memory query.\n",
    )
    return
  }
  let status = execute_sort(parsed) catch {
    CliError(message) => {
      @stdio.stderr.write(message + "\n")
      2
    }
    error => {
      @stdio.stderr.write("sort: \{error}\n")
      2
    }
  }
  if status != 0 {
    @sys.exit(status)
  }
}