use async_trait::async_trait;
use clap::{CommandFactory, Parser};
use crate::ast::Value;
use crate::interpreter::{ExecResult, OutputData};
use crate::tools::{schema_from_clap, ExecContext, ToolCtx, GlobalFlags, Tool, ToolArgs, ToolSchema};
pub struct Export;
#[derive(Parser, Debug)]
#[command(name = "export", about = "Mark variables for export to child processes")]
struct ExportArgs {
#[arg(short = 'p', long = "p")]
print: bool,
#[command(flatten)]
global: GlobalFlags,
names: Vec<String>,
}
#[async_trait]
impl Tool for Export {
fn name(&self) -> &str {
"export"
}
fn schema(&self) -> ToolSchema {
schema_from_clap(
&ExportArgs::command(),
"export",
"Mark variables for export to child processes",
[
("Set and export", "export MY_VAR=value"),
("Export existing variable", "export PATH"),
("List exports", "export -p"),
],
)
}
async fn execute(&self, args: ToolArgs, ctx: &mut dyn ToolCtx) -> ExecResult {
let Some(ctx) = ctx.as_any_mut().downcast_mut::<ExecContext>() else {
return ExecResult::failure(1, "internal error: kernel builtin requires ExecContext");
};
let mut clap_argv: Vec<String> = Vec::new();
let mut sorted_flags: Vec<&String> = args.flags.iter().collect();
sorted_flags.sort();
for flag in sorted_flags {
clap_argv.push(if flag.chars().count() == 1 {
format!("-{flag}")
} else {
format!("--{flag}")
});
}
let parsed = match ExportArgs::try_parse_from(
std::iter::once("export".to_string()).chain(clap_argv),
) {
Ok(p) => p,
Err(e) => return ExecResult::failure(2, format!("export: {e}")),
};
parsed.global.apply(ctx);
if parsed.print {
return print_exports(ctx);
}
if args.positional.is_empty() && args.named.is_empty() {
return print_exports(ctx);
}
for (name, value) in &args.named {
if !is_valid_name(name) {
return ExecResult::failure(
1,
format!("export: `{}': not a valid identifier", name),
);
}
ctx.scope.set_exported(name, value.clone());
}
for arg in &args.positional {
let arg_str = match arg {
Value::String(s) => s.as_str(),
_ => continue,
};
if let Some(eq_pos) = arg_str.find('=') {
let name = &arg_str[..eq_pos];
let value = &arg_str[eq_pos + 1..];
if !is_valid_name(name) {
return ExecResult::failure(
1,
format!("export: `{}': not a valid identifier", name),
);
}
ctx.scope.set_exported(name, Value::String(value.to_string()));
} else {
if !is_valid_name(arg_str) {
return ExecResult::failure(
1,
format!("export: `{}': not a valid identifier", arg_str),
);
}
ctx.scope.export(arg_str);
}
}
ExecResult::success("")
}
}
fn print_exports(ctx: &ExecContext) -> ExecResult {
let mut output = String::new();
for (name, value) in ctx.scope.exported_vars() {
let value_str = format_value(&value);
output.push_str(&format!("declare -x {}={}\n", name, value_str));
}
for name in ctx.scope.exported_names() {
if ctx.scope.get(name).is_none() {
output.push_str(&format!("declare -x {}\n", name));
}
}
ExecResult::with_output(OutputData::text(output.trim_end()))
}
fn format_value(value: &Value) -> String {
match value {
Value::Null => "".to_string(),
Value::Bool(b) => b.to_string(),
Value::Int(i) => i.to_string(),
Value::Float(f) => f.to_string(),
Value::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
Value::Json(json) => format!("'{}'", json.to_string().replace('\'', "'\\''")),
Value::Blob(blob) => format!("[blob: {} {}]", blob.formatted_size(), blob.content_type),
}
}
fn is_valid_name(name: &str) -> bool {
if name.is_empty() {
return false;
}
let mut chars = name.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
#[cfg(test)]
mod tests {
use super::*;
use crate::vfs::{MemoryFs, VfsRouter};
use std::sync::Arc;
fn make_ctx() -> ExecContext {
let mut vfs = VfsRouter::new();
vfs.mount("/", MemoryFs::new());
ExecContext::new(Arc::new(vfs))
}
#[tokio::test]
async fn test_export_marks_variable() {
let mut ctx = make_ctx();
ctx.scope.set("X", Value::Int(42));
let mut args = ToolArgs::new();
args.positional.push(Value::String("X".into()));
let result = Export.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.scope.is_exported("X"));
}
#[tokio::test]
async fn test_export_with_value() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional
.push(Value::String("MY_VAR=hello world".into()));
let result = Export.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.scope.is_exported("MY_VAR"));
assert_eq!(
ctx.scope.get("MY_VAR"),
Some(&Value::String("hello world".into()))
);
}
#[tokio::test]
async fn test_export_multiple() {
let mut ctx = make_ctx();
ctx.scope.set("A", Value::Int(1));
ctx.scope.set("B", Value::Int(2));
let mut args = ToolArgs::new();
args.positional.push(Value::String("A".into()));
args.positional.push(Value::String("B".into()));
let result = Export.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.scope.is_exported("A"));
assert!(ctx.scope.is_exported("B"));
}
#[tokio::test]
async fn test_export_p_prints_exports() {
let mut ctx = make_ctx();
ctx.scope.set_exported("PATH", Value::String("/usr/bin".into()));
ctx.scope.set_exported("HOME", Value::String("/home/user".into()));
let mut args = ToolArgs::new();
args.flags.insert("p".to_string());
let result = Export.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.text_out().contains("declare -x HOME="));
assert!(result.text_out().contains("declare -x PATH="));
}
#[tokio::test]
async fn test_export_no_args_prints_exports() {
let mut ctx = make_ctx();
ctx.scope.set_exported("VAR", Value::String("value".into()));
let args = ToolArgs::new();
let result = Export.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(result.text_out().contains("declare -x VAR="));
}
#[tokio::test]
async fn test_export_invalid_name() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String("123invalid".into()));
let result = Export.execute(args, &mut ctx).await;
assert!(!result.ok());
assert!(result.err.contains("not a valid identifier"));
}
#[tokio::test]
async fn test_export_empty_value() {
let mut ctx = make_ctx();
let mut args = ToolArgs::new();
args.positional.push(Value::String("EMPTY=".into()));
let result = Export.execute(args, &mut ctx).await;
assert!(result.ok());
assert!(ctx.scope.is_exported("EMPTY"));
assert_eq!(ctx.scope.get("EMPTY"), Some(&Value::String("".into())));
}
}