envx_core/
exporter.rs

1use crate::EnvVar;
2use color_eyre::Result;
3use std::fs;
4use std::path::Path;
5
6#[derive(Debug, Clone, Copy)]
7pub enum ExportFormat {
8    DotEnv,
9    Json,
10    Yaml,
11    Text,
12    PowerShell,
13    Shell,
14}
15
16impl ExportFormat {
17    /// Determines the export format from a file path's extension.
18    ///
19    /// # Errors
20    ///
21    /// Currently this function never returns an error and always succeeds,
22    /// defaulting to `Text` format for unknown extensions.
23    pub fn from_extension(path: &str) -> Result<Self> {
24        let ext = Path::new(path).extension().and_then(|s| s.to_str()).unwrap_or("");
25
26        match ext.to_lowercase().as_str() {
27            "env" => Ok(Self::DotEnv),
28            "json" => Ok(Self::Json),
29            "yaml" | "yml" => Ok(Self::Yaml),
30            "txt" | "text" => Ok(Self::Text),
31            "ps1" => Ok(Self::PowerShell),
32            "sh" | "bash" => Ok(Self::Shell),
33            _ => {
34                // Check if filename is .env or similar
35                let filename = Path::new(path).file_name().and_then(|s| s.to_str()).unwrap_or("");
36
37                if filename.starts_with('.') && filename.contains("env") {
38                    Ok(Self::DotEnv)
39                } else {
40                    Ok(Self::Text) // Default to text format
41                }
42            }
43        }
44    }
45}
46
47pub struct Exporter {
48    variables: Vec<EnvVar>,
49    include_metadata: bool,
50}
51
52impl Exporter {
53    #[must_use]
54    pub const fn new(variables: Vec<EnvVar>, include_metadata: bool) -> Self {
55        Self {
56            variables,
57            include_metadata,
58        }
59    }
60
61    #[must_use]
62    pub fn count(&self) -> usize {
63        self.variables.len()
64    }
65
66    /// Exports environment variables to a file in the specified format.
67    ///
68    /// # Errors
69    ///
70    /// Returns an error if:
71    /// - The file cannot be created or written to due to filesystem permissions or disk space issues
72    /// - JSON serialization fails when using JSON format
73    /// - YAML formatting fails when using YAML format
74    pub fn export_to_file(&self, path: &str, format: ExportFormat) -> Result<()> {
75        let content = match format {
76            ExportFormat::DotEnv => self.to_dotenv(),
77            ExportFormat::Json => self.to_json()?,
78            ExportFormat::Yaml => self.to_yaml(),
79            ExportFormat::Text => self.to_text(),
80            ExportFormat::PowerShell => self.to_powershell(),
81            ExportFormat::Shell => self.to_shell(),
82        };
83
84        fs::write(path, content)?;
85        Ok(())
86    }
87
88    fn to_dotenv(&self) -> String {
89        let mut lines = Vec::new();
90
91        if self.include_metadata {
92            lines.push("# Environment variables exported by envx".to_string());
93            lines.push(format!(
94                "# Date: {}",
95                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
96            ));
97            lines.push(format!("# Count: {}", self.variables.len()));
98            lines.push(String::new());
99        }
100
101        for var in &self.variables {
102            if self.include_metadata {
103                lines.push(format!(
104                    "# Source: {:?}, Modified: {}",
105                    var.source,
106                    var.modified.format("%Y-%m-%d %H:%M:%S")
107                ));
108            }
109
110            // For .env format, we need to handle escaping more carefully
111            // Only escape actual escape sequences, not all backslashes
112            let needs_quotes = var.value.contains(' ')
113                || var.value.contains('=')
114                || var.value.contains('#')
115                || var.value.contains('"')
116                || var.value.contains('\'')
117                || var.value.contains('\n')
118                || var.value.contains('\r')
119                || var.value.contains('\t');
120
121            if needs_quotes {
122                // In quoted strings, only escape quotes and actual escape sequences
123                let escaped_value = var
124                    .value
125                    .replace('"', "\\\"") // Escape quotes
126                    .replace('\n', "\\n") // Escape newlines
127                    .replace('\r', "\\r") // Escape carriage returns
128                    .replace('\t', "\\t"); // Escape tabs
129                // Don't escape backslashes in paths!
130
131                lines.push(format!("{}=\"{}\"", var.name, escaped_value));
132            } else {
133                // For unquoted values, we might need different escaping
134                // But for simple values, just use as-is
135                lines.push(format!("{}={}", var.name, var.value));
136            }
137        }
138
139        lines.join("\n")
140    }
141
142    fn to_json(&self) -> Result<String> {
143        if self.include_metadata {
144            // Export with full metadata
145            let export_data = serde_json::json!({
146                "exported_at": chrono::Utc::now(),
147                "count": self.variables.len(),
148                "variables": self.variables
149            });
150            Ok(serde_json::to_string_pretty(&export_data)?)
151        } else {
152            // Export as simple key-value pairs
153            let mut map = serde_json::Map::new();
154            for var in &self.variables {
155                map.insert(var.name.clone(), serde_json::Value::String(var.value.clone()));
156            }
157            Ok(serde_json::to_string_pretty(&map)?)
158        }
159    }
160
161    fn to_yaml(&self) -> String {
162        let mut lines = Vec::new();
163
164        if self.include_metadata {
165            lines.push("# Environment variables exported by envx".to_string());
166            lines.push(format!(
167                "# Date: {}",
168                chrono::Utc::now().format("%Y-%m-%d %H:%M:%S UTC")
169            ));
170            lines.push("---".to_string());
171        }
172
173        for var in &self.variables {
174            if self.include_metadata {
175                lines.push(format!("# Source: {:?}", var.source));
176            }
177
178            // For YAML, we need to quote values that contain special YAML characters
179            // but we should NOT escape backslashes in paths
180            let value = if var.value.contains(':')
181                || var.value.contains('#')
182                || var.value.contains('"')
183                || var.value.contains('\'')
184                || var.value.contains('\n')
185                || var.value.contains('\r')
186                || var.value.contains('\t')
187                || var.value.starts_with(' ')
188                || var.value.ends_with(' ')
189                || var.value.starts_with('-')
190                || var.value.starts_with('*')
191                || var.value.starts_with('&')
192                || var.value.starts_with('!')
193                || var.value.starts_with('[')
194                || var.value.starts_with('{')
195                || var.value.starts_with('>')
196                || var.value.starts_with('|')
197            {
198                // In YAML quoted strings, only escape quotes and control characters
199                let escaped = var
200                    .value
201                    .replace('"', "\\\"") // Escape quotes
202                    .replace('\n', "\\n") // Escape newlines
203                    .replace('\r', "\\r") // Escape carriage returns
204                    .replace('\t', "\\t"); // Escape tabs
205                // Don't escape backslashes!
206
207                format!("\"{escaped}\"")
208            } else {
209                var.value.clone()
210            };
211
212            lines.push(format!("{}: {}", var.name, value));
213        }
214
215        lines.join("\n")
216    }
217
218    fn to_text(&self) -> String {
219        let mut lines = Vec::new();
220
221        if self.include_metadata {
222            lines.push("# Environment Variables Export".to_string());
223            lines.push(format!("# Generated: {}", chrono::Utc::now()));
224            lines.push(format!("# Total: {} variables", self.variables.len()));
225            lines.push("#".repeat(50));
226            lines.push(String::new());
227        }
228
229        for var in &self.variables {
230            if self.include_metadata {
231                lines.push(format!("# Name: {}", var.name));
232                lines.push(format!("# Source: {:?}", var.source));
233                lines.push(format!("# Modified: {}", var.modified));
234            }
235            lines.push(format!("{}={}", var.name, var.value));
236            if self.include_metadata {
237                lines.push(String::new());
238            }
239        }
240
241        lines.join("\n")
242    }
243
244    fn to_powershell(&self) -> String {
245        let mut lines = Vec::new();
246
247        lines.push("# PowerShell Environment Variables Script".to_string());
248        lines.push(format!("# Generated by envx - {}", chrono::Utc::now()));
249        lines.push(String::new());
250
251        for var in &self.variables {
252            if self.include_metadata {
253                lines.push(format!("# {} ({:?})", var.name, var.source));
254            }
255
256            // Escape PowerShell special characters
257            let escaped_value = var.value.replace('`', "``").replace('"', "`\"");
258            lines.push(format!("$env:{} = \"{}\"", var.name, escaped_value));
259        }
260
261        lines.join("\n")
262    }
263
264    fn to_shell(&self) -> String {
265        let mut lines = Vec::new();
266
267        lines.push("#!/bin/bash".to_string());
268        lines.push("# Shell Environment Variables Script".to_string());
269        lines.push(format!("# Generated by envx - {}", chrono::Utc::now()));
270        lines.push(String::new());
271
272        for var in &self.variables {
273            if self.include_metadata {
274                lines.push(format!("# {} ({:?})", var.name, var.source));
275            }
276
277            // Escape shell special characters
278            let escaped_value = var
279                .value
280                .replace('\\', "\\\\")
281                .replace('"', "\\\"")
282                .replace('$', "\\$")
283                .replace('`', "\\`");
284
285            lines.push(format!("export {}=\"{}\"", var.name, escaped_value));
286        }
287
288        lines.join("\n")
289    }
290}
291
292// ...existing code...
293
294#[cfg(test)]
295mod tests {
296    #![allow(clippy::cognitive_complexity)]
297    use super::*;
298    use crate::EnvVar;
299    use crate::EnvVarSource as VarSource;
300    use chrono::{DateTime, Utc};
301    use std::fs;
302    use tempfile::NamedTempFile;
303
304    // Helper function to create test environment variables
305    fn create_test_vars() -> Vec<EnvVar> {
306        vec![
307            EnvVar {
308                name: "SIMPLE_VAR".to_string(),
309                value: "simple_value".to_string(),
310                source: VarSource::User,
311                modified: Utc::now(),
312                original_value: None,
313            },
314            EnvVar {
315                name: "PATH_VAR".to_string(),
316                value: "C:\\Program Files\\App;C:\\Windows\\System32".to_string(),
317                source: VarSource::System,
318                modified: Utc::now(),
319                original_value: None,
320            },
321            EnvVar {
322                name: "QUOTED_VAR".to_string(),
323                value: "value with \"quotes\" and 'single quotes'".to_string(),
324                source: VarSource::User,
325                modified: Utc::now(),
326                original_value: None,
327            },
328            EnvVar {
329                name: "SPECIAL_CHARS".to_string(),
330                value: "line1\nline2\ttab\\backslash".to_string(),
331                source: VarSource::Process,
332                modified: Utc::now(),
333                original_value: None,
334            },
335            EnvVar {
336                name: "EMPTY_VAR".to_string(),
337                value: String::new(),
338                source: VarSource::User,
339                modified: Utc::now(),
340                original_value: None,
341            },
342            EnvVar {
343                name: "UNICODE_VAR".to_string(),
344                value: "Hello δΈ–η•Œ 🌍".to_string(),
345                source: VarSource::User,
346                modified: Utc::now(),
347                original_value: None,
348            },
349        ]
350    }
351
352    #[test]
353    fn test_export_format_from_extension() {
354        assert!(matches!(
355            ExportFormat::from_extension("file.env").unwrap(),
356            ExportFormat::DotEnv
357        ));
358        assert!(matches!(
359            ExportFormat::from_extension("file.ENV").unwrap(),
360            ExportFormat::DotEnv
361        ));
362        assert!(matches!(
363            ExportFormat::from_extension("file.json").unwrap(),
364            ExportFormat::Json
365        ));
366        assert!(matches!(
367            ExportFormat::from_extension("file.JSON").unwrap(),
368            ExportFormat::Json
369        ));
370        assert!(matches!(
371            ExportFormat::from_extension("file.yaml").unwrap(),
372            ExportFormat::Yaml
373        ));
374        assert!(matches!(
375            ExportFormat::from_extension("file.yml").unwrap(),
376            ExportFormat::Yaml
377        ));
378        assert!(matches!(
379            ExportFormat::from_extension("file.txt").unwrap(),
380            ExportFormat::Text
381        ));
382        assert!(matches!(
383            ExportFormat::from_extension("file.text").unwrap(),
384            ExportFormat::Text
385        ));
386        assert!(matches!(
387            ExportFormat::from_extension("file.ps1").unwrap(),
388            ExportFormat::PowerShell
389        ));
390        assert!(matches!(
391            ExportFormat::from_extension("file.sh").unwrap(),
392            ExportFormat::Shell
393        ));
394        assert!(matches!(
395            ExportFormat::from_extension("file.bash").unwrap(),
396            ExportFormat::Shell
397        ));
398
399        // Special case for .env files
400        assert!(matches!(
401            ExportFormat::from_extension(".env").unwrap(),
402            ExportFormat::DotEnv
403        ));
404        assert!(matches!(
405            ExportFormat::from_extension(".env.local").unwrap(),
406            ExportFormat::DotEnv
407        ));
408        assert!(matches!(
409            ExportFormat::from_extension(".env.production").unwrap(),
410            ExportFormat::DotEnv
411        ));
412
413        // Default to Text for unknown extensions
414        assert!(matches!(
415            ExportFormat::from_extension("file.xyz").unwrap(),
416            ExportFormat::Text
417        ));
418        assert!(matches!(
419            ExportFormat::from_extension("file").unwrap(),
420            ExportFormat::Text
421        ));
422    }
423
424    #[test]
425    fn test_exporter_new() {
426        let vars = create_test_vars();
427        let exporter = Exporter::new(vars.clone(), true);
428
429        assert_eq!(exporter.count(), vars.len());
430    }
431
432    #[test]
433    fn test_to_dotenv_without_metadata() {
434        let vars = create_test_vars();
435        let exporter = Exporter::new(vars, false);
436
437        let output = exporter.to_dotenv();
438
439        // Verify basic variables
440        assert!(output.contains("SIMPLE_VAR=simple_value"));
441
442        // Verify quoted values (contains spaces or special chars)
443        // Windows paths should NOT have escaped backslashes in quoted strings
444        assert!(output.contains("PATH_VAR=\"C:\\Program Files\\App;C:\\Windows\\System32\""));
445        assert!(output.contains("QUOTED_VAR=\"value with \\\"quotes\\\" and 'single quotes'\""));
446
447        // Verify escaped characters - only actual control characters are escaped
448        assert!(output.contains("SPECIAL_CHARS=\"line1\\nline2\\ttab\\backslash\""));
449
450        // Verify empty value
451        assert!(output.contains("EMPTY_VAR="));
452
453        // Verify no metadata comments
454        assert!(!output.contains("# Environment variables exported by envx"));
455        assert!(!output.contains("# Source:"));
456    }
457
458    #[test]
459    fn test_to_dotenv_with_metadata() {
460        let vars = create_test_vars();
461        let exporter = Exporter::new(vars, true);
462
463        let output = exporter.to_dotenv();
464
465        // Verify metadata is included
466        assert!(output.contains("# Environment variables exported by envx"));
467        assert!(output.contains("# Date:"));
468        assert!(output.contains("# Count: 6"));
469        assert!(output.contains("# Source:"));
470        assert!(output.contains("Modified:"));
471    }
472
473    #[test]
474    fn test_to_dotenv_edge_cases() {
475        let vars = vec![
476            EnvVar {
477                name: "HASH_VALUE".to_string(),
478                value: "value#with#hashes".to_string(),
479                source: VarSource::User,
480                modified: Utc::now(),
481                original_value: None,
482            },
483            EnvVar {
484                name: "EQUALS_VALUE".to_string(),
485                value: "key=value=pairs".to_string(),
486                source: VarSource::User,
487                modified: Utc::now(),
488                original_value: None,
489            },
490            EnvVar {
491                name: "SPACES_AROUND".to_string(),
492                value: "  spaces at start and end  ".to_string(),
493                source: VarSource::User,
494                modified: Utc::now(),
495                original_value: None,
496            },
497        ];
498
499        let exporter = Exporter::new(vars, false);
500        let output = exporter.to_dotenv();
501
502        // Values with # or = should be quoted
503        assert!(output.contains("HASH_VALUE=\"value#with#hashes\""));
504        assert!(output.contains("EQUALS_VALUE=\"key=value=pairs\""));
505        assert!(output.contains("SPACES_AROUND=\"  spaces at start and end  \""));
506    }
507
508    #[test]
509    fn test_to_json_without_metadata() {
510        let vars = create_test_vars();
511        let exporter = Exporter::new(vars, false);
512
513        let output = exporter.to_json().unwrap();
514        let json: serde_json::Value = serde_json::from_str(&output).unwrap();
515
516        // Should be a simple object with key-value pairs
517        assert!(json.is_object());
518        assert_eq!(json["SIMPLE_VAR"], "simple_value");
519        assert_eq!(json["PATH_VAR"], "C:\\Program Files\\App;C:\\Windows\\System32");
520        assert_eq!(json["QUOTED_VAR"], "value with \"quotes\" and 'single quotes'");
521        assert_eq!(json["SPECIAL_CHARS"], "line1\nline2\ttab\\backslash");
522        assert_eq!(json["EMPTY_VAR"], "");
523        assert_eq!(json["UNICODE_VAR"], "Hello δΈ–η•Œ 🌍");
524    }
525
526    #[test]
527    fn test_to_json_with_metadata() {
528        let vars = create_test_vars();
529        let exporter = Exporter::new(vars, true);
530
531        let output = exporter.to_json().unwrap();
532        let json: serde_json::Value = serde_json::from_str(&output).unwrap();
533
534        // Should have metadata structure
535        assert!(json.is_object());
536        assert!(json["exported_at"].is_string());
537        assert_eq!(json["count"], 6);
538        assert!(json["variables"].is_array());
539
540        let variables = json["variables"].as_array().unwrap();
541        assert_eq!(variables.len(), 6);
542
543        // Check first variable has all fields
544        let first_var = &variables[0];
545        assert!(first_var["name"].is_string());
546        assert!(first_var["value"].is_string());
547        assert!(first_var["source"].is_string());
548        assert!(first_var["modified"].is_string());
549    }
550
551    #[test]
552    fn test_to_yaml_without_metadata() {
553        let vars = create_test_vars();
554        let exporter = Exporter::new(vars, false);
555
556        let output = exporter.to_yaml();
557
558        // Verify basic YAML format
559        assert!(output.contains("SIMPLE_VAR: simple_value"));
560        assert!(output.contains("EMPTY_VAR: "));
561
562        // Values with colons should be quoted
563        assert!(output.contains("PATH_VAR: \"C:\\Program Files\\App;C:\\Windows\\System32\""));
564
565        // Values with quotes should be escaped and quoted
566        assert!(output.contains("QUOTED_VAR: \"value with \\\"quotes\\\" and 'single quotes'\""));
567
568        // No metadata
569        assert!(!output.contains("# Environment variables exported by envx"));
570    }
571
572    #[test]
573    fn test_to_yaml_with_metadata() {
574        let vars = create_test_vars();
575        let exporter = Exporter::new(vars, true);
576
577        let output = exporter.to_yaml();
578
579        // Verify metadata
580        assert!(output.contains("# Environment variables exported by envx"));
581        assert!(output.contains("# Date:"));
582        assert!(output.contains("---"));
583        assert!(output.contains("# Source:"));
584    }
585
586    #[test]
587    fn test_to_yaml_special_cases() {
588        let vars = vec![
589            EnvVar {
590                name: "URL".to_string(),
591                value: "https://example.com:8080/path".to_string(),
592                source: VarSource::User,
593                modified: Utc::now(),
594                original_value: None,
595            },
596            EnvVar {
597                name: "COMMENT".to_string(),
598                value: "value # with comment".to_string(),
599                source: VarSource::User,
600                modified: Utc::now(),
601                original_value: None,
602            },
603            EnvVar {
604                name: "LEADING_SPACE".to_string(),
605                value: "  value".to_string(),
606                source: VarSource::User,
607                modified: Utc::now(),
608                original_value: None,
609            },
610            EnvVar {
611                name: "TRAILING_SPACE".to_string(),
612                value: "value  ".to_string(),
613                source: VarSource::User,
614                modified: Utc::now(),
615                original_value: None,
616            },
617        ];
618
619        let exporter = Exporter::new(vars, false);
620        let output = exporter.to_yaml();
621
622        // All these should be quoted due to special characters
623        assert!(output.contains("URL: \"https://example.com:8080/path\""));
624        assert!(output.contains("COMMENT: \"value # with comment\""));
625        assert!(output.contains("LEADING_SPACE: \"  value\""));
626        assert!(output.contains("TRAILING_SPACE: \"value  \""));
627    }
628
629    #[test]
630    fn test_to_text() {
631        let vars = create_test_vars();
632
633        // Without metadata
634        let exporter = Exporter::new(vars.clone(), false);
635        let output = exporter.to_text();
636
637        assert!(output.contains("SIMPLE_VAR=simple_value"));
638        assert!(output.contains("PATH_VAR=C:\\Program Files\\App;C:\\Windows\\System32"));
639        assert!(!output.contains("# Environment Variables Export"));
640
641        // With metadata
642        let exporter = Exporter::new(vars, true);
643        let output = exporter.to_text();
644
645        assert!(output.contains("# Environment Variables Export"));
646        assert!(output.contains("# Generated:"));
647        assert!(output.contains("# Total: 6 variables"));
648        assert!(output.contains("# Name: SIMPLE_VAR"));
649        assert!(output.contains("# Source:"));
650        assert!(output.contains("# Modified:"));
651    }
652
653    #[test]
654    fn test_to_powershell() {
655        let vars = create_test_vars();
656        let exporter = Exporter::new(vars, false);
657
658        let output = exporter.to_powershell();
659
660        // Verify PowerShell header
661        assert!(output.contains("# PowerShell Environment Variables Script"));
662        assert!(output.contains("# Generated by envx"));
663
664        // Verify PowerShell format
665        assert!(output.contains("$env:SIMPLE_VAR = \"simple_value\""));
666        assert!(output.contains("$env:PATH_VAR = \"C:\\Program Files\\App;C:\\Windows\\System32\""));
667
668        // Verify escaped characters
669        assert!(output.contains("$env:QUOTED_VAR = \"value with `\"quotes`\" and 'single quotes'\""));
670        assert!(output.contains("$env:SPECIAL_CHARS = \"line1\nline2\ttab\\backslash\""));
671    }
672
673    #[test]
674    fn test_to_powershell_escaping() {
675        let vars = vec![
676            EnvVar {
677                name: "BACKTICK".to_string(),
678                value: "value`with`backticks".to_string(),
679                source: VarSource::User,
680                modified: Utc::now(),
681                original_value: None,
682            },
683            EnvVar {
684                name: "DOLLAR".to_string(),
685                value: "$variable $test".to_string(),
686                source: VarSource::User,
687                modified: Utc::now(),
688                original_value: None,
689            },
690        ];
691
692        let exporter = Exporter::new(vars, false);
693        let output = exporter.to_powershell();
694
695        // Backticks should be escaped
696        assert!(output.contains("$env:BACKTICK = \"value``with``backticks\""));
697        // Dollar signs are not escaped in PowerShell strings with double quotes
698        assert!(output.contains("$env:DOLLAR = \"$variable $test\""));
699    }
700
701    #[test]
702    fn test_to_shell() {
703        let vars = create_test_vars();
704        let exporter = Exporter::new(vars, false);
705
706        let output = exporter.to_shell();
707
708        // Verify shell header
709        assert!(output.contains("#!/bin/bash"));
710        assert!(output.contains("# Shell Environment Variables Script"));
711        assert!(output.contains("# Generated by envx"));
712
713        // Verify shell format
714        assert!(output.contains("export SIMPLE_VAR=\"simple_value\""));
715        assert!(output.contains("export PATH_VAR=\"C:\\\\Program Files\\\\App;C:\\\\Windows\\\\System32\""));
716
717        // Verify escaped characters
718        assert!(output.contains("export QUOTED_VAR=\"value with \\\"quotes\\\" and 'single quotes'\""));
719        assert!(output.contains("export SPECIAL_CHARS=\"line1\nline2\ttab\\\\backslash\""));
720    }
721
722    #[test]
723    fn test_to_shell_escaping() {
724        let vars = vec![
725            EnvVar {
726                name: "DOLLAR".to_string(),
727                value: "$HOME/path".to_string(),
728                source: VarSource::User,
729                modified: Utc::now(),
730                original_value: None,
731            },
732            EnvVar {
733                name: "BACKTICK".to_string(),
734                value: "`command`".to_string(),
735                source: VarSource::User,
736                modified: Utc::now(),
737                original_value: None,
738            },
739            EnvVar {
740                name: "BACKSLASH".to_string(),
741                value: "path\\to\\file".to_string(),
742                source: VarSource::User,
743                modified: Utc::now(),
744                original_value: None,
745            },
746        ];
747
748        let exporter = Exporter::new(vars, false);
749        let output = exporter.to_shell();
750
751        // Shell special characters should be escaped
752        assert!(output.contains("export DOLLAR=\"\\$HOME/path\""));
753        assert!(output.contains("export BACKTICK=\"\\`command\\`\""));
754        assert!(output.contains("export BACKSLASH=\"path\\\\to\\\\file\""));
755    }
756
757    #[test]
758    fn test_export_to_file() {
759        let vars = create_test_vars();
760        let exporter = Exporter::new(vars, false);
761
762        // Test exporting to different formats
763        let formats = vec![
764            (ExportFormat::DotEnv, ".env"),
765            (ExportFormat::Json, ".json"),
766            (ExportFormat::Yaml, ".yaml"),
767            (ExportFormat::Text, ".txt"),
768            (ExportFormat::PowerShell, ".ps1"),
769            (ExportFormat::Shell, ".sh"),
770        ];
771
772        for (format, ext) in formats {
773            let temp_file = NamedTempFile::with_suffix(ext).unwrap();
774            let path = temp_file.path().to_str().unwrap();
775
776            exporter.export_to_file(path, format).unwrap();
777
778            // Verify file was created and has content
779            let content = fs::read_to_string(path).unwrap();
780            assert!(!content.is_empty());
781            assert!(content.contains("SIMPLE_VAR"));
782        }
783    }
784
785    #[test]
786    fn test_empty_export() {
787        let exporter = Exporter::new(vec![], true);
788
789        assert_eq!(exporter.count(), 0);
790
791        // Test all formats with empty variables
792        let dotenv = exporter.to_dotenv();
793        assert!(dotenv.contains("# Count: 0"));
794
795        let json = exporter.to_json().unwrap();
796        assert!(json.contains("\"count\": 0"));
797        assert!(json.contains("\"variables\": []"));
798
799        let yaml = exporter.to_yaml();
800        assert!(yaml.contains("---"));
801
802        let text = exporter.to_text();
803        assert!(text.contains("# Total: 0 variables"));
804
805        let ps = exporter.to_powershell();
806        assert!(ps.contains("# PowerShell Environment Variables Script"));
807
808        let sh = exporter.to_shell();
809        assert!(sh.contains("#!/bin/bash"));
810    }
811
812    #[test]
813    fn test_variable_name_edge_cases() {
814        let vars = vec![
815            EnvVar {
816                name: "SIMPLE-NAME-WITH-DASHES".to_string(),
817                value: "value1".to_string(),
818                source: VarSource::User,
819                modified: Utc::now(),
820                original_value: None,
821            },
822            EnvVar {
823                name: "NAME.WITH.DOTS".to_string(),
824                value: "value2".to_string(),
825                source: VarSource::User,
826                modified: Utc::now(),
827                original_value: None,
828            },
829            EnvVar {
830                name: "_UNDERSCORE_START".to_string(),
831                value: "value3".to_string(),
832                source: VarSource::User,
833                modified: Utc::now(),
834                original_value: None,
835            },
836            EnvVar {
837                name: "123_NUMBER_START".to_string(),
838                value: "value4".to_string(),
839                source: VarSource::User,
840                modified: Utc::now(),
841                original_value: None,
842            },
843        ];
844
845        let exporter = Exporter::new(vars, false);
846
847        // All formats should handle these names correctly
848        let dotenv = exporter.to_dotenv();
849        assert!(dotenv.contains("SIMPLE-NAME-WITH-DASHES=value1"));
850        assert!(dotenv.contains("NAME.WITH.DOTS=value2"));
851
852        let json = exporter.to_json().unwrap();
853        assert!(json.contains("\"SIMPLE-NAME-WITH-DASHES\": \"value1\""));
854
855        let yaml = exporter.to_yaml();
856        assert!(yaml.contains("SIMPLE-NAME-WITH-DASHES: value1"));
857
858        let ps = exporter.to_powershell();
859        assert!(ps.contains("$env:SIMPLE-NAME-WITH-DASHES = \"value1\""));
860
861        let sh = exporter.to_shell();
862        assert!(sh.contains("export SIMPLE-NAME-WITH-DASHES=\"value1\""));
863    }
864
865    #[test]
866    fn test_very_long_values() {
867        let long_value = "a".repeat(1000);
868        let vars = vec![EnvVar {
869            name: "LONG_VALUE".to_string(),
870            value: long_value.clone(),
871            source: VarSource::User,
872            modified: Utc::now(),
873            original_value: None,
874        }];
875
876        let exporter = Exporter::new(vars, false);
877
878        // All formats should handle long values
879        let dotenv = exporter.to_dotenv();
880        assert!(dotenv.contains(&format!("LONG_VALUE={long_value}")));
881
882        let json = exporter.to_json().unwrap();
883        let parsed: serde_json::Value = serde_json::from_str(&json).unwrap();
884        assert_eq!(parsed["LONG_VALUE"].as_str().unwrap().len(), 1000);
885    }
886
887    #[test]
888    fn test_metadata_consistency() {
889        let fixed_time = DateTime::parse_from_rfc3339("2024-01-01T12:00:00Z")
890            .unwrap()
891            .with_timezone(&Utc);
892
893        let vars = vec![EnvVar {
894            name: "TEST_VAR".to_string(),
895            value: "test_value".to_string(),
896            source: VarSource::System,
897            modified: fixed_time,
898            original_value: None,
899        }];
900
901        let exporter = Exporter::new(vars, true);
902
903        // Check that metadata is formatted consistently
904        let dotenv = exporter.to_dotenv();
905        assert!(dotenv.contains("# Source: System"));
906        assert!(dotenv.contains("2024-01-01 12:00:00"));
907
908        let text = exporter.to_text();
909        assert!(text.contains("# Source: System"));
910
911        let ps = exporter.to_powershell();
912        assert!(ps.contains("# TEST_VAR (System)"));
913
914        let sh = exporter.to_shell();
915        assert!(sh.contains("# TEST_VAR (System)"));
916    }
917}