Skip to main content

mdsf/
cli.rs

1use std::io::Read;
2
3use clap::{Args, Parser, Subcommand};
4
5const HELP_TEMPLATE: &str = "\
6{before-help}{name} {version}
7{about-with-newline}{author-with-newline}
8{usage-heading} {usage}
9
10{all-args}{after-help}
11";
12
13#[derive(Parser, Debug)]
14#[command(author, version, about, long_about = None, propagate_version = true, help_template = HELP_TEMPLATE)]
15pub struct Cli {
16    #[command(subcommand)]
17    pub command: Commands,
18
19    #[arg(long, value_enum, global = true, default_value_t)]
20    pub log_level: LogLevel,
21}
22
23#[derive(Subcommand, Debug)]
24pub enum Commands {
25    /// Run tools on input files.
26    Format(FormatCommandArguments),
27
28    /// Verify files are formatted.
29    Verify(VerifyCommandArguments),
30
31    /// Create a new mdsf config.
32    Init(InitCommandArguments),
33
34    /// Generate shell completion.
35    Completions(CompletionsCommandArguments),
36
37    /// Remove caches.
38    CachePrune,
39}
40
41#[derive(
42    Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize, Hash,
43)]
44#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
45pub enum OnMissingToolBinary {
46    /// Allow missing binaries.
47    #[default]
48    #[serde(rename = "ignore")]
49    Ignore,
50
51    /// Exit with status code 1 when finished.
52    #[serde(rename = "fail")]
53    Fail,
54
55    /// Instantly exit with status code 1.
56    #[serde(rename = "fail-fast")]
57    FailFast,
58}
59
60impl clap::ValueEnum for OnMissingToolBinary {
61    #[inline]
62    fn value_variants<'a>() -> &'a [Self] {
63        &[Self::Ignore, Self::Fail, Self::FailFast]
64    }
65
66    #[inline]
67    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
68        Some(match self {
69            Self::Fail => clap::builder::PossibleValue::new("fail"),
70            Self::FailFast => clap::builder::PossibleValue::new("fail-fast"),
71            Self::Ignore => clap::builder::PossibleValue::new("ignore"),
72        })
73    }
74}
75
76#[derive(
77    Clone, Copy, PartialEq, Eq, Debug, Default, serde::Serialize, serde::Deserialize, Hash,
78)]
79#[cfg_attr(feature = "json-schema", derive(schemars::JsonSchema))]
80pub enum OnMissingLanguageDefinition {
81    /// Allow missing binaries.
82    #[default]
83    #[serde(rename = "ignore")]
84    Ignore,
85
86    /// Exit with status code 1 when finished.
87    #[serde(rename = "fail")]
88    Fail,
89
90    /// Instantly exit with status code 1.
91    #[serde(rename = "fail-fast")]
92    FailFast,
93}
94
95impl clap::ValueEnum for OnMissingLanguageDefinition {
96    #[inline]
97    fn value_variants<'a>() -> &'a [Self] {
98        &[Self::Ignore, Self::Fail, Self::FailFast]
99    }
100
101    #[inline]
102    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
103        Some(match self {
104            Self::Fail => clap::builder::PossibleValue::new("fail"),
105            Self::FailFast => clap::builder::PossibleValue::new("fail-fast"),
106            Self::Ignore => clap::builder::PossibleValue::new("ignore"),
107        })
108    }
109}
110
111#[derive(Args, Debug)]
112pub struct FormatCommandArguments {
113    /// Path to files and/or directories.
114    #[arg()]
115    pub input: Vec<std::path::PathBuf>,
116
117    /// Read input from stdin and write output to stdout.
118    #[arg(long, default_value_t = false)]
119    pub stdin: bool,
120
121    /// Path to config file.
122    #[arg(long)]
123    pub config: Option<std::path::PathBuf>,
124
125    /// Log stdout and stderr of formatters.
126    #[arg(long, default_value_t = false)]
127    pub debug: bool,
128
129    /// Amount of threads to use.
130    ///
131    /// Defaults to 0 (auto).
132    #[arg(long)]
133    pub threads: Option<usize>,
134
135    /// Cache results
136    #[arg(long, default_value_t = false)]
137    pub cache: bool,
138
139    /// Tool timeout in seconds.
140    ///
141    /// Defaults to no timeout.
142    #[arg(long)]
143    pub timeout: Option<u64>,
144
145    /// What to do when a codeblock language has no tools defined.
146    ///
147    /// Falls back to the value defined in your config file, if no argument is provided.
148    #[arg(long)]
149    pub on_missing_language_definition: Option<OnMissingLanguageDefinition>,
150
151    /// What to do when the binary of a tool cannot be found.
152    ///
153    /// Falls back to the value defined in your config file, if no argument is provided.
154    #[arg(long)]
155    pub on_missing_tool_binary: Option<OnMissingToolBinary>,
156}
157
158#[derive(Args, Debug)]
159pub struct VerifyCommandArguments {
160    /// Path to files and/or directories.
161    #[arg()]
162    pub input: Vec<std::path::PathBuf>,
163
164    /// Read input from stdin and write output to stdout.
165    #[arg(long, default_value_t = false)]
166    pub stdin: bool,
167
168    /// Path to config file.
169    #[arg(long)]
170    pub config: Option<std::path::PathBuf>,
171
172    /// Log stdout and stderr of formatters.
173    #[arg(long, default_value_t = false)]
174    pub debug: bool,
175
176    /// Amount of threads to use.
177    ///
178    /// Defaults to 0 (auto).
179    #[arg(long)]
180    pub threads: Option<usize>,
181
182    /// Tool timeout in seconds.
183    ///
184    /// Defaults to no timeout.
185    #[arg(long)]
186    pub timeout: Option<u64>,
187
188    /// What to do when a codeblock language has no tools defined.
189    ///
190    /// Falls back to the value defined in your config file, if no argument is provided.
191    #[arg(long)]
192    pub on_missing_language_definition: Option<OnMissingLanguageDefinition>,
193
194    /// What to do when the binary of a tool cannot be found.
195    ///
196    /// Falls back to the value defined in your config file, if no argument is provided.
197    #[arg(long)]
198    pub on_missing_tool_binary: Option<OnMissingToolBinary>,
199}
200
201impl From<VerifyCommandArguments> for FormatCommandArguments {
202    #[inline]
203    fn from(value: VerifyCommandArguments) -> Self {
204        Self {
205            cache: false,
206            config: value.config,
207            debug: value.debug,
208            input: value.input,
209            on_missing_language_definition: value.on_missing_language_definition,
210            on_missing_tool_binary: value.on_missing_tool_binary,
211            stdin: value.stdin,
212            threads: value.threads,
213            timeout: value.timeout,
214        }
215    }
216}
217
218#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
219pub enum ConfigFileFormat {
220    #[default]
221    Json,
222    Json5,
223    JsonC,
224    Toml,
225    Yaml,
226}
227
228impl ConfigFileFormat {
229    #[inline]
230    pub const fn extension(&self) -> &'static str {
231        match self {
232            Self::Json => "json",
233            Self::Json5 => "json5",
234            Self::JsonC => "jsonc",
235            Self::Toml => "toml",
236            Self::Yaml => "yml",
237        }
238    }
239}
240
241impl core::fmt::Display for ConfigFileFormat {
242    #[inline]
243    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
244        match self {
245            Self::Json => write!(f, "json"),
246            Self::Json5 => write!(f, "json5"),
247            Self::JsonC => write!(f, "jsonc"),
248            Self::Toml => write!(f, "toml"),
249            Self::Yaml => write!(f, "yaml"),
250        }
251    }
252}
253
254impl clap::ValueEnum for ConfigFileFormat {
255    #[inline]
256    fn value_variants<'a>() -> &'a [Self] {
257        &[Self::Json, Self::Json5, Self::JsonC, Self::Toml, Self::Yaml]
258    }
259
260    #[inline]
261    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
262        Some(match self {
263            Self::Json => clap::builder::PossibleValue::new("json"),
264            Self::Json5 => clap::builder::PossibleValue::new("json5").hide(true),
265            Self::JsonC => clap::builder::PossibleValue::new("jsonc").hide(true),
266            Self::Toml => clap::builder::PossibleValue::new("toml"),
267            Self::Yaml => clap::builder::PossibleValue::new("yaml").alias("yml"),
268        })
269    }
270}
271
272#[derive(Clone, Copy, PartialEq, Eq, Debug, Default)]
273pub enum InitCommandSchemaVersion {
274    /// Schema that uses your current mdsf version.
275    #[default]
276    Locked,
277
278    /// Schema of the latest released version of mdsf.
279    Stable,
280
281    /// Schema based on the latest commit on the main branch of mdsf.
282    Development,
283}
284
285impl clap::ValueEnum for InitCommandSchemaVersion {
286    #[inline]
287    fn value_variants<'a>() -> &'a [Self] {
288        &[Self::Locked, Self::Stable, Self::Development]
289    }
290
291    #[inline]
292    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
293        Some(match self {
294            Self::Development => clap::builder::PossibleValue::new("development"),
295            Self::Locked => clap::builder::PossibleValue::new("locked"),
296            Self::Stable => clap::builder::PossibleValue::new("stable"),
297        })
298    }
299}
300
301impl core::fmt::Display for InitCommandSchemaVersion {
302    #[inline]
303    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
304        match self {
305            Self::Development => write!(f, "development"),
306            Self::Locked => write!(f, "locked"),
307            Self::Stable => write!(f, "stable"),
308        }
309    }
310}
311
312#[derive(Args, Debug)]
313pub struct InitCommandArguments {
314    /// Create config even if one already exists in current directory.
315    #[arg(long, default_value_t = false)]
316    pub force: bool,
317
318    #[arg(long, default_value_t)]
319    pub format: ConfigFileFormat,
320
321    #[arg(long, default_value_t)]
322    pub schema_version: InitCommandSchemaVersion,
323}
324
325#[derive(clap::ValueEnum, Clone, Copy, PartialEq, Eq, Debug, Default)]
326pub enum LogLevel {
327    Trace,
328    #[default]
329    Debug,
330    Info,
331    Warn,
332    Error,
333    Off,
334}
335
336#[derive(Clone, Copy, PartialEq, Eq, Debug, Hash)]
337pub enum Shell {
338    /// Bourne Again `SHell` (bash).
339    Bash,
340
341    /// Elvish shell (elvish).
342    Elvish,
343
344    /// Friendly Interactive `SHell` (fish).
345    Fish,
346
347    /// `Nushell` (nushell).
348    Nushell,
349
350    /// `PowerShell` (powershell).
351    PowerShell,
352
353    /// Z `SHell` (zsh).
354    Zsh,
355}
356
357impl clap::ValueEnum for Shell {
358    #[inline]
359    fn value_variants<'a>() -> &'a [Self] {
360        &[
361            Self::Bash,
362            Self::Elvish,
363            Self::Fish,
364            Self::Nushell,
365            Self::PowerShell,
366            Self::Zsh,
367        ]
368    }
369
370    #[inline]
371    fn to_possible_value(&self) -> Option<clap::builder::PossibleValue> {
372        Some(match self {
373            Self::Bash => clap::builder::PossibleValue::new("bash"),
374            Self::Elvish => clap::builder::PossibleValue::new("elvish"),
375            Self::Fish => clap::builder::PossibleValue::new("fish"),
376            Self::Nushell => clap::builder::PossibleValue::new("nushell"),
377            Self::PowerShell => clap::builder::PossibleValue::new("powershell"),
378            Self::Zsh => clap::builder::PossibleValue::new("zsh"),
379        })
380    }
381}
382
383#[derive(Args, Debug)]
384pub struct CompletionsCommandArguments {
385    pub shell: Shell,
386}
387
388#[derive(Args, Debug)]
389pub struct CachePruneArguments {}
390
391#[inline]
392pub fn read_stdin() -> std::io::Result<String> {
393    let stdin = std::io::stdin();
394
395    let mut input = String::new();
396
397    stdin.lock().read_to_string(&mut input)?;
398
399    Ok(input)
400}