Refactor process args & env, improve errors

This commit is contained in:
Filip Tibell 2023-01-20 15:21:20 -05:00
parent b6822e603e
commit bce6958c46
No known key found for this signature in database
12 changed files with 310 additions and 227 deletions

View file

@ -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

View file

@ -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

4
Cargo.lock generated
View file

@ -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"

View file

@ -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" }

View file

@ -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,

View file

@ -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

View file

@ -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,

View file

@ -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::<Result<Vec<_>>>()?;
lua.load(&file_contents)
.call_async(MultiValue::from_vec(lua_args))
.set_name(file_path.with_extension("").display().to_string())?
.exec_async()
.await?;
Ok(())
}

View file

@ -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::<Value, Value>() {
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<String> {
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: AsRef<str>>(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: AsRef<str>>(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()?;

View file

@ -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<String>,
}
impl LuneProcess {
pub fn new() -> Self {
Self()
pub fn new(args: Vec<String>) -> 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<Vec<String>> {
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<Value> {
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<Value<'lua>> {
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<Function<'lua>> {
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<i32>) -> Result<()> {
if let Some(code) = exit_code {
exit(code);

View file

@ -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);
}

View file

@ -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: AsRef<str>>(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: AsRef<str>>(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: AsRef<str>>(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::<Value, Value>() {
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<String> {
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) => {