1use std::cmp::Ordering;
2use std::collections::BTreeMap;
3use std::fmt;
4use std::path::{Path, PathBuf};
5
6use anyhow::{Result, anyhow, bail};
7use serde::Deserialize;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
10pub enum Shell {
11 Bash,
12 Gbash,
13 Bashkit,
14 Zsh,
15 Dash,
16 Mksh,
17 Busybox,
18}
19
20impl Shell {
21 pub fn as_str(self) -> &'static str {
22 match self {
23 Self::Bash => "bash",
24 Self::Gbash => "gbash",
25 Self::Bashkit => "bashkit",
26 Self::Zsh => "zsh",
27 Self::Dash => "dash",
28 Self::Mksh => "mksh",
29 Self::Busybox => "busybox",
30 }
31 }
32
33 pub fn from_name(name: &str) -> Option<Self> {
34 match name.trim().to_ascii_lowercase().as_str() {
35 "bash" => Some(Self::Bash),
36 "gbash" => Some(Self::Gbash),
37 "bashkit" => Some(Self::Bashkit),
38 "zsh" => Some(Self::Zsh),
39 "dash" | "sh" => Some(Self::Dash),
40 "mksh" | "ksh" => Some(Self::Mksh),
41 "busybox" => Some(Self::Busybox),
42 _ => None,
43 }
44 }
45
46 pub(crate) fn ensure_supported_on_current_platform(self) -> Result<()> {
47 if self != Self::Busybox || std::env::consts::OS == "linux" {
48 return Ok(());
49 }
50
51 bail!("busybox is only supported on Linux");
52 }
53
54 pub(crate) fn infer(source: &str, path: Option<&Path>) -> Option<Self> {
55 Self::infer_from_shebang(source).or_else(|| {
56 path.and_then(|path| {
57 let ext = path.extension()?.to_str()?;
58 Self::from_name(ext)
59 })
60 })
61 }
62
63 fn infer_from_shebang(source: &str) -> Option<Self> {
64 let interpreter = shuck_parser::shebang::interpreter_name(source.lines().next()?)?;
65 Self::from_name(interpreter)
66 }
67}
68
69impl fmt::Display for Shell {
70 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
71 f.write_str(self.as_str())
72 }
73}
74
75#[derive(Debug, Clone)]
76pub struct Version {
77 raw: String,
78 tokens: Vec<VersionToken>,
79 segment_count: usize,
80 prefix_match: bool,
81}
82
83impl Version {
84 pub fn parse(raw: &str) -> Result<Self> {
85 let raw = raw.trim();
86 if raw.is_empty() {
87 bail!("version cannot be empty");
88 }
89 if !raw
90 .chars()
91 .all(|ch| ch.is_ascii_alphanumeric() || ch == '.')
92 {
93 bail!("invalid version `{raw}`");
94 }
95 if raw.split('.').any(|segment| segment.is_empty()) {
96 bail!("invalid version `{raw}`");
97 }
98
99 let tokens = tokenize_version(raw)?;
100 if tokens.is_empty() {
101 bail!("invalid version `{raw}`");
102 }
103
104 let segment_count = raw.split('.').count();
105 let prefix_match = should_treat_as_prefix(raw, &tokens, segment_count);
106
107 Ok(Self {
108 raw: raw.to_owned(),
109 tokens,
110 segment_count,
111 prefix_match,
112 })
113 }
114
115 pub fn as_str(&self) -> &str {
116 &self.raw
117 }
118
119 fn matches_prefix(&self, other: &Self) -> bool {
120 if !self.prefix_match {
121 return self == other;
122 }
123
124 let mut matched_segments = 0usize;
125 let mut left = self.tokens.iter().peekable();
126 let mut right = other.tokens.iter().peekable();
127 while let Some(left_token) = left.next() {
128 let Some(right_token) = right.next() else {
129 return false;
130 };
131 if left_token != right_token {
132 return false;
133 }
134 if matches!(left_token, VersionToken::Numeric(_) | VersionToken::Text(_))
135 && (left.peek().is_none()
136 || matches!(
137 left.peek(),
138 Some(VersionToken::Numeric(_) | VersionToken::Text(_))
139 ))
140 {
141 matched_segments += 1;
142 }
143 }
144
145 matched_segments >= self.segment_count
146 }
147}
148
149impl PartialEq for Version {
150 fn eq(&self, other: &Self) -> bool {
151 self.cmp(other) == Ordering::Equal
152 }
153}
154
155impl Eq for Version {}
156
157impl fmt::Display for Version {
158 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
159 f.write_str(&self.raw)
160 }
161}
162
163impl PartialOrd for Version {
164 fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
165 Some(self.cmp(other))
166 }
167}
168
169impl Ord for Version {
170 fn cmp(&self, other: &Self) -> Ordering {
171 let mut left = self.tokens.iter();
172 let mut right = other.tokens.iter();
173 loop {
174 match (left.next(), right.next()) {
175 (Some(a), Some(b)) => {
176 let ordering = a.cmp(b);
177 if ordering != Ordering::Equal {
178 return ordering;
179 }
180 }
181 (Some(_), None) => return Ordering::Greater,
182 (None, Some(_)) => return Ordering::Less,
183 (None, None) => return Ordering::Equal,
184 }
185 }
186 }
187}
188
189#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
190enum VersionToken {
191 Numeric(u64),
192 Text(String),
193}
194
195fn tokenize_version(raw: &str) -> Result<Vec<VersionToken>> {
196 let mut tokens = Vec::new();
197 let mut chars = raw.chars().peekable();
198 while let Some(ch) = chars.peek().copied() {
199 if ch.is_ascii_digit() {
200 let mut digits = String::new();
201 while let Some(next) = chars.peek().copied() {
202 if next.is_ascii_digit() {
203 digits.push(next);
204 chars.next();
205 } else {
206 break;
207 }
208 }
209 let value = digits
210 .parse::<u64>()
211 .map_err(|_| anyhow!("invalid version `{raw}`"))?;
212 tokens.push(VersionToken::Numeric(value));
213 continue;
214 }
215
216 if ch.is_ascii_alphabetic() {
217 let mut text = String::new();
218 while let Some(next) = chars.peek().copied() {
219 if next.is_ascii_alphabetic() {
220 text.push(next.to_ascii_lowercase());
221 chars.next();
222 } else {
223 break;
224 }
225 }
226 tokens.push(VersionToken::Text(text));
227 continue;
228 }
229
230 if ch == '.' {
231 chars.next();
232 continue;
233 }
234
235 break;
236 }
237
238 Ok(tokens)
239}
240
241fn should_treat_as_prefix(raw: &str, tokens: &[VersionToken], segment_count: usize) -> bool {
242 raw.chars().all(|ch| ch.is_ascii_digit() || ch == '.')
243 && segment_count == 2
244 && tokens
245 .iter()
246 .all(|token| matches!(token, VersionToken::Numeric(_)))
247}
248
249#[derive(Debug, Clone, PartialEq, Eq)]
250pub enum VersionConstraint {
251 Latest,
252 Exact(Version),
253 ExactPrefix(Version),
254 Range(Vec<VersionPredicate>),
255}
256
257impl VersionConstraint {
258 pub fn parse(raw: &str) -> Result<Self> {
259 let raw = raw.trim();
260 if raw.eq_ignore_ascii_case("latest") {
261 return Ok(Self::Latest);
262 }
263
264 if raw.contains('>') || raw.contains('<') || raw.contains('=') {
265 let mut predicates = Vec::new();
266 for item in raw.split(',') {
267 let item = item.trim();
268 if item.is_empty() {
269 bail!("invalid version constraint `{raw}`");
270 }
271 predicates.push(VersionPredicate::parse(item)?);
272 }
273 return Ok(Self::Range(predicates));
274 }
275
276 let version = Version::parse(raw)?;
277 if version.prefix_match {
278 Ok(Self::ExactPrefix(version))
279 } else {
280 Ok(Self::Exact(version))
281 }
282 }
283
284 pub fn matches(&self, version: &Version) -> bool {
285 match self {
286 Self::Latest => true,
287 Self::Exact(expected) => expected == version,
288 Self::ExactPrefix(prefix) => prefix.matches_prefix(version),
289 Self::Range(predicates) => predicates
290 .iter()
291 .all(|predicate| predicate.matches(version)),
292 }
293 }
294
295 pub(crate) fn describe(&self) -> String {
296 match self {
297 Self::Latest => "latest".to_owned(),
298 Self::Exact(version) | Self::ExactPrefix(version) => version.as_str().to_owned(),
299 Self::Range(predicates) => predicates
300 .iter()
301 .map(VersionPredicate::to_string)
302 .collect::<Vec<_>>()
303 .join(","),
304 }
305 }
306}
307
308#[derive(Debug, Clone, PartialEq, Eq)]
309pub struct AvailableShell {
310 pub shell: Shell,
311 pub versions: Vec<Version>,
312}
313
314#[derive(Debug, Clone, Default, PartialEq, Eq, Deserialize)]
315#[serde(default, rename_all = "kebab-case")]
316pub struct RunConfig {
317 pub shell: Option<String>,
318 pub shell_version: Option<String>,
319 pub shells: BTreeMap<String, String>,
320}
321
322#[derive(Debug, Clone, PartialEq, Eq)]
323pub struct ResolvedInterpreter {
324 pub shell: Shell,
325 pub version: Version,
326 pub path: PathBuf,
327 pub source: ResolutionSource,
328}
329
330#[derive(Debug, Clone, Copy, PartialEq, Eq)]
331pub enum ResolutionSource {
332 Managed,
333 System,
334}
335
336#[derive(Debug, Clone)]
337pub struct ResolveOptions<'a> {
338 pub shell: Option<Shell>,
339 pub version: Option<VersionConstraint>,
340 pub system: bool,
341 pub implicit_system_fallback: bool,
342 pub script: Option<&'a Path>,
343 pub config: Option<&'a RunConfig>,
344 pub verbose: bool,
345 pub refresh_registry: bool,
346}
347
348impl<'a> ResolveOptions<'a> {
349 pub fn new(
350 shell: Option<Shell>,
351 version: Option<VersionConstraint>,
352 system: bool,
353 script: Option<&'a Path>,
354 config: Option<&'a RunConfig>,
355 ) -> Self {
356 Self {
357 shell,
358 version,
359 system,
360 implicit_system_fallback: false,
361 script,
362 config,
363 verbose: false,
364 refresh_registry: false,
365 }
366 }
367}
368
369pub(crate) fn parse_shell_name(raw: &str) -> Result<Shell> {
370 Shell::from_name(raw).ok_or_else(|| {
371 anyhow!(
372 "unsupported shell `{raw}`; expected one of: bash, gbash, bashkit, zsh, dash, mksh, busybox"
373 )
374 })
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
378pub struct VersionPredicate {
379 operator: VersionOperator,
380 version: Version,
381}
382
383impl VersionPredicate {
384 fn parse(raw: &str) -> Result<Self> {
385 let (operator, version) = if let Some(version) = raw.strip_prefix(">=") {
386 (VersionOperator::GreaterOrEqual, version)
387 } else if let Some(version) = raw.strip_prefix("<=") {
388 (VersionOperator::LessOrEqual, version)
389 } else if let Some(version) = raw.strip_prefix('>') {
390 (VersionOperator::Greater, version)
391 } else if let Some(version) = raw.strip_prefix('<') {
392 (VersionOperator::Less, version)
393 } else if let Some(version) = raw.strip_prefix('=') {
394 (VersionOperator::Equal, version)
395 } else {
396 bail!("invalid version predicate `{raw}`");
397 };
398
399 Ok(Self {
400 operator,
401 version: Version::parse(version)?,
402 })
403 }
404
405 fn matches(&self, version: &Version) -> bool {
406 match self.operator {
407 VersionOperator::Greater => version > &self.version,
408 VersionOperator::GreaterOrEqual => version >= &self.version,
409 VersionOperator::Less => version < &self.version,
410 VersionOperator::LessOrEqual => version <= &self.version,
411 VersionOperator::Equal => version == &self.version,
412 }
413 }
414}
415
416impl fmt::Display for VersionPredicate {
417 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
418 write!(f, "{}{}", self.operator, self.version)
419 }
420}
421
422#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423enum VersionOperator {
424 Greater,
425 GreaterOrEqual,
426 Less,
427 LessOrEqual,
428 Equal,
429}
430
431impl fmt::Display for VersionOperator {
432 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
433 let symbol = match self {
434 Self::Greater => ">",
435 Self::GreaterOrEqual => ">=",
436 Self::Less => "<",
437 Self::LessOrEqual => "<=",
438 Self::Equal => "=",
439 };
440 f.write_str(symbol)
441 }
442}