1pub(crate) mod check;
2#[cfg(test)]
3mod display;
4mod eval;
5mod parse;
6#[cfg(test)]
7mod proptests;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
10pub struct Script(pub Vec<Stmt>);
11
12#[derive(Debug, Clone, PartialEq, Eq)]
13pub struct Stmt {
14 pub pipeline: Pipeline,
15 pub op: Option<ListOp>,
16}
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq)]
19pub enum ListOp {
20 And,
21 Or,
22 Semi,
23 Amp,
24}
25
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct Pipeline {
28 pub bang: bool,
29 pub commands: Vec<Cmd>,
30}
31
32#[derive(Debug, Clone, PartialEq, Eq)]
33pub enum Cmd {
34 Simple(SimpleCmd),
35 Subshell {
36 body: Script,
37 redirs: Vec<Redir>,
38 },
39 BraceGroup {
40 body: Script,
41 redirs: Vec<Redir>,
42 },
43 For {
44 var: String,
45 items: Vec<Word>,
46 body: Script,
47 },
48 While {
49 cond: Script,
50 body: Script,
51 },
52 Until {
53 cond: Script,
54 body: Script,
55 },
56 If {
57 branches: Vec<Branch>,
58 else_body: Option<Script>,
59 },
60 DoubleBracket {
61 words: Vec<Word>,
62 redirs: Vec<Redir>,
63 },
64}
65
66#[derive(Debug, Clone, PartialEq, Eq)]
67pub struct Branch {
68 pub cond: Script,
69 pub body: Script,
70}
71
72#[derive(Debug, Clone, PartialEq, Eq)]
73pub struct SimpleCmd {
74 pub env: Vec<(String, Word)>,
75 pub words: Vec<Word>,
76 pub redirs: Vec<Redir>,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Word(pub Vec<WordPart>);
81
82#[derive(Debug, Clone, PartialEq, Eq)]
83pub enum WordPart {
84 Lit(String),
85 Escape(char),
86 SQuote(String),
87 DQuote(Word),
88 CmdSub(Script),
89 ProcSub(Script),
90 Backtick(String),
91 Arith(String),
92}
93
94#[derive(Debug, Clone, PartialEq, Eq)]
95pub enum Redir {
96 Write {
97 fd: u32,
98 target: Word,
99 append: bool,
100 },
101 Read {
102 fd: u32,
103 target: Word,
104 },
105 HereStr(Word),
106 HereDoc {
107 delimiter: String,
108 strip_tabs: bool,
109 },
110 DupFd {
111 src: u32,
112 dst: String,
113 },
114}
115
116pub use check::{command_verdict, is_safe_command, is_safe_pipeline};
117pub use parse::parse;
118
119impl Word {
120 pub fn eval(&self) -> String {
121 eval::eval_word(self)
122 }
123
124 pub fn literal(s: &str) -> Self {
125 Word(vec![WordPart::Lit(s.to_string())])
126 }
127
128 pub fn normalize(&self) -> Self {
129 let mut parts = Vec::new();
130 for part in &self.0 {
131 let part = match part {
132 WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
133 WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
134 WordPart::ProcSub(s) => WordPart::ProcSub(s.normalize()),
135 other => other.clone(),
136 };
137 if let WordPart::Lit(s) = &part
138 && let Some(WordPart::Lit(prev)) = parts.last_mut()
139 {
140 prev.push_str(s);
141 continue;
142 }
143 parts.push(part);
144 }
145 Word(parts)
146 }
147}
148
149impl Script {
150 pub fn is_empty(&self) -> bool {
151 self.0.is_empty()
152 }
153
154 pub fn normalize(&self) -> Self {
155 Script(
156 self.0
157 .iter()
158 .map(|stmt| Stmt {
159 pipeline: stmt.pipeline.normalize(),
160 op: stmt.op,
161 })
162 .collect(),
163 )
164 }
165
166 pub fn normalize_as_body(&self) -> Self {
167 let mut s = self.normalize();
168 if let Some(last) = s.0.last_mut()
169 && last.op.is_none()
170 {
171 last.op = Some(ListOp::Semi);
172 }
173 s
174 }
175}
176
177impl Pipeline {
178 fn normalize(&self) -> Self {
179 Pipeline {
180 bang: self.bang,
181 commands: self.commands.iter().map(|c| c.normalize()).collect(),
182 }
183 }
184}
185
186impl Cmd {
187 fn normalize(&self) -> Self {
188 match self {
189 Cmd::Simple(s) => Cmd::Simple(s.normalize()),
190 Cmd::Subshell { body, redirs } => Cmd::Subshell {
191 body: body.normalize(),
192 redirs: normalize_redirs(redirs),
193 },
194 Cmd::BraceGroup { body, redirs } => Cmd::BraceGroup {
195 body: body.normalize_as_body(),
196 redirs: normalize_redirs(redirs),
197 },
198 Cmd::For { var, items, body } => Cmd::For {
199 var: var.clone(),
200 items: items.iter().map(|w| w.normalize()).collect(),
201 body: body.normalize_as_body(),
202 },
203 Cmd::While { cond, body } => Cmd::While {
204 cond: cond.normalize_as_body(),
205 body: body.normalize_as_body(),
206 },
207 Cmd::Until { cond, body } => Cmd::Until {
208 cond: cond.normalize_as_body(),
209 body: body.normalize_as_body(),
210 },
211 Cmd::If { branches, else_body } => Cmd::If {
212 branches: branches
213 .iter()
214 .map(|b| Branch {
215 cond: b.cond.normalize_as_body(),
216 body: b.body.normalize_as_body(),
217 })
218 .collect(),
219 else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
220 },
221 Cmd::DoubleBracket { words, redirs } => Cmd::DoubleBracket {
222 words: words.iter().map(|w| w.normalize()).collect(),
223 redirs: normalize_redirs(redirs),
224 },
225 }
226 }
227}
228
229impl SimpleCmd {
230 fn normalize(&self) -> Self {
231 SimpleCmd {
232 env: self
233 .env
234 .iter()
235 .map(|(k, v)| (k.clone(), v.normalize()))
236 .collect(),
237 words: self.words.iter().map(|w| w.normalize()).collect(),
238 redirs: normalize_redirs(&self.redirs),
239 }
240 }
241}
242
243fn normalize_redirs(redirs: &[Redir]) -> Vec<Redir> {
244 redirs
245 .iter()
246 .map(|r| match r {
247 Redir::Write { fd, target, append } => Redir::Write {
248 fd: *fd,
249 target: target.normalize(),
250 append: *append,
251 },
252 Redir::Read { fd, target } => Redir::Read {
253 fd: *fd,
254 target: target.normalize(),
255 },
256 Redir::HereStr(w) => Redir::HereStr(w.normalize()),
257 Redir::HereDoc { .. } | Redir::DupFd { .. } => r.clone(),
258 })
259 .collect()
260}