Skip to main content

shuck_formatter/
options.rs

1use std::path::Path;
2
3use shuck_format::{FormatOptions, IndentStyle, LineEnding, PrinterOptions};
4use shuck_parser::ShellDialect as ParseDialect;
5
6#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7pub enum ShellDialect {
8    #[default]
9    Auto,
10    Bash,
11    Posix,
12    Mksh,
13    Zsh,
14}
15
16impl ShellDialect {
17    #[must_use]
18    pub fn resolve(self, source: &str, path: Option<&Path>) -> ParseDialect {
19        match self {
20            Self::Auto => infer_dialect(source, path),
21            Self::Bash => ParseDialect::Bash,
22            Self::Posix => ParseDialect::Posix,
23            Self::Mksh => ParseDialect::Mksh,
24            Self::Zsh => ParseDialect::Zsh,
25        }
26    }
27}
28
29#[derive(Debug, Clone, PartialEq, Eq)]
30pub struct ShellFormatOptions {
31    dialect: ShellDialect,
32    indent_style: IndentStyle,
33    indent_width: u8,
34    binary_next_line: bool,
35    switch_case_indent: bool,
36    space_redirects: bool,
37    keep_padding: bool,
38    function_next_line: bool,
39    never_split: bool,
40    simplify: bool,
41    minify: bool,
42}
43
44impl Default for ShellFormatOptions {
45    fn default() -> Self {
46        Self {
47            dialect: ShellDialect::Auto,
48            indent_style: IndentStyle::Tab,
49            indent_width: 8,
50            binary_next_line: false,
51            switch_case_indent: false,
52            space_redirects: false,
53            keep_padding: false,
54            function_next_line: false,
55            never_split: false,
56            simplify: false,
57            minify: false,
58        }
59    }
60}
61
62impl ShellFormatOptions {
63    #[must_use]
64    pub fn dialect(&self) -> ShellDialect {
65        self.dialect
66    }
67
68    #[must_use]
69    pub fn indent_style(&self) -> IndentStyle {
70        self.indent_style
71    }
72
73    #[must_use]
74    pub fn indent_width(&self) -> u8 {
75        self.indent_width
76    }
77
78    #[must_use]
79    pub fn binary_next_line(&self) -> bool {
80        self.binary_next_line
81    }
82
83    #[must_use]
84    pub fn switch_case_indent(&self) -> bool {
85        self.switch_case_indent
86    }
87
88    #[must_use]
89    pub fn space_redirects(&self) -> bool {
90        self.space_redirects
91    }
92
93    #[must_use]
94    pub fn keep_padding(&self) -> bool {
95        self.keep_padding
96    }
97
98    #[must_use]
99    pub fn function_next_line(&self) -> bool {
100        self.function_next_line
101    }
102
103    #[must_use]
104    pub fn never_split(&self) -> bool {
105        self.never_split
106    }
107
108    #[must_use]
109    pub fn simplify(&self) -> bool {
110        self.simplify
111    }
112
113    #[must_use]
114    pub fn minify(&self) -> bool {
115        self.minify
116    }
117
118    #[must_use]
119    pub fn with_dialect(mut self, dialect: ShellDialect) -> Self {
120        self.dialect = dialect;
121        self
122    }
123
124    #[must_use]
125    pub fn with_indent_style(mut self, indent_style: IndentStyle) -> Self {
126        self.indent_style = indent_style;
127        self
128    }
129
130    #[must_use]
131    pub fn with_indent_width(mut self, indent_width: u8) -> Self {
132        self.indent_width = indent_width.max(1);
133        self
134    }
135
136    #[must_use]
137    pub fn with_binary_next_line(mut self, enabled: bool) -> Self {
138        self.binary_next_line = enabled;
139        self
140    }
141
142    #[must_use]
143    pub fn with_switch_case_indent(mut self, enabled: bool) -> Self {
144        self.switch_case_indent = enabled;
145        self
146    }
147
148    #[must_use]
149    pub fn with_space_redirects(mut self, enabled: bool) -> Self {
150        self.space_redirects = enabled;
151        self
152    }
153
154    #[must_use]
155    pub fn with_keep_padding(mut self, enabled: bool) -> Self {
156        self.keep_padding = enabled;
157        self
158    }
159
160    #[must_use]
161    pub fn with_function_next_line(mut self, enabled: bool) -> Self {
162        self.function_next_line = enabled;
163        self
164    }
165
166    #[must_use]
167    pub fn with_never_split(mut self, enabled: bool) -> Self {
168        self.never_split = enabled;
169        self
170    }
171
172    #[must_use]
173    pub fn with_simplify(mut self, enabled: bool) -> Self {
174        self.simplify = enabled;
175        self
176    }
177
178    #[must_use]
179    pub fn with_minify(mut self, enabled: bool) -> Self {
180        self.minify = enabled;
181        self
182    }
183
184    #[must_use]
185    pub fn resolve(&self, source: &str, path: Option<&Path>) -> ResolvedShellFormatOptions {
186        ResolvedShellFormatOptions {
187            dialect: self.dialect.resolve(source, path),
188            indent_style: self.indent_style,
189            indent_width: self.indent_width.max(1),
190            binary_next_line: self.binary_next_line,
191            switch_case_indent: self.switch_case_indent,
192            space_redirects: self.space_redirects,
193            keep_padding: self.keep_padding,
194            function_next_line: self.function_next_line,
195            never_split: self.never_split,
196            simplify: self.simplify,
197            minify: self.minify,
198            line_ending: detect_line_ending(source),
199        }
200    }
201}
202
203#[derive(Debug, Clone, PartialEq, Eq)]
204pub struct ResolvedShellFormatOptions {
205    dialect: ParseDialect,
206    indent_style: IndentStyle,
207    indent_width: u8,
208    binary_next_line: bool,
209    switch_case_indent: bool,
210    space_redirects: bool,
211    keep_padding: bool,
212    function_next_line: bool,
213    never_split: bool,
214    simplify: bool,
215    minify: bool,
216    line_ending: LineEnding,
217}
218
219impl ResolvedShellFormatOptions {
220    #[must_use]
221    pub fn dialect(&self) -> ParseDialect {
222        self.dialect
223    }
224
225    #[must_use]
226    pub fn indent_style(&self) -> IndentStyle {
227        self.indent_style
228    }
229
230    #[must_use]
231    pub fn indent_width(&self) -> u8 {
232        self.indent_width
233    }
234
235    #[must_use]
236    pub fn binary_next_line(&self) -> bool {
237        self.binary_next_line
238    }
239
240    #[must_use]
241    pub fn switch_case_indent(&self) -> bool {
242        self.switch_case_indent
243    }
244
245    #[must_use]
246    pub fn space_redirects(&self) -> bool {
247        self.space_redirects
248    }
249
250    #[must_use]
251    pub fn keep_padding(&self) -> bool {
252        self.keep_padding
253    }
254
255    #[must_use]
256    pub fn function_next_line(&self) -> bool {
257        self.function_next_line
258    }
259
260    #[must_use]
261    pub fn never_split(&self) -> bool {
262        self.never_split
263    }
264
265    #[must_use]
266    pub fn simplify(&self) -> bool {
267        self.simplify
268    }
269
270    #[must_use]
271    pub fn minify(&self) -> bool {
272        self.minify
273    }
274
275    #[must_use]
276    pub fn compact_layout(&self) -> bool {
277        self.minify || self.never_split
278    }
279
280    #[must_use]
281    pub fn line_ending(&self) -> LineEnding {
282        self.line_ending
283    }
284}
285
286impl FormatOptions for ResolvedShellFormatOptions {
287    fn as_print_options(&self) -> PrinterOptions {
288        PrinterOptions {
289            indent_style: self.indent_style,
290            indent_width: self.indent_width,
291            line_width: 80,
292            line_ending: self.line_ending,
293        }
294    }
295}
296
297fn infer_dialect(source: &str, path: Option<&Path>) -> ParseDialect {
298    if let Some(first_line) = source.lines().next()
299        && let Some(shebang) = first_line.strip_prefix("#!")
300    {
301        let mut parts = shebang.split_whitespace();
302        let first = parts.next().unwrap_or_default();
303        let interpreter = if Path::new(first)
304            .file_name()
305            .and_then(|name| name.to_str())
306            .is_some_and(|name| name == "env")
307        {
308            parts.next().unwrap_or_default()
309        } else {
310            first
311        };
312        let interpreter = interpreter.rsplit('/').next().unwrap_or_default();
313        return ParseDialect::from_name(interpreter);
314    }
315
316    path.and_then(Path::extension)
317        .and_then(|extension| extension.to_str())
318        .map(ParseDialect::from_name)
319        .unwrap_or(ParseDialect::Bash)
320}
321
322fn detect_line_ending(source: &str) -> LineEnding {
323    if source.contains("\r\n") {
324        LineEnding::CrLf
325    } else {
326        LineEnding::Lf
327    }
328}