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