diff --git a/CHANGELOG.md b/CHANGELOG.md index 256de660..ad84ed9b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] ### 🚀 Added +- Add `compare`-command [#282](https://github.com/dotenv-linter/dotenv-linter/pull/282) ([@mstruebing](https://github.com/mstruebing)) - Add colored output feature and `--no-color` flag to disable colors [#307](https://github.com/dotenv-linter/dotenv-linter/pull/307) ([@Nikhil0487](https://github.com/Nikhil0487)) - Display linted files when run [#311](https://github.com/dotenv-linter/dotenv-linter/pull/311) ([@Anthuang](https://github.com/anthuang)) - Add export prefix support [#340](https://github.com/dotenv-linter/dotenv-linter/pull/340)([@skonik](https://github.com/skonik)) diff --git a/src/common.rs b/src/common.rs index 15d311ad..7f76b597 100644 --- a/src/common.rs +++ b/src/common.rs @@ -1,13 +1,17 @@ pub(crate) mod comment; +mod compare; mod file_entry; mod line_entry; pub(crate) mod output; mod warning; use colored::*; +pub use compare::CompareFileType; +pub use compare::CompareWarning; pub use file_entry::FileEntry; pub use line_entry::LineEntry; pub use output::check::CheckOutput; +pub use output::compare::CompareOutput; pub use output::fix::FixOutput; pub use warning::Warning; diff --git a/src/common/compare.rs b/src/common/compare.rs new file mode 100644 index 00000000..d92f6178 --- /dev/null +++ b/src/common/compare.rs @@ -0,0 +1,31 @@ +use std::fmt; +use std::path::PathBuf; + +use crate::common::*; + +// A structure used to compare environment files +pub struct CompareFileType { + pub path: PathBuf, + pub keys: Vec, + pub missing: Vec, +} + +pub struct CompareWarning { + pub path: PathBuf, + pub missing_keys: Vec, +} + +impl fmt::Display for CompareWarning { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "{}", + format!( + "{} is missing keys: {}", + self.path.display(), + self.missing_keys.join(", ") + ) + .italic(), + ) + } +} diff --git a/src/common/output/compare.rs b/src/common/output/compare.rs new file mode 100644 index 00000000..137f197c --- /dev/null +++ b/src/common/output/compare.rs @@ -0,0 +1,24 @@ +use crate::common::{CompareWarning, FileEntry}; + +pub struct CompareOutput { + // Quiet program output mode + is_quiet_mode: bool, +} + +impl CompareOutput { + pub fn new(is_quiet_mode: bool) -> Self { + CompareOutput { is_quiet_mode } + } + + /// Prints information about a file in process + pub fn print_processing_info(&self, file: &FileEntry) { + if !self.is_quiet_mode { + println!("Comparing {}", file); + } + } + + /// Prints warnings without any additional information + pub fn print_warnings(&self, warnings: &[CompareWarning]) { + warnings.iter().for_each(|w| println!("{}", w)) + } +} diff --git a/src/common/output/mod.rs b/src/common/output/mod.rs index 4216636d..8da4ff7f 100644 --- a/src/common/output/mod.rs +++ b/src/common/output/mod.rs @@ -1,2 +1,3 @@ pub mod check; +pub mod compare; pub mod fix; diff --git a/src/lib.rs b/src/lib.rs index e870718d..8c5d284f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,5 +1,6 @@ use crate::common::*; use std::collections::BTreeMap; +use std::collections::HashSet; use std::error::Error; use std::path::PathBuf; @@ -9,6 +10,7 @@ mod fixes; mod fs_utils; pub use checks::available_check_names; +use common::CompareWarning; pub fn check(args: &clap::ArgMatches, current_dir: &PathBuf) -> Result> { let lines_map = get_lines(args, current_dir)?; @@ -124,6 +126,68 @@ fn get_needed_file_paths(args: &clap::ArgMatches) -> Vec { file_paths } +// Compares if different environment files contains the same variables and +// returns warnings if not +pub fn compare( + args: &clap::ArgMatches, + current_dir: &PathBuf, +) -> Result, Box> { + let mut all_keys: HashSet = HashSet::new(); + let lines_map = get_lines(args, current_dir)?; + let output = CompareOutput::new(args.is_present("quiet")); + + let mut warnings: Vec = Vec::new(); + let mut files_to_compare: Vec = Vec::new(); + + // Nothing to check + if lines_map.is_empty() { + return Ok(warnings); + } + + // // Create CompareFileType structures for each file + for (_, (fe, strings)) in lines_map.into_iter().enumerate() { + output.print_processing_info(&fe); + let lines = get_line_entries(&fe, strings); + let mut keys: Vec = Vec::new(); + + for line in lines { + if let Some(key) = line.get_key() { + all_keys.insert(key.to_string()); + keys.push(key.to_string()); + } + } + + let file_to_compare: CompareFileType = CompareFileType { + path: fe.path, + keys, + missing: Vec::new(), + }; + + files_to_compare.push(file_to_compare); + } + + // Create warnings if any file misses any key + for file in files_to_compare { + let missing_keys: Vec<_> = all_keys + .iter() + .filter(|key| !file.keys.contains(key)) + .map(|key| key.to_owned()) + .collect(); + + if !missing_keys.is_empty() { + let warning = CompareWarning { + path: file.path, + missing_keys, + }; + + warnings.push(warning) + } + } + + output.print_warnings(&warnings); + Ok(warnings) +} + fn get_file_paths( dir_entries: Vec, excludes: &[PathBuf], diff --git a/src/main.rs b/src/main.rs index 7440f770..578396e7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,6 +3,13 @@ use std::error::Error; use std::ffi::OsStr; use std::{env, process}; +fn quiet_flag() -> clap::Arg<'static, 'static> { + Arg::with_name("quiet") + .short("q") + .long("quiet") + .help("Doesn't display additional information") +} + fn main() -> Result<(), Box> { #[cfg(windows)] colored::control::set_virtual_terminal(true).ok(); @@ -33,6 +40,12 @@ fn main() -> Result<(), Box> { process::exit(0); } + ("compare", Some(files)) => { + let warnings = dotenv_linter::compare(&files, ¤t_dir)?; + if warnings.is_empty() { + process::exit(0); + } + } _ => { eprintln!("unknown command"); } @@ -71,6 +84,21 @@ fn get_args(current_dir: &OsStr) -> clap::ArgMatches { .usage("dotenv-linter fix [FLAGS] [OPTIONS] ...") .about("Automatically fixes warnings"), ) + .subcommand( + SubCommand::with_name("compare") + .setting(AppSettings::ColoredHelp) + .visible_alias("c") + .args(&vec![ + Arg::with_name("input") + .help("Files to compare") + .multiple(true) + .min_values(2) + .required(true), + quiet_flag(), + ]) + .about("Compares if files have the same keys") + .usage("dotenv-linter compare ..."), + ) .get_matches() } @@ -103,9 +131,6 @@ fn common_args(current_dir: &OsStr) -> Vec { Arg::with_name("no-color") .long("no-color") .help("Turns off the colored output"), - Arg::with_name("quiet") - .short("q") - .long("quiet") - .help("Doesn't display additional information"), + quiet_flag(), ] } diff --git a/tests/cli.rs b/tests/cli.rs index cb0cf674..3c3c323d 100644 --- a/tests/cli.rs +++ b/tests/cli.rs @@ -1,6 +1,7 @@ mod args; mod checks; mod common; +mod compare; mod fixes; mod flags; mod options; diff --git a/tests/compare/compare.rs b/tests/compare/compare.rs new file mode 100644 index 00000000..999fc9d7 --- /dev/null +++ b/tests/compare/compare.rs @@ -0,0 +1,63 @@ +use crate::common::TestDir; + +#[test] +fn files_with_same_environment_variables() { + let test_dir = TestDir::new(); + let testfile_one = test_dir.create_testfile(".env1", "FOO=abc\nBAR=def"); + let testfile_two = test_dir.create_testfile(".env2", "FOO=abc\nBAR=def"); + let expected_output = format!("Comparing .env1\nComparing .env2\n"); + + test_dir.test_command_success_with_args( + &["compare", testfile_one.as_str(), testfile_two.as_str()], + expected_output, + ); +} + +#[test] +fn files_with_same_environment_variables_in_quiet_mode() { + let test_dir = TestDir::new(); + let testfile_one = test_dir.create_testfile(".env1", "FOO=abc\nBAR=def"); + let testfile_two = test_dir.create_testfile(".env2", "FOO=abc\nBAR=def"); + let expected_output = format!(""); + + test_dir.test_command_success_with_args( + &[ + "compare", + "--quiet", + testfile_one.as_str(), + testfile_two.as_str(), + ], + expected_output, + ); +} + +#[test] +fn files_with_different_environment_variables() { + let test_dir = TestDir::new(); + let testfile_one = test_dir.create_testfile(".env1", "FOO=abc"); + let testfile_two = test_dir.create_testfile(".env2", "FOO=abc\nBAR=def"); + let expected_output = format!("Comparing .env1\nComparing .env2\n.env1 is missing keys: BAR\n"); + + test_dir.test_command_fail_with_args( + &["compare", testfile_one.as_str(), testfile_two.as_str()], + expected_output, + ) +} + +#[test] +fn files_with_different_environment_variables_in_quiet_mode() { + let test_dir = TestDir::new(); + let testfile_one = test_dir.create_testfile(".env1", "FOO=abc"); + let testfile_two = test_dir.create_testfile(".env2", "FOO=abc\nBAR=def"); + let expected_output = format!(".env1 is missing keys: BAR\n"); + + test_dir.test_command_fail_with_args( + &[ + "compare", + "--quiet", + testfile_one.as_str(), + testfile_two.as_str(), + ], + expected_output, + ) +} diff --git a/tests/compare/mod.rs b/tests/compare/mod.rs new file mode 100644 index 00000000..e6420454 --- /dev/null +++ b/tests/compare/mod.rs @@ -0,0 +1 @@ +mod compare;