1pub(crate) mod check;
2mod display;
3mod 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 },
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 explain::{Explanation, SegmentReport, explain, explain_with_coverage};
118pub use parse::parse;
119
120impl Word {
121 pub fn eval(&self) -> String {
122 eval::eval_word(self)
123 }
124
125 pub fn literal(s: &str) -> Self {
126 Word(vec![WordPart::Lit(s.to_string())])
127 }
128
129 pub fn normalize(&self) -> Self {
130 let mut parts = Vec::new();
131 for part in &self.0 {
132 let part = match part {
133 WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
134 WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
135 WordPart::ProcSub(s) => WordPart::ProcSub(s.normalize()),
136 other => other.clone(),
137 };
138 if let WordPart::Lit(s) = &part
139 && let Some(WordPart::Lit(prev)) = parts.last_mut()
140 {
141 prev.push_str(s);
142 continue;
143 }
144 parts.push(part);
145 }
146 Word(parts)
147 }
148}
149
150impl Script {
151 pub fn is_empty(&self) -> bool {
152 self.0.is_empty()
153 }
154
155 pub fn normalize(&self) -> Self {
156 Script(
157 self.0
158 .iter()
159 .map(|stmt| Stmt {
160 pipeline: stmt.pipeline.normalize(),
161 op: stmt.op,
162 })
163 .collect(),
164 )
165 }
166
167 pub fn normalize_as_body(&self) -> Self {
168 let mut s = self.normalize();
169 if let Some(last) = s.0.last_mut()
170 && last.op.is_none()
171 {
172 last.op = Some(ListOp::Semi);
173 }
174 s
175 }
176}
177
178impl Pipeline {
179 fn normalize(&self) -> Self {
180 Pipeline {
181 bang: self.bang,
182 commands: self.commands.iter().map(|c| c.normalize()).collect(),
183 }
184 }
185}
186
187impl Cmd {
188 fn normalize(&self) -> Self {
189 match self {
190 Cmd::Simple(s) => Cmd::Simple(s.normalize()),
191 Cmd::Subshell { body, redirs } => Cmd::Subshell {
192 body: body.normalize(),
193 redirs: normalize_redirs(redirs),
194 },
195 Cmd::BraceGroup { body, redirs } => Cmd::BraceGroup {
196 body: body.normalize_as_body(),
197 redirs: normalize_redirs(redirs),
198 },
199 Cmd::For { var, items, body } => Cmd::For {
200 var: var.clone(),
201 items: items.iter().map(|w| w.normalize()).collect(),
202 body: body.normalize_as_body(),
203 },
204 Cmd::While { cond, body } => Cmd::While {
205 cond: cond.normalize_as_body(),
206 body: body.normalize_as_body(),
207 },
208 Cmd::Until { cond, body } => Cmd::Until {
209 cond: cond.normalize_as_body(),
210 body: body.normalize_as_body(),
211 },
212 Cmd::If { branches, else_body } => Cmd::If {
213 branches: branches
214 .iter()
215 .map(|b| Branch {
216 cond: b.cond.normalize_as_body(),
217 body: b.body.normalize_as_body(),
218 })
219 .collect(),
220 else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
221 },
222 Cmd::DoubleBracket { words, redirs } => Cmd::DoubleBracket {
223 words: words.iter().map(|w| w.normalize()).collect(),
224 redirs: normalize_redirs(redirs),
225 },
226 }
227 }
228}
229
230impl SimpleCmd {
231 fn normalize(&self) -> Self {
232 SimpleCmd {
233 env: self
234 .env
235 .iter()
236 .map(|(k, v)| (k.clone(), v.normalize()))
237 .collect(),
238 words: self.words.iter().map(|w| w.normalize()).collect(),
239 redirs: normalize_redirs(&self.redirs),
240 }
241 }
242}
243
244fn normalize_redirs(redirs: &[Redir]) -> Vec<Redir> {
245 redirs
246 .iter()
247 .map(|r| match r {
248 Redir::Write { fd, target, append } => Redir::Write {
249 fd: *fd,
250 target: target.normalize(),
251 append: *append,
252 },
253 Redir::Read { fd, target } => Redir::Read {
254 fd: *fd,
255 target: target.normalize(),
256 },
257 Redir::HereStr(w) => Redir::HereStr(w.normalize()),
258 Redir::HereDoc { .. } | Redir::DupFd { .. } => r.clone(),
259 })
260 .collect()
261}