dockerfile_roast/
parser.rs1#[derive(Debug, Clone, PartialEq)]
4pub struct Instruction {
5 pub line: usize,
6 pub instruction: String,
7 pub arguments: String,
8 pub raw: String,
9}
10
11pub fn parse(content: &str) -> Vec<Instruction> {
12 let mut instructions = Vec::new();
13 let mut pending_line: Option<usize> = None;
14 let mut pending_parts: Vec<String> = Vec::new();
15 let mut pending_instr: Option<String> = None;
16
17 for (idx, raw_line) in content.lines().enumerate() {
18 let line_no = idx + 1;
19 let trimmed = raw_line.trim();
20
21 if let Some(ref instr) = pending_instr.clone() {
23 if trimmed.is_empty() || trimmed.starts_with('#') {
27 continue;
28 }
29 let continued = if trimmed.ends_with('\\') {
31 &trimmed[..trimmed.len() - 1]
32 } else {
33 trimmed
34 };
35 pending_parts.push(continued.trim().to_string());
36
37 if !trimmed.ends_with('\\') {
38 let args = pending_parts.join(" ").trim().to_string();
39 let first_line = pending_line.unwrap();
40 instructions.push(Instruction {
41 line: first_line,
42 instruction: instr.to_uppercase(),
43 arguments: args,
44 raw: raw_line.to_string(),
45 });
46 pending_instr = None;
47 pending_line = None;
48 pending_parts.clear();
49 }
50 continue;
51 }
52
53 if trimmed.is_empty() || trimmed.starts_with('#') {
55 continue;
56 }
57
58 let (kw, rest) = match trimmed.split_once(|c: char| c.is_whitespace()) {
60 Some((k, r)) => (k.to_uppercase(), r.trim().to_string()),
61 None => (trimmed.to_uppercase(), String::new()),
62 };
63
64 if rest.ends_with('\\') {
65 pending_instr = Some(kw);
67 pending_line = Some(line_no);
68 pending_parts.push(rest[..rest.len() - 1].trim().to_string());
69 } else {
70 instructions.push(Instruction {
71 line: line_no,
72 instruction: kw,
73 arguments: rest,
74 raw: trimmed.to_string(),
75 });
76 }
77 }
78
79 if let Some(instr) = pending_instr {
81 let args = pending_parts.join(" ").trim().to_string();
82 instructions.push(Instruction {
83 line: pending_line.unwrap(),
84 instruction: instr,
85 arguments: args,
86 raw: String::new(),
87 });
88 }
89
90 instructions
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96
97 #[test]
98 fn test_basic_parse() {
99 let df = "FROM ubuntu:latest\nRUN apt-get install curl\n";
100 let instrs = parse(df);
101 assert_eq!(instrs.len(), 2);
102 assert_eq!(instrs[0].instruction, "FROM");
103 assert_eq!(instrs[0].arguments, "ubuntu:latest");
104 assert_eq!(instrs[1].instruction, "RUN");
105 }
106
107 #[test]
108 fn test_skip_comments() {
109 let df = "# comment\nFROM alpine\n";
110 let instrs = parse(df);
111 assert_eq!(instrs.len(), 1);
112 }
113
114 #[test]
115 fn test_multiline() {
116 let df = "RUN apt-get install \\\n curl \\\n wget\n";
117 let instrs = parse(df);
118 assert_eq!(instrs.len(), 1);
119 assert_eq!(instrs[0].instruction, "RUN");
120 }
121
122 #[test]
123 fn test_comment_inside_continuation() {
124 let df = "RUN apt-get install \\\n curl \\\n # comment line\n wget && \\\n apt-get clean\n";
125 let instrs = parse(df);
126 assert_eq!(instrs.len(), 1);
127 assert_eq!(instrs[0].arguments, "apt-get install curl wget && apt-get clean");
128 }
129
130 #[test]
131 fn test_empty_line_inside_continuation() {
132 let df = "RUN apt-get install \\\n\n curl\n";
133 let instrs = parse(df);
134 assert_eq!(instrs.len(), 1);
135 assert_eq!(instrs[0].arguments, "apt-get install curl");
136 }
137}