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(Word),
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Eq)]
130pub enum WriteMode {
131 Truncate,
133 Append,
135 TruncateBoth,
139 AppendBoth,
141 Clobber,
143}
144
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum Redir {
147 Write {
148 fd: u32,
149 target: Word,
150 mode: WriteMode,
151 },
152 Read {
153 fd: u32,
154 target: Word,
155 },
156 ReadWrite {
160 fd: u32,
161 target: Word,
162 },
163 HereStr(Word),
164 HereDoc {
165 delimiter: String,
166 strip_tabs: bool,
167 body: Word,
172 },
173 DupFd {
174 src: u32,
175 dst: String,
176 },
177}
178
179pub use check::{command_verdict, is_safe_command, is_safe_pipeline};
180pub use explain::{Explanation, SegmentReport, explain, explain_with_coverage};
181pub use parse::parse;
182
183impl Word {
184 pub fn eval(&self) -> String {
185 eval::eval_word(self)
186 }
187
188 pub fn expand(&self) -> Vec<String> {
192 eval::expand_word(self)
193 }
194
195 pub fn literal(s: &str) -> Self {
196 Word(vec![WordPart::Lit(s.to_string())])
197 }
198
199 pub fn normalize(&self) -> Self {
200 let mut parts = Vec::new();
201 for part in &self.0 {
202 let part = match part {
203 WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
204 WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
205 WordPart::ProcSub(s) => WordPart::ProcSub(s.normalize()),
206 other => other.clone(),
207 };
208 if let WordPart::Lit(s) = &part
209 && let Some(WordPart::Lit(prev)) = parts.last_mut()
210 {
211 prev.push_str(s);
212 continue;
213 }
214 parts.push(part);
215 }
216 Word(parts)
217 }
218}
219
220impl Script {
221 pub fn is_empty(&self) -> bool {
222 self.0.is_empty()
223 }
224
225 pub fn normalize(&self) -> Self {
226 Script(
227 self.0
228 .iter()
229 .map(|stmt| Stmt {
230 pipeline: stmt.pipeline.normalize(),
231 op: stmt.op,
232 })
233 .collect(),
234 )
235 }
236
237 pub fn normalize_as_body(&self) -> Self {
238 let mut s = self.normalize();
239 if let Some(last) = s.0.last_mut()
240 && last.op.is_none()
241 {
242 last.op = Some(ListOp::Semi);
243 }
244 s
245 }
246}
247
248impl Pipeline {
249 fn normalize(&self) -> Self {
250 Pipeline {
251 bang: self.bang,
252 commands: self.commands.iter().map(|c| c.normalize()).collect(),
253 }
254 }
255}
256
257impl Cmd {
258 fn normalize(&self) -> Self {
259 match self {
260 Cmd::Simple(s) => Cmd::Simple(s.normalize()),
261 Cmd::Subshell { body, redirs } => Cmd::Subshell {
262 body: body.normalize(),
263 redirs: normalize_redirs(redirs),
264 },
265 Cmd::BraceGroup { body, redirs } => Cmd::BraceGroup {
266 body: body.normalize_as_body(),
267 redirs: normalize_redirs(redirs),
268 },
269 Cmd::For { var, items, body, redirs } => Cmd::For {
270 var: var.clone(),
271 items: items.iter().map(|w| w.normalize()).collect(),
272 body: body.normalize_as_body(),
273 redirs: normalize_redirs(redirs),
274 },
275 Cmd::While { cond, body, redirs } => Cmd::While {
276 cond: cond.normalize_as_body(),
277 body: body.normalize_as_body(),
278 redirs: normalize_redirs(redirs),
279 },
280 Cmd::Until { cond, body, redirs } => Cmd::Until {
281 cond: cond.normalize_as_body(),
282 body: body.normalize_as_body(),
283 redirs: normalize_redirs(redirs),
284 },
285 Cmd::If { branches, else_body, redirs } => Cmd::If {
286 branches: branches
287 .iter()
288 .map(|b| Branch {
289 cond: b.cond.normalize_as_body(),
290 body: b.body.normalize_as_body(),
291 })
292 .collect(),
293 else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
294 redirs: normalize_redirs(redirs),
295 },
296 Cmd::DoubleBracket { words, redirs } => Cmd::DoubleBracket {
297 words: words.iter().map(|w| w.normalize()).collect(),
298 redirs: normalize_redirs(redirs),
299 },
300 Cmd::Case { subject, arms, redirs } => Cmd::Case {
301 subject: subject.normalize(),
302 arms: arms
303 .iter()
304 .map(|a| CaseArm {
305 patterns: a.patterns.iter().map(|w| w.normalize()).collect(),
306 body: a.body.normalize_as_body(),
307 })
308 .collect(),
309 redirs: normalize_redirs(redirs),
310 },
311 Cmd::FunctionDef { name, body } => Cmd::FunctionDef {
312 name: name.clone(),
313 body: body.normalize_as_body(),
314 },
315 }
316 }
317}
318
319impl SimpleCmd {
320 fn normalize(&self) -> Self {
321 SimpleCmd {
322 env: self
323 .env
324 .iter()
325 .map(|(k, v)| (k.clone(), v.normalize()))
326 .collect(),
327 words: self.words.iter().map(|w| w.normalize()).collect(),
328 redirs: normalize_redirs(&self.redirs),
329 }
330 }
331}
332
333fn normalize_redirs(redirs: &[Redir]) -> Vec<Redir> {
334 redirs
335 .iter()
336 .map(|r| match r {
337 Redir::Write { fd, target, mode } => Redir::Write {
338 fd: *fd,
339 target: target.normalize(),
340 mode: *mode,
341 },
342 Redir::Read { fd, target } => Redir::Read {
343 fd: *fd,
344 target: target.normalize(),
345 },
346 Redir::ReadWrite { fd, target } => Redir::ReadWrite {
347 fd: *fd,
348 target: target.normalize(),
349 },
350 Redir::HereStr(w) => Redir::HereStr(w.normalize()),
351 Redir::HereDoc { .. } | Redir::DupFd { .. } => r.clone(),
352 })
353 .collect()
354}