diff --git a/.lune/hello_lune.luau b/.lune/hello_lune.luau index 99f8770..4cef0b1 100644 --- a/.lune/hello_lune.luau +++ b/.lune/hello_lune.luau @@ -15,10 +15,12 @@ module.sayHello() Using arguments given to the program ]==] -local arg: string? = ... -if arg then - print("\nGot an argument while running hello_lune:") - print(arg) +if #process.args > 0 then + print("\nGot arguments while running hello_lune:") + console.log(process.args) + if #process.args > 3 then + error("Too many arguments!") + end end --[==[ @@ -31,14 +33,13 @@ end ]==] print("\nReading current environment 🔎") -local vars = process.getEnvVars() -table.sort(vars) -assert(table.find(vars, "PATH") ~= nil, "Missing PATH") -assert(table.find(vars, "PWD") ~= nil, "Missing PWD") +-- Environment variables can be read directly +assert(process.env.PATH ~= nil, "Missing PATH") +assert(process.env.PWD ~= nil, "Missing PWD") -for _, key in vars do - local value = process.getEnvVar(key) +-- And they can also be accessed using generalized iteration (not pairs!) +for key, value in process.env do local box = if value and value ~= "" then "✅" else "❌" print(string.format("[%s] %s", box, key)) end @@ -85,7 +86,7 @@ end -- NOTE: We skip the ping example in GitHub Actions -- since the ping command does not work in azure -if not process.getEnvVar("GITHUB_ACTIONS") then +if not process.env.GITHUB_ACTIONS then --[==[ EXAMPLE #5 diff --git a/CHANGELOG.md b/CHANGELOG.md index c31fd73..6b7133d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,22 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Added + +- Added `process.args` for inspecting values given to Lune when running (read only) +- Added `process.env` which is a plain table where you can get & set environment variables + +### Changed + +- Improved error formatting & added proper file name to stack traces + +### Removed + +- Removed `...` for process arguments, use `process.args` instead +- Removed individual functions for getting & setting environment variables, use `process.env` instead + ## `0.0.3` - January 19th, 2023 ### Added diff --git a/Cargo.lock b/Cargo.lock index 66c70b3..dce89ee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -516,6 +516,7 @@ dependencies = [ "anyhow", "clap", "mlua", + "os_str_bytes", "reqwest", "serde", "serde_json", @@ -668,6 +669,9 @@ name = "os_str_bytes" version = "6.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b7820b9daea5457c9f21c69448905d723fbd21136ccf521748f23fd49e723ee" +dependencies = [ + "memchr", +] [[package]] name = "parking_lot" diff --git a/Cargo.toml b/Cargo.toml index ece4fd3..00d4fae 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -22,6 +22,7 @@ panic = "abort" # Remove extra panic info anyhow = { version = "1.0.68" } clap = { version = "4.1.1", features = ["derive"] } mlua = { version = "0.8.7", features = ["luau", "async", "serialize"] } +os_str_bytes = "6.4.1" reqwest = { version = "0.11.13", features = ["gzip", "deflate"] } serde = { version = "1.0.152", features = ["derive"] } serde_json = { version = "1.0.91" } diff --git a/README.md b/README.md index 67a2cee..96e8133 100644 --- a/README.md +++ b/README.md @@ -98,9 +98,8 @@ type net = { ```lua type process = { - getEnvVars: () -> { string }, - getEnvVar: (key: string) -> string?, - setEnvVar: (key: string, value: string) -> (), + args: { string }, + env: { [string]: string? }, exit: (code: number?) -> (), spawn: (program: string, params: { string }?) -> { ok: boolean, diff --git a/lune.yml b/lune.yml index bb6eca0..a57c5f4 100644 --- a/lune.yml +++ b/lune.yml @@ -63,15 +63,7 @@ globals: net.request: args: - type: any - # Process - process.getEnvVars: - process.getEnvVar: - args: - - type: string - process.setEnvVar: - args: - - type: string - - type: string + # Processs process.exit: args: - required: false diff --git a/luneTypes.d.luau b/luneTypes.d.luau index 7e2f805..a7cf4f1 100644 --- a/luneTypes.d.luau +++ b/luneTypes.d.luau @@ -41,9 +41,8 @@ declare net: { } declare process: { - getEnvVars: () -> { string }, - getEnvVar: (key: string) -> string?, - setEnvVar: (key: string, value: string) -> (), + args: { string }, + env: { [string]: string? }, exit: (code: number?) -> (), spawn: (program: string, params: { string }?) -> { ok: boolean, diff --git a/src/cli.rs b/src/cli.rs index f01abc0..3d9f7be 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -4,7 +4,7 @@ use std::{ }; use clap::{CommandFactory, Parser}; -use mlua::{Lua, MultiValue, Result, ToLua}; +use mlua::{Lua, Result}; use crate::{ lune::{console::LuneConsole, fs::LuneFs, net::LuneNet, process::LuneProcess}, @@ -97,23 +97,19 @@ impl Cli { } // Parse and read the wanted file let file_path = find_parse_file_path(&self.script_path.unwrap())?; - let file_contents = read_to_string(file_path)?; + let file_contents = read_to_string(&file_path)?; // Create a new lua state and add in all lune globals let lua = Lua::new(); let globals = lua.globals(); globals.set("console", LuneConsole::new())?; globals.set("fs", LuneFs::new())?; globals.set("net", LuneNet::new())?; - globals.set("process", LuneProcess::new())?; + globals.set("process", LuneProcess::new(self.script_args))?; lua.sandbox(true)?; // Load & call the file with the given args - let lua_args = self - .script_args - .iter() - .map(|value| value.to_owned().to_lua(&lua)) - .collect::>>()?; lua.load(&file_contents) - .call_async(MultiValue::from_vec(lua_args)) + .set_name(file_path.with_extension("").display().to_string())? + .exec_async() .await?; Ok(()) } diff --git a/src/lune/console.rs b/src/lune/console.rs index 77ef8b3..2daa803 100644 --- a/src/lune/console.rs +++ b/src/lune/console.rs @@ -1,28 +1,9 @@ -use std::{ - fmt::Write, - io::{self, Write as IoWrite}, +use mlua::{Lua, MultiValue, Result, UserData, UserDataMethods}; + +use crate::utils::{ + flush_stdout, pretty_format_multi_value, print_color, print_label, print_style, }; -use mlua::{Lua, MultiValue, Result, UserData, UserDataMethods, Value}; - -const MAX_FORMAT_DEPTH: usize = 4; - -const INDENT: &str = " "; - -const COLOR_RESET: &str = "\x1B[0m"; -const COLOR_BLACK: &str = "\x1B[30m"; -const COLOR_RED: &str = "\x1B[31m"; -const COLOR_GREEN: &str = "\x1B[32m"; -const COLOR_YELLOW: &str = "\x1B[33m"; -const COLOR_BLUE: &str = "\x1B[34m"; -const COLOR_PURPLE: &str = "\x1B[35m"; -const COLOR_CYAN: &str = "\x1B[36m"; -const COLOR_WHITE: &str = "\x1B[37m"; - -const STYLE_RESET: &str = "\x1B[22m"; -const STYLE_BOLD: &str = "\x1B[1m"; -const STYLE_DIM: &str = "\x1B[2m"; - pub struct LuneConsole(); impl LuneConsole { @@ -45,134 +26,6 @@ impl UserData for LuneConsole { } } -fn flush_stdout() -> Result<()> { - io::stdout().flush().map_err(mlua::Error::external) -} - -fn can_be_plain_lua_table_key(s: &mlua::String) -> bool { - let str = s.to_string_lossy().to_string(); - let first_char = str.chars().next().unwrap(); - if first_char.is_alphabetic() { - str.chars().all(|c| c == '_' || c.is_alphanumeric()) - } else { - false - } -} - -fn pretty_format_value(buffer: &mut String, value: &Value, depth: usize) -> anyhow::Result<()> { - // TODO: Handle tables with cyclic references - // TODO: Handle other types like function, userdata, ... - match &value { - Value::Nil => write!(buffer, "nil")?, - Value::Boolean(true) => write!(buffer, "{}true{}", COLOR_YELLOW, COLOR_RESET)?, - Value::Boolean(false) => write!(buffer, "{}false{}", COLOR_YELLOW, COLOR_RESET)?, - Value::Number(n) => write!(buffer, "{}{}{}", COLOR_BLUE, n, COLOR_RESET)?, - Value::Integer(i) => write!(buffer, "{}{}{}", COLOR_BLUE, i, COLOR_RESET)?, - Value::String(s) => write!( - buffer, - "{}\"{}\"{}", - COLOR_GREEN, - s.to_string_lossy() - .replace('"', r#"\""#) - .replace('\n', r#"\n"#), - COLOR_RESET - )?, - Value::Table(ref tab) => { - if depth >= MAX_FORMAT_DEPTH { - write!(buffer, "{}{{ ... }}{}", STYLE_DIM, STYLE_RESET)?; - } else { - let depth_indent = INDENT.repeat(depth); - write!(buffer, "{}{{{}", STYLE_DIM, STYLE_RESET)?; - for pair in tab.clone().pairs::() { - let (key, value) = pair?; - match &key { - Value::String(s) if can_be_plain_lua_table_key(s) => write!( - buffer, - "\n{}{}{} {}={} ", - depth_indent, - INDENT, - s.to_string_lossy(), - STYLE_DIM, - STYLE_RESET - )?, - _ => { - write!(buffer, "\n{}{}[", depth_indent, INDENT)?; - pretty_format_value(buffer, &key, depth)?; - write!(buffer, "] {}={} ", STYLE_DIM, STYLE_RESET)?; - } - } - pretty_format_value(buffer, &value, depth + 1)?; - write!(buffer, "{},{}", STYLE_DIM, STYLE_RESET)?; - } - write!(buffer, "\n{}{}}}{}", depth_indent, STYLE_DIM, STYLE_RESET)?; - } - } - _ => write!(buffer, "?")?, - } - Ok(()) -} - -fn pretty_format_multi_value(multi: &MultiValue) -> Result { - let mut buffer = String::new(); - let mut counter = 0; - for value in multi { - counter += 1; - if let Value::String(s) = value { - write!(buffer, "{}", s.to_string_lossy()).map_err(mlua::Error::external)? - } else { - pretty_format_value(&mut buffer, value, 0).map_err(mlua::Error::external)?; - } - if counter < multi.len() { - write!(&mut buffer, " ").map_err(mlua::Error::external)?; - } - } - Ok(buffer) -} - -fn print_style>(s: S) -> Result<()> { - print!( - "{}", - match s.as_ref() { - "reset" => STYLE_RESET, - "bold" => STYLE_BOLD, - "dim" => STYLE_DIM, - _ => { - return Err(mlua::Error::RuntimeError(format!( - "The style '{}' is not a valid style name", - s.as_ref() - ))); - } - } - ); - flush_stdout()?; - Ok(()) -} - -fn print_color>(s: S) -> Result<()> { - print!( - "{}", - match s.as_ref() { - "reset" => COLOR_RESET, - "black" => COLOR_BLACK, - "red" => COLOR_RED, - "green" => COLOR_GREEN, - "yellow" => COLOR_YELLOW, - "blue" => COLOR_BLUE, - "purple" => COLOR_PURPLE, - "cyan" => COLOR_CYAN, - "white" => COLOR_WHITE, - _ => { - return Err(mlua::Error::RuntimeError(format!( - "The color '{}' is not a valid color name", - s.as_ref() - ))); - } - } - ); - flush_stdout()?; - Ok(()) -} - fn console_reset_color(_: &Lua, _: ()) -> Result<()> { print_color("reset")?; flush_stdout()?; @@ -207,10 +60,7 @@ fn console_log(_: &Lua, args: MultiValue) -> Result<()> { } fn console_info(_: &Lua, args: MultiValue) -> Result<()> { - print!( - "{}[{}INFO{}{}]{} ", - STYLE_BOLD, COLOR_CYAN, COLOR_RESET, STYLE_BOLD, STYLE_RESET - ); + print_label("info")?; let s = pretty_format_multi_value(&args)?; println!("{}", s); flush_stdout()?; @@ -218,10 +68,7 @@ fn console_info(_: &Lua, args: MultiValue) -> Result<()> { } fn console_warn(_: &Lua, args: MultiValue) -> Result<()> { - print!( - "{}[{}WARN{}{}]{} ", - STYLE_BOLD, COLOR_YELLOW, COLOR_RESET, STYLE_BOLD, STYLE_RESET - ); + print_label("warn")?; let s = pretty_format_multi_value(&args)?; println!("{}", s); flush_stdout()?; @@ -229,10 +76,7 @@ fn console_warn(_: &Lua, args: MultiValue) -> Result<()> { } fn console_error(_: &Lua, args: MultiValue) -> Result<()> { - eprint!( - "{}[{}ERROR{}{}]{} ", - STYLE_BOLD, COLOR_RED, COLOR_RESET, STYLE_BOLD, STYLE_RESET - ); + print_label("error")?; let s = pretty_format_multi_value(&args)?; eprintln!("{}", s); flush_stdout()?; diff --git a/src/lune/process.rs b/src/lune/process.rs index ff9ca51..3b6132f 100644 --- a/src/lune/process.rs +++ b/src/lune/process.rs @@ -1,53 +1,111 @@ use std::{ - env::{self, VarError}, + env, process::{exit, Stdio}, }; -use mlua::{Error, Lua, Result, Table, UserData, UserDataMethods, Value}; +use mlua::{ + Error, Function, Lua, MetaMethod, Result, Table, UserData, UserDataFields, UserDataMethods, + Value, +}; +use os_str_bytes::RawOsString; use tokio::process::Command; -pub struct LuneProcess(); +pub struct LuneProcess { + args: Vec, +} impl LuneProcess { - pub fn new() -> Self { - Self() + pub fn new(args: Vec) -> Self { + Self { args } } } impl UserData for LuneProcess { + fn add_fields<'lua, F: UserDataFields<'lua, Self>>(fields: &mut F) { + fields.add_field_method_get("args", |lua, this| { + // TODO: Use the same strategy as env uses below to avoid + // copying each time args are accessed? is it worth it? + let tab = lua.create_table()?; + for arg in &this.args { + tab.push(arg.to_owned())?; + } + Ok(tab) + }); + fields.add_field_method_get("env", |lua, _| { + let meta = lua.create_table()?; + meta.raw_set( + MetaMethod::Index.name(), + lua.create_function(process_env_get)?, + )?; + meta.raw_set( + MetaMethod::NewIndex.name(), + lua.create_function(process_env_set)?, + )?; + meta.raw_set( + MetaMethod::Iter.name(), + lua.create_function(process_env_iter)?, + )?; + let tab = lua.create_table()?; + tab.set_metatable(Some(meta)); + tab.set_readonly(true); + Ok(tab) + }) + } + fn add_methods<'lua, M: UserDataMethods<'lua, Self>>(methods: &mut M) { - methods.add_function("getEnvVars", process_get_env_vars); - methods.add_function("getEnvVar", process_get_env_var); - methods.add_function("setEnvVar", process_set_env_var); methods.add_function("exit", process_exit); methods.add_async_function("spawn", process_spawn); } } -fn process_get_env_vars(_: &Lua, _: ()) -> Result> { - let mut vars = Vec::new(); - for (key, _) in env::vars() { - vars.push(key); - } - Ok(vars) -} - -fn process_get_env_var(lua: &Lua, key: String) -> Result { - match env::var(&key) { - Ok(value) => Ok(Value::String(lua.create_string(&value)?)), - Err(VarError::NotPresent) => Ok(Value::Nil), - Err(VarError::NotUnicode(_)) => Err(Error::external(format!( - "The env var '{}' contains invalid utf8", - &key - ))), +fn process_env_get<'lua>(lua: &'lua Lua, (_, key): (Value<'lua>, String)) -> Result> { + match env::var_os(key) { + Some(value) => { + let raw_value = RawOsString::new(value); + Ok(Value::String(lua.create_string(raw_value.as_raw_bytes())?)) + } + None => Ok(Value::Nil), } } -fn process_set_env_var(_: &Lua, (key, value): (String, String)) -> Result<()> { - env::set_var(key, value); +fn process_env_set(_: &Lua, (_, key, value): (Value, String, String)) -> Result<()> { + // Make sure key is valid, otherwise set_var will panic + if key.is_empty() { + return Err(Error::RuntimeError("Key must not be empty".to_string())); + } else if key.contains('=') { + return Err(Error::RuntimeError( + "Key must not contain the equals character '='".to_string(), + )); + } else if key.contains('\0') { + return Err(Error::RuntimeError( + "Key must not contain the NUL character".to_string(), + )); + } + // Make sure value is valid, otherwise set_var will panic + if value.contains('\0') { + return Err(Error::RuntimeError( + "Value must not contain the NUL character".to_string(), + )); + } + env::set_var(&key, &value); Ok(()) } +fn process_env_iter<'lua>(lua: &'lua Lua, (_, _): (Value<'lua>, ())) -> Result> { + let mut vars = env::vars_os(); + lua.create_function_mut(move |lua, _: ()| match vars.next() { + Some((key, value)) => { + let raw_key = RawOsString::new(key); + let raw_value = RawOsString::new(value); + Ok(( + Value::String(lua.create_string(raw_key.as_raw_bytes())?), + Value::String(lua.create_string(raw_value.as_raw_bytes())?), + )) + } + None => Ok((Value::Nil, Value::Nil)), + }) +} + fn process_exit(_: &Lua, exit_code: Option) -> Result<()> { if let Some(code) = exit_code { exit(code); diff --git a/src/main.rs b/src/main.rs index 029b8a6..e8983f0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -8,7 +8,7 @@ mod lune; mod utils; use cli::Cli; -use utils::pretty_print_luau_error; +use utils::{pretty_print_luau_error, print_label}; #[tokio::main] async fn main() -> Result<()> { @@ -17,7 +17,8 @@ async fn main() -> Result<()> { Ok(_) => Ok(()), Err(e) => { eprintln!(); - eprintln!("[ERROR]"); + print_label("ERROR").unwrap(); + eprintln!(); pretty_print_luau_error(&e); std::process::exit(1); } diff --git a/src/utils.rs b/src/utils.rs index 6ac1dd3..3bc3c05 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,12 +1,35 @@ -use std::env::current_dir; +use std::{ + env::current_dir, + fmt::Write, + io::{self, Write as IoWrite}, +}; use anyhow::{bail, Context, Result}; +use mlua::{MultiValue, Value}; use reqwest::{ header::{HeaderMap, HeaderValue}, Client, }; use serde::{Deserialize, Serialize}; +const MAX_FORMAT_DEPTH: usize = 4; + +const INDENT: &str = " "; + +const COLOR_RESET: &str = "\x1B[0m"; +const COLOR_BLACK: &str = "\x1B[30m"; +const COLOR_RED: &str = "\x1B[31m"; +const COLOR_GREEN: &str = "\x1B[32m"; +const COLOR_YELLOW: &str = "\x1B[33m"; +const COLOR_BLUE: &str = "\x1B[34m"; +const COLOR_PURPLE: &str = "\x1B[35m"; +const COLOR_CYAN: &str = "\x1B[36m"; +const COLOR_WHITE: &str = "\x1B[37m"; + +const STYLE_RESET: &str = "\x1B[22m"; +const STYLE_BOLD: &str = "\x1B[1m"; +const STYLE_DIM: &str = "\x1B[2m"; + #[derive(Clone, Deserialize, Serialize)] pub struct GithubReleaseAsset { id: u64, @@ -138,6 +161,155 @@ pub fn get_github_user_agent_header() -> String { format!("{}-{}-cli", github_owner, github_repo) } +// TODO: Separate utils out into github & formatting + +pub fn flush_stdout() -> mlua::Result<()> { + io::stdout().flush().map_err(mlua::Error::external) +} + +pub fn print_label>(s: S) -> mlua::Result<()> { + print!( + "{}[{}{}{}{}]{} ", + STYLE_BOLD, + match s.as_ref().to_ascii_lowercase().as_str() { + "info" => COLOR_BLUE, + "warn" => COLOR_YELLOW, + "error" => COLOR_RED, + _ => COLOR_WHITE, + }, + s.as_ref().to_ascii_uppercase(), + COLOR_RESET, + STYLE_BOLD, + STYLE_RESET + ); + flush_stdout()?; + Ok(()) +} + +pub fn print_style>(s: S) -> mlua::Result<()> { + print!( + "{}", + match s.as_ref() { + "reset" => STYLE_RESET, + "bold" => STYLE_BOLD, + "dim" => STYLE_DIM, + _ => { + return Err(mlua::Error::RuntimeError(format!( + "The style '{}' is not a valid style name", + s.as_ref() + ))); + } + } + ); + flush_stdout()?; + Ok(()) +} + +pub fn print_color>(s: S) -> mlua::Result<()> { + print!( + "{}", + match s.as_ref() { + "reset" => COLOR_RESET, + "black" => COLOR_BLACK, + "red" => COLOR_RED, + "green" => COLOR_GREEN, + "yellow" => COLOR_YELLOW, + "blue" => COLOR_BLUE, + "purple" => COLOR_PURPLE, + "cyan" => COLOR_CYAN, + "white" => COLOR_WHITE, + _ => { + return Err(mlua::Error::RuntimeError(format!( + "The color '{}' is not a valid color name", + s.as_ref() + ))); + } + } + ); + flush_stdout()?; + Ok(()) +} + +fn can_be_plain_lua_table_key(s: &mlua::String) -> bool { + let str = s.to_string_lossy().to_string(); + let first_char = str.chars().next().unwrap(); + if first_char.is_alphabetic() { + str.chars().all(|c| c == '_' || c.is_alphanumeric()) + } else { + false + } +} + +fn pretty_format_value(buffer: &mut String, value: &Value, depth: usize) -> anyhow::Result<()> { + // TODO: Handle tables with cyclic references + // TODO: Handle other types like function, userdata, ... + match &value { + Value::Nil => write!(buffer, "nil")?, + Value::Boolean(true) => write!(buffer, "{}true{}", COLOR_YELLOW, COLOR_RESET)?, + Value::Boolean(false) => write!(buffer, "{}false{}", COLOR_YELLOW, COLOR_RESET)?, + Value::Number(n) => write!(buffer, "{}{}{}", COLOR_BLUE, n, COLOR_RESET)?, + Value::Integer(i) => write!(buffer, "{}{}{}", COLOR_BLUE, i, COLOR_RESET)?, + Value::String(s) => write!( + buffer, + "{}\"{}\"{}", + COLOR_GREEN, + s.to_string_lossy() + .replace('"', r#"\""#) + .replace('\n', r#"\n"#), + COLOR_RESET + )?, + Value::Table(ref tab) => { + if depth >= MAX_FORMAT_DEPTH { + write!(buffer, "{}{{ ... }}{}", STYLE_DIM, STYLE_RESET)?; + } else { + let depth_indent = INDENT.repeat(depth); + write!(buffer, "{}{{{}", STYLE_DIM, STYLE_RESET)?; + for pair in tab.clone().pairs::() { + let (key, value) = pair?; + match &key { + Value::String(s) if can_be_plain_lua_table_key(s) => write!( + buffer, + "\n{}{}{} {}={} ", + depth_indent, + INDENT, + s.to_string_lossy(), + STYLE_DIM, + STYLE_RESET + )?, + _ => { + write!(buffer, "\n{}{}[", depth_indent, INDENT)?; + pretty_format_value(buffer, &key, depth)?; + write!(buffer, "] {}={} ", STYLE_DIM, STYLE_RESET)?; + } + } + pretty_format_value(buffer, &value, depth + 1)?; + write!(buffer, "{},{}", STYLE_DIM, STYLE_RESET)?; + } + write!(buffer, "\n{}{}}}{}", depth_indent, STYLE_DIM, STYLE_RESET)?; + } + } + _ => write!(buffer, "?")?, + } + Ok(()) +} + +pub fn pretty_format_multi_value(multi: &MultiValue) -> mlua::Result { + let mut buffer = String::new(); + let mut counter = 0; + for value in multi { + counter += 1; + if let Value::String(s) = value { + write!(buffer, "{}", s.to_string_lossy()).map_err(mlua::Error::external)? + } else { + pretty_format_value(&mut buffer, value, 0).map_err(mlua::Error::external)?; + } + if counter < multi.len() { + write!(&mut buffer, " ").map_err(mlua::Error::external)?; + } + } + Ok(buffer) +} + pub fn pretty_print_luau_error(e: &mlua::Error) { match e { mlua::Error::RuntimeError(e) => {