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 DupFd {
93 src: u32,
94 dst: String,
95 },
96}
97
98pub use check::{is_safe_command, is_safe_pipeline};
99pub use parse::parse;
100
101impl Word {
102 pub fn eval(&self) -> String {
103 eval::eval_word(self)
104 }
105
106 pub fn literal(s: &str) -> Self {
107 Word(vec![WordPart::Lit(s.to_string())])
108 }
109
110 pub fn normalize(&self) -> Self {
111 let mut parts = Vec::new();
112 for part in &self.0 {
113 let part = match part {
114 WordPart::DQuote(inner) => WordPart::DQuote(inner.normalize()),
115 WordPart::CmdSub(s) => WordPart::CmdSub(s.normalize()),
116 other => other.clone(),
117 };
118 if let WordPart::Lit(s) = &part
119 && let Some(WordPart::Lit(prev)) = parts.last_mut()
120 {
121 prev.push_str(s);
122 continue;
123 }
124 parts.push(part);
125 }
126 Word(parts)
127 }
128}
129
130impl Script {
131 pub fn is_empty(&self) -> bool {
132 self.0.is_empty()
133 }
134
135 pub fn normalize(&self) -> Self {
136 Script(
137 self.0
138 .iter()
139 .map(|stmt| Stmt {
140 pipeline: stmt.pipeline.normalize(),
141 op: stmt.op,
142 })
143 .collect(),
144 )
145 }
146
147 pub fn normalize_as_body(&self) -> Self {
148 let mut s = self.normalize();
149 if let Some(last) = s.0.last_mut()
150 && last.op.is_none()
151 {
152 last.op = Some(ListOp::Semi);
153 }
154 s
155 }
156}
157
158impl Pipeline {
159 fn normalize(&self) -> Self {
160 Pipeline {
161 bang: self.bang,
162 commands: self.commands.iter().map(|c| c.normalize()).collect(),
163 }
164 }
165}
166
167impl Cmd {
168 fn normalize(&self) -> Self {
169 match self {
170 Cmd::Simple(s) => Cmd::Simple(s.normalize()),
171 Cmd::Subshell(s) => Cmd::Subshell(s.normalize()),
172 Cmd::For { var, items, body } => Cmd::For {
173 var: var.clone(),
174 items: items.iter().map(|w| w.normalize()).collect(),
175 body: body.normalize_as_body(),
176 },
177 Cmd::While { cond, body } => Cmd::While {
178 cond: cond.normalize_as_body(),
179 body: body.normalize_as_body(),
180 },
181 Cmd::Until { cond, body } => Cmd::Until {
182 cond: cond.normalize_as_body(),
183 body: body.normalize_as_body(),
184 },
185 Cmd::If { branches, else_body } => Cmd::If {
186 branches: branches
187 .iter()
188 .map(|b| Branch {
189 cond: b.cond.normalize_as_body(),
190 body: b.body.normalize_as_body(),
191 })
192 .collect(),
193 else_body: else_body.as_ref().map(|e| e.normalize_as_body()),
194 },
195 }
196 }
197}
198
199impl SimpleCmd {
200 fn normalize(&self) -> Self {
201 SimpleCmd {
202 env: self
203 .env
204 .iter()
205 .map(|(k, v)| (k.clone(), v.normalize()))
206 .collect(),
207 words: self.words.iter().map(|w| w.normalize()).collect(),
208 redirs: self
209 .redirs
210 .iter()
211 .map(|r| match r {
212 Redir::Write { fd, target, append } => Redir::Write {
213 fd: *fd,
214 target: target.normalize(),
215 append: *append,
216 },
217 Redir::Read { fd, target } => Redir::Read {
218 fd: *fd,
219 target: target.normalize(),
220 },
221 Redir::HereStr(w) => Redir::HereStr(w.normalize()),
222 other => other.clone(),
223 })
224 .collect(),
225 }
226 }
227}