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}