tatara-lisp-script 0.2.1

tatara-script — standalone interpreter that makes tatara-lisp a full scripting language for nix-run apps. Wraps tatara-lisp-eval with a scripting-oriented stdlib (http, json, yaml, sops, file I/O, env, sha256, string ops) so a .tlisp file can replace a bash script one-for-one.
Documentation
//! Environment-variable access.
//!
//!   (env-get NAME)                → string or nil
//!   (env-get NAME DEFAULT)        → string (default if missing)
//!   (env-required NAME)           → string or raises
//!   (env-set NAME VALUE)          → nil; sets the var in the script's
//!                                   process so subsequent `exec-check`
//!                                   children inherit it. Use to plumb
//!                                   sops-decrypted creds (or any other
//!                                   computed value) into subprocesses
//!                                   without resorting to `sh -c`.
//!   (argv)                        → list of strings (ScriptCtx::argv)
//!   (argv-get N)                  → nth arg, or nil if out of range
//!   (argv-get N DEFAULT)          → nth arg, or default

use std::sync::Arc;

use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};

use crate::script_ctx::ScriptCtx;

pub fn install(interp: &mut Interpreter<ScriptCtx>) {
    interp.register_fn(
        "env-get",
        Arity::Range(1, 2),
        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
            let name = str_arg(&args[0], "env-get", sp)?;
            match std::env::var(&*name) {
                Ok(v) => Ok(Value::Str(Arc::from(v))),
                Err(_) => Ok(args.get(1).cloned().unwrap_or(Value::Nil)),
            }
        },
    );

    interp.register_fn(
        "env-required",
        Arity::Exact(1),
        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
            let name = str_arg(&args[0], "env-required", sp)?;
            std::env::var(&*name)
                .map(|v| Value::Str(Arc::from(v)))
                .map_err(|_| {
                    EvalError::native_fn(
                        "env-required",
                        format!("environment variable {name} is not set"),
                        sp,
                    )
                })
        },
    );

    interp.register_fn(
        "env-set",
        Arity::Exact(2),
        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
            let name = str_arg(&args[0], "env-set", sp)?;
            let value = str_arg(&args[1], "env-set", sp)?;
            // SAFETY: set_var is documented as unsafe on multi-threaded
            // processes that don't synchronize env access. Tatara-lisp
            // scripts run single-threaded by design (the interpreter
            // owns the process), and the only consumers of env are the
            // exec-* primitives that fork child processes afterward,
            // so this is the documented use case.
            unsafe {
                std::env::set_var(&*name, &*value);
            }
            Ok(Value::Nil)
        },
    );

    interp.register_fn(
        "argv",
        Arity::Exact(0),
        |_args: &[Value], ctx: &mut ScriptCtx, _sp| {
            Ok(Value::list(
                ctx.argv
                    .iter()
                    .map(|s| Value::Str(Arc::from(s.as_str())))
                    .collect::<Vec<_>>(),
            ))
        },
    );

    interp.register_fn(
        "argv-get",
        Arity::Range(1, 2),
        |args: &[Value], ctx: &mut ScriptCtx, sp| {
            let n = int_arg(&args[0], "argv-get", sp)?;
            if n < 0 {
                return Err(EvalError::native_fn(
                    "argv-get",
                    format!("index must be >= 0, got {n}"),
                    sp,
                ));
            }
            let idx = n as usize;
            if idx < ctx.argv.len() {
                Ok(Value::Str(Arc::from(ctx.argv[idx].as_str())))
            } else {
                Ok(args.get(1).cloned().unwrap_or(Value::Nil))
            }
        },
    );
}

pub(crate) fn str_arg(
    v: &Value,
    fname: &'static str,
    sp: tatara_lisp::Span,
) -> Result<Arc<str>, EvalError> {
    match v {
        Value::Str(s) => Ok(s.clone()),
        other => Err(EvalError::type_mismatch("string", other.type_name(), sp))
            .map_err(|e| EvalError::native_fn(fname, format!("expected string, got {e:?}"), sp)),
    }
}

pub(crate) fn int_arg(
    v: &Value,
    fname: &'static str,
    sp: tatara_lisp::Span,
) -> Result<i64, EvalError> {
    match v {
        Value::Int(n) => Ok(*n),
        other => Err(EvalError::native_fn(
            fname,
            format!("expected integer, got {}", other.type_name()),
            sp,
        )),
    }
}