dockerfile_parser/instructions/
run.rs

1// (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP
2
3use std::convert::TryFrom;
4
5use crate::Span;
6use crate::dockerfile_parser::Instruction;
7use crate::error::*;
8use crate::util::*;
9use crate::parser::*;
10
11/// A Dockerfile [`RUN` instruction][run].
12///
13/// An run command may be defined as either a single string (to be run in the
14/// default shell), or a list of strings (to be run directly).
15///
16/// [run]: https://docs.docker.com/engine/reference/builder/#run
17#[derive(Debug, PartialEq, Eq, Clone)]
18pub struct RunInstruction {
19  pub span: Span,
20  pub expr: ShellOrExecExpr,
21}
22
23impl RunInstruction {
24  pub(crate) fn from_record(record: Pair) -> Result<RunInstruction> {
25    let span = Span::from_pair(&record);
26    let field = record.into_inner().next().unwrap();
27
28    match field.as_rule() {
29      Rule::run_exec => Ok(RunInstruction {
30        span,
31        expr: ShellOrExecExpr::Exec(parse_string_array(field)?),
32      }),
33      Rule::run_shell => Ok(RunInstruction {
34        span,
35        expr: ShellOrExecExpr::Shell(parse_any_breakable(field)?),
36      }),
37      _ => Err(unexpected_token(field)),
38    }
39  }
40
41  /// Unpacks this instruction into its inner value if it is a Shell-form
42  /// instruction, otherwise returns None.
43  pub fn into_shell(self) -> Option<BreakableString> {
44    self.expr.into_shell()
45  }
46
47  /// Unpacks this instruction into its inner value if it is a Shell-form
48  /// instruction, otherwise returns None.
49  pub fn as_shell(&self) -> Option<&BreakableString> {
50    self.expr.as_shell()
51  }
52
53  /// Unpacks this instruction into its inner value if it is an Exec-form
54  /// instruction, otherwise returns None.
55  pub fn into_exec(self) -> Option<StringArray> {
56    self.expr.into_exec()
57  }
58
59  /// Unpacks this instruction into its inner value if it is an Exec-form
60  /// instruction, otherwise returns None.
61  pub fn as_exec(&self) -> Option<&StringArray> {
62    self.expr.as_exec()
63  }
64}
65
66impl<'a> TryFrom<&'a Instruction> for &'a RunInstruction {
67  type Error = Error;
68
69  fn try_from(instruction: &'a Instruction) -> std::result::Result<Self, Self::Error> {
70    if let Instruction::Run(r) = instruction {
71      Ok(r)
72    } else {
73      Err(Error::ConversionError {
74        from: format!("{:?}", instruction),
75        to: "RunInstruction".into()
76      })
77    }
78  }
79}
80
81#[cfg(test)]
82mod tests {
83  use indoc::indoc;
84  use pretty_assertions::assert_eq;
85
86  use super::*;
87  use crate::Span;
88  use crate::test_util::*;
89
90  #[test]
91  fn run_basic() -> Result<()> {
92    assert_eq!(
93      parse_single(r#"run echo "hello world""#, Rule::run)?
94        .as_run().unwrap()
95        .as_shell().unwrap(),
96      &BreakableString::new((4, 22))
97        .add_string((4, 22), "echo \"hello world\"")
98    );
99
100    assert_eq!(
101      parse_single(r#"run ["echo", "hello world"]"#, Rule::run)?,
102      RunInstruction {
103        span: Span::new(0, 27),
104        expr: ShellOrExecExpr::Exec(StringArray {
105          span: Span::new(4, 27),
106          elements: vec![SpannedString {
107            span: Span::new(5, 11),
108            content: "echo".to_string(),
109          }, SpannedString {
110            span: Span::new(13, 26),
111            content: "hello world".to_string(),
112          }]
113        }),
114      }.into()
115    );
116
117    Ok(())
118  }
119
120  #[test]
121  fn run_multiline_shell() -> Result<()> {
122    assert_eq!(
123      parse_single(indoc!(r#"
124        run echo \
125          "hello world"
126      "#), Rule::run)?
127        .as_run().unwrap()
128        .as_shell().unwrap(),
129      &BreakableString::new((4, 26))
130        .add_string((4, 9), "echo ")
131        .add_string((11, 26), "  \"hello world\"")
132    );
133
134    assert_eq!(
135      parse_single(indoc!(r#"
136        run echo \
137          "hello world"
138      "#), Rule::run)?
139        .as_run().unwrap()
140        .as_shell().unwrap()
141        .to_string(),
142      "echo   \"hello world\""
143    );
144
145    // whitespace should be allowed, but my editor removes trailing whitespace
146    // :)
147    assert_eq!(
148      parse_single("run echo \\    \t  \t\t\n  \"hello world\"", Rule::run)?
149        .as_run().unwrap()
150        .as_shell().unwrap()
151        .to_string(),
152      "echo   \"hello world\""
153    );
154
155    Ok(())
156  }
157
158  #[test]
159  fn run_multiline_shell_comment() -> Result<()> {
160    assert_eq!(
161      parse_single(
162        indoc!(r#"
163          run foo && \
164              # implicitly escaped
165              bar && \
166              # explicitly escaped \
167              baz
168        "#),
169        Rule::run
170      )?
171        .into_run().unwrap()
172        .into_shell().unwrap(),
173      BreakableString::new((4, 85))
174        .add_string((4, 11), "foo && ")
175        .add_comment((17, 37), "# implicitly escaped")
176        .add_string((38, 49), "    bar && ")
177        .add_comment((55, 77), "# explicitly escaped \\")
178        .add_string((78, 85), "    baz")
179    );
180
181    Ok(())
182  }
183
184  #[test]
185  fn run_multiline_shell_large() -> Result<()> {
186    // note: the trailing `\` at the end is _almost_ nonsense and generates a
187    // warning from docker
188    let ins = parse_single(
189      indoc!(r#"
190        run set -x && \
191            # lorem ipsum
192            echo "hello world" && \
193            # dolor sit amet,
194            # consectetur \
195            # adipiscing elit, \
196            # sed do eiusmod
197            # tempor incididunt ut labore
198            echo foo && \
199            echo 'bar' \
200            && echo baz \
201            # et dolore magna aliqua."#),
202      Rule::run
203    )?.into_run().unwrap().into_shell().unwrap();
204
205    assert_eq!(
206      ins,
207      BreakableString::new((4, 266))
208        .add_string((4, 14), "set -x && ")
209        .add_comment((20, 33), "# lorem ipsum")
210        .add_string((34, 60), "    echo \"hello world\" && ")
211        .add_comment((66, 83), "# dolor sit amet,")
212        .add_comment((88, 103), "# consectetur \\")
213        .add_comment((108, 128), "# adipiscing elit, \\")
214        .add_comment((133, 149), "# sed do eiusmod")
215        .add_comment((154, 183), "# tempor incididunt ut labore")
216        .add_string((184, 200), "    echo foo && ")
217        .add_string((202, 217), "    echo 'bar' ")
218        .add_string((219, 235), "    && echo baz ")
219        .add_comment((241, 266), "# et dolore magna aliqua.")
220    );
221
222    assert_eq!(
223      ins.to_string(),
224      r#"set -x &&     echo "hello world" &&     echo foo &&     echo 'bar'     && echo baz "#
225    );
226
227    Ok(())
228  }
229
230  #[test]
231  fn run_multline_exec() -> Result<()> {
232    assert_eq!(
233      parse_single(r#"run\
234        [\
235        "echo", \
236        "hello world"\
237        ]"#, Rule::run)?,
238      RunInstruction {
239        span: Span::new(0, 66),
240        expr: ShellOrExecExpr::Exec(StringArray {
241          span: Span::new(13, 66),
242          elements: vec![SpannedString {
243            span: Span::new(24, 30),
244            content: "echo".to_string(),
245          }, SpannedString {
246            span: Span::new(42, 55),
247            content: "hello world".to_string(),
248          }],
249        }),
250      }.into()
251    );
252
253    Ok(())
254  }
255
256  #[test]
257  fn run_multiline_exec_comment() -> Result<()> {
258    assert_eq!(
259      parse_single(r#"run\
260        [\
261        "echo", \
262        "hello world"\
263        ]"#, Rule::run)?,
264      RunInstruction {
265        span: Span::new(0, 66),
266        expr: ShellOrExecExpr::Exec(StringArray {
267          span: Span::new(13, 66),
268          elements: vec![SpannedString {
269            span: Span::new(24, 30),
270            content: "echo".to_string(),
271          }, SpannedString {
272            span: Span::new(42, 55),
273            content: "hello world".to_string(),
274          }],
275        })
276      }.into()
277    );
278
279    Ok(())
280  }
281}