dockerfile_parser/
dockerfile_parser.rs

1// (C) Copyright 2019-2020 Hewlett Packard Enterprise Development LP
2
3use std::convert::TryFrom;
4use std::io::{Read, BufReader};
5use std::str::FromStr;
6
7use pest::Parser;
8use snafu::ResultExt;
9
10pub use crate::image::*;
11pub use crate::error::*;
12pub use crate::parser::*;
13pub use crate::instructions::*;
14pub use crate::splicer::*;
15pub use crate::stage::*;
16
17/// A single Dockerfile instruction.
18///
19/// Individual instructions structures may be unpacked with pattern matching or
20/// via the `TryFrom` impls on each instruction type.
21///
22/// # Example
23///
24/// ```
25/// use std::convert::TryInto;
26/// use dockerfile_parser::*;
27///
28/// let dockerfile = Dockerfile::parse("FROM alpine:3.11").unwrap();
29/// let from: &FromInstruction = dockerfile.instructions
30///   .get(0).unwrap()
31///   .try_into().unwrap();
32///
33/// assert_eq!(from.image_parsed.tag, Some("3.11".to_string()));
34/// ```
35#[derive(Debug, PartialEq, Eq, Clone)]
36pub enum Instruction {
37  From(FromInstruction),
38  Arg(ArgInstruction),
39  Label(LabelInstruction),
40  Run(RunInstruction),
41  Entrypoint(EntrypointInstruction),
42  Cmd(CmdInstruction),
43  Copy(CopyInstruction),
44  Env(EnvInstruction),
45  Misc(MiscInstruction)
46}
47
48impl Instruction {
49  /// Attempts to convert this instruction into a FromInstruction, returning
50  /// None if impossible.
51  pub fn into_from(self) -> Option<FromInstruction> {
52    match self {
53      Instruction::From(f) => Some(f),
54      _ => None,
55    }
56  }
57
58  /// Attempts to convert this instruction into a FromInstruction, returning
59  /// None if impossible.
60  pub fn as_from(&self) -> Option<&FromInstruction> {
61    match self {
62      Instruction::From(f) => Some(f),
63      _ => None,
64    }
65  }
66
67  /// Attempts to convert this instruction into an ArgInstruction, returning
68  /// None if impossible.
69  pub fn into_arg(self) -> Option<ArgInstruction> {
70    match self {
71      Instruction::Arg(a) => Some(a),
72      _ => None,
73    }
74  }
75
76  /// Attempts to convert this instruction into an ArgInstruction, returning
77  /// None if impossible.
78  pub fn as_arg(&self) -> Option<&ArgInstruction> {
79    match self {
80      Instruction::Arg(a) => Some(a),
81      _ => None,
82    }
83  }
84
85  /// Attempts to convert this instruction into a LabelInstruction, returning
86  /// None if impossible.
87  pub fn into_label(self) -> Option<LabelInstruction> {
88    match self {
89      Instruction::Label(l) => Some(l),
90      _ => None,
91    }
92  }
93
94  /// Attempts to convert this instruction into a LabelInstruction, returning
95  /// None if impossible.
96  pub fn as_label(&self) -> Option<&LabelInstruction> {
97    match self {
98      Instruction::Label(l) => Some(l),
99      _ => None,
100    }
101  }
102
103  /// Attempts to convert this instruction into a RunInstruction, returning
104  /// None if impossible.
105  pub fn into_run(self) -> Option<RunInstruction> {
106    match self {
107      Instruction::Run(r) => Some(r),
108      _ => None,
109    }
110  }
111
112  /// Attempts to convert this instruction into a RunInstruction, returning
113  /// None if impossible.
114  pub fn as_run(&self) -> Option<&RunInstruction> {
115    match self {
116      Instruction::Run(r) => Some(r),
117      _ => None,
118    }
119  }
120
121  /// Attempts to convert this instruction into an EntrypointInstruction,
122  /// returning None if impossible.
123  pub fn into_entrypoint(self) -> Option<EntrypointInstruction> {
124    match self {
125      Instruction::Entrypoint(e) => Some(e),
126      _ => None,
127    }
128  }
129
130  /// Attempts to convert this instruction into an EntrypointInstruction,
131  /// returning None if impossible.
132  pub fn as_entrypoint(&self) -> Option<&EntrypointInstruction> {
133    match self {
134      Instruction::Entrypoint(e) => Some(e),
135      _ => None,
136    }
137  }
138
139  /// Attempts to convert this instruction into a CmdInstruction, returning
140  /// None if impossible.
141  pub fn into_cmd(self) -> Option<CmdInstruction> {
142    match self {
143      Instruction::Cmd(c) => Some(c),
144      _ => None,
145    }
146  }
147
148  /// Attempts to convert this instruction into a CmdInstruction, returning
149  /// None if impossible.
150  pub fn as_cmd(&self) -> Option<&CmdInstruction> {
151    match self {
152      Instruction::Cmd(c) => Some(c),
153      _ => None,
154    }
155  }
156
157  /// Attempts to convert this instruction into a CopyInstruction, returning
158  /// None if impossible.
159  pub fn into_copy(self) -> Option<CopyInstruction> {
160    match self {
161      Instruction::Copy(c) => Some(c),
162      _ => None,
163    }
164  }
165
166  /// Attempts to convert this instruction into a CopyInstruction, returning
167  /// None if impossible.
168  pub fn as_copy(&self) -> Option<&CopyInstruction> {
169    match self {
170      Instruction::Copy(c) => Some(c),
171      _ => None,
172    }
173  }
174
175  /// Attempts to convert this instruction into an EnvInstruction, returning
176  /// None if impossible.
177  pub fn into_env(self) -> Option<EnvInstruction> {
178    match self {
179      Instruction::Env(e) => Some(e),
180      _ => None,
181    }
182  }
183
184  /// Attempts to convert this instruction into an EnvInstruction, returning
185  /// None if impossible.
186  pub fn as_env(&self) -> Option<&EnvInstruction> {
187    match self {
188      Instruction::Env(e) => Some(e),
189      _ => None,
190    }
191  }
192
193  /// Attempts to convert this instruction into a MiscInstruction, returning
194  /// None if impossible.
195  pub fn into_misc(self) -> Option<MiscInstruction> {
196    match self {
197      Instruction::Misc(m) => Some(m),
198      _ => None,
199    }
200  }
201
202  /// Attempts to convert this instruction into a MiscInstruction, returning
203  /// None if impossible.
204  pub fn as_misc(&self) -> Option<&MiscInstruction> {
205    match self {
206      Instruction::Misc(m) => Some(m),
207      _ => None,
208    }
209  }
210
211  /// Gets the span of the instruction.
212  pub fn span(&self) -> Span {
213    match self {
214      Instruction::From(instruction) => instruction.span,
215      Instruction::Arg(instruction) => instruction.span,
216      Instruction::Label(instruction) => instruction.span,
217      Instruction::Run(instruction) => instruction.span,
218      Instruction::Entrypoint(instruction) => instruction.span,
219      Instruction::Cmd(instruction) => instruction.span,
220      Instruction::Copy(instruction) => instruction.span,
221      Instruction::Env(instruction) => instruction.span,
222      Instruction::Misc(instruction) => instruction.span,
223    }
224  }
225}
226
227/// Maps an instruction struct to its enum variant, implementing From<T> on
228/// Instruction for it.
229macro_rules! impl_from_instruction {
230  ($struct:ident, $enum:expr) => {
231    impl From<$struct> for Instruction {
232      fn from(ins: $struct) -> Self {
233        $enum(ins)
234      }
235    }
236  };
237}
238
239impl_from_instruction!(FromInstruction, Instruction::From);
240impl_from_instruction!(ArgInstruction, Instruction::Arg);
241impl_from_instruction!(LabelInstruction, Instruction::Label);
242impl_from_instruction!(RunInstruction, Instruction::Run);
243impl_from_instruction!(EntrypointInstruction, Instruction::Entrypoint);
244impl_from_instruction!(CmdInstruction, Instruction::Cmd);
245impl_from_instruction!(CopyInstruction, Instruction::Copy);
246impl_from_instruction!(EnvInstruction, Instruction::Env);
247impl_from_instruction!(MiscInstruction, Instruction::Misc);
248
249impl TryFrom<Pair<'_>> for Instruction {
250  type Error = Error;
251
252  fn try_from(record: Pair) -> std::result::Result<Self, Self::Error> {
253    let instruction: Instruction = match record.as_rule() {
254      Rule::from => FromInstruction::from_record(record, 0)?.into(),
255      Rule::arg => ArgInstruction::from_record(record)?.into(),
256      Rule::label => LabelInstruction::from_record(record)?.into(),
257
258      Rule::run => RunInstruction::from_record(record)?.into(),
259
260      Rule::entrypoint => EntrypointInstruction::from_record(record)?.into(),
261
262      Rule::cmd => CmdInstruction::from_record(record)?.into(),
263
264      Rule::copy => Instruction::Copy(CopyInstruction::from_record(record)?),
265
266      Rule::env => EnvInstruction::from_record(record)?.into(),
267
268      Rule::misc => MiscInstruction::from_record(record)?.into(),
269
270      // TODO: consider exposing comments
271      // Rule::comment => ...,
272      _ => return Err(unexpected_token(record))
273    };
274
275    Ok(instruction)
276  }
277}
278
279/// A parsed Dockerfile.
280///
281/// An ordered list of all instructions is available via `instructions`, and
282/// individual stages in a multi-stage build may be iterated over using
283/// `Dockerfile::iter_stages()`.
284///
285/// # Example
286/// ```
287/// use dockerfile_parser::Dockerfile;
288/// use std::io::Read;
289///
290/// let s = r#"
291///   FROM alpine:3.11
292///   RUN echo "hello world"
293/// "#;
294///
295/// assert_eq!(
296///   Dockerfile::parse(&s).unwrap(),
297///   s.parse::<Dockerfile>().unwrap()
298/// );
299/// assert_eq!(
300///   Dockerfile::parse(&s).unwrap(),
301///   Dockerfile::from_reader(s.as_bytes()).unwrap()
302/// );
303/// ```
304#[derive(Debug, Clone, PartialEq)]
305pub struct Dockerfile {
306  /// The raw content of the Dockerfile
307  pub content: String,
308
309  /// An ordered list of parsed ARG instructions preceding the first FROM
310  pub global_args: Vec<ArgInstruction>,
311
312  /// An ordered list of all parsed instructions, including global_args
313  pub instructions: Vec<Instruction>
314}
315
316fn parse_dockerfile(input: &str) -> Result<Dockerfile> {
317  let dockerfile = DockerfileParser::parse(Rule::dockerfile, input)
318    .context(ParseError)?
319    .next()
320    .ok_or(Error::UnknownParseError)?;
321
322  let mut instructions = Vec::new();
323  let mut global_args = Vec::new();
324  let mut from_found = false;
325  let mut from_index = 0;
326
327  for record in dockerfile.into_inner() {
328    if let Rule::EOI = record.as_rule() {
329      continue;
330    }
331
332    // TODO: consider exposing comments in the parse result
333    if let Rule::comment = record.as_rule() {
334      continue;
335    }
336
337    let mut instruction = Instruction::try_from(record)?;
338    match &mut instruction {
339      Instruction::From(ref mut from) => {
340        // fix the from index since we can't know that in parse_instruction()
341        from.index = from_index;
342        from_index += 1;
343        from_found = true;
344      },
345      Instruction::Arg(ref arg) => {
346        // args preceding the first FROM instruction may be substituted into
347        // all subsequent FROM image refs
348        if !from_found {
349          global_args.push(arg.clone());
350        }
351      },
352      _ => ()
353    };
354
355    instructions.push(instruction);
356  }
357
358  Ok(Dockerfile {
359    content: input.into(),
360    global_args, instructions
361  })
362}
363
364impl Dockerfile {
365  /// Parses a Dockerfile from a string.
366  pub fn parse(input: &str) -> Result<Dockerfile> {
367    parse_dockerfile(input)
368  }
369
370  /// Parses a Dockerfile from a reader.
371  pub fn from_reader<R>(reader: R) -> Result<Dockerfile>
372  where
373    R: Read
374  {
375    let mut buf = String::new();
376    let mut buf_reader = BufReader::new(reader);
377    buf_reader.read_to_string(&mut buf).context(ReadError)?;
378
379    Dockerfile::parse(&buf)
380  }
381
382  /// Returns a `Stages`, which splits this Dockerfile into its build stages.
383  pub fn stages(&self) -> Stages {
384    Stages::new(self)
385  }
386
387  pub fn iter_stages(&self) -> std::vec::IntoIter<Stage<'_>> {
388    self.stages().into_iter()
389  }
390
391  /// Creates a `Splicer` for this Dockerfile.
392  ///
393  /// Note that the original input string is needed to actually perform any
394  /// splicing.
395  pub fn splicer(&self) -> Splicer {
396    Splicer::from(self)
397  }
398
399  /// Attempts to find a global argument by name. Returns None if no global ARG
400  /// with the given name exists.
401  pub fn get_global_arg(&self, name: &str) -> Option<&ArgInstruction> {
402    for ins in &self.instructions {
403      match ins {
404        Instruction::Arg(a) => {
405          if a.name.content == name {
406            return Some(a);
407          } else {
408            continue
409          }
410        },
411        _ => return None
412      }
413    }
414
415    None
416  }
417}
418
419impl FromStr for Dockerfile {
420  type Err = Error;
421
422  fn from_str(s: &str) -> Result<Self, Self::Err> {
423    Dockerfile::parse(s)
424  }
425}