dockerfile_parser_rs/
lib.rs

1mod ast;
2mod error;
3mod parser;
4mod symbols;
5mod utils;
6
7use std::fs::File;
8use std::io::Write;
9use std::path::PathBuf;
10
11// public API
12pub use crate::ast::Instruction;
13pub use crate::error::ParseError;
14
15use crate::parser::instructions::add;
16use crate::parser::instructions::arg;
17use crate::parser::instructions::cmd;
18use crate::parser::instructions::copy;
19use crate::parser::instructions::entrypoint;
20use crate::parser::instructions::env;
21use crate::parser::instructions::expose;
22use crate::parser::instructions::from;
23use crate::parser::instructions::label;
24use crate::parser::instructions::run;
25use crate::parser::instructions::user;
26use crate::parser::instructions::volume;
27use crate::parser::instructions::workdir;
28use crate::symbols::chars::HASHTAG;
29use crate::utils::read_lines;
30
31fn tokenize_line(line: &str) -> anyhow::Result<(String, Vec<String>)> {
32    // https://docs.docker.com/reference/dockerfile/#format
33    let regex = regex::Regex::new(r"^(?P<instruction>[A-Z]+)\s*(?P<arguments>.*)").unwrap();
34
35    let captures = regex
36        .captures(line)
37        .ok_or_else(|| ParseError::SyntaxError(line.to_string()))?;
38
39    let instruction = captures
40        .name("instruction")
41        .ok_or_else(|| ParseError::SyntaxError(line.to_string()))?
42        .as_str();
43
44    let arguments = captures
45        .name("arguments")
46        .ok_or_else(|| ParseError::SyntaxError(line.to_string()))?
47        .as_str();
48
49    Ok((
50        instruction.to_string(),
51        arguments
52            .split_whitespace()
53            .map(|s| s.to_string())
54            .collect(),
55    ))
56}
57
58#[derive(Debug)]
59pub struct Dockerfile {
60    pub path: PathBuf,
61    pub instructions: Vec<Instruction>,
62}
63
64impl Dockerfile {
65    pub fn new(path: PathBuf) -> Self {
66        Dockerfile {
67            path,
68            instructions: Vec::new(),
69        }
70    }
71
72    pub fn from(path: PathBuf) -> anyhow::Result<Self> {
73        let mut dockerfile = Dockerfile::new(path);
74        dockerfile.instructions = dockerfile.parse()?;
75        Ok(dockerfile)
76    }
77
78    pub fn parse(&self) -> anyhow::Result<Vec<Instruction>> {
79        let lines = read_lines(&self.path);
80        let mut instructions = Vec::new();
81
82        for line in lines {
83            // preserve empty lines
84            if line.is_empty() {
85                instructions.push(Instruction::Empty);
86            // preserve comments
87            } else if line.starts_with(HASHTAG) {
88                instructions.push(Instruction::Comment(line.to_string()));
89            } else {
90                let (instruction, arguments) = tokenize_line(&line)?;
91                let instruction = match instruction.as_str() {
92                    "ADD" => add::parse(arguments),
93                    "ARG" => arg::parse(arguments),
94                    "CMD" => cmd::parse(arguments),
95                    "COPY" => copy::parse(arguments),
96                    "ENTRYPOINT" => entrypoint::parse(arguments),
97                    "ENV" => env::parse(arguments),
98                    "EXPOSE" => expose::parse(arguments),
99                    "LABEL" => label::parse(arguments),
100                    "FROM" => from::parse(arguments),
101                    "RUN" => run::parse(arguments),
102                    "USER" => user::parse(arguments),
103                    "VOLUME" => volume::parse(arguments),
104                    "WORKDIR" => workdir::parse(arguments),
105                    _ => return Err(ParseError::UnknownInstruction(instruction))?,
106                }?;
107                instructions.push(instruction);
108            }
109        }
110        Ok(instructions)
111    }
112
113    pub fn dump(&self) -> anyhow::Result<()> {
114        let mut file = File::create(&self.path)?;
115        for instruction in &self.instructions {
116            writeln!(file, "{}", instruction)?;
117        }
118        Ok(())
119    }
120}