///|
priv struct ShellCapture {
  bytes : Array[Byte]
}

///|
impl @io.Writer for ShellCapture with fn write_once(self, data, offset~, len~) {
  for index in offset..<(offset + len) {
    self.bytes.push(data[index])
  }
  len
}

///|
async fn capture_shell(
  script : String,
  environment : Map[String, String],
  cwd : String,
  error~ : &@io.Writer = @stdio.stderr,
) -> (Int32, String) {
  let output : ShellCapture = { bytes: [], }
  let session = @shell.Session::new(
    name="make",
    env=environment,
    cwd~,
    output~,
    error~,
  )
  let status = execute_shell(session, script, error~)
  let mut length = output.bytes.length()
  for index, byte in output.bytes {
    if byte == 0 {
      length = index
      break
    }
  }
  let text = @utf8.decode(Bytes::from_array(output.bytes[:length])) catch {
    _ => raise MakeError("shell output before its first NUL is not valid UTF-8")
  }
  (status, text)
}

///|
async fn execute_shell(
  session : @shell.Session,
  script : String,
  error~ : &@io.Writer = @stdio.stderr,
) -> Int32 {
  try {
    match session.feed(script) {
      Exit(status) | Completed(status) => status
      NeedMoreInput =>
        match session.finish() {
          Exit(status) | Completed(status) => status
          NeedMoreInput => raise MakeError("shell session did not finish")
        }
    }
  } catch {
    @shell.ShellError(message) => {
      // A shell-language error is the shell's exit status, not a make variable
      // expansion error. Keep bytes already written by that shell invocation.
      error.write("sh: \{message}\n")
      2
    }
    error => raise error
  }
}