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