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