lulu 0.0.721

A mini lua runtime
Documentation
use mlua::prelude::*;
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, Stdio};
use std::sync::{Arc, Mutex};

pub fn split_command(s: &str) -> Vec<String> {
  let mut parts = Vec::new();
  let mut cur = String::new();
  let mut chars = s.chars().peekable();
  let mut in_single = false;
  let mut in_double = false;

  while let Some(ch) = chars.next() {
    match ch {
      '\\' => {
        if let Some(next) = chars.next() {
          cur.push(next);
        }
      }
      '\'' if !in_double => {
        in_single = !in_single;
      }
      '"' if !in_single => {
        in_double = !in_double;
      }
      c if c.is_whitespace() && !in_single && !in_double => {
        if !cur.is_empty() {
          parts.push(cur.clone());
          cur.clear();
        }
      }
      c => cur.push(c),
    }
  }

  if !cur.is_empty() {
    parts.push(cur);
  }

  parts
}

pub fn register_exec(lua: &Lua) -> mlua::Result<()> {
  lua.globals().set(
    "exec",
    lua.create_function(|lua, (command, inherit): (String, Option<bool>)| {
      let parts = split_command(&command);
      if parts.is_empty() {
        return Err(mlua::Error::external("empty command"));
      }

      let program = &parts[0];
      let args = &parts[1..];

      let inherit = inherit.unwrap_or(false);

      if inherit {
        let status = std::process::Command::new(program)
          .args(args)
          .stdin(std::process::Stdio::inherit())
          .stdout(std::process::Stdio::inherit())
          .stderr(std::process::Stdio::inherit())
          .status()
          .map_err(mlua::Error::external)?;

        let result = lua.create_table()?;
        result.set("status", status.code().unwrap_or(-1))?;
        result.set("success", status.success())?;

        Ok(mlua::Value::Table(result))
      } else {
        let output = std::process::Command::new(program)
          .args(args)
          .output()
          .map_err(mlua::Error::external)?;

        let result = lua.create_table()?;
        result.set(
          "stdout",
          String::from_utf8_lossy(&output.stdout).to_string(),
        )?;
        result.set(
          "stderr",
          String::from_utf8_lossy(&output.stderr).to_string(),
        )?;
        result.set("status", output.status.code().unwrap_or(-1))?;
        result.set("success", output.status.success())?;

        Ok(mlua::Value::Table(result))
      }
    })?,
  )?;

  lua.globals().set(
    "spawn",
    lua.create_function(|_, command: String| spawn_process_with_buffer(&command))?,
  )?;

  Ok(())
}

pub struct ProcessHandle {
  stdin: Arc<Mutex<Option<ChildStdin>>>,
  lines: Arc<Mutex<Vec<String>>>,
  child: Arc<Mutex<Child>>,
}

impl mlua::UserData for ProcessHandle {
  fn add_methods<M: mlua::UserDataMethods<Self>>(methods: &mut M) {
    methods.add_method_mut("write", |_, this, input: String| {
      if let Some(stdin) = &mut *this.stdin.lock().unwrap() {
        stdin
          .write_all(input.as_bytes())
          .map_err(mlua::Error::external)?;
        stdin.flush().map_err(mlua::Error::external)?;
      }
      Ok(())
    });

    methods.add_method("read", |lua, this, ()| {
      let mut lines = this.lines.lock().unwrap();
      if lines.is_empty() {
        return Ok(mlua::Value::Nil);
      }
      let line = lines.remove(0);
      Ok(mlua::Value::String(lua.create_string(&line)?))
    });

    methods.add_method_mut("wait", |lua, this, ()| {
      let mut child = this.child.lock().unwrap();
      let status = child.wait().map_err(mlua::Error::external)?;
      let result = lua.create_table()?;
      result.set("status", status.code().unwrap_or(-1))?;
      result.set("success", status.success())?;
      Ok(result)
    });

    methods.add_method("wait_nonblocking", |lua, this, ()| {
      let mut child = this.child.lock().unwrap();
      match child.try_wait() {
        Ok(Some(status)) => {
          let result = lua.create_table()?;
          result.set("status", status.code().unwrap_or(-1))?;
          result.set("success", status.success())?;
          Ok(mlua::Value::Table(result))
        }
        Ok(Option::None) => Ok(mlua::Value::Nil),
        Err(e) => Err(mlua::Error::external(e)),
      }
    });

    methods.add_method_mut("close_stdin", |_, this, ()| {
      *this.stdin.lock().unwrap() = None;
      Ok(())
    });
  }
}

pub fn spawn_process_with_buffer(command: &str) -> mlua::Result<ProcessHandle> {
  let mut parts = split_command(command);
  let program = parts.remove(0);

  let mut child = std::process::Command::new(program)
    .args(parts)
    .stdin(Stdio::piped())
    .stdout(Stdio::piped())
    .stderr(Stdio::inherit())
    .spawn()
    .map_err(mlua::Error::external)?;

  let stdout = child.stdout.take().unwrap();
  let lines = Arc::new(Mutex::new(Vec::new()));
  let lines_clone = Arc::clone(&lines);

  std::thread::spawn(move || {
    let reader = BufReader::new(stdout);
    for line in reader.lines() {
      if let Ok(line) = line {
        {
          let mut buffer = lines_clone.lock().unwrap();
          buffer.push(line.clone());
        }
      }
    }
  });

  Ok(ProcessHandle {
    stdin: Arc::new(Mutex::new(child.stdin.take())),
    lines,
    child: Arc::new(Mutex::new(child)),
  })
}