diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 088884a3..9c413100 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -23,11 +23,8 @@ jobs: with: components: rustfmt - - name: Install Just - uses: extractions/setup-just@v2 - - name: Install Tooling - uses: ok-nick/setup-aftman@v0.4.2 + uses: CompeyDev/setup-rokit@v0.1.0 - name: Check Formatting run: just fmt-check @@ -40,11 +37,8 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Install Just - uses: extractions/setup-just@v2 - - name: Install Tooling - uses: ok-nick/setup-aftman@v0.4.2 + uses: CompeyDev/setup-rokit@v0.1.0 - name: Analyze run: just analyze diff --git a/.lune/hello_lune.luau b/.lune/hello_lune.luau index 197fe321..c50fd7b8 100644 --- a/.lune/hello_lune.luau +++ b/.lune/hello_lune.luau @@ -129,7 +129,7 @@ end ]] print("Sending 4 pings to google 🌏") -local result = process.spawn("ping", { +local result = process.exec("ping", { "google.com", "-c 4", }) diff --git a/Cargo.lock b/Cargo.lock index 6ba3d92f..3470b8bc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1629,6 +1629,8 @@ dependencies = [ name = "lune-std-process" version = "0.1.3" dependencies = [ + "bstr", + "bytes", "directories", "lune-utils", "mlua", diff --git a/crates/lune-roblox/src/instance/terrain.rs b/crates/lune-roblox/src/instance/terrain.rs index 96a036b8..8d770085 100644 --- a/crates/lune-roblox/src/instance/terrain.rs +++ b/crates/lune-roblox/src/instance/terrain.rs @@ -27,9 +27,8 @@ pub fn add_methods<'lua, M: LuaUserDataMethods<'lua, Instance>>(methods: &mut M) } fn get_or_create_material_colors(instance: &Instance) -> MaterialColors { - if let Some(Variant::MaterialColors(material_colors)) = instance.get_property("MaterialColors") - { - material_colors + if let Some(Variant::MaterialColors(inner)) = instance.get_property("MaterialColors") { + inner } else { MaterialColors::default() } diff --git a/crates/lune-std-net/src/lib.rs b/crates/lune-std-net/src/lib.rs index 3f428891..47db8f70 100644 --- a/crates/lune-std-net/src/lib.rs +++ b/crates/lune-std-net/src/lib.rs @@ -65,9 +65,9 @@ async fn net_request(lua: &Lua, config: RequestConfig) -> LuaResult { res.await?.into_lua_table(lua) } -async fn net_socket(lua: &Lua, url: String) -> LuaResult { +async fn net_socket(lua: &Lua, url: String) -> LuaResult { let (ws, _) = tokio_tungstenite::connect_async(url).await.into_lua_err()?; - NetWebSocket::new(ws).into_lua_table(lua) + NetWebSocket::new(ws).into_lua(lua) } async fn net_serve<'lua>( diff --git a/crates/lune-std-net/src/server/service.rs b/crates/lune-std-net/src/server/service.rs index 7bc7e539..2787ca6f 100644 --- a/crates/lune-std-net/src/server/service.rs +++ b/crates/lune-std-net/src/server/service.rs @@ -40,13 +40,13 @@ impl Service> for Svc { lua.spawn_local(async move { let sock = sock.await.unwrap(); let lua_sock = NetWebSocket::new(sock); - let lua_tab = lua_sock.into_lua_table(&lua_inner).unwrap(); + let lua_val = lua_sock.into_lua(&lua_inner).unwrap(); let handler_websocket: LuaFunction = keys.websocket_handler(&lua_inner).unwrap().unwrap(); lua_inner - .push_thread_back(handler_websocket, lua_tab) + .push_thread_back(handler_websocket, lua_val) .unwrap(); }); diff --git a/crates/lune-std-net/src/websocket.rs b/crates/lune-std-net/src/websocket.rs index ae2208ac..02254518 100644 --- a/crates/lune-std-net/src/websocket.rs +++ b/crates/lune-std-net/src/websocket.rs @@ -23,29 +23,6 @@ use hyper_tungstenite::{ WebSocketStream, }; -use lune_utils::TableBuilder; - -// Wrapper implementation for compatibility and changing colon syntax to dot syntax -const WEB_SOCKET_IMPL_LUA: &str = r#" -return freeze(setmetatable({ - close = function(...) - return websocket:close(...) - end, - send = function(...) - return websocket:send(...) - end, - next = function(...) - return websocket:next(...) - end, -}, { - __index = function(self, key) - if key == "closeCode" then - return websocket.closeCode - end - end, -})) -"#; - #[derive(Debug)] pub struct NetWebSocket { close_code_exists: Arc, @@ -125,25 +102,6 @@ where let mut ws = self.write_stream.lock().await; ws.close().await.into_lua_err() } - - pub fn into_lua_table(self, lua: &Lua) -> LuaResult { - let setmetatable = lua.globals().get::<_, LuaFunction>("setmetatable")?; - let table_freeze = lua - .globals() - .get::<_, LuaTable>("table")? - .get::<_, LuaFunction>("freeze")?; - - let env = TableBuilder::new(lua)? - .with_value("websocket", self.clone())? - .with_value("setmetatable", setmetatable)? - .with_value("freeze", table_freeze)? - .build_readonly()?; - - lua.load(WEB_SOCKET_IMPL_LUA) - .set_name("websocket") - .set_environment(env) - .eval() - } } impl LuaUserData for NetWebSocket diff --git a/crates/lune-std-process/Cargo.toml b/crates/lune-std-process/Cargo.toml index 8668dc02..86f54409 100644 --- a/crates/lune-std-process/Cargo.toml +++ b/crates/lune-std-process/Cargo.toml @@ -20,6 +20,9 @@ directories = "5.0" pin-project = "1.0" os_str_bytes = { version = "7.0", features = ["conversions"] } +bstr = "1.9" +bytes = "1.6.0" + tokio = { version = "1", default-features = false, features = [ "io-std", "io-util", diff --git a/crates/lune-std-process/src/lib.rs b/crates/lune-std-process/src/lib.rs index 29d73ea8..b66bc0df 100644 --- a/crates/lune-std-process/src/lib.rs +++ b/crates/lune-std-process/src/lib.rs @@ -1,27 +1,33 @@ #![allow(clippy::cargo_common_metadata)] use std::{ + cell::RefCell, env::{ self, consts::{ARCH, OS}, }, path::MAIN_SEPARATOR, process::Stdio, + rc::Rc, + sync::Arc, }; use mlua::prelude::*; use lune_utils::TableBuilder; use mlua_luau_scheduler::{Functions, LuaSpawnExt}; +use options::ProcessSpawnOptionsStdio; use os_str_bytes::RawOsString; -use tokio::io::AsyncWriteExt; +use stream::{ChildProcessReader, ChildProcessWriter}; +use tokio::{io::AsyncWriteExt, process::Child, sync::RwLock}; mod options; +mod stream; mod tee_writer; mod wait_for_child; use self::options::ProcessSpawnOptions; -use self::wait_for_child::{wait_for_child, WaitForChildResult}; +use self::wait_for_child::wait_for_child; use lune_utils::path::get_current_dir; @@ -73,7 +79,8 @@ pub fn module(lua: &Lua) -> LuaResult { .with_value("cwd", cwd_str)? .with_value("env", env_tab)? .with_value("exit", process_exit)? - .with_async_function("spawn", process_spawn)? + .with_async_function("exec", process_exec)? + .with_function("create", process_create)? .build_readonly() } @@ -141,11 +148,16 @@ fn process_env_iter<'lua>( }) } -async fn process_spawn( +async fn process_exec( lua: &Lua, (program, args, options): (String, Option>, ProcessSpawnOptions), ) -> LuaResult { - let res = lua.spawn(spawn_command(program, args, options)).await?; + let res = lua + .spawn(async move { + let cmd = spawn_command_with_stdin(program, args, options.clone()).await?; + wait_for_child(cmd, options.stdio.stdout, options.stdio.stderr).await + }) + .await?; /* NOTE: If an exit code was not given by the child process, @@ -168,30 +180,104 @@ async fn process_spawn( .build_readonly() } -async fn spawn_command( +#[allow(clippy::await_holding_refcell_ref)] +fn process_create( + lua: &Lua, + (program, args, options): (String, Option>, ProcessSpawnOptions), +) -> LuaResult { + // We do not want the user to provide stdio options for process.create, + // so we reset the options, regardless of what the user provides us + let mut spawn_options = options.clone(); + spawn_options.stdio = ProcessSpawnOptionsStdio::default(); + + let (code_tx, code_rx) = tokio::sync::broadcast::channel(4); + let code_rx_rc = Rc::new(RefCell::new(code_rx)); + + let child = spawn_command(program, args, spawn_options)?; + + let child_arc = Arc::new(RwLock::new(child)); + + let child_arc_clone = Arc::clone(&child_arc); + let mut child_lock = tokio::task::block_in_place(|| child_arc_clone.blocking_write()); + + let stdin = child_lock.stdin.take().unwrap(); + let stdout = child_lock.stdout.take().unwrap(); + let stderr = child_lock.stderr.take().unwrap(); + + let child_arc_inner = Arc::clone(&child_arc); + + // Spawn a background task to wait for the child to exit and send the exit code + let status_handle = tokio::spawn(async move { + let res = child_arc_inner.write().await.wait().await; + + if let Ok(output) = res { + let code = output.code().unwrap_or_default(); + + code_tx + .send(code) + .expect("ExitCode receiver was unexpectedly dropped"); + } + }); + + TableBuilder::new(lua)? + .with_value("stdout", ChildProcessReader(stdout))? + .with_value("stderr", ChildProcessReader(stderr))? + .with_value("stdin", ChildProcessWriter(stdin))? + .with_async_function("kill", move |_, ()| { + // First, stop the status task so the RwLock is dropped + status_handle.abort(); + let child_arc_clone = Arc::clone(&child_arc); + + // Then get another RwLock to write to the child process and kill it + async move { Ok(child_arc_clone.write().await.kill().await?) } + })? + .with_async_function("status", move |lua, ()| { + let code_rx_rc_clone = Rc::clone(&code_rx_rc); + async move { + // Exit code of 9 corresponds to SIGKILL, which should be the only case where + // the receiver gets suddenly dropped + let code = code_rx_rc_clone.borrow_mut().recv().await.unwrap_or(9); + + TableBuilder::new(lua)? + .with_value("code", code)? + .with_value("ok", code == 0)? + .build_readonly() + } + })? + .build_readonly() +} + +async fn spawn_command_with_stdin( program: String, args: Option>, mut options: ProcessSpawnOptions, -) -> LuaResult { - let stdout = options.stdio.stdout; - let stderr = options.stdio.stderr; +) -> LuaResult { let stdin = options.stdio.stdin.take(); - let mut child = options - .into_command(program, args) - .stdin(if stdin.is_some() { - Stdio::piped() - } else { - Stdio::null() - }) - .stdout(stdout.as_stdio()) - .stderr(stderr.as_stdio()) - .spawn()?; + let mut child = spawn_command(program, args, options)?; if let Some(stdin) = stdin { let mut child_stdin = child.stdin.take().unwrap(); child_stdin.write_all(&stdin).await.into_lua_err()?; } - wait_for_child(child, stdout, stderr).await + Ok(child) +} + +fn spawn_command( + program: String, + args: Option>, + options: ProcessSpawnOptions, +) -> LuaResult { + let stdout = options.stdio.stdout; + let stderr = options.stdio.stderr; + + let child = options + .into_command(program, args) + .stdin(Stdio::piped()) + .stdout(stdout.as_stdio()) + .stderr(stderr.as_stdio()) + .spawn()?; + + Ok(child) } diff --git a/crates/lune-std-process/src/stream.rs b/crates/lune-std-process/src/stream.rs new file mode 100644 index 00000000..830e0556 --- /dev/null +++ b/crates/lune-std-process/src/stream.rs @@ -0,0 +1,58 @@ +use bstr::BString; +use bytes::BytesMut; +use mlua::prelude::*; +use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt}; + +const CHUNK_SIZE: usize = 8; + +#[derive(Debug, Clone)] +pub struct ChildProcessReader(pub R); +#[derive(Debug, Clone)] +pub struct ChildProcessWriter(pub W); + +impl ChildProcessReader { + pub async fn read(&mut self, chunk_size: Option) -> LuaResult> { + let mut buf = BytesMut::with_capacity(chunk_size.unwrap_or(CHUNK_SIZE)); + self.0.read_buf(&mut buf).await?; + + Ok(buf.to_vec()) + } + + pub async fn read_to_end(&mut self) -> LuaResult> { + let mut buf = vec![]; + self.0.read_to_end(&mut buf).await?; + + Ok(buf) + } +} + +impl LuaUserData for ChildProcessReader { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_async_method_mut("read", |lua, this, chunk_size: Option| async move { + let buf = this.read(chunk_size).await?; + + if buf.is_empty() { + return Ok(LuaValue::Nil); + } + + Ok(LuaValue::String(lua.create_string(buf)?)) + }); + + methods.add_async_method_mut("readToEnd", |lua, this, ()| async { + Ok(lua.create_string(this.read_to_end().await?)) + }); + } +} + +impl ChildProcessWriter { + pub async fn write(&mut self, data: BString) -> LuaResult<()> { + self.0.write_all(data.as_ref()).await?; + Ok(()) + } +} + +impl LuaUserData for ChildProcessWriter { + fn add_methods<'lua, M: LuaUserDataMethods<'lua, Self>>(methods: &mut M) { + methods.add_async_method_mut("write", |_, this, data| async { this.write(data).await }); + } +} diff --git a/crates/lune/src/cli/run.rs b/crates/lune/src/cli/run.rs index c180e6ab..e693e227 100644 --- a/crates/lune/src/cli/run.rs +++ b/crates/lune/src/cli/run.rs @@ -39,9 +39,9 @@ impl RunCommand { let file_display_name = file_path.with_extension("").display().to_string(); (file_display_name, file_contents) }; - - // Create a new lune object with all globals & run the script - let result = Runtime::new() + + // Create a new lune runtime with all globals & run the script + let mut rt = Runtime::new() .with_args(self.script_args) // Enable JIT compilation unless it was requested to be disabled .with_jit( @@ -49,15 +49,18 @@ impl RunCommand { env::var("LUNE_LUAU_JIT").ok(), Some(jit_enabled) if jit_enabled == "0" || jit_enabled == "false" || jit_enabled == "off" ) - ) + ); + + let result = rt .run(&script_display_name, strip_shebang(script_contents)) .await; + Ok(match result { Err(err) => { eprintln!("{err}"); ExitCode::FAILURE } - Ok(code) => code, + Ok((code, _)) => ExitCode::from(code), }) } } diff --git a/crates/lune/src/rt/runtime.rs b/crates/lune/src/rt/runtime.rs index 641e7bb2..4e0bbed7 100644 --- a/crates/lune/src/rt/runtime.rs +++ b/crates/lune/src/rt/runtime.rs @@ -1,7 +1,6 @@ #![allow(clippy::missing_panics_doc)] use std::{ - process::ExitCode, rc::Rc, sync::{ atomic::{AtomicBool, Ordering}, @@ -153,7 +152,7 @@ impl Runtime { &mut self, script_name: impl AsRef, script_contents: impl AsRef<[u8]>, - ) -> RuntimeResult { + ) -> RuntimeResult<(u8, Vec)> { let lua = self.inner.lua(); let sched = self.inner.scheduler(); @@ -171,18 +170,19 @@ impl Runtime { .set_name(script_name.as_ref()); // Run it on our scheduler until it and any other spawned threads complete - sched.push_thread_back(main, ())?; + let main_thread_id = sched.push_thread_back(main, ())?; sched.run().await; - // Return the exit code - default to FAILURE if we got any errors - let exit_code = sched.get_exit_code().unwrap_or({ - if got_any_error.load(Ordering::SeqCst) { - ExitCode::FAILURE - } else { - ExitCode::SUCCESS - } - }); - - Ok(exit_code) + let main_thread_res = match sched.get_thread_result(main_thread_id) { + Some(res) => res, + None => LuaValue::Nil.into_lua_multi(lua), + }?; + + Ok(( + sched + .get_exit_code() + .unwrap_or(u8::from(got_any_error.load(Ordering::SeqCst))), + main_thread_res.into_vec(), + )) } } diff --git a/crates/lune/src/standalone/mod.rs b/crates/lune/src/standalone/mod.rs index fe589130..40321dd5 100644 --- a/crates/lune/src/standalone/mod.rs +++ b/crates/lune/src/standalone/mod.rs @@ -29,16 +29,15 @@ pub async fn run(patched_bin: impl AsRef<[u8]>) -> Result { let args = env::args().skip(1).collect::>(); let meta = Metadata::from_bytes(patched_bin).expect("must be a standalone binary"); - let result = Runtime::new() - .with_args(args) - .run("STANDALONE", meta.bytecode) - .await; + let mut rt = Runtime::new().with_args(args); + + let result = rt.run("STANDALONE", meta.bytecode).await; Ok(match result { Err(err) => { eprintln!("{err}"); ExitCode::FAILURE } - Ok(code) => code, + Ok((code, _)) => ExitCode::from(code), }) } diff --git a/crates/lune/src/tests.rs b/crates/lune/src/tests.rs index 2e866dc3..d5f56404 100644 --- a/crates/lune/src/tests.rs +++ b/crates/lune/src/tests.rs @@ -42,8 +42,8 @@ macro_rules! create_tests { .trim_end_matches(".luau") .trim_end_matches(".lua") .to_string(); - let exit_code = lune.run(&script_name, &script).await?; - Ok(exit_code) + let (exit_code, _) = lune.run(&script_name, &script).await?; + Ok(ExitCode::from(exit_code)) } )* } } @@ -138,13 +138,16 @@ create_tests! { process_cwd: "process/cwd", process_env: "process/env", process_exit: "process/exit", - process_spawn_async: "process/spawn/async", - process_spawn_basic: "process/spawn/basic", - process_spawn_cwd: "process/spawn/cwd", - process_spawn_no_panic: "process/spawn/no_panic", - process_spawn_shell: "process/spawn/shell", - process_spawn_stdin: "process/spawn/stdin", - process_spawn_stdio: "process/spawn/stdio", + process_exec_async: "process/exec/async", + process_exec_basic: "process/exec/basic", + process_exec_cwd: "process/exec/cwd", + process_exec_no_panic: "process/exec/no_panic", + process_exec_shell: "process/exec/shell", + process_exec_stdin: "process/exec/stdin", + process_exec_stdio: "process/exec/stdio", + process_spawn_non_blocking: "process/create/non_blocking", + process_spawn_status: "process/create/status", + process_spawn_stream: "process/create/stream", } #[cfg(feature = "std-regex")] diff --git a/crates/mlua-luau-scheduler/examples/exit_code.rs b/crates/mlua-luau-scheduler/examples/exit_code.rs index ee4a9a49..a6ede573 100644 --- a/crates/mlua-luau-scheduler/examples/exit_code.rs +++ b/crates/mlua-luau-scheduler/examples/exit_code.rs @@ -32,7 +32,7 @@ pub fn main() -> LuaResult<()> { // Verify that we got a correct exit code let code = sched.get_exit_code().unwrap_or_default(); - assert!(format!("{code:?}").contains("(1)")); + assert_eq!(code, 1); Ok(()) } diff --git a/crates/mlua-luau-scheduler/src/exit.rs b/crates/mlua-luau-scheduler/src/exit.rs index a2794ddd..d8d9bd3b 100644 --- a/crates/mlua-luau-scheduler/src/exit.rs +++ b/crates/mlua-luau-scheduler/src/exit.rs @@ -1,10 +1,10 @@ -use std::{cell::Cell, process::ExitCode, rc::Rc}; +use std::{cell::Cell, rc::Rc}; use event_listener::Event; #[derive(Debug, Clone)] pub(crate) struct Exit { - code: Rc>>, + code: Rc>>, event: Rc, } @@ -16,12 +16,12 @@ impl Exit { } } - pub fn set(&self, code: ExitCode) { + pub fn set(&self, code: u8) { self.code.set(Some(code)); self.event.notify(usize::MAX); } - pub fn get(&self) -> Option { + pub fn get(&self) -> Option { self.code.get() } diff --git a/crates/mlua-luau-scheduler/src/functions.rs b/crates/mlua-luau-scheduler/src/functions.rs index 7230b991..920cc85d 100644 --- a/crates/mlua-luau-scheduler/src/functions.rs +++ b/crates/mlua-luau-scheduler/src/functions.rs @@ -1,15 +1,11 @@ -#![allow(unused_imports)] #![allow(clippy::too_many_lines)] -use std::process::ExitCode; - use mlua::prelude::*; use crate::{ error_callback::ThreadErrorCallback, queue::{DeferredThreadQueue, SpawnedThreadQueue}, result_map::ThreadResultMap, - scheduler::Scheduler, thread_id::ThreadId, traits::LuaSchedulerExt, util::{is_poll_pending, LuaThreadOrFunction, ThreadResult}, @@ -232,7 +228,7 @@ impl<'lua> Functions<'lua> { "exit", lua.create_function(|lua, code: Option| { let _span = tracing::trace_span!("Scheduler::fn_exit").entered(); - let code = code.map(ExitCode::from).unwrap_or_default(); + let code = code.unwrap_or_default(); lua.set_exit_code(code); Ok(()) })?, diff --git a/crates/mlua-luau-scheduler/src/scheduler.rs b/crates/mlua-luau-scheduler/src/scheduler.rs index 31f699ed..9a43aa39 100644 --- a/crates/mlua-luau-scheduler/src/scheduler.rs +++ b/crates/mlua-luau-scheduler/src/scheduler.rs @@ -2,7 +2,6 @@ use std::{ cell::Cell, - process::ExitCode, rc::{Rc, Weak as WeakRc}, sync::{Arc, Weak as WeakArc}, thread::panicking, @@ -168,7 +167,7 @@ impl<'lua> Scheduler<'lua> { Gets the exit code for this scheduler, if one has been set. */ #[must_use] - pub fn get_exit_code(&self) -> Option { + pub fn get_exit_code(&self) -> Option { self.exit.get() } @@ -177,7 +176,7 @@ impl<'lua> Scheduler<'lua> { This will cause [`Scheduler::run`] to exit immediately. */ - pub fn set_exit_code(&self, code: ExitCode) { + pub fn set_exit_code(&self, code: u8) { self.exit.set(code); } diff --git a/crates/mlua-luau-scheduler/src/traits.rs b/crates/mlua-luau-scheduler/src/traits.rs index cbe2e6ec..caca3871 100644 --- a/crates/mlua-luau-scheduler/src/traits.rs +++ b/crates/mlua-luau-scheduler/src/traits.rs @@ -82,7 +82,7 @@ pub trait LuaSchedulerExt<'lua> { Panics if called outside of a running [`Scheduler`]. */ - fn set_exit_code(&self, code: ExitCode); + fn set_exit_code(&self, code: u8); /** Pushes (spawns) a lua thread to the **front** of the current scheduler. @@ -283,7 +283,7 @@ pub trait LuaSpawnExt<'lua> { } impl<'lua> LuaSchedulerExt<'lua> for Lua { - fn set_exit_code(&self, code: ExitCode) { + fn set_exit_code(&self, code: u8) { let exit = self .app_data_ref::() .expect("exit code can only be set from within an active scheduler"); diff --git a/aftman.toml b/rokit.toml similarity index 76% rename from aftman.toml rename to rokit.toml index 0c5e7565..8d9cd5dc 100644 --- a/aftman.toml +++ b/rokit.toml @@ -1,3 +1,4 @@ [tools] luau-lsp = "JohnnyMorganz/luau-lsp@1.32.1" stylua = "JohnnyMorganz/StyLua@0.20.0" +just = "casey/just@1.34.0" diff --git a/scripts/generate_compression_test_files.luau b/scripts/generate_compression_test_files.luau index 954929a1..ce7ac823 100644 --- a/scripts/generate_compression_test_files.luau +++ b/scripts/generate_compression_test_files.luau @@ -108,7 +108,7 @@ local BIN_ZLIB = if process.os == "macos" then "/opt/homebrew/bin/pigz" else "pi local function checkInstalled(program: string, args: { string }?) print("Checking if", program, "is installed") - local result = process.spawn(program, args) + local result = process.exec(program, args) if not result.ok then stdio.ewrite(string.format("Program '%s' is not installed\n", program)) process.exit(1) @@ -123,7 +123,7 @@ checkInstalled(BIN_ZLIB, { "--version" }) -- Run them to generate files local function run(program: string, args: { string }): string - local result = process.spawn(program, args) + local result = process.exec(program, args) if not result.ok then stdio.ewrite(string.format("Command '%s' failed\n", program)) if #result.stdout > 0 then diff --git a/tests/datetime/formatLocalTime.luau b/tests/datetime/formatLocalTime.luau index 4e2f6575..8bd000cc 100644 --- a/tests/datetime/formatLocalTime.luau +++ b/tests/datetime/formatLocalTime.luau @@ -31,7 +31,7 @@ if not runLocaleTests then return end -local dateCmd = process.spawn("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, { +local dateCmd = process.exec("bash", { "-c", "date +\"%A, %d %B %Y\" --date='@1693068988'" }, { env = { LC_ALL = "fr_FR.UTF-8 ", }, diff --git a/tests/net/serve/websockets.luau b/tests/net/serve/websockets.luau index ad53a386..51aea82f 100644 --- a/tests/net/serve/websockets.luau +++ b/tests/net/serve/websockets.luau @@ -24,10 +24,10 @@ local handle = net.serve(PORT, { return "unreachable" end, handleWebSocket = function(socket) - local socketMessage = socket.next() + local socketMessage = socket:next() assert(socketMessage == REQUEST, "Invalid web socket request from client") - socket.send(RESPONSE) - socket.close() + socket:send(RESPONSE) + socket:close() end, }) @@ -43,19 +43,19 @@ end) local socket = net.socket(WS_URL) -socket.send(REQUEST) +socket:send(REQUEST) -local socketMessage = socket.next() +local socketMessage = socket:next() assert(socketMessage ~= nil, "Got no web socket response from server") assert(socketMessage == RESPONSE, "Invalid web socket response from server") -socket.close() +socket:close() task.cancel(thread2) -- Wait for the socket to close and make sure we can't send messages afterwards task.wait() -local success3, err2 = (pcall :: any)(socket.send, "") +local success3, err2 = (pcall :: any)(socket.send, socket, "") assert(not success3, "Sending messages after the socket has been closed should error") local message2 = tostring(err2) assert( diff --git a/tests/net/socket/basic.luau b/tests/net/socket/basic.luau index 2eb4a3ba..aad61c5c 100644 --- a/tests/net/socket/basic.luau +++ b/tests/net/socket/basic.luau @@ -8,17 +8,17 @@ assert(type(socket.send) == "function", "send must be a function") assert(type(socket.close) == "function", "close must be a function") -- Request to close the socket -socket.close() +socket:close() -- Drain remaining messages, until we got our close message -while socket.next() do +while socket:next() do end assert(type(socket.closeCode) == "number", "closeCode should exist after closing") assert(socket.closeCode == 1000, "closeCode should be 1000 after closing") local success, message = pcall(function() - socket.send("Hello, world!") + socket:send("Hello, world!") end) assert(not success, "send should fail after closing") diff --git a/tests/net/socket/wss.luau b/tests/net/socket/wss.luau index 6594d08c..74afba3a 100644 --- a/tests/net/socket/wss.luau +++ b/tests/net/socket/wss.luau @@ -8,7 +8,7 @@ local task = require("@lune/task") local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json") while not socket.closeCode do - local response = socket.next() + local response = socket:next() if response then local decodeSuccess, decodeMessage = pcall(serde.decode, "json" :: "json", response) @@ -23,6 +23,6 @@ while not socket.closeCode do -- Close the connection after a second with the success close code task.wait(1) - socket.close(1000) + socket:close(1000) end end diff --git a/tests/net/socket/wss_rw.luau b/tests/net/socket/wss_rw.luau index fefb244f..2f9d96cb 100644 --- a/tests/net/socket/wss_rw.luau +++ b/tests/net/socket/wss_rw.luau @@ -10,7 +10,7 @@ local socket = net.socket("wss://gateway.discord.gg/?v=10&encoding=json") local spawnedThread = task.spawn(function() while not socket.closeCode do - socket.next() + socket:next() end end) @@ -23,9 +23,9 @@ end) task.wait(1) local payload = '{"op":1,"d":null}' -socket.send(payload) -socket.send(buffer.fromstring(payload)) -socket.close(1000) +socket:send(payload) +socket:send(buffer.fromstring(payload)) +socket:close(1000) task.cancel(delayedThread) task.cancel(spawnedThread) diff --git a/tests/process/create/kill.luau b/tests/process/create/kill.luau new file mode 100644 index 00000000..e0cbbc7b --- /dev/null +++ b/tests/process/create/kill.luau @@ -0,0 +1,21 @@ +local process = require("@lune/process") + +-- Killing a child process should work as expected + +local message = "Hello, world!" +local child = process.create("cat") + +child.stdin:write(message) +child.kill() + +assert(child.status().code == 9, "Child process should have an exit code of 9 (SIGKILL)") + +assert( + child.stdout:readToEnd() == message, + "Reading from stdout of child process should work even after kill" +) + +local stdinWriteOk = pcall(function() + child.stdin:write(message) +end) +assert(not stdinWriteOk, "Writing to stdin of child process should not work after kill") diff --git a/tests/process/create/non_blocking.luau b/tests/process/create/non_blocking.luau new file mode 100644 index 00000000..82352a7a --- /dev/null +++ b/tests/process/create/non_blocking.luau @@ -0,0 +1,13 @@ +local process = require("@lune/process") + +-- Spawning a child process should not block the thread + +local childThread = coroutine.create(process.create) + +local ok, err = coroutine.resume(childThread, "echo", { "hello, world" }) +assert(ok, err) + +assert( + coroutine.status(childThread) == "dead", + "Child process should not block the thread it is running on" +) diff --git a/tests/process/create/status.luau b/tests/process/create/status.luau new file mode 100644 index 00000000..418c132a --- /dev/null +++ b/tests/process/create/status.luau @@ -0,0 +1,15 @@ +local process = require("@lune/process") + +-- The exit code of an child process should be correct + +local randomExitCode = math.random(0, 255) +local isOk = randomExitCode == 0 +local child = process.create("exit", { tostring(randomExitCode) }, { shell = true }) +local status = child.status() + +assert( + status.code == randomExitCode, + `Child process exited with wrong exit code, expected {randomExitCode}` +) + +assert(status.ok == isOk, `Child status should be {if status.ok then "ok" else "not ok"}`) diff --git a/tests/process/create/stream.luau b/tests/process/create/stream.luau new file mode 100644 index 00000000..89bb61ac --- /dev/null +++ b/tests/process/create/stream.luau @@ -0,0 +1,18 @@ +local process = require("@lune/process") + +-- Should be able to write and read from child process streams + +local msg = "hello, world" + +local catChild = process.create("cat") +catChild.stdin:write(msg) +assert( + msg == catChild.stdout:read(#msg), + "Failed to write to stdin or read from stdout of child process" +) + +local echoChild = if process.os == "windows" + then process.create("/c", { "echo", msg, "1>&2" }, { shell = "cmd" }) + else process.create("echo", { msg, ">>/dev/stderr" }, { shell = true }) + +assert(msg == echoChild.stderr:read(#msg), "Failed to read from stderr of child process") diff --git a/tests/process/spawn/async.luau b/tests/process/exec/async.luau similarity index 89% rename from tests/process/spawn/async.luau rename to tests/process/exec/async.luau index 2f60f3af..205eccc6 100644 --- a/tests/process/spawn/async.luau +++ b/tests/process/exec/async.luau @@ -4,7 +4,7 @@ local task = require("@lune/task") local IS_WINDOWS = process.os == "windows" --- Spawning a process should not block any lua thread(s) +-- Executing a command should not block any lua thread(s) local SLEEP_DURATION = 1 / 4 local SLEEP_SAMPLES = 2 @@ -31,7 +31,7 @@ for i = 1, SLEEP_SAMPLES, 1 do table.insert(args, 1, "-Milliseconds") end -- Windows does not have `sleep` as a process, so we use powershell instead. - process.spawn("sleep", args, if IS_WINDOWS then { shell = true } else nil) + process.exec("sleep", args, if IS_WINDOWS then { shell = true } else nil) sleepCounter += 1 end) end diff --git a/tests/process/spawn/basic.luau b/tests/process/exec/basic.luau similarity index 89% rename from tests/process/spawn/basic.luau rename to tests/process/exec/basic.luau index 012a8ee7..41b0847a 100644 --- a/tests/process/spawn/basic.luau +++ b/tests/process/exec/basic.luau @@ -2,7 +2,7 @@ local process = require("@lune/process") local stdio = require("@lune/stdio") local task = require("@lune/task") --- Spawning a child process should work, with options +-- Executing a command should work, with options local thread = task.delay(1, function() stdio.ewrite("Spawning a process should take a reasonable amount of time\n") @@ -12,7 +12,7 @@ end) local IS_WINDOWS = process.os == "windows" -local result = process.spawn( +local result = process.exec( if IS_WINDOWS then "cmd" else "ls", if IS_WINDOWS then { "/c", "dir" } else { "-a" } ) diff --git a/tests/process/spawn/cwd.luau b/tests/process/exec/cwd.luau similarity index 81% rename from tests/process/spawn/cwd.luau rename to tests/process/exec/cwd.luau index d9989dfc..96a7fe42 100644 --- a/tests/process/spawn/cwd.luau +++ b/tests/process/exec/cwd.luau @@ -6,7 +6,7 @@ local pwdCommand = if IS_WINDOWS then "cmd" else "pwd" local pwdArgs = if IS_WINDOWS then { "/c", "cd" } else {} -- Make sure the cwd option actually uses the directory we want -local rootPwd = process.spawn(pwdCommand, pwdArgs, { +local rootPwd = process.exec(pwdCommand, pwdArgs, { cwd = "/", }).stdout rootPwd = string.gsub(rootPwd, "^%s+", "") @@ -27,24 +27,24 @@ end -- Setting cwd should not change the cwd of this process -local pwdBefore = process.spawn(pwdCommand, pwdArgs).stdout -process.spawn("ls", {}, { +local pwdBefore = process.exec(pwdCommand, pwdArgs).stdout +process.exec("ls", {}, { cwd = "/", shell = true, }) -local pwdAfter = process.spawn(pwdCommand, pwdArgs).stdout +local pwdAfter = process.exec(pwdCommand, pwdArgs).stdout assert(pwdBefore == pwdAfter, "Current working directory changed after running child process") -- Setting the cwd on a child process should properly -- replace any leading ~ with the users real home dir -local homeDir1 = process.spawn("echo $HOME", nil, { +local homeDir1 = process.exec("echo $HOME", nil, { shell = true, }).stdout -- NOTE: Powershell for windows uses `$pwd.Path` instead of `pwd` as pwd would return -- a PathInfo object, using $pwd.Path gets the Path property of the PathInfo object -local homeDir2 = process.spawn(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, { +local homeDir2 = process.exec(if IS_WINDOWS then "$pwd.Path" else "pwd", nil, { shell = true, cwd = "~", }).stdout diff --git a/tests/process/exec/no_panic.luau b/tests/process/exec/no_panic.luau new file mode 100644 index 00000000..a7d289f6 --- /dev/null +++ b/tests/process/exec/no_panic.luau @@ -0,0 +1,7 @@ +local process = require("@lune/process") + +-- Executing a non existent command as a child process +-- should not panic, but should error + +local success = pcall(process.exec, "someProgramThatDoesNotExist") +assert(not success, "Spawned a non-existent program") diff --git a/tests/process/spawn/shell.luau b/tests/process/exec/shell.luau similarity index 94% rename from tests/process/spawn/shell.luau rename to tests/process/exec/shell.luau index 6f64791d..729f15a5 100644 --- a/tests/process/spawn/shell.luau +++ b/tests/process/exec/shell.luau @@ -5,7 +5,7 @@ local IS_WINDOWS = process.os == "windows" -- Default shell should be /bin/sh on unix and powershell on Windows, -- note that powershell needs slightly different command flags for ls -local shellResult = process.spawn("ls", { +local shellResult = process.exec("ls", { if IS_WINDOWS then "-Force" else "-a", }, { shell = true, diff --git a/tests/process/spawn/stdin.luau b/tests/process/exec/stdin.luau similarity index 79% rename from tests/process/spawn/stdin.luau rename to tests/process/exec/stdin.luau index 56c77a50..f85cd0bf 100644 --- a/tests/process/spawn/stdin.luau +++ b/tests/process/exec/stdin.luau @@ -10,8 +10,8 @@ local echoMessage = "Hello from child process!" -- When passing stdin to powershell on windows we must "accept" using the double newline local result = if IS_WINDOWS - then process.spawn("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" }) - else process.spawn("xargs", { "echo" }, { stdin = echoMessage }) + then process.exec("powershell", { "echo" }, { stdin = echoMessage .. "\n\n" }) + else process.exec("xargs", { "echo" }, { stdin = echoMessage }) local resultStdout = if IS_WINDOWS then string.sub(result.stdout, #result.stdout - #echoMessage - 1) diff --git a/tests/process/spawn/stdio.luau b/tests/process/exec/stdio.luau similarity index 79% rename from tests/process/spawn/stdio.luau rename to tests/process/exec/stdio.luau index 0ea5b1c3..524713b8 100644 --- a/tests/process/spawn/stdio.luau +++ b/tests/process/exec/stdio.luau @@ -5,12 +5,12 @@ local IS_WINDOWS = process.os == "windows" -- Inheriting stdio & environment variables should work local echoMessage = "Hello from child process!" -local echoResult = process.spawn("echo", { +local echoResult = process.exec("echo", { if IS_WINDOWS then '"$Env:TEST_VAR"' else '"$TEST_VAR"', }, { env = { TEST_VAR = echoMessage }, shell = if IS_WINDOWS then "powershell" else "bash", - stdio = "inherit", + stdio = "inherit" :: process.SpawnOptionsStdioKind, -- FIXME: This should just work without a cast? }) -- Windows uses \r\n (CRLF) and unix uses \n (LF) diff --git a/tests/process/spawn/no_panic.luau b/tests/process/spawn/no_panic.luau deleted file mode 100644 index 3a57a9bd..00000000 --- a/tests/process/spawn/no_panic.luau +++ /dev/null @@ -1,7 +0,0 @@ -local process = require("@lune/process") - --- Spawning a child process for a non-existent --- program should not panic, but should error - -local success = pcall(process.spawn, "someProgramThatDoesNotExist") -assert(not success, "Spawned a non-existent program") diff --git a/tests/stdio/format.luau b/tests/stdio/format.luau index 7ade5f5b..c0cc7cfc 100644 --- a/tests/stdio/format.luau +++ b/tests/stdio/format.luau @@ -109,7 +109,7 @@ assertContains( local _, errorMessage = pcall(function() local function innerInnerFn() - process.spawn("PROGRAM_THAT_DOES_NOT_EXIST") + process.exec("PROGRAM_THAT_DOES_NOT_EXIST") end local function innerFn() innerInnerFn() diff --git a/types/datetime.luau b/types/datetime.luau index 8992dab5..04b42aa9 100644 --- a/types/datetime.luau +++ b/types/datetime.luau @@ -87,10 +87,19 @@ export type DateTimeValueArguments = DateTimeValues & OptionalMillisecond ]=] export type DateTimeValueReturns = DateTimeValues & Millisecond +--[=[ + @prop unixTimestamp number + @within DateTime + Number of seconds passed since the UNIX epoch. +]=] + +--[=[ + @prop unixTimestampMillis number + @within DateTime + Number of milliseconds passed since the UNIX epoch. +]=] local DateTime = { - --- Number of seconds passed since the UNIX epoch. unixTimestamp = (nil :: any) :: number, - --- Number of milliseconds passed since the UNIX epoch. unixTimestampMillis = (nil :: any) :: number, } diff --git a/types/net.luau b/types/net.luau index e9b793e9..7a7204b5 100644 --- a/types/net.luau +++ b/types/net.luau @@ -173,9 +173,9 @@ export type ServeHandle = { ]=] export type WebSocket = { closeCode: number?, - close: (code: number?) -> (), - send: (message: (string | buffer)?, asBinaryMessage: boolean?) -> (), - next: () -> string?, + close: (self: WebSocket, code: number?) -> (), + send: (self: WebSocket, message: (string | buffer)?, asBinaryMessage: boolean?) -> (), + next: (self: WebSocket) -> string?, } --[=[ diff --git a/types/process.luau b/types/process.luau index 7b820527..6a4a12ec 100644 --- a/types/process.luau +++ b/types/process.luau @@ -5,6 +5,9 @@ export type SpawnOptionsStdioKind = "default" | "inherit" | "forward" | "none" export type SpawnOptionsStdio = { stdout: SpawnOptionsStdioKind?, stderr: SpawnOptionsStdioKind?, +} + +export type ExecuteOptionsStdio = SpawnOptionsStdio & { stdin: string?, } @@ -12,27 +15,117 @@ export type SpawnOptionsStdio = { @interface SpawnOptions @within Process - A dictionary of options for `process.spawn`, with the following available values: + A dictionary of options for `process.create`, with the following available values: * `cwd` - The current working directory for the process * `env` - Extra environment variables to give to the process * `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell * `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `SpawnOptionsStdio` for more info - * `stdin` - Optional standard input to pass to spawned child process ]=] export type SpawnOptions = { cwd: string?, env: { [string]: string }?, shell: (boolean | string)?, +} + +--[=[ + @interface ExecuteOptions + @within Process + + A dictionary of options for `process.exec`, with the following available values: + + * `cwd` - The current working directory for the process + * `env` - Extra environment variables to give to the process + * `shell` - Whether to run in a shell or not - set to `true` to run using the default shell, or a string to run using a specific shell + * `stdio` - How to treat output and error streams from the child process - see `SpawnOptionsStdioKind` and `ExecuteOptionsStdio` for more info + * `stdin` - Optional standard input to pass to executed child process +]=] +export type ExecuteOptions = SpawnOptions & { stdio: (SpawnOptionsStdioKind | SpawnOptionsStdio)?, stdin: string?, -- TODO: Remove this since it is now available in stdio above, breaking change } --[=[ - @interface SpawnResult + @class ChildProcessReader @within Process - Result type for child processes in `process.spawn`. + A reader class to read data from a child process' streams in realtime. +]=] +local ChildProcessReader = {} + +--[=[ + @within ChildProcessReader + + Reads a chunk of data (specified length or a default of 8 bytes at a time) from + the reader as a string. Returns nil if there is no more data to read. + + This function may yield until there is new data to read from reader, if all data + till present has already been read, and the process has not exited. + + @return The string containing the data read from the reader +]=] +function ChildProcessReader:read(chunkSize: number?): string? + return nil :: any +end + +--[=[ + @within ChildProcessReader + + Reads all the data currently present in the reader as a string. + This function will yield until the process exits. + + @return The string containing the data read from the reader +]=] +function ChildProcessReader:readToEnd(): string + return nil :: any +end + +--[=[ + @class ChildProcessWriter + @within Process + + A writer class to write data to a child process' streams in realtime. +]=] +local ChildProcessWriter = {} + +--[=[ + @within ChildProcessWriter + + Writes a buffer or string of data to the writer. + + @param data The data to write to the writer +]=] +function ChildProcessWriter:write(data: buffer | string): () + return nil :: any +end + +--[=[ + @interface ChildProcess + @within Process + + Result type for child processes in `process.create`. + + This is a dictionary containing the following values: + + * `stdin` - A writer to write to the child process' stdin - see `ChildProcessWriter` for more info + * `stdout` - A reader to read from the child process' stdout - see `ChildProcessReader` for more info + * `stderr` - A reader to read from the child process' stderr - see `ChildProcessReader` for more info + * `kill` - A function that kills the child process + * `status` - A function that yields and returns the exit status of the child process +]=] +export type ChildProcess = { + stdin: typeof(ChildProcessWriter), + stdout: typeof(ChildProcessReader), + stderr: typeof(ChildProcessReader), + kill: () -> (); + status: () -> { ok: boolean, code: number } +} + +--[=[ + @interface ExecuteResult + @within Process + + Result type for child processes in `process.exec`. This is a dictionary containing the following values: @@ -41,7 +134,7 @@ export type SpawnOptions = { * `stdout` - The full contents written to stdout by the child process, or an empty string if nothing was written * `stderr` - The full contents written to stderr by the child process, or an empty string if nothing was written ]=] -export type SpawnResult = { +export type ExecuteResult = { ok: boolean, code: number, stdout: string, @@ -73,8 +166,8 @@ export type SpawnResult = { -- Getting the current os and processor architecture print("Running " .. process.os .. " on " .. process.arch .. "!") - -- Spawning a child process - local result = process.spawn("program", { + -- Executing a command + local result = process.exec("program", { "cli argument", "other cli argument" }) @@ -83,6 +176,19 @@ export type SpawnResult = { else print(result.stderr) end + + -- Spawning a child process + local child = process.create("program", { + "cli argument", + "other cli argument" + }) + + -- Writing to the child process' stdin + child.stdin:write("Hello from Lune!") + + -- Reading from the child process' stdout + local data = child.stdout:read() + print(buffer.tostring(data)) ``` ]=] local process = {} @@ -163,19 +269,44 @@ end --[=[ @within Process - Spawns a child process that will run the program `program`, and returns a dictionary that describes the final status and ouput of the child process. + Spawns a child process in the background that runs the program `program`, and immediately returns + readers and writers to communicate with it. + + In order to execute a command and wait for its output, see `process.exec`. The second argument, `params`, can be passed as a list of string parameters to give to the program. The third argument, `options`, can be passed as a dictionary of options to give to the child process. Refer to the documentation for `SpawnOptions` for specific option keys and their values. - @param program The program to spawn as a child process + @param program The program to Execute as a child process + @param params Additional parameters to pass to the program + @param options A dictionary of options for the child process + @return A dictionary with the readers and writers to communicate with the child process +]=] +function process.create(program: string, params: { string }?, options: SpawnOptions?): ChildProcess + return nil :: any +end + +--[=[ + @within Process + + Executes a child process that will execute the command `program`, waiting for it to exit. + Upon exit, it returns a dictionary that describes the final status and ouput of the child process. + + In order to spawn a child process in the background, see `process.create`. + + The second argument, `params`, can be passed as a list of string parameters to give to the program. + + The third argument, `options`, can be passed as a dictionary of options to give to the child process. + Refer to the documentation for `ExecuteOptions` for specific option keys and their values. + + @param program The program to Execute as a child process @param params Additional parameters to pass to the program @param options A dictionary of options for the child process @return A dictionary representing the result of the child process ]=] -function process.spawn(program: string, params: { string }?, options: SpawnOptions?): SpawnResult +function process.exec(program: string, params: { string }?, options: ExecuteOptions?): ExecuteResult return nil :: any end diff --git a/types/regex.luau b/types/regex.luau index 59756f37..068b8dda 100644 --- a/types/regex.luau +++ b/types/regex.luau @@ -19,67 +19,82 @@ local RegexMatch = { type RegexMatch = typeof(RegexMatch) ---[=[ - @class RegexCaptures - - Captures from a regular expression. -]=] local RegexCaptures = {} ---[=[ - @within RegexCaptures - @tag Method - - Returns the match at the given index, if one exists. - - @param index -- The index of the match to get - @return RegexMatch -- The match, if one exists -]=] function RegexCaptures.get(self: RegexCaptures, index: number): RegexMatch? return nil :: any end ---[=[ - @within RegexCaptures - @tag Method - - Returns the match for the given named match group, if one exists. - - @param group -- The name of the group to get - @return RegexMatch -- The match, if one exists -]=] function RegexCaptures.group(self: RegexCaptures, group: string): RegexMatch? return nil :: any end +function RegexCaptures.format(self: RegexCaptures, format: string): string + return nil :: any +end + --[=[ - @within RegexCaptures - @tag Method + @class RegexCaptures - Formats the captures using the given format string. + Captures from a regular expression. +]=] +export type RegexCaptures = typeof(setmetatable( + {} :: { + --[=[ + @within RegexCaptures + @tag Method + @method get - ### Example usage + Returns the match at the given index, if one exists. - ```lua - local regex = require("@lune/regex") + @param index -- The index of the match to get + @return RegexMatch -- The match, if one exists + ]=] - local re = regex.new("(?[0-9]{2})-(?[0-9]{2})-(?[0-9]{4})") + get: (self: RegexCaptures, index: number) -> RegexMatch?, - local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb."); - assert(caps ~= nil, "Example pattern should match example text") + --[=[ + @within RegexCaptures + @tag Method + @method group - local formatted = caps:format("year=$year, month=$month, day=$day") - print(formatted) -- "year=2010, month=03, day=14" - ``` + Returns the match for the given named match group, if one exists. - @param format -- The format string to use - @return string -- The formatted string -]=] -function RegexCaptures.format(self: RegexCaptures, format: string): string - return nil :: any -end + @param group -- The name of the group to get + @return RegexMatch -- The match, if one exists + ]=] + group: (self: RegexCaptures, group: string) -> RegexMatch?, + + --[=[ + @within RegexCaptures + @tag Method + @method format + + Formats the captures using the given format string. + + ### Example usage + + ```lua + local regex = require("@lune/regex") + + local re = regex.new("(?[0-9]{2})-(?[0-9]{2})-(?[0-9]{4})") + + local caps = re:captures("On 14-03-2010, I became a Tenneessee lamb."); + assert(caps ~= nil, "Example pattern should match example text") + + local formatted = caps:format("year=$year, month=$month, day=$day") + print(formatted) -- "year=2010, month=03, day=14" + ``` -export type RegexCaptures = typeof(RegexCaptures) + @param format -- The format string to use + @return string -- The formatted string + ]=] + format: (self: RegexCaptures, format: string) -> string, + }, + {} :: { + __len: (self: RegexCaptures) -> number, + } +)) local Regex = {}