1pub(crate) mod check;
2mod display;
3pub(crate) mod eval;
4mod explain;
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 redirs: Vec<Redir>,
48 },
49 While {
50 cond: Script,
51 body: Script,
52 redirs: Vec<Redir>,
53 },
54 Until {
55 cond: Script,
56 body: Script,
57 redirs: Vec<Redir>,
58 },
59 If {
60 branches: Vec<Branch>,
61 else_body: Option<Script>,
62 redirs: Vec<Redir>,
63 },
64 DoubleBracket {
65 words: Vec<Word>,
66 redirs: Vec<Redir>,
67 },
68 Case {
72 subject: Word,
73 arms: Vec<CaseArm>,
74 redirs: Vec<Redir>,
75 },
76 FunctionDef {
80 name: String,
81 body: Script,
82 },
83}
84
85#[derive(Debug, Clone, PartialEq, Eq)]
86pub struct Branch {
87 pub cond: Script,
88 pub body: Script,
89}
90
91#[derive(Debug, Clone, PartialEq, Eq)]
94pub struct CaseArm {
95 pub patterns: Vec<Word>,
96 pub body: Script,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq)]
100pub struct SimpleCmd {
101 pub env: Vec<(String, Word)>,
102 pub words: Vec<Word>,
103 pub redirs: Vec<Redir>,
104}
105
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub struct Word(pub Vec<WordPart>);
108
109#[derive(Debug, Clone, PartialEq, Eq)]
110pub enum WordPart {
111 Lit(String),
112 Escape(char),
113 SQuote(String),
114 DQuote(Word),
115 CmdSub(Script),
116 ProcSub(Script),
117 Backtick(String),
118 Arith(String),
119}
120
121#[derive(Debug, Clone, Copy, PartialEq, Eq)]
126pub enum WriteMode {
127 Truncate,
129 Append,
131 TruncateBoth,
135 AppendBoth,
137 Clobber,
139}
140
141#[derive(Debug, Clone, PartialEq, Eq)]
142pub enum Redir {
143 Write {
144 fd: u32,
145 target: Word,
146 mode: WriteMode,
147 },
148 Read {
149 fd: u32,
150 target: Word,
151 },
152 ReadWrite {
156 fd: u32,
157 target: Word,
158 },
159 HereStr(Word),
160 HereDoc {
161 delimiter: String,
162 strip_tabs: bool,
163 body: Word,
168 },
169 DupFd {
170 src: u32,
171 dst: String,
172 },
173}
174
175pub use check::{command_verdict, is_safe_command, is_safe_pipeline};
176pub use explain::{Explanation, SegmentReport, explain, explain_with_coverage};
177pub use parse::parse;
178
179impl Word {
180 pub fn eval(&self) -> String {
181 eval::eval_word(self)
182 }
183
184 pub fn expand(&self) -> Vec<String> {
188 eval::expand_word(self)
189 }
190
191 pub fn literal(s: &str) -> Self {
192 Word(vec![WordPart::Lit(s.to_string())])
193 }
194
195 pub fn normalize(&self) -> Self {
196 let mut parts = Vec::new();
197 for part in &self.0 {
198 let part = match part {
199 WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
200 WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
201 WordPart::ProcSub(s) => WordPart::ProcSub(s.normalize()),
202 other => other.clone(),
203 };
204 if let WordPart::Lit(s) = &part
205 && let Some(WordPart::Lit(prev)) = parts.last_mut()
206 {
207 prev.push_str(s);
208 continue;
209 }
210 parts.push(part);
211 }
212 Word(parts)
213 }
214}
215
216impl Script {
217 pub fn is_empty(&self) -> bool {
218 self.0.is_empty()
219 }
220
221 pub fn normalize(&self) -> Self {
222 Script(
223 self.0
224 .iter()
225 .map(|stmt| Stmt {
226 pipeline: stmt.pipeline.normalize(),
227 op: stmt.op,
228 })
229 .collect(),
230 )
231 }
232
233 pub fn normalize_as_body(&self) -> Self {
234 let mut s = self.normalize();
235 if let Some(last) = s.0.last_mut()
236 && last.op.is_none()
237 {
238 last.op = Some(ListOp::Semi);
239 }
240 s
241 }
242}
243
244impl Pipeline {
245 fn normalize(&self) -> Self {
246 Pipeline {
247 bang: self.bang,
248 commands: self.commands.iter().map(|c| c.normalize()).collect(),
249 }
250 }
251}
252
253impl Cmd {
254 fn normalize(&self) -> Self {
255 match self {
256 Cmd::Simple(s) => Cmd::Simple(s.normalize()),
257 Cmd::Subshell { body, redirs } => Cmd::Subshell {
258 body: body.normalize(),
259 redirs: normalize_redirs(redirs),
260 },
261 Cmd::BraceGroup { body, redirs } => Cmd::BraceGroup {
262 body: body.normalize_as_body(),
263 redirs: normalize_redirs(redirs),
264 },
265 Cmd::For { var, items, body, redirs } => Cmd::For {
266 var: var.clone(),
267 items: items.iter().map(|w| w.normalize()).collect(),
268 body: body.normalize_as_body(),
269 redirs: normalize_redirs(redirs),
270 },
271 Cmd::While { cond, body, redirs } => Cmd::While {
272 cond: cond.normalize_as_body(),
273 body: body.normalize_as_body(),
274 redirs: normalize_redirs(redirs),
275 },
276 Cmd::Until { cond, body, redirs } => Cmd::Until {
277 cond: cond.normalize_as_body(),
278 body: body.normalize_as_body(),
279 redirs: normalize_redirs(redirs),
280 },
281 Cmd::If { branches, else_body, redirs } => Cmd::If {
282 branches: branches
283 .iter()
284 .map(|b| Branch {
285 cond: b.cond.normalize_as_body(),
286 body: b.body.normalize_as_body(),
287 })
288 .collect(),
289 else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
290 redirs: normalize_redirs(redirs),
291 },
292 Cmd::DoubleBracket { words, redirs } => Cmd::DoubleBracket {
293 words: words.iter().map(|w| w.normalize()).collect(),
294 redirs: normalize_redirs(redirs),
295 },
296 Cmd::Case { subject, arms, redirs } => Cmd::Case {
297 subject: subject.normalize(),
298 arms: arms
299 .iter()
300 .map(|a| CaseArm {
301 patterns: a.patterns.iter().map(|w| w.normalize()).collect(),
302 body: a.body.normalize_as_body(),
303 })
304 .collect(),
305 redirs: normalize_redirs(redirs),
306 },
307 Cmd::FunctionDef { name, body } => Cmd::FunctionDef {
308 name: name.clone(),
309 body: body.normalize_as_body(),
310 },
311 }
312 }
313}
314
315impl SimpleCmd {
316 fn normalize(&self) -> Self {
317 SimpleCmd {
318 env: self
319 .env
320 .iter()
321 .map(|(k, v)| (k.clone(), v.normalize()))
322 .collect(),
323 words: self.words.iter().map(|w| w.normalize()).collect(),
324 redirs: normalize_redirs(&self.redirs),
325 }
326 }
327}
328
329fn normalize_redirs(redirs: &[Redir]) -> Vec<Redir> {
330 redirs
331 .iter()
332 .map(|r| match r {
333 Redir::Write { fd, target, mode } => Redir::Write {
334 fd: *fd,
335 target: target.normalize(),
336 mode: *mode,
337 },
338 Redir::Read { fd, target } => Redir::Read {
339 fd: *fd,
340 target: target.normalize(),
341 },
342 Redir::ReadWrite { fd, target } => Redir::ReadWrite {
343 fd: *fd,
344 target: target.normalize(),
345 },
346 Redir::HereStr(w) => Redir::HereStr(w.normalize()),
347 Redir::HereDoc { .. } | Redir::DupFd { .. } => r.clone(),
348 })
349 .collect()
350}