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}