use std::path::Path;
use crate::cli;
use crate::error::{Error, Result};
use crate::store::queries;
pub fn run(
cwd: &Path,
version: Option<&str>,
filter: Option<&str>,
shell: &str,
key_file: Option<&str>,
) -> Result<()> {
let conn = cli::require_store()?;
let aes_key = cli::load_encryption_key(&conn, key_file)?;
let (project_path, git_ctx) = cli::resolve_project(cwd)?;
let current_branch = git_ctx.as_ref().map(|c| c.branch.as_str());
let entries = if let Some(v) = version {
let save = cli::resolve_version(&conn, &project_path, current_branch, v)?;
cli::load_entries(&conn, &save, aes_key.as_deref())?
} else {
let branch = current_branch.unwrap_or("");
let saves = queries::list_saves(&conn, &project_path, Some(branch), None, 1, None)?;
let save = saves
.first()
.ok_or_else(|| Error::SaveNotFound("no saves on current branch".to_string()))?;
cli::load_entries(&conn, save, aes_key.as_deref())?
};
let filtered: Vec<_> = entries
.iter()
.filter(|e| filter.is_none_or(|f| cli::matches_filter(&e.key, f)))
.collect();
match shell {
"bash" => {
for entry in &filtered {
if !is_valid_env_key(&entry.key) {
eprintln!("warning: skipping invalid variable name: {}", entry.key);
continue;
}
println!("export {}='{}'", entry.key, shell_escape_bash(&entry.value));
}
}
"fish" => {
for entry in &filtered {
if !is_valid_env_key(&entry.key) {
eprintln!("warning: skipping invalid variable name: {}", entry.key);
continue;
}
println!("set -x {} '{}'", entry.key, shell_escape_fish(&entry.value));
}
}
"json" => {
let map: serde_json::Map<String, serde_json::Value> = filtered
.iter()
.map(|e| (e.key.clone(), serde_json::Value::String(e.value.clone())))
.collect();
println!("{}", serde_json::to_string_pretty(&map)?);
}
other => {
return Err(Error::Other(format!("Unknown shell format: {other}")));
}
}
Ok(())
}
fn is_valid_env_key(key: &str) -> bool {
let mut chars = key.chars();
match chars.next() {
Some(c) if c.is_ascii_alphabetic() || c == '_' => {}
_ => return false,
}
chars.all(|c| c.is_ascii_alphanumeric() || c == '_')
}
fn shell_escape_bash(value: &str) -> String {
value.replace('\'', "'\\''")
}
fn shell_escape_fish(value: &str) -> String {
value.replace('\\', "\\\\").replace('\'', "\\'")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn valid_env_key() {
assert!(is_valid_env_key("DB_HOST"));
assert!(is_valid_env_key("_PRIVATE"));
assert!(is_valid_env_key("A"));
assert!(is_valid_env_key("a1_B2"));
}
#[test]
fn invalid_env_key() {
assert!(!is_valid_env_key(""));
assert!(!is_valid_env_key("1STARTS_WITH_DIGIT"));
assert!(!is_valid_env_key("FOO;rm -rf /;X"));
assert!(!is_valid_env_key("KEY WITH SPACES"));
assert!(!is_valid_env_key("KEY=VAL"));
assert!(!is_valid_env_key("$(cmd)"));
}
#[test]
fn bash_escape_simple() {
assert_eq!(shell_escape_bash("hello"), "hello");
}
#[test]
fn bash_escape_single_quote() {
assert_eq!(shell_escape_bash("it's"), "it'\\''s");
}
#[test]
fn bash_escape_double_quote() {
assert_eq!(shell_escape_bash("say \"hi\""), "say \"hi\"");
}
#[test]
fn bash_escape_dollar_subst() {
assert_eq!(shell_escape_bash("$(rm -rf /)"), "$(rm -rf /)");
}
#[test]
fn bash_escape_backtick() {
assert_eq!(shell_escape_bash("`whoami`"), "`whoami`");
}
#[test]
fn bash_escape_spaces() {
assert_eq!(shell_escape_bash("hello world"), "hello world");
}
#[test]
fn bash_escape_newline() {
assert_eq!(shell_escape_bash("line1\nline2"), "line1\nline2");
}
#[test]
fn bash_escape_combined() {
let input = "it's $(dangerous) `stuff`";
let escaped = shell_escape_bash(input);
assert_eq!(escaped, "it'\\''s $(dangerous) `stuff`");
}
#[test]
fn fish_escape_simple() {
assert_eq!(shell_escape_fish("hello"), "hello");
}
#[test]
fn fish_escape_single_quote() {
assert_eq!(shell_escape_fish("it's"), "it\\'s");
}
#[test]
fn fish_escape_backslash() {
assert_eq!(shell_escape_fish("path\\to\\file"), "path\\\\to\\\\file");
}
#[test]
fn fish_escape_dollar_subst() {
assert_eq!(shell_escape_fish("$(rm -rf /)"), "$(rm -rf /)");
}
#[test]
fn fish_escape_backtick() {
assert_eq!(shell_escape_fish("`whoami`"), "`whoami`");
}
#[test]
fn fish_escape_spaces() {
assert_eq!(shell_escape_fish("hello world"), "hello world");
}
#[test]
fn fish_escape_newline() {
assert_eq!(shell_escape_fish("line1\nline2"), "line1\nline2");
}
#[test]
fn fish_escape_combined() {
let input = "it's a \\path";
let escaped = shell_escape_fish(input);
assert_eq!(escaped, "it\\'s a \\\\path");
}
#[test]
fn bash_output_format() {
let key = "DB_HOST";
let value = "local'host";
let line = format!("export {}='{}'", key, shell_escape_bash(value));
assert_eq!(line, "export DB_HOST='local'\\''host'");
}
#[test]
fn fish_output_format() {
let key = "DB_HOST";
let value = "local'host";
let line = format!("set -x {} '{}'", key, shell_escape_fish(value));
assert_eq!(line, "set -x DB_HOST 'local\\'host'");
}
}