• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

containerbuildsystem / dockerfile-parse / 3640376223

pending completion
3640376223

push

github

Martin
Pylint: silence no-absolute-import

666 of 669 relevant lines covered (99.55%)

1.99 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.09
/dockerfile_parse/util.py
1
# -*- coding: utf-8 -*-
2
"""
2✔
3
Copyright (c) 2015 Red Hat, Inc
4
All rights reserved.
5

6
This software may be modified and distributed under the terms
7
of the BSD license. See the LICENSE file for details.
8
"""
9

10
from io import StringIO
2✔
11

12

13
def b2u(string):
2✔
14
    """ bytes to unicode """
15
    if isinstance(string, bytes):
2✔
16
        return string.decode('utf-8')
2✔
17
    return string
2✔
18

19

20
def u2b(string):
2✔
21
    """ unicode to bytes"""
22
    if isinstance(string, str):
2✔
23
        return string.encode('utf-8')
2✔
24
    return string
2✔
25

26

27
class WordSplitter(object):
2✔
28
    """
29
    Split string into words, substituting environment variables if provided
30

31
    Methods defined here:
32

33
    dequote()
34
        Returns the string with escaped and quotes consumed
35

36
    split(maxsplit=None, dequote=True)
37
        Returns an iterable of words, split at whitespace
38
    """
39

40
    SQUOTE = "'"
2✔
41
    DQUOTE = '"'
2✔
42

43
    def __init__(self, s, args=None, envs=None):
2✔
44
        """
45
        :param s: str, string to process
46
        :param args: dict, build arguments to use; if None, do not
47
            attempt substitution
48
        :param envs: dict, environment variables to use; if None, do not
49
            attempt substitution
50
        """
51
        self.stream = StringIO(s)
2✔
52
        self.args = args
2✔
53
        self.envs = envs
2✔
54

55
        # Initial state
56
        self.quotes = None  # the quoting character in force, or None
2✔
57
        self.escaped = False
2✔
58

59
    def _update_quoting_state(self, ch):
2✔
60
        """
61
        Update self.quotes and self.escaped
62

63
        :param ch: str, current character
64
        :return: ch if it was not used to update quoting state, else ''
65
        """
66

67
        # Set whether the next character is escaped
68
        # Unquoted:
69
        #   a backslash escapes the next character
70
        # Double-quoted:
71
        #   a backslash escapes the next character only if it is a double-quote
72
        # Single-quoted:
73
        #   a backslash is not special
74
        is_escaped = self.escaped
2✔
75
        self.escaped = (not self.escaped and
2✔
76
                        ch == '\\' and
77
                        self.quotes != self.SQUOTE)
78
        if self.escaped:
2✔
79
            return ''
2✔
80

81
        if is_escaped:
2✔
82
            if self.quotes == self.DQUOTE:
2✔
83
                if ch == '"':
2✔
84
                    return ch
2✔
85
                return "{0}{1}".format('\\', ch)
2✔
86

87
            return ch
2✔
88

89
        if self.quotes is None:
2✔
90
            if ch in (self.SQUOTE, self.DQUOTE):
2✔
91
                self.quotes = ch
2✔
92
                return ''
2✔
93

94
        elif self.quotes == ch:
2✔
95
            self.quotes = None
2✔
96
            return ''
2✔
97

98
        return ch
2✔
99

100
    def dequote(self):
2✔
101
        return ''.join(self.split(maxsplit=0))
2✔
102

103
    def split(self, maxsplit=None, dequote=True):
2✔
104
        """
105
        Generator for the words of the string
106

107
        :param maxsplit: perform at most maxsplit splits;
108
            if None, do not limit the number of splits
109
        :param dequote: remove quotes and escape characters once consumed
110
        """
111

112
        class Word(object):
2✔
113
            """
114
            A None-or-str object which can always be appended to.
115
            Similar to a defaultdict but with only a single value.
116
            """
117

118
            def __init__(self):
2✔
119
                self.value = None
2✔
120

121
            @property
2✔
122
            def valid(self):
2✔
123
                return self.value is not None
2✔
124

125
            def append(self, s):
2✔
126
                if self.value is None:
2✔
127
                    self.value = s
2✔
128
                else:
129
                    self.value += s
2✔
130

131
        num_splits = 0
2✔
132
        word = Word()
2✔
133
        while True:
×
134
            ch = self.stream.read(1)
2✔
135
            if not ch:
2✔
136
                # EOF
137
                if word.valid:
2✔
138
                    yield word.value
2✔
139

140
                return
2✔
141

142
            if (not self.escaped and
2✔
143
                    (self.envs is not None or self.args is not None) and
144
                    ch == '$' and
145
                    self.quotes != self.SQUOTE):
146
                while True:
×
147
                    # Substitute environment variable
148
                    braced = False
2✔
149
                    varname = ''
2✔
150
                    while True:
×
151
                        ch = self.stream.read(1)
2✔
152
                        if varname == '' and ch == '{':
2✔
153
                            braced = True
2✔
154
                            continue
2✔
155

156
                        if not ch:
2✔
157
                            # EOF
158
                            break
2✔
159

160
                        if braced and ch == '}':
2✔
161
                            break
2✔
162

163
                        if not ch.isalnum() and ch != '_':
2✔
164
                            break
2✔
165

166
                        varname += ch
2✔
167

168
                    if self.envs is not None and varname in self.envs:
2✔
169
                        word.append(self.envs[varname])
2✔
170
                    elif self.args is not None and varname in self.args:
2✔
171
                        word.append(self.args[varname])
2✔
172

173
                    # Check whether there is another envvar
174
                    if ch != '$':
2✔
175
                        break
2✔
176

177
                if braced and ch == '}':
2✔
178
                    continue
2✔
179

180
                # ch now holds the next character
181

182
            # Figure out what our quoting/escaping state will be
183
            # after this character
184
            is_escaped = self.escaped
2✔
185
            ch_unless_consumed = self._update_quoting_state(ch)
2✔
186

187
            if dequote:
2✔
188
                # If we just processed a quote or escape character,
189
                # and were asked to dequote the string, consume it now
190
                ch = ch_unless_consumed
2✔
191

192
            # If word-splitting has been requested, check whether we are
193
            # at a whitespace character
194
            may_split = maxsplit != 0 and (maxsplit is None or
2✔
195
                                           num_splits < maxsplit)
196
            at_split = may_split and (self.quotes is None and
2✔
197
                                      not is_escaped and
198
                                      ch.isspace())
199
            if at_split:
2✔
200
                # It is time to yield a word
201
                if word.valid:
2✔
202
                    num_splits += 1
2✔
203
                    yield word.value
2✔
204

205
                word = Word()
2✔
206
            else:
207
                word.append(ch)
2✔
208

209

210
def extract_key_values(env_replace, args, envs, instruction_value):
2✔
211
    words = list(WordSplitter(instruction_value).split(dequote=False))
2✔
212
    key_val_list = []
2✔
213

214
    def substitute_vars(val):
2✔
215
        kwargs = {}
2✔
216
        if env_replace:
2✔
217
            kwargs['args'] = args
2✔
218
            kwargs['envs'] = envs
2✔
219

220
        return WordSplitter(val, **kwargs).dequote()
2✔
221

222
    if '=' not in words[0]:
2✔
223
        # This form is:
224
        #   LABEL/ENV name value
225
        # The first word is the name, remainder are the value.
226
        key_val = [substitute_vars(x) for x in instruction_value.split(None, 1)]
2✔
227
        key = key_val[0]
2✔
228
        try:
2✔
229
            val = key_val[1]
2✔
230
        except IndexError:
2✔
231
            val = ''
2✔
232

233
        key_val_list.append((key, val))
2✔
234
    else:
235
        # This form is:
236
        #   LABEL/ENV "name"="value" ["name"="value"...]
237
        # Each word is a key=value pair.
238
        for k_v in words:
2✔
239
            if '=' not in k_v:
2✔
240
                raise ValueError('Syntax error - can\'t find = in "{word}". '
2✔
241
                                 'Must be of the form: name=value'
242
                                 .format(word=k_v))
243
            key, val = [substitute_vars(x) for x in k_v.split('=', 1)]
2✔
244
            key_val_list.append((key, val))
2✔
245

246
    return key_val_list
2✔
247

248

249
def get_key_val_dictionary(instruction_value, env_replace=False, args=None, envs=None):
2✔
250
    args = args or {}
2✔
251
    envs = envs or {}
2✔
252
    return dict(extract_key_values(instruction_value=instruction_value,
2✔
253
                                   env_replace=env_replace,
254
                                   args=args, envs=envs))
255

256

257
class Context(object):
2✔
258
    def __init__(self, args=None, envs=None, labels=None,
2✔
259
                 line_args=None, line_envs=None, line_labels=None):
260
        """
261
        Class representing current state of build arguments, environment variables and labels.
262

263
        :param args: dict with arguments valid for this line
264
            (all variables defined to this line)
265
        :param envs: dict with variables valid for this line
266
            (all variables defined to this line)
267
        :param labels: dict with labels valid for this line
268
            (all labels defined to this line)
269
        :param line_args: dict with arguments defined on this line
270
        :param line_envs: dict with variables defined on this line
271
        :param line_labels: dict with labels defined on this line
272
        """
273
        self.args = args or {}
2✔
274
        self.envs = envs or {}
2✔
275
        self.labels = labels or {}
2✔
276
        self.line_args = line_args or {}
2✔
277
        self.line_envs = line_envs or {}
2✔
278
        self.line_labels = line_labels or {}
2✔
279

280
    def set_line_value(self, context_type, value):
2✔
281
        """
282
        Set value defined on this line ('line_args'/'line_envs'/'line_labels')
283
        and update 'args'/'envs'/'labels'.
284

285
        :param context_type: "ARG" or "ENV" or "LABEL"
286
        :param value: new value for this line
287
        """
288
        if context_type.upper() == "ARG":
2✔
289
            self.line_args = value
2✔
290
            self.args.update(value)
2✔
291
        elif context_type.upper() == "ENV":
2✔
292
            self.line_envs = value
2✔
293
            self.envs.update(value)
2✔
294
        elif context_type.upper() == "LABEL":
2✔
295
            self.line_labels = value
2✔
296
            self.labels.update(value)
2✔
297
        else:
298
            raise ValueError("Unexpected context type: " + context_type)
2✔
299

300
    def get_line_value(self, context_type):
2✔
301
        """
302
        Get the values defined on this line.
303

304
        :param context_type: "ARG" or "ENV" or "LABEL"
305
        :return: values of given type defined on this line
306
        """
307
        if context_type.upper() == "ARG":
2✔
308
            return self.line_args
2✔
309
        if context_type.upper() == "ENV":
2✔
310
            return self.line_envs
2✔
311
        if context_type.upper() == "LABEL":
2✔
312
            return self.line_labels
2✔
313
        raise ValueError("Unexpected context type: " + context_type)
2✔
314

315
    def get_values(self, context_type):
2✔
316
        """
317
        Get the values valid on this line.
318

319
        :param context_type: "ARG" or "ENV" or "LABEL"
320
        :return: values of given type valid on this line
321
        """
322
        if context_type.upper() == "ARG":
2✔
323
            return self.args
2✔
324
        if context_type.upper() == "ENV":
2✔
325
            return self.envs
2✔
326
        if context_type.upper() == "LABEL":
2✔
327
            return self.labels
2✔
328
        raise ValueError("Unexpected context type: " + context_type)
2✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc