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