add os.args() to stdlib

master
Ondřej Hruška 1 day ago
parent 298b09ddc9
commit 0a1bf00701
  1. 2
      docs/README.md
  2. 42
      docs/os.md
  3. 36
      lua/demo/args.lua
  4. 10
      lua/stubs/os.lua
  5. 39
      src/lua_tests/os_tests.rs
  6. 35
      src/main.rs
  7. 4
      src/stdlib/mod.rs
  8. 24
      src/stdlib/os_ext.rs

@ -11,7 +11,7 @@ also works and returns the same table).
| [`utils`](utils.md) | JSON encode/decode, the `NULL` marker, error-trapping (`try`/`tryn`), value `dump`. |
| [`math`](math.md) | Additions to stock `math`: `round`, `clamp`, `isFinite`, `sign`, `scale`. |
| [`table`](table.md) | Additions to stock `table`: map/filter/reduce, merge, deep-copy, aggregates, lookups, read-only proxy. |
| [`os`](os.md) | Sandbox additions: `microtime` (sub-second clock) and async `sleep`. |
| [`os`](os.md) | Sandbox additions: `microtime` (sub-second clock), async `sleep`, and `args` (script arguments). |
| [`log`](log.md) | Level-tagged diagnostic logging (`trace`…`error`), gated by `LUNA_LOG`. |
| [`sqlite`](sqlite.md) | Embedded SQLite: connect, query, parameters, transactions. |
| [`http`](http.md) | HTTP/HTTPS client: requests, JSON, auth, cookie-jar sessions, per-origin permissions. |

@ -2,9 +2,10 @@
Luna runs scripts in a sandbox, so the standard `os` table is trimmed to its
safe, time-related functions — `os.time`, `os.clock`, `os.date`,
`os.difftime`, `os.getenv` — while the parts that touch the system
(`os.execute`, `os.remove`, `os.exit`, …) are removed. To that trimmed table Luna
adds two functions: a high-resolution clock and an async sleep.
`os.difftime` — while the parts that touch the system
(`os.execute`, `os.getenv`, `os.remove`, `os.exit`, …) are removed. To that
trimmed table Luna adds a high-resolution clock, an async sleep, and access to
the script's command-line arguments.
```lua
local t0 = os.microtime()
@ -29,6 +30,37 @@ It reads the system wall clock, so it tracks real time (and can jump if the cloc
is adjusted); for interval timing the difference of two readings is what you
want.
## `os.args()`
Return the command-line arguments passed to the script, as an array (a fresh
table on every call, so it is safe to mutate). On the `luna` command line the
script's arguments are everything after the script path — even a token spelled
like a luna flag goes to the script there, so luna's own flags (`--sandbox` &
co.) must come before the path. A `--` between the path and the arguments is
accepted and skipped:
```sh
luna script.lua one two --three
luna --sandbox script.lua -- one two --three
```
```lua
-- script.lua
for i, arg in ipairs(os.args()) do
print(i, arg) --> 1 one / 2 two / 3 --three
end
```
An executable script with a `#!/usr/bin/env luna` shebang line receives its
arguments the same way, with no `--` needed:
```sh
./script.lua one two --three
```
In the REPL `os.args()` returns an empty table. Arguments are passed through
as raw byte strings, without UTF-8 conversion.
## `os.sleep(seconds)`
Pause for `seconds` (a float, so `os.sleep(0.1)` is 100 ms). This is an **async**
@ -64,8 +96,8 @@ explanation — the rule is to run anything that needs to sleep through
## Notes
- **The sandbox keeps the time functions.** `os.time`, `os.clock`, `os.date`,
`os.difftime`, and `os.getenv` work as in stock Lua; system-mutating functions
are not present.
and `os.difftime` work as in stock Lua; functions that touch the system are
not present.
- **`microtime` for durations, `time` for timestamps.** Use `os.microtime` when
you need sub-second precision or are timing an interval; `os.time` when whole
seconds suffice.

@ -0,0 +1,36 @@
#!/usr/bin/env luna
-- Showcase os.args(): the command-line arguments passed to the script.
--
-- Try:
-- luna lua/demo/args.lua greet --name=World extra
-- luna --sandbox lua/demo/args.lua -- greet --name=World
-- or make the file executable and call it directly (the shebang works too):
-- chmod +x lua/demo/args.lua
-- ./lua/demo/args.lua greet --name=World
local args = os.args()
print("os.args() = " .. utils.dump(args))
if #args == 0 then
print("no arguments given — try: luna lua/demo/args.lua greet --name=World extra")
return
end
-- A tiny argument parser: --key=value pairs become options, the rest positionals.
local opts, positional = {}, {}
for _, arg in ipairs(args) do
local key, value = arg:match("^%-%-([%w-]+)=(.*)$")
if key then
opts[key] = value
else
table.insert(positional, arg)
end
end
print("options = " .. utils.dump(opts))
print("positionals = " .. utils.dump(positional))
if positional[1] == "greet" then
print(("Hello, %s!"):format(opts.name or "stranger"))
end

@ -3,12 +3,20 @@
-- Type stubs for the Luna additions to the sandboxed `os` table.
--
-- The sandbox trims `os` to its time-related functions — os.time, os.clock,
-- os.date, os.difftime, os.getenv — plus the two additions below. The
-- os.date, os.difftime, os.getenv — plus the additions below. The
-- system-access functions (os.execute, os.exit, os.remove, os.rename,
-- os.tmpname, os.setlocale) do NOT exist at runtime; lua-language-server
-- cannot hide individual fields, so it still suggests them — do not use them.
-- Reference: docs/os.md
---Command-line arguments passed to the script: everything after the script
---path on the `luna` command line (or after a literal `--`), including
---arguments given directly to an executable `#!/usr/bin/env luna` script.
---Returns a fresh array each call; empty in the REPL. Arguments are raw byte
---strings (no UTF-8 conversion).
---@return string[] args
function os.args() end
---Current time as a Unix timestamp in seconds, as a float with sub-second
---precision (wall clock — may jump if the system clock is adjusted). Use the
---difference of two readings for interval timing.

@ -155,6 +155,45 @@ fn test_os_missing_dangerous_functions() {
}
}
#[test]
fn test_os_args() {
let lua = super::lua();
// Without ScriptArgs app data (tests, and structurally the REPL): empty.
let n: i64 = lua.load(r#"return #os.args()"#).eval().unwrap();
assert_eq!(n, 0);
// main plants the parsed trailing arguments; non-UTF-8 bytes pass through.
use std::os::unix::ffi::OsStringExt;
lua.set_app_data(crate::stdlib::os_ext::ScriptArgs(vec![
"hello".into(),
"--flag".into(),
std::ffi::OsString::from_vec(vec![0xff, b'x']),
]));
let ok: bool = lua
.load(
r#"
local a = os.args()
return #a == 3 and a[1] == "hello" and a[2] == "--flag" and a[3] == "\xffx"
"#,
)
.eval()
.unwrap();
assert!(ok);
// Each call returns a fresh table — mutations do not leak into the next.
let ok: bool = lua
.load(
r#"
os.args()[1] = "changed"
return os.args()[1] == "hello"
"#,
)
.eval()
.unwrap();
assert!(ok);
}
#[test]
fn test_os_microtime() {
let lua = super::lua();

@ -16,8 +16,17 @@ mod stdlib;
#[derive(Parser, Debug)]
#[command(version, about)]
struct Args {
/// Path to the Lua script to execute. Omit to start an interactive REPL.
filepath: Option<PathBuf>,
/// Path to the Lua script to execute, followed by the script's arguments
/// (readable as os.args()). Omit the path to start an interactive REPL.
///
/// Everything after the script path belongs to the script — even tokens
/// spelled like the flags above — so luna's own flags must come first.
/// One positional per the interpreter convention (`python x.py -v` passes
/// -v to x.py); it also makes `#!/usr/bin/env luna` scripts receive their
/// command-line arguments. An optional `--` between the path and the
/// arguments is skipped.
#[arg(trailing_var_arg = true, allow_hyphen_values = true, value_name = "FILEPATH [ARGS]")]
script: Vec<std::ffi::OsString>,
/// Grant all filesystem access without prompting or recording anything.
#[arg(long, conflicts_with = "no_fs")]
@ -111,9 +120,29 @@ async fn main() {
async fn run() -> Result<(), Box<dyn std::error::Error>> {
let args = Args::parse();
// Split the trailing positionals into the script path and its arguments.
// clap only treats `--` as an escape before the first positional
// (`luna -- x.lua`); after the path it comes through literally, so a
// leading one is dropped here — `luna x.lua -- -v` and `luna x.lua -v`
// pass the same args. Later occurrences are the script's to interpret.
let (filepath, script_args) = match args.script.split_first() {
None => (None, &[] as &[std::ffi::OsString]),
Some((path, rest)) => {
let rest = match rest.split_first() {
Some((sep, rest)) if sep.as_os_str() == "--" => rest,
_ => rest,
};
(Some(PathBuf::from(path)), rest)
}
};
let lua = sandbox::create_sandboxed_lua()?;
let Some(filepath) = args.filepath.clone() else {
// Exposed to scripts as os.args(). Empty in the REPL (any trailing
// argument would have been taken as the script path).
lua.set_app_data(stdlib::os_ext::ScriptArgs(script_args.to_vec()));
let Some(filepath) = filepath else {
// In the REPL, `.env` and modules are looked up from the CWD.
// Permissions are prompted for but never persisted (no script
// identity to record them under).

@ -5,7 +5,9 @@ mod http;
mod logging;
mod lua_dump;
mod math;
mod os_ext;
// pub(crate) for ScriptArgs — `main` plants the command-line arguments
// destined for the script (os.args()).
pub(crate) mod os_ext;
// pub(crate) for Policy & co. — `main` builds the permission policy from
// the CLI flags and the script's canonical path.
pub(crate) mod perm;

@ -1,12 +1,36 @@
use std::ffi::OsString;
use std::os::unix::ffi::OsStrExt;
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use mlua::prelude::LuaResult;
use mlua::{Lua, Table as LuaTable, Value as LuaValue};
/// Command-line arguments destined for the script — everything after the
/// script path (or after `--`) on the `luna` command line. Planted into app
/// data by `main`; absent in tests, where `os.args()` returns an empty table.
pub(crate) struct ScriptArgs(pub(crate) Vec<OsString>);
/// Extend the already-sandboxed `os` table with non-standard additions.
pub(super) fn install(lua: &Lua) -> LuaResult<()> {
let os: LuaTable = lua.globals().get("os")?;
// os.args() — the arguments passed to the script, as a fresh array each
// call so the caller can mutate the result freely.
os.raw_set(
"args",
lua.create_function(|lua, ()| {
let table = lua.create_table()?;
if let Some(args) = lua.app_data_ref::<ScriptArgs>() {
for (i, arg) in args.0.iter().enumerate() {
// OS arguments are arbitrary bytes, and so are Lua strings —
// no lossy UTF-8 conversion.
table.raw_set(i + 1, lua.create_string(arg.as_bytes())?)?;
}
}
Ok(table)
})?,
)?;
// os.microtime() — Unix timestamp as f64 with sub-second precision.
os.raw_set(
"microtime",

Loading…
Cancel
Save