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 #[arg(long, global = true)]
36 plain: bool,
37
38 #[arg(short, long, global = true)]
40 quiet: bool,
41}
42
43#[derive(Subcommand)]
44enum Command {
45 Check {
47 #[arg(
49 num_args(1..),
50 required = true,
51 )]
52 files: Vec<PathBuf>,
53
54 #[command(flatten)]
55 common: CommonArgs,
56
57 #[arg(short('s'), long, value_name = "PATH")]
59 schema: Option<PathBuf>,
60
61 #[cfg(feature = "update-informer")]
63 #[arg(long, env = "DOTENV_LINTER_SKIP_UPDATES")]
64 skip_updates: bool,
65 },
66 Fix {
68 #[arg(
70 num_args(1..),
71 required = true,
72 )]
73 files: Vec<PathBuf>,
74
75 #[command(flatten)]
76 common: CommonArgs,
77
78 #[arg(long)]
80 no_backup: bool,
81
82 #[arg(long)]
84 dry_run: bool,
85 },
86 Diff {
88 #[arg(
90 num_args(1..),
91 required = true,
92 )]
93 files: Vec<PathBuf>,
94 },
95}
96
97#[derive(Args)]
98struct CommonArgs {
99 #[arg(short = 'e', long, value_name = "PATH")]
101 exclude: Vec<PathBuf>,
102
103 #[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 #[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 ¤t_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 ¤t_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 ¤t_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}