root/masterdriverz/snakeoil/snakeoil/fileutils.py @ ferringb%2540gmail.com-20070816035418-xbl5yv5z937o9wak

Revision ferringb%2540gmail.com-20070816035418-xbl5yv5z937o9wak, 9.1 kB (checked in by Brian Harring <ferringb@…>, 17 months ago)

fix a NameError?, s/temp_fd/temp_fp/

Line 
1# Copyright: 2005-2006 Brian Harring <ferringb@gmail.com>
2# License: GPL2
3
4"""
5file related operations, mainly reading
6"""
7
8import re, os
9from shlex import shlex
10from snakeoil.mappings import ProtectedDict
11from snakeoil.osutils import readlines
12
13class AtomicWriteFile(file):
14
15    """File class that stores the changes in a tempfile.
16
17    Upon close call, uses rename to replace the destination.
18
19    Similar to file protocol behaviour, except for the C{__init__}, and
20    that close *must* be called for the changes to be made live,
21
22    if C{__del__} is triggered it's assumed that an exception occured,
23    thus the changes shouldn't be made live.
24    """
25    def __init__(self, fp, binary=False, perms=None, uid=-1, gid=-1, **kwds):
26        self.is_finalized = False
27        if binary:
28            file_mode = "wb"
29        else:
30            file_mode = "w"
31        fp = os.path.realpath(fp)
32        self.original_fp = fp
33        self.temp_fp = os.path.join(
34            os.path.dirname(fp), ".update.%s" % os.path.basename(fp))
35        old_umask = None
36        if perms:
37            # give it just write perms
38            old_umask = os.umask(0200)
39        try:
40            file.__init__(self, self.temp_fp, mode=file_mode, **kwds)
41        finally:
42            if old_umask is not None:
43                os.umask(old_umask)
44        if perms:
45            os.chmod(self.temp_fp, perms)
46        if (gid, uid) != (-1, -1):
47            os.chown(self.temp_fp, uid, gid)
48
49    def close(self):
50        file.close(self)
51        os.rename(self.temp_fp, self.original_fp)
52        self.is_finalized = True
53
54    def __del__(self):
55        file.close(self)
56        if not self.is_finalized:
57            os.unlink(self.temp_fp)
58
59
60def iter_read_bash(bash_source):
61    """
62    Read file honoring bash commenting rules.
63
64    Note that it's considered good behaviour to close filehandles, as
65    such, either iterate fully through this, or use read_bash instead.
66    Once the file object is no longer referenced the handle will be
67    closed, but be proactive instead of relying on the garbage
68    collector.
69
70    @param bash_source: either a file to read from
71        or a string holding the filename to open.
72    """
73    if isinstance(bash_source, basestring):
74        bash_source = readlines(bash_source, True)
75    for s in bash_source:
76        s = s.strip()
77        if s and s[0] != "#":
78            yield s
79
80
81def read_bash(bash_source):
82    return list(iter_read_bash(bash_source))
83read_bash.__doc__ = iter_read_bash.__doc__
84
85
86def read_dict(bash_source, splitter="=", source_isiter=False):
87    """
88    read key value pairs, ignoring bash-style comments.
89
90    @param splitter: the string to split on.  Can be None to
91        default to str.split's default
92    @param bash_source: either a file to read from,
93        or a string holding the filename to open.
94    """
95    d = {}
96    if not source_isiter:
97        filename = bash_source
98        i = iter_read_bash(bash_source)
99    else:
100        # XXX what to do?
101        filename = '<unknown>'
102        i = bash_source
103    line_count = 1
104    try:
105        for k in i:
106            line_count += 1
107            try:
108                k, v = k.split(splitter, 1)
109            except ValueError:
110                raise ParseError(filename, line_count)
111            if len(v) > 2 and v[0] == v[-1] and v[0] in ("'", '"'):
112                v = v[1:-1]
113            d[k] = v
114    finally:
115        del i
116    return d
117
118def read_bash_dict(bash_source, vars_dict=None, sourcing_command=None):
119    """
120    read bash source, yielding a dict of vars
121
122    @param bash_source: either a file to read from
123        or a string holding the filename to open
124    @param vars_dict: initial 'env' for the sourcing.
125        Is protected from modification.
126    @type vars_dict: dict or None
127    @param sourcing_command: controls whether a source command exists.
128        If one does and is encountered, then this func is called.
129    @type sourcing_command: callable
130    @raise ParseError: thrown if invalid syntax is encountered.
131    @return: dict representing the resultant env if bash executed the source.
132    """
133
134    # quite possibly I'm missing something here, but the original
135    # portage_util getconfig/varexpand seemed like it only went
136    # halfway. The shlex posix mode *should* cover everything.
137
138    if vars_dict is not None:
139        d, protected = ProtectedDict(vars_dict), True
140    else:
141        d, protected = {}, False
142    if isinstance(bash_source, basestring):
143        f = open(bash_source, "r")
144    else:
145        f = bash_source
146    s = bash_parser(f, sourcing_command=sourcing_command, env=d)
147
148    try:
149        tok = ""
150        try:
151            while tok is not None:
152                key = s.get_token()
153                if key is None:
154                    break
155                elif key.isspace():
156                    # we specifically have to check this, since we're
157                    # screwing with the whitespace filters below to
158                    # detect empty assigns
159                    continue
160                eq = s.get_token()
161                if eq != '=':
162                    raise ParseError(bash_source, s.lineno,
163                        "got token %r, was expecting '='" % eq)
164                val = s.get_token()
165                if val is None:
166                    val = ''
167                # look ahead to see if we just got an empty assign.
168                next_tok = s.get_token()
169                if next_tok == '=':
170                    # ... we did.
171                    # leftmost insertions, thus reversed ordering
172                    s.push_token(next_tok)
173                    s.push_token(val)
174                    val = ''
175                else:
176                    s.push_token(next_tok)
177                d[key] = val
178        except ValueError, e:
179            raise ParseError(bash_source, s.lineno, str(e))
180    finally:
181        del f
182    if protected:
183        d = d.new
184    return d
185
186
187var_find = re.compile(r'\\?(\${\w+}|\$\w+)')
188backslash_find = re.compile(r'\\.')
189def nuke_backslash(s):
190    s = s.group()
191    if s == "\\\n":
192        return "\n"
193    try:
194        return chr(ord(s))
195    except TypeError:
196        return s[1]
197
198class bash_parser(shlex):
199    def __init__(self, source, sourcing_command=None, env=None):
200        self.__dict__['state'] = ' '
201        shlex.__init__(self, source, posix=True)
202        self.wordchars += "@${}/.-+/:~^"
203        self.wordchars = frozenset(self.wordchars)
204        if sourcing_command is not None:
205            self.source = sourcing_command
206        if env is None:
207            env = {}
208        self.env = env
209        self.__pos = 0
210
211    def __setattr__(self, attr, val):
212        if attr == "state":
213            if (self.state, val) in (
214                ('"', 'a'), ('a', '"'), ('a', ' '), ("'", 'a')):
215                strl = len(self.token)
216                if self.__pos != strl:
217                    self.changed_state.append(
218                        (self.state, self.token[self.__pos:]))
219                self.__pos = strl
220        self.__dict__[attr] = val
221
222    def sourcehook(self, newfile):
223        try:
224            return shlex.sourcehook(self, newfile)
225        except IOError, ie:
226            raise ParseError(newfile, 0, str(ie))
227
228    def read_token(self):
229        self.changed_state = []
230        self.__pos = 0
231        tok = shlex.read_token(self)
232        if tok is None:
233            return tok
234        self.changed_state.append((self.state, self.token[self.__pos:]))
235        tok = ''
236        for s, t in self.changed_state:
237            if s in ('"', "a"):
238                tok += self.var_expand(t).replace("\\\n", '')
239            else:
240                tok += t
241        return tok
242
243    def var_expand(self, val):
244        prev, pos = 0, 0
245        l = []
246        match = var_find.search(val)
247        while match is not None:
248            pos = match.start()
249            if val[pos] == '\\':
250                # it's escaped. either it's \\$ or \\${ , either way,
251                # skipping two ahead handles it.
252                pos += 2
253            else:
254                var = val[match.start():match.end()].strip("${}")
255                if prev != pos:
256                    l.append(val[prev:pos])
257                if var in self.env:
258                    if not isinstance(self.env[var], basestring):
259                        raise ValueError(
260                            "env key %r must be a string, not %s: %r" % (
261                                var, type(self.env[var]), self.env[var]))
262                    l.append(self.env[var])
263                else:
264                    l.append("")
265                prev = pos = match.end()
266            match = var_find.search(val, pos)
267
268        # do \\ cleansing, collapsing val down also.
269        val = backslash_find.sub(nuke_backslash, ''.join(l) + val[prev:])
270        return val
271
272
273class ParseError(Exception):
274
275    def __init__(self, filename, line, errmsg=None):
276        if errmsg is not None:
277            Exception.__init__(self,
278                               "error parsing '%s' on or before %i: err %s" %
279                               (filename, line, errmsg))
280        else:
281            Exception.__init__(self,
282                               "error parsing '%s' on or before %i" %
283                               (filename, line))
284        self.file, self.line, self.errmsg = filename, line, errmsg
Note: See TracBrowser for help on using the browser.