1use std::any::Any;
4use std::env;
5use std::panic;
6
7use runmat_builtins::{CharArray, Value};
8use runmat_macros::runtime_builtin;
9
10use crate::builtins::common::spec::{
11 BroadcastSemantics, BuiltinFusionSpec, BuiltinGpuSpec, ConstantStrategy, GpuOpKind,
12 ReductionNaN, ResidencyPolicy, ShapeRequirements,
13};
14#[cfg(feature = "doc_export")]
15use crate::register_builtin_doc_text;
16use crate::{gather_if_needed, register_builtin_fusion_spec, register_builtin_gpu_spec};
17
18const ERR_TOO_FEW_INPUTS: &str = "setenv: not enough input arguments";
19const ERR_TOO_MANY_INPUTS: &str = "setenv: too many input arguments";
20const ERR_NAME_TYPE: &str = "setenv: NAME must be a string scalar or character vector";
21const ERR_VALUE_TYPE: &str = "setenv: VALUE must be a string scalar or character vector";
22
23const MESSAGE_EMPTY_NAME: &str = "Environment variable name must not be empty.";
24const MESSAGE_NAME_HAS_EQUAL: &str = "Environment variable names must not contain '='.";
25const MESSAGE_NAME_HAS_NULL: &str = "Environment variable names must not contain null characters.";
26const MESSAGE_VALUE_HAS_NULL: &str =
27 "Environment variable values must not contain null characters.";
28const MESSAGE_OPERATION_FAILED: &str = "Unable to update environment variable: ";
29
30#[cfg(feature = "doc_export")]
31pub const DOC_MD: &str = r#"---
32title: "setenv"
33category: "io/repl_fs"
34keywords: ["setenv", "environment variable", "status", "message", "unset"]
35summary: "Set or clear environment variables with MATLAB-compatible status outputs."
36references:
37 - https://www.mathworks.com/help/matlab/ref/setenv.html
38gpu_support:
39 elementwise: false
40 reduction: false
41 precisions: []
42 broadcasting: "none"
43 notes: "Host-only operation. RunMat gathers GPU-resident inputs before mutating the process environment; providers do not expose hooks for this builtin."
44fusion:
45 elementwise: false
46 reduction: false
47 max_inputs: 2
48 constants: "inline"
49requires_feature: null
50tested:
51 unit: "builtins::io::repl_fs::setenv::tests"
52 integration: "builtins::io::repl_fs::setenv::tests::setenv_reports_failure_for_illegal_name"
53---
54
55# What does the `setenv` function do in MATLAB / RunMat?
56`setenv` updates the process environment. Provide a variable name and value to create or modify an
57entry, or pass an empty value to remove the variable. The builtin mirrors MATLAB by returning a
58status code and optional diagnostic message instead of throwing for platform-defined failures.
59
60## How does the `setenv` function behave in MATLAB / RunMat?
61- `status = setenv(name, value)` returns `0` when the update succeeds and `1` when the operating
62 system rejects the request. The status output is a double scalar, matching MATLAB.
63- `[status, message] = setenv(name, value)` returns the status plus a character vector describing
64 failures. On success, `message` is an empty `1×0` character array.
65- Set `value` to an empty string (`""`) or empty character vector (`''`) to remove the variable from
66 the current process environment.
67- Names must be string scalars or character vectors containing only valid environment variable
68 characters. MATLAB raises an error when `name` is not text; RunMat mirrors this check.
69- Character vector inputs trim trailing padding spaces (common with MATLAB character matrices). To
70 retain trailing spaces, pass a string scalar instead.
71- Environment updates apply to the RunMat process and any child processes it spawns. They do not
72 modify the parent shell.
73
74## `setenv` Function GPU Execution Behaviour
75`setenv` always runs on the CPU. If a caller stores the arguments on the GPU—for instance via an
76accelerated string builtin—RunMat gathers them to host memory automatically before mutating the
77environment. Acceleration providers do not implement hooks for this builtin.
78
79## GPU residency in RunMat (Do I need `gpuArray`?)
80No. `setenv` is a host-side operation. GPU residency offers no benefit, and RunMat gathers
81GPU-backed values automatically if they appear as inputs.
82
83## Examples of using the `setenv` function in MATLAB / RunMat
84
85### Set a new environment variable for the current session
86```matlab
87status = setenv("RUNMAT_MODE", "development")
88```
89Expected output:
90```matlab
91status =
92 0
93```
94
95### Update an existing environment variable
96```matlab
97status = setenv("PATH", string(getenv("PATH")) + ":~/runmat/bin")
98```
99Expected output:
100```matlab
101status =
102 0
103```
104
105### Remove an environment variable with an empty value
106```matlab
107[status, message] = setenv("OLD_SETTING", "")
108```
109Expected output:
110```matlab
111status =
112 0
113
114message =
115
116```
117
118### Capture diagnostic messages when a name is invalid
119```matlab
120[status, message] = setenv("INVALID=NAME", "value")
121```
122Expected output:
123```matlab
124status =
125 1
126
127message =
128Environment variable names must not contain '='.
129```
130
131### Use character vectors from legacy code
132```matlab
133status = setenv('RUNMAT_LEGACY', 'enabled')
134```
135Expected output:
136```matlab
137status =
138 0
139```
140
141### Combine `setenv` with child process launches
142```matlab
143setenv("RUNMAT_DATASET", "demo");
144status = system("runmat-cli process-data")
145```
146Expected output:
147```matlab
148status =
149 0
150```
151
152## FAQ
153- **What status codes does `setenv` return?** `0` means success; `1` means the operating system
154 rejected the request (for example, due to an invalid name or an oversized value on Windows).
155- **Does `setenv` throw errors?** Only when the inputs are the wrong type (non-text). Platform
156 failures are reported through the status and message outputs so scripts can handle them
157 programmatically.
158- **How do I remove a variable?** Pass an empty string or empty character vector as the value.
159- **Are names case-sensitive?** RunMat defers to the operating system: case-sensitive on
160 Unix-like systems and case-insensitive on Windows.
161- **Can I include trailing spaces in the value?** Use string scalars to preserve trailing spaces.
162 Character vector inputs trim trailing padding spaces by design.
163- **Does `setenv` affect the parent shell?** No. Changes are limited to the current RunMat process
164 and any child processes launched afterwards.
165- **What characters are disallowed in names?** `setenv` rejects names containing `=` or null
166 characters. Additional platform-specific restrictions are enforced by the operating system and
167 reported through the status/message outputs.
168- **Can I call `setenv` from GPU-enabled code?** Yes. Arguments are gathered from the GPU before
169 updating the environment; the operation itself always runs on the CPU.
170- **How can I check whether the update succeeded?** Inspect the returned `status`. When it is `1`,
171 read the accompanying message to determine why the operation failed.
172- **Will the variable persist after I exit RunMat?** No. Environment modifications are scoped to the
173 current process.
174
175## See Also
176[getenv](./getenv), [mkdir](./mkdir), [pwd](./pwd)
177
178## Source & Feedback
179- Source: [`crates/runmat-runtime/src/builtins/io/repl_fs/setenv.rs`](https://github.com/runmat-org/runmat/blob/main/crates/runmat-runtime/src/builtins/io/repl_fs/setenv.rs)
180- Issues: [Open a GitHub ticket](https://github.com/runmat-org/runmat/issues/new/choose) with a minimal reproduction.
181"#;
182
183pub const GPU_SPEC: BuiltinGpuSpec = BuiltinGpuSpec {
184 name: "setenv",
185 op_kind: GpuOpKind::Custom("io"),
186 supported_precisions: &[],
187 broadcast: BroadcastSemantics::None,
188 provider_hooks: &[],
189 constant_strategy: ConstantStrategy::InlineLiteral,
190 residency: ResidencyPolicy::GatherImmediately,
191 nan_mode: ReductionNaN::Include,
192 two_pass_threshold: None,
193 workgroup_size: None,
194 accepts_nan_mode: false,
195 notes:
196 "Host-only environment mutation. GPU-resident arguments are gathered automatically before invoking the OS APIs.",
197};
198
199register_builtin_gpu_spec!(GPU_SPEC);
200
201pub const FUSION_SPEC: BuiltinFusionSpec = BuiltinFusionSpec {
202 name: "setenv",
203 shape: ShapeRequirements::Any,
204 constant_strategy: ConstantStrategy::InlineLiteral,
205 elementwise: None,
206 reduction: None,
207 emits_nan: false,
208 notes: "Environment updates terminate fusion; metadata registered for completeness.",
209};
210
211register_builtin_fusion_spec!(FUSION_SPEC);
212
213#[cfg(feature = "doc_export")]
214register_builtin_doc_text!("setenv", DOC_MD);
215
216#[runtime_builtin(
217 name = "setenv",
218 category = "io/repl_fs",
219 summary = "Set or clear environment variables with MATLAB-compatible status outputs.",
220 keywords = "setenv,environment variable,status,message,unset",
221 accel = "cpu"
222)]
223fn setenv_builtin(args: Vec<Value>) -> Result<Value, String> {
224 let eval = evaluate(&args)?;
225 Ok(eval.first_output())
226}
227
228pub fn evaluate(args: &[Value]) -> Result<SetenvResult, String> {
230 let gathered = gather_arguments(args)?;
231 match gathered.len() {
232 0 | 1 => Err(ERR_TOO_FEW_INPUTS.to_string()),
233 2 => apply(&gathered[0], &gathered[1]),
234 _ => Err(ERR_TOO_MANY_INPUTS.to_string()),
235 }
236}
237
238#[derive(Debug, Clone)]
239pub struct SetenvResult {
240 status: f64,
241 message: String,
242}
243
244impl SetenvResult {
245 fn success() -> Self {
246 Self {
247 status: 0.0,
248 message: String::new(),
249 }
250 }
251
252 fn failure(message: String) -> Self {
253 Self {
254 status: 1.0,
255 message,
256 }
257 }
258
259 pub fn first_output(&self) -> Value {
260 Value::Num(self.status)
261 }
262
263 pub fn outputs(&self) -> Vec<Value> {
264 vec![Value::Num(self.status), char_array_value(&self.message)]
265 }
266
267 #[cfg(test)]
268 pub(crate) fn status(&self) -> f64 {
269 self.status
270 }
271
272 #[cfg(test)]
273 pub(crate) fn message(&self) -> &str {
274 &self.message
275 }
276}
277
278fn apply(name_value: &Value, value_value: &Value) -> Result<SetenvResult, String> {
279 let name = extract_scalar_text(name_value, ERR_NAME_TYPE)?;
280 let value = extract_scalar_text(value_value, ERR_VALUE_TYPE)?;
281
282 if name.is_empty() {
283 return Ok(SetenvResult::failure(MESSAGE_EMPTY_NAME.to_string()));
284 }
285 if name.chars().any(|ch| ch == '=') {
286 return Ok(SetenvResult::failure(MESSAGE_NAME_HAS_EQUAL.to_string()));
287 }
288 if name.chars().any(|ch| ch == '\0') {
289 return Ok(SetenvResult::failure(MESSAGE_NAME_HAS_NULL.to_string()));
290 }
291 if value.chars().any(|ch| ch == '\0') {
292 return Ok(SetenvResult::failure(MESSAGE_VALUE_HAS_NULL.to_string()));
293 }
294
295 Ok(update_environment(&name, &value))
296}
297
298fn update_environment(name: &str, value: &str) -> SetenvResult {
299 if value.is_empty() {
300 match panic::catch_unwind(|| env::remove_var(name)) {
301 Ok(()) => SetenvResult::success(),
302 Err(payload) => SetenvResult::failure(format!(
303 "{}{}",
304 MESSAGE_OPERATION_FAILED,
305 panic_payload_to_string(payload)
306 )),
307 }
308 } else {
309 match panic::catch_unwind(|| env::set_var(name, value)) {
310 Ok(()) => SetenvResult::success(),
311 Err(payload) => SetenvResult::failure(format!(
312 "{}{}",
313 MESSAGE_OPERATION_FAILED,
314 panic_payload_to_string(payload)
315 )),
316 }
317 }
318}
319
320fn gather_arguments(args: &[Value]) -> Result<Vec<Value>, String> {
321 let mut out = Vec::with_capacity(args.len());
322 for value in args {
323 out.push(gather_if_needed(value).map_err(|err| format!("setenv: {err}"))?);
324 }
325 Ok(out)
326}
327
328fn extract_scalar_text(value: &Value, error_message: &str) -> Result<String, String> {
329 match value {
330 Value::String(text) => Ok(text.clone()),
331 Value::CharArray(array) => {
332 if array.rows != 1 {
333 return Err(error_message.to_string());
334 }
335 Ok(char_row_to_string(array))
336 }
337 Value::StringArray(array) => {
338 if array.data.len() == 1 {
339 Ok(array.data[0].clone())
340 } else {
341 Err(error_message.to_string())
342 }
343 }
344 _ => Err(error_message.to_string()),
345 }
346}
347
348fn char_row_to_string(array: &CharArray) -> String {
349 if array.cols == 0 {
350 return String::new();
351 }
352 let mut text = String::with_capacity(array.cols);
353 for col in 0..array.cols {
354 text.push(array.data[col]);
355 }
356 while text.ends_with(' ') {
357 text.pop();
358 }
359 text
360}
361
362fn char_array_value(text: &str) -> Value {
363 Value::CharArray(CharArray::new_row(text))
364}
365
366fn panic_payload_to_string(payload: Box<dyn Any + Send>) -> String {
367 match payload.downcast::<String>() {
368 Ok(msg) => *msg,
369 Err(payload) => match payload.downcast::<&'static str>() {
370 Ok(msg) => (*msg).to_string(),
371 Err(_) => "operation failed".to_string(),
372 },
373 }
374}
375
376#[cfg(test)]
377mod tests {
378 use super::*;
379 use crate::builtins::io::repl_fs::REPL_FS_TEST_LOCK;
380 use runmat_builtins::{CharArray, StringArray, Value};
381
382 fn unique_name(suffix: &str) -> String {
383 format!("RUNMAT_TEST_SETENV_{}", suffix)
384 }
385
386 #[test]
387 fn setenv_sets_variable_and_returns_success() {
388 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
389 let name = unique_name("BASIC");
390 env::remove_var(&name);
391
392 let result = setenv_builtin(vec![
393 Value::String(name.clone()),
394 Value::String("value".to_string()),
395 ])
396 .expect("setenv");
397
398 assert_eq!(result, Value::Num(0.0));
399 assert_eq!(env::var(&name).unwrap(), "value");
400 env::remove_var(name);
401 }
402
403 #[test]
404 fn setenv_removes_variable_when_value_is_empty() {
405 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
406 let name = unique_name("REMOVE");
407 env::set_var(&name, "seed");
408
409 let result = setenv_builtin(vec![
410 Value::String(name.clone()),
411 Value::CharArray(CharArray::new_row("")),
412 ])
413 .expect("setenv");
414
415 assert_eq!(result, Value::Num(0.0));
416 assert!(env::var(&name).is_err());
417 env::remove_var(name);
418 }
419
420 #[test]
421 fn setenv_reports_failure_for_illegal_name() {
422 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
423 let eval = evaluate(&[
424 Value::String("INVALID=NAME".to_string()),
425 Value::String("value".to_string()),
426 ])
427 .expect("evaluate");
428
429 assert_eq!(eval.status(), 1.0);
430 assert_eq!(eval.message(), MESSAGE_NAME_HAS_EQUAL);
431 }
432
433 #[test]
434 fn setenv_reports_failure_for_empty_name() {
435 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
436 let eval = evaluate(&[
437 Value::String(String::new()),
438 Value::String("value".to_string()),
439 ])
440 .expect("evaluate");
441
442 assert_eq!(eval.status(), 1.0);
443 assert_eq!(eval.message(), MESSAGE_EMPTY_NAME);
444 }
445
446 #[test]
447 fn setenv_reports_failure_for_null_in_name() {
448 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
449 let eval = evaluate(&[
450 Value::String("BAD\0NAME".to_string()),
451 Value::String("value".to_string()),
452 ])
453 .expect("evaluate");
454
455 assert_eq!(eval.status(), 1.0);
456 assert_eq!(eval.message(), MESSAGE_NAME_HAS_NULL);
457 }
458
459 #[test]
460 fn setenv_reports_failure_for_null_in_value() {
461 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
462 let eval = evaluate(&[
463 Value::String("RUNMAT_NULL_VALUE".to_string()),
464 Value::String("abc\0def".to_string()),
465 ])
466 .expect("evaluate");
467
468 assert_eq!(eval.status(), 1.0);
469 assert_eq!(eval.message(), MESSAGE_VALUE_HAS_NULL);
470 }
471
472 #[test]
473 fn setenv_errors_when_name_is_not_text() {
474 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
475 let err =
476 setenv_builtin(vec![Value::Num(5.0), Value::String("value".to_string())]).unwrap_err();
477 assert_eq!(err, ERR_NAME_TYPE);
478 }
479
480 #[test]
481 fn setenv_errors_when_value_is_not_text() {
482 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
483 let err = setenv_builtin(vec![
484 Value::String("RUNMAT_INVALID_VALUE".to_string()),
485 Value::Num(1.0),
486 ])
487 .unwrap_err();
488 assert_eq!(err, ERR_VALUE_TYPE);
489 }
490
491 #[test]
492 fn setenv_accepts_scalar_string_array_arguments() {
493 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
494 let name = unique_name("STRING_ARRAY");
495 env::remove_var(&name);
496
497 let name_array =
498 StringArray::new(vec![name.clone()], vec![1]).expect("scalar string array name");
499 let value_array =
500 StringArray::new(vec!["VALUE".to_string()], vec![1]).expect("scalar string array");
501
502 let status = setenv_builtin(vec![
503 Value::StringArray(name_array),
504 Value::StringArray(value_array),
505 ])
506 .expect("setenv");
507
508 assert_eq!(status, Value::Num(0.0));
509 assert_eq!(env::var(&name).unwrap(), "VALUE");
510 env::remove_var(name);
511 }
512
513 #[test]
514 fn setenv_errors_for_string_array_with_multiple_elements() {
515 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
516 let array =
517 StringArray::new(vec!["A".to_string(), "B".to_string()], vec![2]).expect("array");
518 let err = setenv_builtin(vec![
519 Value::StringArray(array),
520 Value::String("value".to_string()),
521 ])
522 .unwrap_err();
523 assert_eq!(err, ERR_NAME_TYPE);
524 }
525
526 #[test]
527 fn setenv_errors_for_char_array_with_multiple_rows() {
528 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
529 let array = CharArray::new(vec!['R', 'M'], 2, 1).expect("two-row char array");
530 let err = setenv_builtin(vec![
531 Value::CharArray(array),
532 Value::String("value".to_string()),
533 ])
534 .unwrap_err();
535 assert_eq!(err, ERR_NAME_TYPE);
536 }
537
538 #[test]
539 fn setenv_char_array_input_trims_padding() {
540 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
541 let chars = vec!['F', 'O', 'O', ' '];
542 let array = CharArray::new(chars, 1, 4).unwrap();
543 let result = extract_scalar_text(&Value::CharArray(array), ERR_NAME_TYPE).unwrap();
544 assert_eq!(result, "FOO");
545 }
546
547 #[test]
548 fn setenv_outputs_success_message_is_empty_char_array() {
549 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
550 let name = unique_name("SUCCESS_MSG");
551 env::remove_var(&name);
552
553 let eval = evaluate(&[
554 Value::String(name.clone()),
555 Value::String("value".to_string()),
556 ])
557 .expect("evaluate");
558 let outputs = eval.outputs();
559 assert_eq!(outputs.len(), 2);
560 match &outputs[1] {
561 Value::CharArray(ca) => {
562 assert_eq!(ca.rows, 1);
563 assert_eq!(ca.cols, 0);
564 }
565 other => panic!("expected empty CharArray, got {other:?}"),
566 }
567
568 env::remove_var(name);
569 }
570
571 #[test]
572 fn setenv_outputs_return_status_and_message() {
573 let _guard = REPL_FS_TEST_LOCK.lock().unwrap();
574 let eval = evaluate(&[
575 Value::String("INVALID=NAME".to_string()),
576 Value::String("value".to_string()),
577 ])
578 .expect("evaluate");
579
580 let outputs = eval.outputs();
581 assert_eq!(outputs.len(), 2);
582 assert!(matches!(outputs[0], Value::Num(1.0)));
583 match &outputs[1] {
584 Value::CharArray(ca) => {
585 assert_eq!(ca.rows, 1);
586 let text: String = ca.data.iter().collect();
587 assert_eq!(text, MESSAGE_NAME_HAS_EQUAL);
588 }
589 other => panic!("expected CharArray message, got {other:?}"),
590 }
591 }
592
593 #[test]
594 #[cfg(feature = "doc_export")]
595 fn doc_examples_present() {
596 let blocks = crate::builtins::common::test_support::doc_examples(DOC_MD);
597 assert!(!blocks.is_empty());
598 }
599}