///|
pub(all) suberror ExecError {
  CommandFailed(cmd~ : String, code~ : Int)
} derive(Eq, Show)

///|
pub fn shell_escape(s : String) -> String {
  let sb = StringBuilder::new()
  sb.write_char('\'')
  for c in s {
    if c == '\'' {
      sb.write_char('\'')
      sb.write_char('\\')
      sb.write_char('\'')
      sb.write_char('\'')
    } else {
      sb.write_char(c)
    }
  }
  sb.write_char('\'')
  sb.to_string()
}

///|
pub fn shell_join(args : Array[String]) -> String {
  args.iter().map(arg => shell_escape(arg)).join(" ")
}

///|
pub fn run_shell(cmd : String) -> Result[Unit, ExecError] {
  let code = system(to_c_string(cmd))
  if code == 0 {
    Ok(())
  } else {
    Err(ExecError::CommandFailed(cmd~, code~))
  }
}

///|
pub fn run_shell_in_dir(
  dir : String,
  args : Array[String],
) -> Result[Unit, ExecError] {
  let cmd = "cd " + shell_escape(dir) + " && " + shell_join(args)
  run_shell(cmd)
}

///|
fn to_c_string(s : String) -> Bytes {
  let bytes = @utf8.encode(s)
  let arr = bytes.to_array()
  arr.push(0)
  Bytes::from_array(arr)
}

///|
#borrow(cmd)
extern "C" fn system(cmd : Bytes) -> Int = "system"