| 1 | # Copyright: 2005-2006 Brian Harring <ferringb@gmail.com> |
|---|
| 2 | # License: GPL2 |
|---|
| 3 | |
|---|
| 4 | """ |
|---|
| 5 | file related operations, mainly reading |
|---|
| 6 | """ |
|---|
| 7 | |
|---|
| 8 | import re, os |
|---|
| 9 | from shlex import shlex |
|---|
| 10 | from snakeoil.mappings import ProtectedDict |
|---|
| 11 | from snakeoil.osutils import readlines |
|---|
| 12 | |
|---|
| 13 | class 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 | |
|---|
| 60 | def 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 | |
|---|
| 81 | def read_bash(bash_source): |
|---|
| 82 | return list(iter_read_bash(bash_source)) |
|---|
| 83 | read_bash.__doc__ = iter_read_bash.__doc__ |
|---|
| 84 | |
|---|
| 85 | |
|---|
| 86 | def 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 | |
|---|
| 118 | def 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 | |
|---|
| 187 | var_find = re.compile(r'\\?(\${\w+}|\$\w+)') |
|---|
| 188 | backslash_find = re.compile(r'\\.') |
|---|
| 189 | def 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 | |
|---|
| 198 | class 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 | |
|---|
| 273 | class 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 |
|---|