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