use std::collections::HashMap;
use std::fs;
use crate::envfile;
use crate::schema::{self, LoadOptions};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum ExportFormat {
Shell, Docker, K8s, Json, Systemd, Dotenv, }
impl ExportFormat {
pub fn from_str(s: &str) -> Option<Self> {
match s.to_lowercase().as_str() {
"shell" | "bash" | "sh" => Some(ExportFormat::Shell),
"docker" | "dockerfile" => Some(ExportFormat::Docker),
"k8s" | "kubernetes" | "configmap" => Some(ExportFormat::K8s),
"json" => Some(ExportFormat::Json),
"systemd" | "service" => Some(ExportFormat::Systemd),
"dotenv" | "env" => Some(ExportFormat::Dotenv),
_ => None,
}
}
#[allow(dead_code)]
pub fn name(&self) -> &'static str {
match self {
ExportFormat::Shell => "shell",
ExportFormat::Docker => "docker",
ExportFormat::K8s => "k8s",
ExportFormat::Json => "json",
ExportFormat::Systemd => "systemd",
ExportFormat::Dotenv => "dotenv",
}
}
}
pub fn run(
env_path: &str,
schema_path: Option<&str>,
format: &str,
output: Option<&str>,
no_cache: bool,
verify_hash: Option<&str>,
ca_cert: Option<&str>,
) -> Result<(), String> {
let export_format = ExportFormat::from_str(format)
.ok_or_else(|| format!("Unknown format '{}'. Valid formats: shell, docker, k8s, json, systemd, dotenv", format))?;
let content = fs::read_to_string(env_path)
.map_err(|e| format!("Failed to read {}: {}", env_path, e))?;
let env_map = envfile::parse_env_file(env_path)
.map_err(|e| format!("Failed to parse {}: {}", env_path, e))?;
let env_map = envfile::interpolate_env(env_map)
.map_err(|e| format!("Interpolation error: {}", e))?;
if let Some(schema_path) = schema_path {
let options = LoadOptions {
no_cache,
verify_hash: verify_hash.map(|s| s.to_string()),
ca_cert: ca_cert.map(|s| s.to_string()),
rate_limit_seconds: None,
};
let schema = schema::load_schema_with_options(schema_path, &options)
.map_err(|e| e.to_string())?;
let filtered: HashMap<String, String> = env_map
.into_iter()
.filter(|(k, _)| schema.contains_key(k))
.collect();
let result = export(&filtered, export_format, &content)?;
output_result(&result, output)
} else {
let result = export(&env_map, export_format, &content)?;
output_result(&result, output)
}
}
fn output_result(result: &str, output: Option<&str>) -> Result<(), String> {
match output {
Some(path) => {
fs::write(path, result)
.map_err(|e| format!("Failed to write {}: {}", path, e))?;
eprintln!("Exported to {}", path);
Ok(())
}
None => {
println!("{}", result);
Ok(())
}
}
}
fn export(env_map: &HashMap<String, String>, format: ExportFormat, _raw_content: &str) -> Result<String, String> {
let mut keys: Vec<&String> = env_map.keys().collect();
keys.sort();
match format {
ExportFormat::Shell => export_shell(&keys, env_map),
ExportFormat::Docker => export_docker(&keys, env_map),
ExportFormat::K8s => export_k8s(&keys, env_map),
ExportFormat::Json => export_json(&keys, env_map),
ExportFormat::Systemd => export_systemd(&keys, env_map),
ExportFormat::Dotenv => export_dotenv(&keys, env_map),
}
}
fn export_shell(keys: &[&String], env_map: &HashMap<String, String>) -> Result<String, String> {
let mut lines = Vec::new();
lines.push("#!/bin/sh".to_string());
lines.push("# Generated by zenv".to_string());
lines.push("".to_string());
for key in keys {
if let Some(value) = env_map.get(*key) {
let escaped = escape_shell_value(value);
lines.push(format!("export {}=\"{}\"", key, escaped));
}
}
Ok(lines.join("\n"))
}
fn export_docker(keys: &[&String], env_map: &HashMap<String, String>) -> Result<String, String> {
let mut lines = Vec::new();
lines.push("# Generated by zenv".to_string());
lines.push("".to_string());
for key in keys {
if let Some(value) = env_map.get(*key) {
let escaped = escape_docker_value(value);
lines.push(format!("ENV {}={}", key, escaped));
}
}
Ok(lines.join("\n"))
}
fn export_k8s(keys: &[&String], env_map: &HashMap<String, String>) -> Result<String, String> {
let mut lines = vec![
"# Generated by zenv".to_string(),
"apiVersion: v1".to_string(),
"kind: ConfigMap".to_string(),
"metadata:".to_string(),
" name: app-config".to_string(),
"data:".to_string(),
];
for key in keys {
if let Some(value) = env_map.get(*key) {
let yaml_value = escape_yaml_value(value);
lines.push(format!(" {}: {}", key, yaml_value));
}
}
Ok(lines.join("\n"))
}
fn export_json(keys: &[&String], env_map: &HashMap<String, String>) -> Result<String, String> {
let mut map = serde_json::Map::new();
for key in keys {
if let Some(value) = env_map.get(*key) {
map.insert((*key).clone(), serde_json::Value::String(value.clone()));
}
}
serde_json::to_string_pretty(&map)
.map_err(|e| format!("JSON serialization error: {}", e))
}
fn export_systemd(keys: &[&String], env_map: &HashMap<String, String>) -> Result<String, String> {
let mut lines = Vec::new();
lines.push("# Generated by zenv".to_string());
lines.push("# Add to [Service] section of your .service file".to_string());
lines.push("".to_string());
for key in keys {
if let Some(value) = env_map.get(*key) {
let escaped = escape_systemd_value(value);
lines.push(format!("Environment=\"{}={}\"", key, escaped));
}
}
Ok(lines.join("\n"))
}
fn export_dotenv(keys: &[&String], env_map: &HashMap<String, String>) -> Result<String, String> {
let mut lines = Vec::new();
lines.push("# Generated by zenv".to_string());
lines.push("".to_string());
for key in keys {
if let Some(value) = env_map.get(*key) {
if needs_quoting(value) {
let escaped = value.replace('\\', "\\\\").replace('"', "\\\"");
lines.push(format!("{}=\"{}\"", key, escaped));
} else {
lines.push(format!("{}={}", key, value));
}
}
}
Ok(lines.join("\n"))
}
fn escape_shell_value(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('$', "\\$")
.replace('`', "\\`")
.replace('\n', "\\n")
}
fn escape_docker_value(value: &str) -> String {
if value.contains(' ') || value.contains('"') || value.contains('$') {
let escaped = value
.replace('\\', "\\\\")
.replace('"', "\\\"");
format!("\"{}\"", escaped)
} else {
value.to_string()
}
}
fn escape_yaml_value(value: &str) -> String {
if value.contains(':') || value.contains('#') || value.contains('\n')
|| value.contains('"') || value.contains('\'') || value.is_empty()
|| value.starts_with(' ') || value.ends_with(' ')
|| value == "true" || value == "false" || value == "null"
|| value.parse::<f64>().is_ok()
{
let escaped = value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n");
format!("\"{}\"", escaped)
} else {
value.to_string()
}
}
fn escape_systemd_value(value: &str) -> String {
value
.replace('\\', "\\\\")
.replace('"', "\\\"")
.replace('\n', "\\n")
}
fn needs_quoting(value: &str) -> bool {
value.contains(' ')
|| value.contains('"')
|| value.contains('\'')
|| value.contains('#')
|| value.contains('\n')
|| value.contains('$')
|| value.is_empty()
}
#[cfg(test)]
mod tests {
use super::*;
fn make_env(entries: Vec<(&str, &str)>) -> HashMap<String, String> {
entries.into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
}
#[test]
fn test_export_format_from_str() {
assert_eq!(ExportFormat::from_str("shell"), Some(ExportFormat::Shell));
assert_eq!(ExportFormat::from_str("bash"), Some(ExportFormat::Shell));
assert_eq!(ExportFormat::from_str("docker"), Some(ExportFormat::Docker));
assert_eq!(ExportFormat::from_str("dockerfile"), Some(ExportFormat::Docker));
assert_eq!(ExportFormat::from_str("k8s"), Some(ExportFormat::K8s));
assert_eq!(ExportFormat::from_str("kubernetes"), Some(ExportFormat::K8s));
assert_eq!(ExportFormat::from_str("configmap"), Some(ExportFormat::K8s));
assert_eq!(ExportFormat::from_str("json"), Some(ExportFormat::Json));
assert_eq!(ExportFormat::from_str("systemd"), Some(ExportFormat::Systemd));
assert_eq!(ExportFormat::from_str("service"), Some(ExportFormat::Systemd));
assert_eq!(ExportFormat::from_str("dotenv"), Some(ExportFormat::Dotenv));
assert_eq!(ExportFormat::from_str("env"), Some(ExportFormat::Dotenv));
assert_eq!(ExportFormat::from_str("invalid"), None);
}
#[test]
fn test_export_shell() {
let env = make_env(vec![("FOO", "bar"), ("BAZ", "qux")]);
let baz = "BAZ".to_string();
let foo = "FOO".to_string();
let keys: Vec<&String> = vec![&baz, &foo];
let result = export_shell(&keys, &env).unwrap();
assert!(result.contains("#!/bin/sh"));
assert!(result.contains("export BAZ=\"qux\""));
assert!(result.contains("export FOO=\"bar\""));
}
#[test]
fn test_export_shell_escapes() {
let env = make_env(vec![("KEY", "value with \"quotes\" and $var")]);
let key = "KEY".to_string();
let keys: Vec<&String> = vec![&key];
let result = export_shell(&keys, &env).unwrap();
assert!(result.contains("\\\"quotes\\\""));
assert!(result.contains("\\$var"));
}
#[test]
fn test_export_docker() {
let env = make_env(vec![("PORT", "3000"), ("NAME", "my app")]);
let name = "NAME".to_string();
let port = "PORT".to_string();
let keys: Vec<&String> = vec![&name, &port];
let result = export_docker(&keys, &env).unwrap();
assert!(result.contains("ENV NAME=\"my app\""));
assert!(result.contains("ENV PORT=3000"));
}
#[test]
fn test_export_k8s() {
let env = make_env(vec![("DATABASE_URL", "postgres://localhost/db")]);
let db_url = "DATABASE_URL".to_string();
let keys: Vec<&String> = vec![&db_url];
let result = export_k8s(&keys, &env).unwrap();
assert!(result.contains("apiVersion: v1"));
assert!(result.contains("kind: ConfigMap"));
assert!(result.contains("DATABASE_URL:"));
}
#[test]
fn test_export_json() {
let env = make_env(vec![("FOO", "bar")]);
let foo = "FOO".to_string();
let keys: Vec<&String> = vec![&foo];
let result = export_json(&keys, &env).unwrap();
assert!(result.contains("\"FOO\": \"bar\""));
}
#[test]
fn test_export_systemd() {
let env = make_env(vec![("FOO", "bar")]);
let foo = "FOO".to_string();
let keys: Vec<&String> = vec![&foo];
let result = export_systemd(&keys, &env).unwrap();
assert!(result.contains("Environment=\"FOO=bar\""));
}
#[test]
fn test_export_dotenv() {
let env = make_env(vec![("SIMPLE", "value"), ("COMPLEX", "has spaces")]);
let complex = "COMPLEX".to_string();
let simple = "SIMPLE".to_string();
let keys: Vec<&String> = vec![&complex, &simple];
let result = export_dotenv(&keys, &env).unwrap();
assert!(result.contains("SIMPLE=value"));
assert!(result.contains("COMPLEX=\"has spaces\""));
}
#[test]
fn test_escape_yaml_special_values() {
assert_eq!(escape_yaml_value("true"), "\"true\"");
assert_eq!(escape_yaml_value("false"), "\"false\"");
assert_eq!(escape_yaml_value("null"), "\"null\"");
assert_eq!(escape_yaml_value("123"), "\"123\"");
assert_eq!(escape_yaml_value("3.14"), "\"3.14\"");
assert_eq!(escape_yaml_value("normal"), "normal");
}
}