Skip to main content

tatara_lisp_script/stdlib/
env.rs

1//! Environment-variable access.
2//!
3//!   (env-get NAME)                → string or nil
4//!   (env-get NAME DEFAULT)        → string (default if missing)
5//!   (env-required NAME)           → string or raises
6//!   (env-set NAME VALUE)          → nil; sets the var in the script's
7//!                                   process so subsequent `exec-check`
8//!                                   children inherit it. Use to plumb
9//!                                   sops-decrypted creds (or any other
10//!                                   computed value) into subprocesses
11//!                                   without resorting to `sh -c`.
12//!   (argv)                        → list of strings (ScriptCtx::argv)
13//!   (argv-get N)                  → nth arg, or nil if out of range
14//!   (argv-get N DEFAULT)          → nth arg, or default
15
16use std::sync::Arc;
17
18use tatara_lisp_eval::{Arity, EvalError, Interpreter, Value};
19
20use crate::script_ctx::ScriptCtx;
21
22pub fn install(interp: &mut Interpreter<ScriptCtx>) {
23    interp.register_fn(
24        "env-get",
25        Arity::Range(1, 2),
26        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
27            let name = str_arg(&args[0], "env-get", sp)?;
28            match std::env::var(&*name) {
29                Ok(v) => Ok(Value::Str(Arc::from(v))),
30                Err(_) => Ok(args.get(1).cloned().unwrap_or(Value::Nil)),
31            }
32        },
33    );
34
35    interp.register_fn(
36        "env-required",
37        Arity::Exact(1),
38        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
39            let name = str_arg(&args[0], "env-required", sp)?;
40            std::env::var(&*name)
41                .map(|v| Value::Str(Arc::from(v)))
42                .map_err(|_| {
43                    EvalError::native_fn(
44                        "env-required",
45                        format!("environment variable {name} is not set"),
46                        sp,
47                    )
48                })
49        },
50    );
51
52    interp.register_fn(
53        "env-set",
54        Arity::Exact(2),
55        |args: &[Value], _ctx: &mut ScriptCtx, sp| {
56            let name = str_arg(&args[0], "env-set", sp)?;
57            let value = str_arg(&args[1], "env-set", sp)?;
58            // SAFETY: set_var is documented as unsafe on multi-threaded
59            // processes that don't synchronize env access. Tatara-lisp
60            // scripts run single-threaded by design (the interpreter
61            // owns the process), and the only consumers of env are the
62            // exec-* primitives that fork child processes afterward,
63            // so this is the documented use case.
64            unsafe {
65                std::env::set_var(&*name, &*value);
66            }
67            Ok(Value::Nil)
68        },
69    );
70
71    interp.register_fn(
72        "argv",
73        Arity::Exact(0),
74        |_args: &[Value], ctx: &mut ScriptCtx, _sp| {
75            Ok(Value::list(
76                ctx.argv
77                    .iter()
78                    .map(|s| Value::Str(Arc::from(s.as_str())))
79                    .collect::<Vec<_>>(),
80            ))
81        },
82    );
83
84    interp.register_fn(
85        "argv-get",
86        Arity::Range(1, 2),
87        |args: &[Value], ctx: &mut ScriptCtx, sp| {
88            let n = int_arg(&args[0], "argv-get", sp)?;
89            if n < 0 {
90                return Err(EvalError::native_fn(
91                    "argv-get",
92                    format!("index must be >= 0, got {n}"),
93                    sp,
94                ));
95            }
96            let idx = n as usize;
97            if idx < ctx.argv.len() {
98                Ok(Value::Str(Arc::from(ctx.argv[idx].as_str())))
99            } else {
100                Ok(args.get(1).cloned().unwrap_or(Value::Nil))
101            }
102        },
103    );
104}
105
106pub(crate) fn str_arg(
107    v: &Value,
108    fname: &'static str,
109    sp: tatara_lisp::Span,
110) -> Result<Arc<str>, EvalError> {
111    match v {
112        Value::Str(s) => Ok(s.clone()),
113        other => Err(EvalError::type_mismatch("string", other.type_name(), sp))
114            .map_err(|e| EvalError::native_fn(fname, format!("expected string, got {e:?}"), sp)),
115    }
116}
117
118pub(crate) fn int_arg(
119    v: &Value,
120    fname: &'static str,
121    sp: tatara_lisp::Span,
122) -> Result<i64, EvalError> {
123    match v {
124        Value::Int(n) => Ok(*n),
125        other => Err(EvalError::native_fn(
126            fname,
127            format!("expected integer, got {}", other.type_name()),
128            sp,
129        )),
130    }
131}