Skip to main content

shuck_parser/parser/
profile.rs

1use super::ZshOptionState;
2
3/// Supported shell dialects for parser syntax decisions.
4///
5/// Dialects select which grammar extensions the parser accepts. They do not
6/// try to model every runtime behavior of a shell; use [`ShellProfile`] when
7/// zsh option state also matters.
8#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
9pub enum ShellDialect {
10    /// POSIX-style parsing used for `sh`, `dash`, and generic portable shell input.
11    Posix,
12    /// mksh-specific parsing.
13    Mksh,
14    /// Bash parsing.
15    #[default]
16    Bash,
17    /// zsh parsing.
18    Zsh,
19}
20
21#[derive(Debug, Clone, Copy, PartialEq, Eq)]
22pub(super) struct DialectFeatures {
23    pub(super) double_bracket: bool,
24    pub(super) arithmetic_command: bool,
25    pub(super) arithmetic_for: bool,
26    pub(super) function_keyword: bool,
27    pub(super) select_loop: bool,
28    pub(super) coproc_keyword: bool,
29    pub(super) zsh_repeat_loop: bool,
30    pub(super) zsh_foreach_loop: bool,
31    pub(super) zsh_parameter_modifiers: bool,
32    pub(super) zsh_brace_if: bool,
33    pub(super) zsh_always: bool,
34    pub(super) zsh_background_operators: bool,
35    pub(super) zsh_glob_qualifiers: bool,
36}
37
38impl ShellDialect {
39    /// Infer a parser dialect from a command name, shebang interpreter name,
40    /// or user-facing shell selector.
41    ///
42    /// Unknown names fall back to [`ShellDialect::Bash`], matching Shuck's
43    /// default parsing mode.
44    pub fn from_name(name: &str) -> Self {
45        match name.trim().to_ascii_lowercase().as_str() {
46            "sh" | "dash" | "ksh" | "posix" => Self::Posix,
47            "mksh" => Self::Mksh,
48            "zsh" => Self::Zsh,
49            _ => Self::Bash,
50        }
51    }
52
53    pub(super) const fn features(self) -> DialectFeatures {
54        match self {
55            Self::Posix => DialectFeatures {
56                double_bracket: false,
57                arithmetic_command: false,
58                arithmetic_for: false,
59                function_keyword: true,
60                select_loop: false,
61                coproc_keyword: false,
62                zsh_repeat_loop: false,
63                zsh_foreach_loop: false,
64                zsh_parameter_modifiers: false,
65                zsh_brace_if: false,
66                zsh_always: false,
67                zsh_background_operators: false,
68                zsh_glob_qualifiers: false,
69            },
70            Self::Mksh => DialectFeatures {
71                double_bracket: true,
72                arithmetic_command: true,
73                arithmetic_for: false,
74                function_keyword: true,
75                select_loop: true,
76                coproc_keyword: false,
77                zsh_repeat_loop: false,
78                zsh_foreach_loop: false,
79                zsh_parameter_modifiers: false,
80                zsh_brace_if: false,
81                zsh_always: false,
82                zsh_background_operators: false,
83                zsh_glob_qualifiers: false,
84            },
85            Self::Bash => DialectFeatures {
86                double_bracket: true,
87                arithmetic_command: true,
88                arithmetic_for: true,
89                function_keyword: true,
90                select_loop: true,
91                coproc_keyword: true,
92                zsh_repeat_loop: false,
93                zsh_foreach_loop: false,
94                zsh_parameter_modifiers: false,
95                zsh_brace_if: false,
96                zsh_always: false,
97                zsh_background_operators: false,
98                zsh_glob_qualifiers: false,
99            },
100            Self::Zsh => DialectFeatures {
101                double_bracket: true,
102                arithmetic_command: true,
103                arithmetic_for: true,
104                function_keyword: true,
105                select_loop: true,
106                coproc_keyword: true,
107                zsh_repeat_loop: true,
108                zsh_foreach_loop: true,
109                zsh_parameter_modifiers: true,
110                zsh_brace_if: true,
111                zsh_always: true,
112                zsh_background_operators: true,
113                zsh_glob_qualifiers: true,
114            },
115        }
116    }
117}
118
119/// Complete shell parsing profile.
120///
121/// A profile combines the broad syntax dialect with any option state that
122/// changes tokenization or grammar. Today only zsh carries parser-visible
123/// options; non-zsh profiles ignore the [`ShellProfile::options`] field.
124#[derive(Debug, Clone, PartialEq, Eq, Hash)]
125pub struct ShellProfile {
126    /// Shell dialect to parse.
127    pub dialect: ShellDialect,
128    /// Optional zsh option state, used only when [`ShellProfile::dialect`] is
129    /// [`ShellDialect::Zsh`].
130    pub options: Option<ZshOptionState>,
131}
132
133impl ShellProfile {
134    /// Build the parser's native profile for `dialect`.
135    ///
136    /// Native zsh profiles include [`ZshOptionState::zsh_default`]. Other
137    /// dialects carry no option state.
138    pub fn native(dialect: ShellDialect) -> Self {
139        Self {
140            dialect,
141            options: (dialect == ShellDialect::Zsh).then(ZshOptionState::zsh_default),
142        }
143    }
144
145    /// Build a profile with explicit zsh option state.
146    ///
147    /// The provided options are retained only for [`ShellDialect::Zsh`]. For
148    /// other dialects, this returns a profile with `options: None` because
149    /// their parser behavior is not currently option-sensitive.
150    pub fn with_zsh_options(dialect: ShellDialect, options: ZshOptionState) -> Self {
151        Self {
152            dialect,
153            options: (dialect == ShellDialect::Zsh).then_some(options),
154        }
155    }
156
157    /// Borrow the zsh option state, if this profile carries one.
158    ///
159    /// Callers should treat `None` as "no parser-visible zsh option state",
160    /// not as "all options are unknown".
161    pub fn zsh_options(&self) -> Option<&ZshOptionState> {
162        self.options.as_ref()
163    }
164}