Skip to main content

shuck_formatter/
options.rs

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