dockerfile_parser/instructions/
env.rs

1// (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP
2
3use std::convert::TryFrom;
4
5use crate::dockerfile_parser::Instruction;
6use crate::Span;
7use crate::error::*;
8use crate::parser::{Pair, Rule};
9use crate::util::*;
10
11use enquote::unquote;
12use snafu::ResultExt;
13
14/// An environment variable key/value pair
15#[derive(Debug, PartialEq, Eq, Clone)]
16pub struct EnvVar {
17  pub span: Span,
18  pub key: SpannedString,
19  pub value: BreakableString,
20}
21
22impl EnvVar {
23  pub fn new(span: Span, key: SpannedString, value: impl Into<BreakableString>) -> Self {
24    EnvVar {
25      span,
26      key: key,
27      value: value.into(),
28    }
29  }
30}
31
32/// A Dockerfile [`ENV` instruction][env].
33///
34/// [env]: https://docs.docker.com/engine/reference/builder/#env
35#[derive(Debug, PartialEq, Eq, Clone)]
36pub struct EnvInstruction {
37  pub span: Span,
38  pub vars: Vec<EnvVar>
39}
40
41/// Parses an env pair token, e.g. key=value or key="value"
42fn parse_env_pair(record: Pair) -> Result<EnvVar> {
43  let span = Span::from_pair(&record);
44  let mut key = None;
45  let mut value = None;
46
47  for field in record.into_inner() {
48    match field.as_rule() {
49      Rule::env_name => key = Some(parse_string(&field)?),
50      Rule::env_pair_value => {
51        value = Some(
52          BreakableString::new(&field).add_string(&field, field.as_str())
53        );
54      },
55      Rule::env_pair_quoted_value => {
56        let v = unquote(field.as_str()).context(UnescapeError)?;
57
58        value = Some(
59          BreakableString::new(&field).add_string(&field, v)
60        );
61      },
62      _ => return Err(unexpected_token(field))
63    }
64  }
65
66  let key = key.ok_or_else(|| Error::GenericParseError {
67    message: "env pair requires a key".into()
68  })?;
69
70  let value = value.ok_or_else(|| Error::GenericParseError {
71    message: "env pair requires a value".into()
72  })?;
73
74  Ok(EnvVar {
75    span,
76    key,
77    value,
78  })
79}
80
81impl EnvInstruction {
82  pub(crate) fn from_record(record: Pair) -> Result<EnvInstruction> {
83    let span = Span::from_pair(&record);
84    let field = record.into_inner().next().unwrap();
85
86    match field.as_rule() {
87      Rule::env_single => EnvInstruction::from_single_record(span, field),
88      Rule::env_pairs => EnvInstruction::from_pairs_record(span, field),
89      _ => Err(unexpected_token(field)),
90    }
91  }
92
93  fn from_pairs_record(span: Span, record: Pair) -> Result<EnvInstruction> {
94    let mut vars = Vec::new();
95
96    for field in record.into_inner() {
97      match field.as_rule() {
98        Rule::env_pair => vars.push(parse_env_pair(field)?),
99        Rule::comment => continue,
100        _ => return Err(unexpected_token(field))
101      }
102    }
103
104    Ok(EnvInstruction {
105      span,
106      vars,
107    })
108  }
109
110  fn from_single_record(span: Span, record: Pair) -> Result<EnvInstruction> {
111    let mut key = None;
112    let mut value = None;
113
114    for field in record.into_inner() {
115      match field.as_rule() {
116        Rule::env_name => key = Some(parse_string(&field)?),
117        Rule::env_single_value => value = Some(parse_any_breakable(field)?),
118        Rule::env_single_quoted_value => {
119          let v = unquote(field.as_str()).context(UnescapeError)?;
120
121          value = Some(
122            BreakableString::new(&field).add_string(&field, v)
123          );
124        },
125        Rule::comment => continue,
126        _ => return Err(unexpected_token(field))
127      }
128    }
129
130    let key = key.ok_or_else(|| Error::GenericParseError {
131      message: "env requires a key".into()
132    })?;
133
134    let value = value.ok_or_else(|| Error::GenericParseError {
135        message: "env requires a value".into()
136    })?;
137
138    Ok(EnvInstruction {
139      span,
140      vars: vec![EnvVar {
141        span: Span::new(key.span.start, value.span.end),
142        key,
143        value,
144      }],
145    })
146  }
147}
148
149impl<'a> TryFrom<&'a Instruction> for &'a EnvInstruction {
150  type Error = Error;
151
152  fn try_from(instruction: &'a Instruction) -> std::result::Result<Self, Self::Error> {
153    if let Instruction::Env(e) = instruction {
154      Ok(e)
155    } else {
156      Err(Error::ConversionError {
157        from: format!("{:?}", instruction),
158        to: "EnvInstruction".into()
159      })
160    }
161  }
162}
163
164#[cfg(test)]
165mod tests {
166  use indoc::indoc;
167  use pretty_assertions::assert_eq;
168
169  use super::*;
170  use crate::Dockerfile;
171  use crate::test_util::*;
172
173  #[test]
174  fn env() -> Result<()> {
175    assert_eq!(
176      parse_single(r#"env foo=bar"#, Rule::env)?.into_env().unwrap(),
177      EnvInstruction {
178        span: Span::new(0, 11),
179        vars: vec![EnvVar::new(
180          Span::new(4, 11),
181          SpannedString {
182            span: Span::new(4, 7),
183            content: "foo".to_string(),
184          },
185          ((8, 11), "bar"),
186        )],
187      }
188    );
189
190    assert_eq!(
191      parse_single(r#"env FOO_BAR="baz""#, Rule::env)?,
192      EnvInstruction {
193        span: Span::new(0, 17),
194        vars: vec![EnvVar::new(
195          Span::new(4, 17),
196          SpannedString {
197            span: Span::new(4, 11),
198            content: "FOO_BAR".to_string(),
199          },
200          ((12, 17), "baz"),
201        )],
202      }.into()
203    );
204
205    assert_eq!(
206      parse_single(r#"env FOO_BAR "baz""#, Rule::env)?,
207      EnvInstruction {
208        span: Span::new(0, 17),
209        vars: vec![EnvVar::new(
210          Span::new(4, 17),
211          SpannedString {
212            span: Span::new(4, 11),
213            content: "FOO_BAR".to_string(),
214          },
215          ((12, 17), "baz")),
216        ],
217      }.into()
218    );
219
220    assert_eq!(
221      parse_single(r#"env foo="bar\"baz""#, Rule::env)?,
222      EnvInstruction {
223        span: Span::new(0, 18),
224        vars: vec![EnvVar::new(
225          Span::new(4, 18),
226          SpannedString {
227            span: Span::new(4, 7),
228            content: "foo".to_string(),
229          },
230          ((8, 18), "bar\"baz"),
231        )],
232      }.into()
233    );
234
235    assert_eq!(
236      parse_single(r#"env foo='bar'"#, Rule::env)?,
237      EnvInstruction {
238        span: Span::new(0, 13),
239        vars: vec![EnvVar::new(
240          Span::new(4, 13),
241          SpannedString {
242            span: Span::new(4, 7),
243            content: "foo".to_string(),
244          },
245          ((8, 13), "bar"),
246        )],
247      }.into()
248    );
249
250    assert_eq!(
251      parse_single(r#"env foo='bar\'baz'"#, Rule::env)?,
252      EnvInstruction {
253        span: Span::new(0, 18),
254        vars: vec![EnvVar::new(
255          Span::new(4, 18),
256          SpannedString {
257            span: Span::new(4, 7),
258            content: "foo".to_string(),
259          },
260          ((8, 18), "bar'baz"),
261        )],
262      }.into()
263    );
264
265    assert_eq!(
266      parse_single(r#"env foo="123" bar='456' baz=789"#, Rule::env)?,
267      EnvInstruction {
268        span: Span::new(0, 31),
269        vars: vec![
270          EnvVar::new(
271            Span::new(4, 13),
272            SpannedString {
273              span: Span::new(4, 7),
274              content: "foo".to_string(),
275            },
276            ((8, 13), "123")
277          ),
278          EnvVar::new(
279            Span::new(14, 23),
280            SpannedString {
281              span: Span::new(14, 17),
282              content: "bar".to_string(),
283            },
284            ((18, 23), "456")
285          ),
286          EnvVar::new(
287            Span::new(24, 31),
288            SpannedString {
289              span: Span::new(24, 27),
290              content: "baz".to_string(),
291            },
292            ((28, 31), "789")
293          ),
294        ],
295      }.into()
296    );
297
298    assert!(Dockerfile::parse(r#"env foo="bar"bar"#).is_err());
299    assert!(Dockerfile::parse(r#"env foo='bar'bar"#).is_err());
300
301    Ok(())
302  }
303
304  #[test]
305  fn test_multiline_pairs() -> Result<()> {
306    // note: docker allows empty line continuations (but may print a warning)
307    assert_eq!(
308      parse_single(
309        indoc!(r#"
310          env foo=a \
311            bar=b \
312            baz=c \
313
314        "#),
315        Rule::env
316      )?.into_env().unwrap().vars,
317      vec![
318        EnvVar::new(
319          Span::new(4, 9),
320          SpannedString {
321            span: Span::new(4, 7),
322            content: "foo".to_string(),
323          },
324          ((8, 9), "a")
325        ),
326        EnvVar::new(
327          Span::new(14, 19),
328          SpannedString {
329            span: Span::new(14, 17),
330            content: "bar".to_string(),
331          },
332          ((18, 19), "b")
333        ),
334        EnvVar::new(
335          Span::new(24, 29),
336          SpannedString {
337            span: Span::new(24, 27),
338            content: "baz".to_string(),
339          },
340          ((28, 29), "c")
341        )
342      ]
343    );
344
345    Ok(())
346  }
347
348  #[test]
349  fn test_multiline_single_env() -> Result<()> {
350    assert_eq!(
351      parse_single(
352        indoc!(r#"
353          env foo Lorem ipsum dolor sit amet, \
354            consectetur adipiscing elit, \
355            sed do eiusmod tempor incididunt ut \
356            labore et dolore magna aliqua.
357        "#),
358        Rule::env
359      )?.into_env().unwrap().vars,
360      vec![
361        EnvVar::new(
362          Span::new(4, 143),
363          SpannedString {
364            span: Span::new(4, 7),
365            content: "foo".to_string(),
366          },
367          BreakableString::new((8, 143))
368            .add_string((8, 36), "Lorem ipsum dolor sit amet, ")
369            .add_string((38, 69), "  consectetur adipiscing elit, ")
370            .add_string((71, 109), "  sed do eiusmod tempor incididunt ut ")
371            .add_string((111, 143), "  labore et dolore magna aliqua.")
372        )
373      ]
374    );
375
376    // note: maybe a small bug here, leading whitespace on the first value line
377    // is eaten (this will hopefully never matter...)
378    assert_eq!(
379      parse_single(
380        indoc!(r#"
381          env \
382            foo \
383            Lorem ipsum dolor sit amet, \
384            consectetur adipiscing elit
385        "#),
386        Rule::env
387      )?.into_env().unwrap().vars,
388      vec![
389        EnvVar::new(
390          Span::new(8, 75),
391          SpannedString {
392            span: Span::new(8, 11),
393            content: "foo".to_string(),
394          },
395          BreakableString::new((16, 75))
396            .add_string((16, 44), "Lorem ipsum dolor sit amet, ")
397            .add_string((46, 75), "  consectetur adipiscing elit")
398        )
399      ]
400    );
401
402    assert_eq!(
403      parse_single(
404        indoc!(r#"
405          env \
406            foo \
407            # bar
408            Lorem ipsum dolor sit amet, \
409            # baz
410            consectetur adipiscing elit
411        "#),
412        Rule::env
413      )?.into_env().unwrap().vars,
414      vec![
415        EnvVar::new(
416          Span::new(8, 91),
417          SpannedString {
418            span: Span::new(8, 11),
419            content: "foo".to_string(),
420          },
421          BreakableString::new((24, 91))
422            .add_string((24, 52), "Lorem ipsum dolor sit amet, ")
423            .add_comment((56, 61), "# baz")
424            .add_string((62, 91), "  consectetur adipiscing elit")
425        )
426      ]
427    );
428
429    Ok(())
430  }
431}