dotenv_linter/
cli.rs

1use std::path::PathBuf;
2
3use clap::{Args, Parser, Subcommand, command};
4use dotenv_analyzer::LintKind;
5use dotenv_schema::DotEnvSchema;
6
7use crate::{CheckOptions, DiffOptions, FixOptions, Result};
8
9const HELP_TEMPLATE: &str = "
10{before-help}{name} {version}
11{author-with-newline}{about-with-newline}
12{usage-heading} {usage}
13
14{all-args}{after-help}
15";
16
17#[derive(Parser)]
18#[command(
19    version,
20    about,
21    author,
22    help_template = HELP_TEMPLATE,
23    styles = clap::builder::Styles::styled()
24        .header(clap::builder::styling::AnsiColor::Yellow.on_default())
25        .usage(clap::builder::styling::AnsiColor::Yellow.on_default())
26        .literal(clap::builder::styling::AnsiColor::Cyan.on_default())
27        .placeholder(clap::builder::styling::AnsiColor::Blue.on_default())
28        .context(clap::builder::styling::AnsiColor::Green.on_default())
29)]
30struct Cli {
31    #[command(subcommand)]
32    command: Command,
33
34    /// Switch to plain text output without colors
35    #[arg(long, global = true)]
36    plain: bool,
37
38    /// Display only critical results, suppressing extra details
39    #[arg(short, long, global = true)]
40    quiet: bool,
41}
42
43#[derive(Subcommand)]
44enum Command {
45    /// Check .env files for errors such as duplicate keys or invalid syntax
46    Check {
47        /// .env files or directories to check (one or more required)
48        #[arg(
49            num_args(1..),
50            required = true,
51        )]
52        files: Vec<PathBuf>,
53
54        #[command(flatten)]
55        common: CommonArgs,
56
57        /// Schema file to validate .env file contents
58        #[arg(short('s'), long, value_name = "PATH")]
59        schema: Option<PathBuf>,
60
61        /// Disable checking for application updates
62        #[cfg(feature = "update-informer")]
63        #[arg(long, env = "DOTENV_LINTER_SKIP_UPDATES")]
64        skip_updates: bool,
65    },
66    /// Automatically fix issues in .env files
67    Fix {
68        /// .env files or directories to fix (one or more required)
69        #[arg(
70            num_args(1..),
71            required = true,
72        )]
73        files: Vec<PathBuf>,
74
75        #[command(flatten)]
76        common: CommonArgs,
77
78        /// Prevent creating backups before applying fixes
79        #[arg(long)]
80        no_backup: bool,
81
82        /// Print fixed .env content to stdout without saving changes
83        #[arg(long)]
84        dry_run: bool,
85    },
86    /// Compare .env files to ensure matching key sets
87    Diff {
88        /// .env files or directories to compare (one or more required)
89        #[arg(
90            num_args(1..),
91            required = true,
92        )]
93        files: Vec<PathBuf>,
94    },
95}
96
97#[derive(Args)]
98struct CommonArgs {
99    /// Files or directories to exclude from linting or fixing
100    #[arg(short = 'e', long, value_name = "PATH")]
101    exclude: Vec<PathBuf>,
102
103    /// Lint checks to bypass
104    #[arg(
105        short,
106        long,
107        value_name = "CHECK_NAME",
108        value_delimiter = ',',
109        env = "DOTENV_LINTER_IGNORE_CHECKS"
110    )]
111    ignore_checks: Vec<LintKind>,
112
113    /// Recursively scan directories for .env files
114    #[arg(short, long)]
115    recursive: bool,
116}
117
118pub fn run() -> Result<i32> {
119    #[cfg(windows)]
120    colored::control::set_virtual_terminal(true).ok();
121
122    let cli = Cli::parse();
123    let current_dir = std::env::current_dir()?;
124
125    if cli.plain {
126        colored::control::set_override(false);
127    }
128
129    match cli.command {
130        Command::Check {
131            files,
132            common,
133            schema,
134            #[cfg(feature = "update-informer")]
135                skip_updates: not_check_updates,
136        } => {
137            let mut dotenv_schema = None;
138            if let Some(path) = schema {
139                dotenv_schema = match DotEnvSchema::load(path) {
140                    Ok(schema) => Some(schema),
141                    Err(err) => {
142                        println!("Error loading schema: {err}");
143                        std::process::exit(1);
144                    }
145                };
146            }
147
148            let total_warnings = crate::check(
149                &CheckOptions {
150                    files: files.iter().collect(),
151                    ignore_checks: common.ignore_checks,
152                    exclude: common.exclude.iter().collect(),
153                    recursive: common.recursive,
154                    quiet: cli.quiet,
155                    schema: dotenv_schema,
156                },
157                &current_dir,
158            )?;
159
160            #[cfg(feature = "update-informer")]
161            if !not_check_updates && !cli.quiet {
162                crate::check_for_updates();
163            }
164
165            if total_warnings == 0 {
166                return Ok(0);
167            }
168        }
169        Command::Fix {
170            files,
171            common,
172            no_backup,
173            dry_run,
174        } => {
175            crate::fix(
176                &FixOptions {
177                    files: files.iter().collect(),
178                    ignore_checks: common.ignore_checks,
179                    exclude: common.exclude.iter().collect(),
180                    recursive: common.recursive,
181                    quiet: cli.quiet,
182
183                    no_backup,
184                    dry_run,
185                },
186                &current_dir,
187            )?;
188
189            return Ok(0);
190        }
191        Command::Diff { files } => {
192            let total_warnings = crate::diff(
193                &DiffOptions {
194                    files: files.iter().collect(),
195                    quiet: cli.quiet,
196                },
197                &current_dir,
198            )?;
199
200            if total_warnings == 0 {
201                return Ok(0);
202            }
203        }
204    }
205
206    Ok(1)
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212
213    #[test]
214    fn verify_cli() {
215        use clap::CommandFactory;
216        Cli::command().debug_assert();
217    }
218}