Skip to content

Commit 9a276f7

Browse files
committed
cstrans-df-run: transform RUN line in a Dockerfile
Closes #2
1 parent 4e68b4c commit 9a276f7

File tree

4 files changed

+316
-2
lines changed

4 files changed

+316
-2
lines changed

CMakeLists.txt

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# Copyright (C) 2011 - 2014 Red Hat, Inc.
1+
# Copyright (C) 2011 - 2019 Red Hat, Inc.
22
#
33
# This file is part of csdiff.
44
#
@@ -56,6 +56,9 @@ add_executable(cshtml cshtml.cc)
5656
add_executable(cslinker cslinker.cc)
5757
add_executable(cssort cssort.cc)
5858

59+
# experimental
60+
add_executable(cstrans-df-run cstrans-df-run.cc)
61+
5962
# load regression tests
6063
add_subdirectory(tests)
6164

@@ -66,6 +69,7 @@ install(TARGETS
6669
cshtml
6770
cslinker
6871
cssort
72+
cstrans-df-run
6973
DESTINATION bin)
7074

7175
# pycsdiff - python binding of csdiff
@@ -145,4 +149,5 @@ if(HELP2MAN)
145149
create_manpage(cshtml)
146150
create_manpage(cslinker)
147151
create_manpage(cssort)
152+
create_manpage(cstrans-df-run)
148153
endif()

cstrans-df-run.cc

Lines changed: 305 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,305 @@
1+
/*
2+
* Copyright (C) 2019 Red Hat, Inc.
3+
*
4+
* This file is part of csdiff.
5+
*
6+
* csdiff is free software: you can redistribute it and/or modify
7+
* it under the terms of the GNU General Public License as published by
8+
* the Free Software Foundation, either version 3 of the License, or
9+
* any later version.
10+
*
11+
* csdiff is distributed in the hope that it will be useful,
12+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
13+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14+
* GNU General Public License for more details.
15+
*
16+
* You should have received a copy of the GNU General Public License
17+
* along with csdiff. If not, see <http://www.gnu.org/licenses/>.
18+
*/
19+
20+
#include "version.hh"
21+
22+
#include <boost/algorithm/string/replace.hpp>
23+
#include <boost/foreach.hpp>
24+
#include <boost/program_options.hpp>
25+
#include <boost/regex.hpp>
26+
#include <boost/tokenizer.hpp>
27+
28+
#include <cctype>
29+
#include <iostream>
30+
31+
typedef std::vector<std::string> TStringList;
32+
33+
const char *prog_name;
34+
35+
class DockerFileTransformer {
36+
public:
37+
DockerFileTransformer(const TStringList &prefixCmd, const bool verbose):
38+
prefixCmd_(prefixCmd),
39+
verbose_(verbose),
40+
reLineRun_("^RUN (.*)$"),
41+
reLineRunExec_("^RUN *\\[(.*)\\] *$"),
42+
reLineCont_("(^.*[^\\\\])\\\\$"),
43+
lineNum_(0)
44+
{
45+
}
46+
47+
/// transform Dockerfile on in and write to out
48+
bool transform(std::istream &in, std::ostream &out);
49+
50+
private:
51+
const TStringList prefixCmd_; ///< cmd-line operands
52+
const bool verbose_; ///< --verbose on cmd-line
53+
const boost::regex reLineRun_; ///< match ... in RUN ...
54+
const boost::regex reLineRunExec_; ///< match ... in RUN [...]
55+
const boost::regex reLineCont_; ///< match ... in ... BS-NL
56+
int lineNum_; ///< line number being read
57+
58+
bool transformRunLine(std::string *);
59+
};
60+
61+
/// parse serialized list in the form: "item1", "item2", ...
62+
void appendExecArgs(TStringList *pExecList, const std::string &str)
63+
{
64+
enum EState {
65+
ES_SEEK_QUOT_OPEN,
66+
ES_BACK_SLASH,
67+
ES_SEEK_QUOT_CLOSE,
68+
ES_SEEK_COMMA
69+
} state = ES_SEEK_QUOT_OPEN;
70+
71+
std::string arg;
72+
73+
// process the given string char by char
74+
BOOST_FOREACH(const unsigned char c, str) {
75+
switch (state) {
76+
case ES_SEEK_QUOT_OPEN:
77+
if ('\"' == c)
78+
// found opening quote
79+
state = ES_SEEK_QUOT_CLOSE;
80+
else if (!isspace(c))
81+
throw std::runtime_error("quote expected");
82+
continue;
83+
84+
case ES_BACK_SLASH:
85+
// one back-slash has been consumed
86+
arg.push_back(c);
87+
state = ES_SEEK_QUOT_CLOSE;
88+
continue;
89+
90+
case ES_SEEK_QUOT_CLOSE:
91+
if ('\"' == c) {
92+
// found closing quote -> append the string
93+
pExecList->push_back(arg);
94+
arg.clear();
95+
state = ES_SEEK_COMMA;
96+
continue;
97+
}
98+
else if ('\\' == c)
99+
// consume one back-slash
100+
state = ES_BACK_SLASH;
101+
else
102+
// pick one char
103+
arg.push_back(c);
104+
continue;
105+
106+
case ES_SEEK_COMMA:
107+
if (',' == c)
108+
// comma found -> look for opening quote
109+
state = ES_SEEK_QUOT_OPEN;
110+
else if (!isspace(c))
111+
throw std::runtime_error("comma expected");
112+
continue;
113+
}
114+
}
115+
116+
if (ES_SEEK_COMMA != state)
117+
throw std::runtime_error("unexpected end of input while parsing list");
118+
}
119+
120+
/// invoke shell interpreter explicitly in case the wrapper uses exec()
121+
void appendShellExec(TStringList *pExecList, const std::string &str)
122+
{
123+
pExecList->push_back("sh");
124+
pExecList->push_back("-c");
125+
pExecList->push_back(str);
126+
}
127+
128+
/// precede each back-slash and each quote by back-slash
129+
std::string runQuoteArg(std::string arg)
130+
{
131+
boost::algorithm::replace_all(arg, "\\", "\\\\");
132+
boost::algorithm::replace_all(arg, "\"", "\\\"");
133+
return arg;
134+
}
135+
136+
std::string runLineFromExecList(const TStringList &execList)
137+
{
138+
// construct RUN ["cmd", "arg1", "arg2", ...] from execList
139+
std::string runLine = "RUN [";
140+
int i = 0;
141+
BOOST_FOREACH (const std::string &arg, execList) {
142+
if (i++)
143+
runLine += ", ";
144+
145+
runLine += "\"" + runQuoteArg(arg) + "\"";
146+
}
147+
runLine += "]";
148+
return runLine;
149+
}
150+
151+
bool DockerFileTransformer::transformRunLine(std::string *pRunLine)
152+
{
153+
// start with the prefix specified on cmd-line
154+
TStringList execList = prefixCmd_;
155+
156+
try {
157+
boost::smatch sm;
158+
if (boost::regex_match(*pRunLine, sm, reLineRunExec_))
159+
// RUN ["cmd", "arg1", "arg2", ...]
160+
appendExecArgs(&execList, sm[1]);
161+
162+
else if (boost::regex_match(*pRunLine, sm, reLineRun_))
163+
// RUN arbitrary shell code...
164+
appendShellExec(&execList, sm[1]);
165+
166+
else
167+
// should never happen
168+
throw std::runtime_error("internal error");
169+
}
170+
catch (const std::runtime_error &e) {
171+
std::cerr << prog_name << "error: parsing error on line "
172+
<< lineNum_ << ": " << e.what() << std::endl;
173+
return false;
174+
}
175+
176+
const std::string newRunLine = runLineFromExecList(execList);
177+
if (verbose_) {
178+
// diagnostic output printed with --verbose
179+
std::cerr << prog_name << " <<< " << *pRunLine << std::endl;
180+
std::cerr << prog_name << " >>> " << newRunLine << std::endl;
181+
}
182+
183+
// return the result of a successful tranformation
184+
*pRunLine = newRunLine;
185+
return true;
186+
}
187+
188+
bool DockerFileTransformer::transform(std::istream &in, std::ostream &out)
189+
{
190+
bool anyError = false;
191+
bool anyRunLine = false; ///< true if any RUN line was transformed
192+
bool readingRunLine = false; ///< true if multi-line RUN is being read
193+
std::string line;
194+
std::string runLine;
195+
lineNum_ = 0;
196+
197+
// read input line by line
198+
while (std::getline(in, line)) {
199+
lineNum_++;
200+
201+
if (!readingRunLine && !boost::regex_match(line, reLineRun_)) {
202+
// pass unrelated contents of Dockerfile unchanged
203+
out << line << std::endl;
204+
continue;
205+
}
206+
207+
// check for line ending with back-slash (multi-line RUN)
208+
boost::smatch sm;
209+
const bool lineCont = boost::regex_match(line, sm, reLineCont_);
210+
if (lineCont)
211+
line = sm[1];
212+
213+
// append the current line to our linearized RUN line
214+
runLine += line;
215+
readingRunLine = lineCont;
216+
if (readingRunLine)
217+
continue;
218+
219+
// transform the linearized RUN line
220+
if (!this->transformRunLine(&runLine))
221+
anyError = true;
222+
223+
// write the transformed RUN line and update state
224+
out << runLine << std::endl;
225+
runLine.clear();
226+
anyRunLine = true;
227+
}
228+
229+
if (!anyRunLine) {
230+
// no match is treated as error
231+
std::cerr << prog_name << ": error: no RUN line found\n";
232+
anyError = true;
233+
}
234+
235+
return !anyError;
236+
}
237+
238+
int main(int argc, char *argv[])
239+
{
240+
// used also in diagnostic messages
241+
::prog_name = argv[0];
242+
243+
namespace po = boost::program_options;
244+
po::variables_map vm;
245+
po::options_description desc(std::string("Usage: ") + prog_name
246+
+ " [--verbose] cmd [arg1 [arg2 [...]]]");
247+
248+
try {
249+
desc.add_options()
250+
("verbose", "print transformations to standard error output");
251+
252+
desc.add_options()
253+
("help", "produce help message")
254+
("version", "print version");
255+
256+
po::options_description hidden("");
257+
hidden.add_options()
258+
("prefix-cmd", po::value<TStringList>(), "cmd [arg1 [arg2 [...]]]");
259+
po::positional_options_description p;
260+
p.add("prefix-cmd", -1);
261+
262+
po::store(po::parse_command_line(argc, argv, desc), vm);
263+
po::notify(vm);
264+
265+
po::options_description opts;
266+
opts.add(desc).add(hidden);
267+
po::store(po::command_line_parser(argc, argv).
268+
options(opts).positional(p).run(), vm);
269+
po::notify(vm);
270+
}
271+
catch (po::error &e) {
272+
std::cerr << prog_name << ": error: " << e.what() << "\n\n";
273+
desc.print(std::cerr);
274+
return 1;
275+
}
276+
277+
if (vm.count("help")) {
278+
desc.print(std::cout);
279+
return 0;
280+
}
281+
282+
if (vm.count("version")) {
283+
std::cout << CS_VERSION << "\n";
284+
return 0;
285+
}
286+
287+
const bool verbose = !!vm.count("verbose");
288+
289+
if (!vm.count("prefix-cmd")) {
290+
desc.print(std::cerr);
291+
return 1;
292+
}
293+
294+
const TStringList &prefixCmd = vm["prefix-cmd"].as<TStringList>();
295+
if (prefixCmd.empty()) {
296+
desc.print(std::cerr);
297+
return 1;
298+
}
299+
300+
// pass cmd-line args to DockerFileTransformer
301+
DockerFileTransformer dft(prefixCmd, verbose);
302+
303+
// transform Dockerfile on stdin and write to stdout
304+
return !dft.transform(std::cin, std::cout);
305+
}

cstrans-df-run.h2m

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[NAME]
2+
cstrans-df-run - transform RUN line in a Dockerfile

make-srpm.sh

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ VER="`echo "$VER" | sed "s/-.*-/.$TIMESTAMP./"`"
4848

4949
BRANCH="`git rev-parse --abbrev-ref HEAD`"
5050
test -n "$BRANCH" || die "failed to get current branch name"
51-
test master = "${BRANCH}" || VER="${VER}.${BRANCH}"
51+
test master = "${BRANCH}" || VER="${VER}.${BRANCH//-/_}"
5252
test -z "`git diff HEAD`" || VER="${VER}.dirty"
5353

5454
NV="${PKG}-${VER}"
@@ -189,11 +189,13 @@ ctest %{?_smp_mflags} --output-on-failure
189189
%{_bindir}/cshtml
190190
%{_bindir}/cslinker
191191
%{_bindir}/cssort
192+
%{_bindir}/cstrans-df-run
192193
%{_mandir}/man1/csdiff.1*
193194
%{_mandir}/man1/csgrep.1*
194195
%{_mandir}/man1/cshtml.1*
195196
%{_mandir}/man1/cslinker.1*
196197
%{_mandir}/man1/cssort.1*
198+
%{_mandir}/man1/cstrans-df-run.1*
197199
%doc COPYING README
198200
199201
%if %{with python2}

0 commit comments

Comments
 (0)