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