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 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 FunctionDef {
72 name: String,
73 body: Script,
74 },
75}
76
77#[derive(Debug, Clone, PartialEq, Eq)]
78pub struct Branch {
79 pub cond: Script,
80 pub body: Script,
81}
82
83#[derive(Debug, Clone, PartialEq, Eq)]
84pub struct SimpleCmd {
85 pub env: Vec<(String, Word)>,
86 pub words: Vec<Word>,
87 pub redirs: Vec<Redir>,
88}
89
90#[derive(Debug, Clone, PartialEq, Eq)]
91pub struct Word(pub Vec<WordPart>);
92
93#[derive(Debug, Clone, PartialEq, Eq)]
94pub enum WordPart {
95 Lit(String),
96 Escape(char),
97 SQuote(String),
98 DQuote(Word),
99 CmdSub(Script),
100 ProcSub(Script),
101 Backtick(String),
102 Arith(String),
103}
104
105#[derive(Debug, Clone, PartialEq, Eq)]
106pub enum Redir {
107 Write {
108 fd: u32,
109 target: Word,
110 append: bool,
111 },
112 Read {
113 fd: u32,
114 target: Word,
115 },
116 HereStr(Word),
117 HereDoc {
118 delimiter: String,
119 strip_tabs: bool,
120 },
121 DupFd {
122 src: u32,
123 dst: String,
124 },
125}
126
127pub use check::{command_verdict, is_safe_command, is_safe_pipeline};
128pub use explain::{Explanation, SegmentReport, explain, explain_with_coverage};
129pub use parse::parse;
130
131impl Word {
132 pub fn eval(&self) -> String {
133 eval::eval_word(self)
134 }
135
136 pub fn expand(&self) -> Vec<String> {
140 eval::expand_word(self)
141 }
142
143 pub fn literal(s: &str) -> Self {
144 Word(vec![WordPart::Lit(s.to_string())])
145 }
146
147 pub fn normalize(&self) -> Self {
148 let mut parts = Vec::new();
149 for part in &self.0 {
150 let part = match part {
151 WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
152 WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
153 WordPart::ProcSub(s) => WordPart::ProcSub(s.normalize()),
154 other => other.clone(),
155 };
156 if let WordPart::Lit(s) = &part
157 && let Some(WordPart::Lit(prev)) = parts.last_mut()
158 {
159 prev.push_str(s);
160 continue;
161 }
162 parts.push(part);
163 }
164 Word(parts)
165 }
166}
167
168impl Script {
169 pub fn is_empty(&self) -> bool {
170 self.0.is_empty()
171 }
172
173 pub fn normalize(&self) -> Self {
174 Script(
175 self.0
176 .iter()
177 .map(|stmt| Stmt {
178 pipeline: stmt.pipeline.normalize(),
179 op: stmt.op,
180 })
181 .collect(),
182 )
183 }
184
185 pub fn normalize_as_body(&self) -> Self {
186 let mut s = self.normalize();
187 if let Some(last) = s.0.last_mut()
188 && last.op.is_none()
189 {
190 last.op = Some(ListOp::Semi);
191 }
192 s
193 }
194}
195
196impl Pipeline {
197 fn normalize(&self) -> Self {
198 Pipeline {
199 bang: self.bang,
200 commands: self.commands.iter().map(|c| c.normalize()).collect(),
201 }
202 }
203}
204
205impl Cmd {
206 fn normalize(&self) -> Self {
207 match self {
208 Cmd::Simple(s) => Cmd::Simple(s.normalize()),
209 Cmd::Subshell { body, redirs } => Cmd::Subshell {
210 body: body.normalize(),
211 redirs: normalize_redirs(redirs),
212 },
213 Cmd::BraceGroup { body, redirs } => Cmd::BraceGroup {
214 body: body.normalize_as_body(),
215 redirs: normalize_redirs(redirs),
216 },
217 Cmd::For { var, items, body, redirs } => Cmd::For {
218 var: var.clone(),
219 items: items.iter().map(|w| w.normalize()).collect(),
220 body: body.normalize_as_body(),
221 redirs: normalize_redirs(redirs),
222 },
223 Cmd::While { cond, body, redirs } => Cmd::While {
224 cond: cond.normalize_as_body(),
225 body: body.normalize_as_body(),
226 redirs: normalize_redirs(redirs),
227 },
228 Cmd::Until { cond, body, redirs } => Cmd::Until {
229 cond: cond.normalize_as_body(),
230 body: body.normalize_as_body(),
231 redirs: normalize_redirs(redirs),
232 },
233 Cmd::If { branches, else_body, redirs } => Cmd::If {
234 branches: branches
235 .iter()
236 .map(|b| Branch {
237 cond: b.cond.normalize_as_body(),
238 body: b.body.normalize_as_body(),
239 })
240 .collect(),
241 else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
242 redirs: normalize_redirs(redirs),
243 },
244 Cmd::DoubleBracket { words, redirs } => Cmd::DoubleBracket {
245 words: words.iter().map(|w| w.normalize()).collect(),
246 redirs: normalize_redirs(redirs),
247 },
248 Cmd::FunctionDef { name, body } => Cmd::FunctionDef {
249 name: name.clone(),
250 body: body.normalize_as_body(),
251 },
252 }
253 }
254}
255
256impl SimpleCmd {
257 fn normalize(&self) -> Self {
258 SimpleCmd {
259 env: self
260 .env
261 .iter()
262 .map(|(k, v)| (k.clone(), v.normalize()))
263 .collect(),
264 words: self.words.iter().map(|w| w.normalize()).collect(),
265 redirs: normalize_redirs(&self.redirs),
266 }
267 }
268}
269
270fn normalize_redirs(redirs: &[Redir]) -> Vec<Redir> {
271 redirs
272 .iter()
273 .map(|r| match r {
274 Redir::Write { fd, target, append } => Redir::Write {
275 fd: *fd,
276 target: target.normalize(),
277 append: *append,
278 },
279 Redir::Read { fd, target } => Redir::Read {
280 fd: *fd,
281 target: target.normalize(),
282 },
283 Redir::HereStr(w) => Redir::HereStr(w.normalize()),
284 Redir::HereDoc { .. } | Redir::DupFd { .. } => r.clone(),
285 })
286 .collect()
287}