Patrick Williams | c124f4f | 2015-09-15 14:41:29 -0500 | [diff] [blame^] | 1 | # interp.py - shell interpreter for pysh. |
| 2 | # |
| 3 | # Copyright 2007 Patrick Mezard |
| 4 | # |
| 5 | # This software may be used and distributed according to the terms |
| 6 | # of the GNU General Public License, incorporated herein by reference. |
| 7 | |
| 8 | """Implement the shell interpreter. |
| 9 | |
| 10 | Most references are made to "The Open Group Base Specifications Issue 6". |
| 11 | <http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html> |
| 12 | """ |
| 13 | # TODO: document the fact input streams must implement fileno() so Popen will work correctly. |
| 14 | # it requires non-stdin stream to be implemented as files. Still to be tested... |
| 15 | # DOC: pathsep is used in PATH instead of ':'. Clearly, there are path syntax issues here. |
| 16 | # TODO: stop command execution upon error. |
| 17 | # TODO: sort out the filename/io_number mess. It should be possible to use filenames only. |
| 18 | # TODO: review subshell implementation |
| 19 | # TODO: test environment cloning for non-special builtins |
| 20 | # TODO: set -x should not rebuild commands from tokens, assignments/redirections are lost |
| 21 | # TODO: unit test for variable assignment |
| 22 | # TODO: test error management wrt error type/utility type |
| 23 | # TODO: test for binary output everywhere |
| 24 | # BUG: debug-parsing does not pass log file to PLY. Maybe a PLY upgrade is necessary. |
| 25 | import base64 |
| 26 | import cPickle as pickle |
| 27 | import errno |
| 28 | import glob |
| 29 | import os |
| 30 | import re |
| 31 | import subprocess |
| 32 | import sys |
| 33 | import tempfile |
| 34 | |
| 35 | try: |
| 36 | s = set() |
| 37 | del s |
| 38 | except NameError: |
| 39 | from Set import Set as set |
| 40 | |
| 41 | import builtin |
| 42 | from sherrors import * |
| 43 | import pyshlex |
| 44 | import pyshyacc |
| 45 | |
| 46 | def mappend(func, *args, **kargs): |
| 47 | """Like map but assume func returns a list. Returned lists are merged into |
| 48 | a single one. |
| 49 | """ |
| 50 | return reduce(lambda a,b: a+b, map(func, *args, **kargs), []) |
| 51 | |
| 52 | class FileWrapper: |
| 53 | """File object wrapper to ease debugging. |
| 54 | |
| 55 | Allow mode checking and implement file duplication through a simple |
| 56 | reference counting scheme. Not sure the latter is really useful since |
| 57 | only real file descriptors can be used. |
| 58 | """ |
| 59 | def __init__(self, mode, file, close=True): |
| 60 | if mode not in ('r', 'w', 'a'): |
| 61 | raise IOError('invalid mode: %s' % mode) |
| 62 | self._mode = mode |
| 63 | self._close = close |
| 64 | if isinstance(file, FileWrapper): |
| 65 | if file._refcount[0] <= 0: |
| 66 | raise IOError(0, 'Error') |
| 67 | self._refcount = file._refcount |
| 68 | self._refcount[0] += 1 |
| 69 | self._file = file._file |
| 70 | else: |
| 71 | self._refcount = [1] |
| 72 | self._file = file |
| 73 | |
| 74 | def dup(self): |
| 75 | return FileWrapper(self._mode, self, self._close) |
| 76 | |
| 77 | def fileno(self): |
| 78 | """fileno() should be only necessary for input streams.""" |
| 79 | return self._file.fileno() |
| 80 | |
| 81 | def read(self, size=-1): |
| 82 | if self._mode!='r': |
| 83 | raise IOError(0, 'Error') |
| 84 | return self._file.read(size) |
| 85 | |
| 86 | def readlines(self, *args, **kwargs): |
| 87 | return self._file.readlines(*args, **kwargs) |
| 88 | |
| 89 | def write(self, s): |
| 90 | if self._mode not in ('w', 'a'): |
| 91 | raise IOError(0, 'Error') |
| 92 | return self._file.write(s) |
| 93 | |
| 94 | def flush(self): |
| 95 | self._file.flush() |
| 96 | |
| 97 | def close(self): |
| 98 | if not self._refcount: |
| 99 | return |
| 100 | assert self._refcount[0] > 0 |
| 101 | |
| 102 | self._refcount[0] -= 1 |
| 103 | if self._refcount[0] == 0: |
| 104 | self._mode = 'c' |
| 105 | if self._close: |
| 106 | self._file.close() |
| 107 | self._refcount = None |
| 108 | |
| 109 | def mode(self): |
| 110 | return self._mode |
| 111 | |
| 112 | def __getattr__(self, name): |
| 113 | if name == 'name': |
| 114 | self.name = getattr(self._file, name) |
| 115 | return self.name |
| 116 | else: |
| 117 | raise AttributeError(name) |
| 118 | |
| 119 | def __del__(self): |
| 120 | self.close() |
| 121 | |
| 122 | |
| 123 | def win32_open_devnull(mode): |
| 124 | return open('NUL', mode) |
| 125 | |
| 126 | |
| 127 | class Redirections: |
| 128 | """Stores open files and their mapping to pseudo-sh file descriptor. |
| 129 | """ |
| 130 | # BUG: redirections are not handled correctly: 1>&3 2>&3 3>&4 does |
| 131 | # not make 1 to redirect to 4 |
| 132 | def __init__(self, stdin=None, stdout=None, stderr=None): |
| 133 | self._descriptors = {} |
| 134 | if stdin is not None: |
| 135 | self._add_descriptor(0, stdin) |
| 136 | if stdout is not None: |
| 137 | self._add_descriptor(1, stdout) |
| 138 | if stderr is not None: |
| 139 | self._add_descriptor(2, stderr) |
| 140 | |
| 141 | def add_here_document(self, interp, name, content, io_number=None): |
| 142 | if io_number is None: |
| 143 | io_number = 0 |
| 144 | |
| 145 | if name==pyshlex.unquote_wordtree(name): |
| 146 | content = interp.expand_here_document(('TOKEN', content)) |
| 147 | |
| 148 | # Write document content in a temporary file |
| 149 | tmp = tempfile.TemporaryFile() |
| 150 | try: |
| 151 | tmp.write(content) |
| 152 | tmp.flush() |
| 153 | tmp.seek(0) |
| 154 | self._add_descriptor(io_number, FileWrapper('r', tmp)) |
| 155 | except: |
| 156 | tmp.close() |
| 157 | raise |
| 158 | |
| 159 | def add(self, interp, op, filename, io_number=None): |
| 160 | if op not in ('<', '>', '>|', '>>', '>&'): |
| 161 | # TODO: add descriptor duplication and here_documents |
| 162 | raise RedirectionError('Unsupported redirection operator "%s"' % op) |
| 163 | |
| 164 | if io_number is not None: |
| 165 | io_number = int(io_number) |
| 166 | |
| 167 | if (op == '>&' and filename.isdigit()) or filename=='-': |
| 168 | # No expansion for file descriptors, quote them if you want a filename |
| 169 | fullname = filename |
| 170 | else: |
| 171 | if filename.startswith('/'): |
| 172 | # TODO: win32 kludge |
| 173 | if filename=='/dev/null': |
| 174 | fullname = 'NUL' |
| 175 | else: |
| 176 | # TODO: handle absolute pathnames, they are unlikely to exist on the |
| 177 | # current platform (win32 for instance). |
| 178 | raise NotImplementedError() |
| 179 | else: |
| 180 | fullname = interp.expand_redirection(('TOKEN', filename)) |
| 181 | if not fullname: |
| 182 | raise RedirectionError('%s: ambiguous redirect' % filename) |
| 183 | # Build absolute path based on PWD |
| 184 | fullname = os.path.join(interp.get_env()['PWD'], fullname) |
| 185 | |
| 186 | if op=='<': |
| 187 | return self._add_input_redirection(interp, fullname, io_number) |
| 188 | elif op in ('>', '>|'): |
| 189 | clobber = ('>|'==op) |
| 190 | return self._add_output_redirection(interp, fullname, io_number, clobber) |
| 191 | elif op=='>>': |
| 192 | return self._add_output_appending(interp, fullname, io_number) |
| 193 | elif op=='>&': |
| 194 | return self._dup_output_descriptor(fullname, io_number) |
| 195 | |
| 196 | def close(self): |
| 197 | if self._descriptors is not None: |
| 198 | for desc in self._descriptors.itervalues(): |
| 199 | desc.flush() |
| 200 | desc.close() |
| 201 | self._descriptors = None |
| 202 | |
| 203 | def stdin(self): |
| 204 | return self._descriptors[0] |
| 205 | |
| 206 | def stdout(self): |
| 207 | return self._descriptors[1] |
| 208 | |
| 209 | def stderr(self): |
| 210 | return self._descriptors[2] |
| 211 | |
| 212 | def clone(self): |
| 213 | clone = Redirections() |
| 214 | for desc, fileobj in self._descriptors.iteritems(): |
| 215 | clone._descriptors[desc] = fileobj.dup() |
| 216 | return clone |
| 217 | |
| 218 | def _add_output_redirection(self, interp, filename, io_number, clobber): |
| 219 | if io_number is None: |
| 220 | # io_number default to standard output |
| 221 | io_number = 1 |
| 222 | |
| 223 | if not clobber and interp.get_env().has_opt('-C') and os.path.isfile(filename): |
| 224 | # File already exist in no-clobber mode, bail out |
| 225 | raise RedirectionError('File "%s" already exists' % filename) |
| 226 | |
| 227 | # Open and register |
| 228 | self._add_file_descriptor(io_number, filename, 'w') |
| 229 | |
| 230 | def _add_output_appending(self, interp, filename, io_number): |
| 231 | if io_number is None: |
| 232 | io_number = 1 |
| 233 | self._add_file_descriptor(io_number, filename, 'a') |
| 234 | |
| 235 | def _add_input_redirection(self, interp, filename, io_number): |
| 236 | if io_number is None: |
| 237 | io_number = 0 |
| 238 | self._add_file_descriptor(io_number, filename, 'r') |
| 239 | |
| 240 | def _add_file_descriptor(self, io_number, filename, mode): |
| 241 | try: |
| 242 | if filename.startswith('/'): |
| 243 | if filename=='/dev/null': |
| 244 | f = win32_open_devnull(mode+'b') |
| 245 | else: |
| 246 | # TODO: handle absolute pathnames, they are unlikely to exist on the |
| 247 | # current platform (win32 for instance). |
| 248 | raise NotImplementedError('cannot open absolute path %s' % repr(filename)) |
| 249 | else: |
| 250 | f = file(filename, mode+'b') |
| 251 | except IOError as e: |
| 252 | raise RedirectionError(str(e)) |
| 253 | |
| 254 | wrapper = None |
| 255 | try: |
| 256 | wrapper = FileWrapper(mode, f) |
| 257 | f = None |
| 258 | self._add_descriptor(io_number, wrapper) |
| 259 | except: |
| 260 | if f: f.close() |
| 261 | if wrapper: wrapper.close() |
| 262 | raise |
| 263 | |
| 264 | def _dup_output_descriptor(self, source_fd, dest_fd): |
| 265 | if source_fd is None: |
| 266 | source_fd = 1 |
| 267 | self._dup_file_descriptor(source_fd, dest_fd, 'w') |
| 268 | |
| 269 | def _dup_file_descriptor(self, source_fd, dest_fd, mode): |
| 270 | source_fd = int(source_fd) |
| 271 | if source_fd not in self._descriptors: |
| 272 | raise RedirectionError('"%s" is not a valid file descriptor' % str(source_fd)) |
| 273 | source = self._descriptors[source_fd] |
| 274 | |
| 275 | if source.mode()!=mode: |
| 276 | raise RedirectionError('Descriptor %s cannot be duplicated in mode "%s"' % (str(source), mode)) |
| 277 | |
| 278 | if dest_fd=='-': |
| 279 | # Close the source descriptor |
| 280 | del self._descriptors[source_fd] |
| 281 | source.close() |
| 282 | else: |
| 283 | dest_fd = int(dest_fd) |
| 284 | if dest_fd not in self._descriptors: |
| 285 | raise RedirectionError('Cannot replace file descriptor %s' % str(dest_fd)) |
| 286 | |
| 287 | dest = self._descriptors[dest_fd] |
| 288 | if dest.mode()!=mode: |
| 289 | raise RedirectionError('Descriptor %s cannot be cannot be redirected in mode "%s"' % (str(dest), mode)) |
| 290 | |
| 291 | self._descriptors[dest_fd] = source.dup() |
| 292 | dest.close() |
| 293 | |
| 294 | def _add_descriptor(self, io_number, file): |
| 295 | io_number = int(io_number) |
| 296 | |
| 297 | if io_number in self._descriptors: |
| 298 | # Close the current descriptor |
| 299 | d = self._descriptors[io_number] |
| 300 | del self._descriptors[io_number] |
| 301 | d.close() |
| 302 | |
| 303 | self._descriptors[io_number] = file |
| 304 | |
| 305 | def __str__(self): |
| 306 | names = [('%d=%r' % (k, getattr(v, 'name', None))) for k,v |
| 307 | in self._descriptors.iteritems()] |
| 308 | names = ','.join(names) |
| 309 | return 'Redirections(%s)' % names |
| 310 | |
| 311 | def __del__(self): |
| 312 | self.close() |
| 313 | |
| 314 | def cygwin_to_windows_path(path): |
| 315 | """Turn /cygdrive/c/foo into c:/foo, or return path if it |
| 316 | is not a cygwin path. |
| 317 | """ |
| 318 | if not path.startswith('/cygdrive/'): |
| 319 | return path |
| 320 | path = path[len('/cygdrive/'):] |
| 321 | path = path[:1] + ':' + path[1:] |
| 322 | return path |
| 323 | |
| 324 | def win32_to_unix_path(path): |
| 325 | if path is not None: |
| 326 | path = path.replace('\\', '/') |
| 327 | return path |
| 328 | |
| 329 | _RE_SHEBANG = re.compile(r'^\#!\s?([^\s]+)(?:\s([^\s]+))?') |
| 330 | _SHEBANG_CMDS = { |
| 331 | '/usr/bin/env': 'env', |
| 332 | '/bin/sh': 'pysh', |
| 333 | 'python': 'python', |
| 334 | } |
| 335 | |
| 336 | def resolve_shebang(path, ignoreshell=False): |
| 337 | """Return a list of arguments as shebang interpreter call or an empty list |
| 338 | if path does not refer to an executable script. |
| 339 | See <http://www.opengroup.org/austin/docs/austin_51r2.txt>. |
| 340 | |
| 341 | ignoreshell - set to True to ignore sh shebangs. Return an empty list instead. |
| 342 | """ |
| 343 | try: |
| 344 | f = file(path) |
| 345 | try: |
| 346 | # At most 80 characters in the first line |
| 347 | header = f.read(80).splitlines()[0] |
| 348 | finally: |
| 349 | f.close() |
| 350 | |
| 351 | m = _RE_SHEBANG.search(header) |
| 352 | if not m: |
| 353 | return [] |
| 354 | cmd, arg = m.group(1,2) |
| 355 | if os.path.isfile(cmd): |
| 356 | # Keep this one, the hg script for instance contains a weird windows |
| 357 | # shebang referencing the current python install. |
| 358 | cmdfile = os.path.basename(cmd).lower() |
| 359 | if cmdfile == 'python.exe': |
| 360 | cmd = 'python' |
| 361 | pass |
| 362 | elif cmd not in _SHEBANG_CMDS: |
| 363 | raise CommandNotFound('Unknown interpreter "%s" referenced in '\ |
| 364 | 'shebang' % header) |
| 365 | cmd = _SHEBANG_CMDS.get(cmd) |
| 366 | if cmd is None or (ignoreshell and cmd == 'pysh'): |
| 367 | return [] |
| 368 | if arg is None: |
| 369 | return [cmd, win32_to_unix_path(path)] |
| 370 | return [cmd, arg, win32_to_unix_path(path)] |
| 371 | except IOError as e: |
| 372 | if e.errno!=errno.ENOENT and \ |
| 373 | (e.errno!=errno.EPERM and not os.path.isdir(path)): # Opening a directory raises EPERM |
| 374 | raise |
| 375 | return [] |
| 376 | |
| 377 | def win32_find_in_path(name, path): |
| 378 | if isinstance(path, str): |
| 379 | path = path.split(os.pathsep) |
| 380 | |
| 381 | exts = os.environ.get('PATHEXT', '').lower().split(os.pathsep) |
| 382 | for p in path: |
| 383 | p_name = os.path.join(p, name) |
| 384 | |
| 385 | prefix = resolve_shebang(p_name) |
| 386 | if prefix: |
| 387 | return prefix |
| 388 | |
| 389 | for ext in exts: |
| 390 | p_name_ext = p_name + ext |
| 391 | if os.path.exists(p_name_ext): |
| 392 | return [win32_to_unix_path(p_name_ext)] |
| 393 | return [] |
| 394 | |
| 395 | class Traps(dict): |
| 396 | def __setitem__(self, key, value): |
| 397 | if key not in ('EXIT',): |
| 398 | raise NotImplementedError() |
| 399 | super(Traps, self).__setitem__(key, value) |
| 400 | |
| 401 | # IFS white spaces character class |
| 402 | _IFS_WHITESPACES = (' ', '\t', '\n') |
| 403 | |
| 404 | class Environment: |
| 405 | """Environment holds environment variables, export table, function |
| 406 | definitions and whatever is defined in 2.12 "Shell Execution Environment", |
| 407 | redirection excepted. |
| 408 | """ |
| 409 | def __init__(self, pwd): |
| 410 | self._opt = set() #Shell options |
| 411 | |
| 412 | self._functions = {} |
| 413 | self._env = {'?': '0', '#': '0'} |
| 414 | self._exported = set([ |
| 415 | 'HOME', 'IFS', 'PATH' |
| 416 | ]) |
| 417 | |
| 418 | # Set environment vars with side-effects |
| 419 | self._ifs_ws = None # Set of IFS whitespace characters |
| 420 | self._ifs_re = None # Regular expression used to split between words using IFS classes |
| 421 | self['IFS'] = ''.join(_IFS_WHITESPACES) #Default environment values |
| 422 | self['PWD'] = pwd |
| 423 | self.traps = Traps() |
| 424 | |
| 425 | def clone(self, subshell=False): |
| 426 | env = Environment(self['PWD']) |
| 427 | env._opt = set(self._opt) |
| 428 | for k,v in self.get_variables().iteritems(): |
| 429 | if k in self._exported: |
| 430 | env.export(k,v) |
| 431 | elif subshell: |
| 432 | env[k] = v |
| 433 | |
| 434 | if subshell: |
| 435 | env._functions = dict(self._functions) |
| 436 | |
| 437 | return env |
| 438 | |
| 439 | def __getitem__(self, key): |
| 440 | if key in ('@', '*', '-', '$'): |
| 441 | raise NotImplementedError('%s is not implemented' % repr(key)) |
| 442 | return self._env[key] |
| 443 | |
| 444 | def get(self, key, defval=None): |
| 445 | try: |
| 446 | return self[key] |
| 447 | except KeyError: |
| 448 | return defval |
| 449 | |
| 450 | def __setitem__(self, key, value): |
| 451 | if key=='IFS': |
| 452 | # Update the whitespace/non-whitespace classes |
| 453 | self._update_ifs(value) |
| 454 | elif key=='PWD': |
| 455 | pwd = os.path.abspath(value) |
| 456 | if not os.path.isdir(pwd): |
| 457 | raise VarAssignmentError('Invalid directory %s' % value) |
| 458 | value = pwd |
| 459 | elif key in ('?', '!'): |
| 460 | value = str(int(value)) |
| 461 | self._env[key] = value |
| 462 | |
| 463 | def __delitem__(self, key): |
| 464 | if key in ('IFS', 'PWD', '?'): |
| 465 | raise VarAssignmentError('%s cannot be unset' % key) |
| 466 | del self._env[key] |
| 467 | |
| 468 | def __contains__(self, item): |
| 469 | return item in self._env |
| 470 | |
| 471 | def set_positional_args(self, args): |
| 472 | """Set the content of 'args' as positional argument from 1 to len(args). |
| 473 | Return previous argument as a list of strings. |
| 474 | """ |
| 475 | # Save and remove previous arguments |
| 476 | prevargs = [] |
| 477 | for i in xrange(int(self._env['#'])): |
| 478 | i = str(i+1) |
| 479 | prevargs.append(self._env[i]) |
| 480 | del self._env[i] |
| 481 | self._env['#'] = '0' |
| 482 | |
| 483 | #Set new ones |
| 484 | for i,arg in enumerate(args): |
| 485 | self._env[str(i+1)] = str(arg) |
| 486 | self._env['#'] = str(len(args)) |
| 487 | |
| 488 | return prevargs |
| 489 | |
| 490 | def get_positional_args(self): |
| 491 | return [self._env[str(i+1)] for i in xrange(int(self._env['#']))] |
| 492 | |
| 493 | def get_variables(self): |
| 494 | return dict(self._env) |
| 495 | |
| 496 | def export(self, key, value=None): |
| 497 | if value is not None: |
| 498 | self[key] = value |
| 499 | self._exported.add(key) |
| 500 | |
| 501 | def get_exported(self): |
| 502 | return [(k,self._env.get(k)) for k in self._exported] |
| 503 | |
| 504 | def split_fields(self, word): |
| 505 | if not self._ifs_ws or not word: |
| 506 | return [word] |
| 507 | return re.split(self._ifs_re, word) |
| 508 | |
| 509 | def _update_ifs(self, value): |
| 510 | """Update the split_fields related variables when IFS character set is |
| 511 | changed. |
| 512 | """ |
| 513 | # TODO: handle NULL IFS |
| 514 | |
| 515 | # Separate characters in whitespace and non-whitespace |
| 516 | chars = set(value) |
| 517 | ws = [c for c in chars if c in _IFS_WHITESPACES] |
| 518 | nws = [c for c in chars if c not in _IFS_WHITESPACES] |
| 519 | |
| 520 | # Keep whitespaces in a string for left and right stripping |
| 521 | self._ifs_ws = ''.join(ws) |
| 522 | |
| 523 | # Build a regexp to split fields |
| 524 | trailing = '[' + ''.join([re.escape(c) for c in ws]) + ']' |
| 525 | if nws: |
| 526 | # First, the single non-whitespace occurence. |
| 527 | nws = '[' + ''.join([re.escape(c) for c in nws]) + ']' |
| 528 | nws = '(?:' + trailing + '*' + nws + trailing + '*' + '|' + trailing + '+)' |
| 529 | else: |
| 530 | # Then mix all parts with quantifiers |
| 531 | nws = trailing + '+' |
| 532 | self._ifs_re = re.compile(nws) |
| 533 | |
| 534 | def has_opt(self, opt, val=None): |
| 535 | return (opt, val) in self._opt |
| 536 | |
| 537 | def set_opt(self, opt, val=None): |
| 538 | self._opt.add((opt, val)) |
| 539 | |
| 540 | def find_in_path(self, name, pwd=False): |
| 541 | path = self._env.get('PATH', '').split(os.pathsep) |
| 542 | if pwd: |
| 543 | path[:0] = [self['PWD']] |
| 544 | if os.name == 'nt': |
| 545 | return win32_find_in_path(name, self._env.get('PATH', '')) |
| 546 | else: |
| 547 | raise NotImplementedError() |
| 548 | |
| 549 | def define_function(self, name, body): |
| 550 | if not is_name(name): |
| 551 | raise ShellSyntaxError('%s is not a valid function name' % repr(name)) |
| 552 | self._functions[name] = body |
| 553 | |
| 554 | def remove_function(self, name): |
| 555 | del self._functions[name] |
| 556 | |
| 557 | def is_function(self, name): |
| 558 | return name in self._functions |
| 559 | |
| 560 | def get_function(self, name): |
| 561 | return self._functions.get(name) |
| 562 | |
| 563 | |
| 564 | name_charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' |
| 565 | name_charset = dict(zip(name_charset,name_charset)) |
| 566 | |
| 567 | def match_name(s): |
| 568 | """Return the length in characters of the longest prefix made of name |
| 569 | allowed characters in s. |
| 570 | """ |
| 571 | for i,c in enumerate(s): |
| 572 | if c not in name_charset: |
| 573 | return s[:i] |
| 574 | return s |
| 575 | |
| 576 | def is_name(s): |
| 577 | return len([c for c in s if c not in name_charset])<=0 |
| 578 | |
| 579 | def is_special_param(c): |
| 580 | return len(c)==1 and c in ('@','*','#','?','-','$','!','0') |
| 581 | |
| 582 | def utility_not_implemented(name, *args, **kwargs): |
| 583 | raise NotImplementedError('%s utility is not implemented' % name) |
| 584 | |
| 585 | |
| 586 | class Utility: |
| 587 | """Define utilities properties: |
| 588 | func -- utility callable. See builtin module for utility samples. |
| 589 | is_special -- see XCU 2.8. |
| 590 | """ |
| 591 | def __init__(self, func, is_special=0): |
| 592 | self.func = func |
| 593 | self.is_special = bool(is_special) |
| 594 | |
| 595 | |
| 596 | def encodeargs(args): |
| 597 | def encodearg(s): |
| 598 | lines = base64.encodestring(s) |
| 599 | lines = [l.splitlines()[0] for l in lines] |
| 600 | return ''.join(lines) |
| 601 | |
| 602 | s = pickle.dumps(args) |
| 603 | return encodearg(s) |
| 604 | |
| 605 | def decodeargs(s): |
| 606 | s = base64.decodestring(s) |
| 607 | return pickle.loads(s) |
| 608 | |
| 609 | |
| 610 | class GlobError(Exception): |
| 611 | pass |
| 612 | |
| 613 | class Options: |
| 614 | def __init__(self): |
| 615 | # True if Mercurial operates with binary streams |
| 616 | self.hgbinary = True |
| 617 | |
| 618 | class Interpreter: |
| 619 | # Implementation is very basic: the execute() method just makes a DFS on the |
| 620 | # AST and execute nodes one by one. Nodes are tuple (name,obj) where name |
| 621 | # is a string identifier and obj the AST element returned by the parser. |
| 622 | # |
| 623 | # Handler are named after the node identifiers. |
| 624 | # TODO: check node names and remove the switch in execute with some |
| 625 | # dynamic getattr() call to find node handlers. |
| 626 | """Shell interpreter. |
| 627 | |
| 628 | The following debugging flags can be passed: |
| 629 | debug-parsing - enable PLY debugging. |
| 630 | debug-tree - print the generated AST. |
| 631 | debug-cmd - trace command execution before word expansion, plus exit status. |
| 632 | debug-utility - trace utility execution. |
| 633 | """ |
| 634 | |
| 635 | # List supported commands. |
| 636 | COMMANDS = { |
| 637 | 'cat': Utility(builtin.utility_cat,), |
| 638 | 'cd': Utility(builtin.utility_cd,), |
| 639 | ':': Utility(builtin.utility_colon,), |
| 640 | 'echo': Utility(builtin.utility_echo), |
| 641 | 'env': Utility(builtin.utility_env), |
| 642 | 'exit': Utility(builtin.utility_exit), |
| 643 | 'export': Utility(builtin.builtin_export, is_special=1), |
| 644 | 'egrep': Utility(builtin.utility_egrep), |
| 645 | 'fgrep': Utility(builtin.utility_fgrep), |
| 646 | 'gunzip': Utility(builtin.utility_gunzip), |
| 647 | 'kill': Utility(builtin.utility_kill), |
| 648 | 'mkdir': Utility(builtin.utility_mkdir), |
| 649 | 'netstat': Utility(builtin.utility_netstat), |
| 650 | 'printf': Utility(builtin.utility_printf), |
| 651 | 'pwd': Utility(builtin.utility_pwd), |
| 652 | 'return': Utility(builtin.builtin_return, is_special=1), |
| 653 | 'sed': Utility(builtin.utility_sed,), |
| 654 | 'set': Utility(builtin.builtin_set,), |
| 655 | 'shift': Utility(builtin.builtin_shift,), |
| 656 | 'sleep': Utility(builtin.utility_sleep,), |
| 657 | 'sort': Utility(builtin.utility_sort,), |
| 658 | 'trap': Utility(builtin.builtin_trap, is_special=1), |
| 659 | 'true': Utility(builtin.utility_true), |
| 660 | 'unset': Utility(builtin.builtin_unset, is_special=1), |
| 661 | 'wait': Utility(builtin.builtin_wait, is_special=1), |
| 662 | } |
| 663 | |
| 664 | def __init__(self, pwd, debugflags = [], env=None, redirs=None, stdin=None, |
| 665 | stdout=None, stderr=None, opts=Options()): |
| 666 | self._env = env |
| 667 | if self._env is None: |
| 668 | self._env = Environment(pwd) |
| 669 | self._children = {} |
| 670 | |
| 671 | self._redirs = redirs |
| 672 | self._close_redirs = False |
| 673 | |
| 674 | if self._redirs is None: |
| 675 | if stdin is None: |
| 676 | stdin = sys.stdin |
| 677 | if stdout is None: |
| 678 | stdout = sys.stdout |
| 679 | if stderr is None: |
| 680 | stderr = sys.stderr |
| 681 | stdin = FileWrapper('r', stdin, False) |
| 682 | stdout = FileWrapper('w', stdout, False) |
| 683 | stderr = FileWrapper('w', stderr, False) |
| 684 | self._redirs = Redirections(stdin, stdout, stderr) |
| 685 | self._close_redirs = True |
| 686 | |
| 687 | self._debugflags = list(debugflags) |
| 688 | self._logfile = sys.stderr |
| 689 | self._options = opts |
| 690 | |
| 691 | def close(self): |
| 692 | """Must be called when the interpreter is no longer used.""" |
| 693 | script = self._env.traps.get('EXIT') |
| 694 | if script: |
| 695 | try: |
| 696 | self.execute_script(script=script) |
| 697 | except: |
| 698 | pass |
| 699 | |
| 700 | if self._redirs is not None and self._close_redirs: |
| 701 | self._redirs.close() |
| 702 | self._redirs = None |
| 703 | |
| 704 | def log(self, s): |
| 705 | self._logfile.write(s) |
| 706 | self._logfile.flush() |
| 707 | |
| 708 | def __getitem__(self, key): |
| 709 | return self._env[key] |
| 710 | |
| 711 | def __setitem__(self, key, value): |
| 712 | self._env[key] = value |
| 713 | |
| 714 | def options(self): |
| 715 | return self._options |
| 716 | |
| 717 | def redirect(self, redirs, ios): |
| 718 | def add_redir(io): |
| 719 | if isinstance(io, pyshyacc.IORedirect): |
| 720 | redirs.add(self, io.op, io.filename, io.io_number) |
| 721 | else: |
| 722 | redirs.add_here_document(self, io.name, io.content, io.io_number) |
| 723 | |
| 724 | map(add_redir, ios) |
| 725 | return redirs |
| 726 | |
| 727 | def execute_script(self, script=None, ast=None, sourced=False, |
| 728 | scriptpath=None): |
| 729 | """If script is not None, parse the input. Otherwise takes the supplied |
| 730 | AST. Then execute the AST. |
| 731 | Return the script exit status. |
| 732 | """ |
| 733 | try: |
| 734 | if scriptpath is not None: |
| 735 | self._env['0'] = os.path.abspath(scriptpath) |
| 736 | |
| 737 | if script is not None: |
| 738 | debug_parsing = ('debug-parsing' in self._debugflags) |
| 739 | cmds, script = pyshyacc.parse(script, True, debug_parsing) |
| 740 | if 'debug-tree' in self._debugflags: |
| 741 | pyshyacc.print_commands(cmds, self._logfile) |
| 742 | self._logfile.flush() |
| 743 | else: |
| 744 | cmds, script = ast, '' |
| 745 | |
| 746 | status = 0 |
| 747 | for cmd in cmds: |
| 748 | try: |
| 749 | status = self.execute(cmd) |
| 750 | except ExitSignal as e: |
| 751 | if sourced: |
| 752 | raise |
| 753 | status = int(e.args[0]) |
| 754 | return status |
| 755 | except ShellError: |
| 756 | self._env['?'] = 1 |
| 757 | raise |
| 758 | if 'debug-utility' in self._debugflags or 'debug-cmd' in self._debugflags: |
| 759 | self.log('returncode ' + str(status)+ '\n') |
| 760 | return status |
| 761 | except CommandNotFound as e: |
| 762 | print >>self._redirs.stderr, str(e) |
| 763 | self._redirs.stderr.flush() |
| 764 | # Command not found by non-interactive shell |
| 765 | # return 127 |
| 766 | raise |
| 767 | except RedirectionError as e: |
| 768 | # TODO: should be handled depending on the utility status |
| 769 | print >>self._redirs.stderr, str(e) |
| 770 | self._redirs.stderr.flush() |
| 771 | # Command not found by non-interactive shell |
| 772 | # return 127 |
| 773 | raise |
| 774 | |
| 775 | def dotcommand(self, env, args): |
| 776 | if len(args) < 1: |
| 777 | raise ShellError('. expects at least one argument') |
| 778 | path = args[0] |
| 779 | if '/' not in path: |
| 780 | found = env.find_in_path(args[0], True) |
| 781 | if found: |
| 782 | path = found[0] |
| 783 | script = file(path).read() |
| 784 | return self.execute_script(script=script, sourced=True) |
| 785 | |
| 786 | def execute(self, token, redirs=None): |
| 787 | """Execute and AST subtree with supplied redirections overriding default |
| 788 | interpreter ones. |
| 789 | Return the exit status. |
| 790 | """ |
| 791 | if not token: |
| 792 | return 0 |
| 793 | |
| 794 | if redirs is None: |
| 795 | redirs = self._redirs |
| 796 | |
| 797 | if isinstance(token, list): |
| 798 | # Commands sequence |
| 799 | res = 0 |
| 800 | for t in token: |
| 801 | res = self.execute(t, redirs) |
| 802 | return res |
| 803 | |
| 804 | type, value = token |
| 805 | status = 0 |
| 806 | if type=='simple_command': |
| 807 | redirs_copy = redirs.clone() |
| 808 | try: |
| 809 | # TODO: define and handle command return values |
| 810 | # TODO: implement set -e |
| 811 | status = self._execute_simple_command(value, redirs_copy) |
| 812 | finally: |
| 813 | redirs_copy.close() |
| 814 | elif type=='pipeline': |
| 815 | status = self._execute_pipeline(value, redirs) |
| 816 | elif type=='and_or': |
| 817 | status = self._execute_and_or(value, redirs) |
| 818 | elif type=='for_clause': |
| 819 | status = self._execute_for_clause(value, redirs) |
| 820 | elif type=='while_clause': |
| 821 | status = self._execute_while_clause(value, redirs) |
| 822 | elif type=='function_definition': |
| 823 | status = self._execute_function_definition(value, redirs) |
| 824 | elif type=='brace_group': |
| 825 | status = self._execute_brace_group(value, redirs) |
| 826 | elif type=='if_clause': |
| 827 | status = self._execute_if_clause(value, redirs) |
| 828 | elif type=='subshell': |
| 829 | status = self.subshell(ast=value.cmds, redirs=redirs) |
| 830 | elif type=='async': |
| 831 | status = self._asynclist(value) |
| 832 | elif type=='redirect_list': |
| 833 | redirs_copy = self.redirect(redirs.clone(), value.redirs) |
| 834 | try: |
| 835 | status = self.execute(value.cmd, redirs_copy) |
| 836 | finally: |
| 837 | redirs_copy.close() |
| 838 | else: |
| 839 | raise NotImplementedError('Unsupported token type ' + type) |
| 840 | |
| 841 | if status < 0: |
| 842 | status = 255 |
| 843 | return status |
| 844 | |
| 845 | def _execute_if_clause(self, if_clause, redirs): |
| 846 | cond_status = self.execute(if_clause.cond, redirs) |
| 847 | if cond_status==0: |
| 848 | return self.execute(if_clause.if_cmds, redirs) |
| 849 | else: |
| 850 | return self.execute(if_clause.else_cmds, redirs) |
| 851 | |
| 852 | def _execute_brace_group(self, group, redirs): |
| 853 | status = 0 |
| 854 | for cmd in group.cmds: |
| 855 | status = self.execute(cmd, redirs) |
| 856 | return status |
| 857 | |
| 858 | def _execute_function_definition(self, fundef, redirs): |
| 859 | self._env.define_function(fundef.name, fundef.body) |
| 860 | return 0 |
| 861 | |
| 862 | def _execute_while_clause(self, while_clause, redirs): |
| 863 | status = 0 |
| 864 | while 1: |
| 865 | cond_status = 0 |
| 866 | for cond in while_clause.condition: |
| 867 | cond_status = self.execute(cond, redirs) |
| 868 | |
| 869 | if cond_status: |
| 870 | break |
| 871 | |
| 872 | for cmd in while_clause.cmds: |
| 873 | status = self.execute(cmd, redirs) |
| 874 | |
| 875 | return status |
| 876 | |
| 877 | def _execute_for_clause(self, for_clause, redirs): |
| 878 | if not is_name(for_clause.name): |
| 879 | raise ShellSyntaxError('%s is not a valid name' % repr(for_clause.name)) |
| 880 | items = mappend(self.expand_token, for_clause.items) |
| 881 | |
| 882 | status = 0 |
| 883 | for item in items: |
| 884 | self._env[for_clause.name] = item |
| 885 | for cmd in for_clause.cmds: |
| 886 | status = self.execute(cmd, redirs) |
| 887 | return status |
| 888 | |
| 889 | def _execute_and_or(self, or_and, redirs): |
| 890 | res = self.execute(or_and.left, redirs) |
| 891 | if (or_and.op=='&&' and res==0) or (or_and.op!='&&' and res!=0): |
| 892 | res = self.execute(or_and.right, redirs) |
| 893 | return res |
| 894 | |
| 895 | def _execute_pipeline(self, pipeline, redirs): |
| 896 | if len(pipeline.commands)==1: |
| 897 | status = self.execute(pipeline.commands[0], redirs) |
| 898 | else: |
| 899 | # Execute all commands one after the other |
| 900 | status = 0 |
| 901 | inpath, outpath = None, None |
| 902 | try: |
| 903 | # Commands inputs and outputs cannot really be plugged as done |
| 904 | # by a real shell. Run commands sequentially and chain their |
| 905 | # input/output throught temporary files. |
| 906 | tmpfd, inpath = tempfile.mkstemp() |
| 907 | os.close(tmpfd) |
| 908 | tmpfd, outpath = tempfile.mkstemp() |
| 909 | os.close(tmpfd) |
| 910 | |
| 911 | inpath = win32_to_unix_path(inpath) |
| 912 | outpath = win32_to_unix_path(outpath) |
| 913 | |
| 914 | for i, cmd in enumerate(pipeline.commands): |
| 915 | call_redirs = redirs.clone() |
| 916 | try: |
| 917 | if i!=0: |
| 918 | call_redirs.add(self, '<', inpath) |
| 919 | if i!=len(pipeline.commands)-1: |
| 920 | call_redirs.add(self, '>', outpath) |
| 921 | |
| 922 | status = self.execute(cmd, call_redirs) |
| 923 | |
| 924 | # Chain inputs/outputs |
| 925 | inpath, outpath = outpath, inpath |
| 926 | finally: |
| 927 | call_redirs.close() |
| 928 | finally: |
| 929 | if inpath: os.remove(inpath) |
| 930 | if outpath: os.remove(outpath) |
| 931 | |
| 932 | if pipeline.reverse_status: |
| 933 | status = int(not status) |
| 934 | self._env['?'] = status |
| 935 | return status |
| 936 | |
| 937 | def _execute_function(self, name, args, interp, env, stdin, stdout, stderr, *others): |
| 938 | assert interp is self |
| 939 | |
| 940 | func = env.get_function(name) |
| 941 | #Set positional parameters |
| 942 | prevargs = None |
| 943 | try: |
| 944 | prevargs = env.set_positional_args(args) |
| 945 | try: |
| 946 | redirs = Redirections(stdin.dup(), stdout.dup(), stderr.dup()) |
| 947 | try: |
| 948 | status = self.execute(func, redirs) |
| 949 | finally: |
| 950 | redirs.close() |
| 951 | except ReturnSignal as e: |
| 952 | status = int(e.args[0]) |
| 953 | env['?'] = status |
| 954 | return status |
| 955 | finally: |
| 956 | #Reset positional parameters |
| 957 | if prevargs is not None: |
| 958 | env.set_positional_args(prevargs) |
| 959 | |
| 960 | def _execute_simple_command(self, token, redirs): |
| 961 | """Can raise ReturnSignal when return builtin is called, ExitSignal when |
| 962 | exit is called, and other shell exceptions upon builtin failures. |
| 963 | """ |
| 964 | debug_command = 'debug-cmd' in self._debugflags |
| 965 | if debug_command: |
| 966 | self.log('word' + repr(token.words) + '\n') |
| 967 | self.log('assigns' + repr(token.assigns) + '\n') |
| 968 | self.log('redirs' + repr(token.redirs) + '\n') |
| 969 | |
| 970 | is_special = None |
| 971 | env = self._env |
| 972 | |
| 973 | try: |
| 974 | # Word expansion |
| 975 | args = [] |
| 976 | for word in token.words: |
| 977 | args += self.expand_token(word) |
| 978 | if is_special is None and args: |
| 979 | is_special = env.is_function(args[0]) or \ |
| 980 | (args[0] in self.COMMANDS and self.COMMANDS[args[0]].is_special) |
| 981 | |
| 982 | if debug_command: |
| 983 | self.log('_execute_simple_command' + str(args) + '\n') |
| 984 | |
| 985 | if not args: |
| 986 | # Redirections happen is a subshell |
| 987 | redirs = redirs.clone() |
| 988 | elif not is_special: |
| 989 | env = self._env.clone() |
| 990 | |
| 991 | # Redirections |
| 992 | self.redirect(redirs, token.redirs) |
| 993 | |
| 994 | # Variables assignments |
| 995 | res = 0 |
| 996 | for type,(k,v) in token.assigns: |
| 997 | status, expanded = self.expand_variable((k,v)) |
| 998 | if status is not None: |
| 999 | res = status |
| 1000 | if args: |
| 1001 | env.export(k, expanded) |
| 1002 | else: |
| 1003 | env[k] = expanded |
| 1004 | |
| 1005 | if args and args[0] in ('.', 'source'): |
| 1006 | res = self.dotcommand(env, args[1:]) |
| 1007 | elif args: |
| 1008 | if args[0] in self.COMMANDS: |
| 1009 | command = self.COMMANDS[args[0]] |
| 1010 | elif env.is_function(args[0]): |
| 1011 | command = Utility(self._execute_function, is_special=True) |
| 1012 | else: |
| 1013 | if not '/' in args[0].replace('\\', '/'): |
| 1014 | cmd = env.find_in_path(args[0]) |
| 1015 | if not cmd: |
| 1016 | # TODO: test error code on unknown command => 127 |
| 1017 | raise CommandNotFound('Unknown command: "%s"' % args[0]) |
| 1018 | else: |
| 1019 | # Handle commands like '/cygdrive/c/foo.bat' |
| 1020 | cmd = cygwin_to_windows_path(args[0]) |
| 1021 | if not os.path.exists(cmd): |
| 1022 | raise CommandNotFound('%s: No such file or directory' % args[0]) |
| 1023 | shebang = resolve_shebang(cmd) |
| 1024 | if shebang: |
| 1025 | cmd = shebang |
| 1026 | else: |
| 1027 | cmd = [cmd] |
| 1028 | args[0:1] = cmd |
| 1029 | command = Utility(builtin.run_command) |
| 1030 | |
| 1031 | # Command execution |
| 1032 | if 'debug-cmd' in self._debugflags: |
| 1033 | self.log('redirections ' + str(redirs) + '\n') |
| 1034 | |
| 1035 | res = command.func(args[0], args[1:], self, env, |
| 1036 | redirs.stdin(), redirs.stdout(), |
| 1037 | redirs.stderr(), self._debugflags) |
| 1038 | |
| 1039 | if self._env.has_opt('-x'): |
| 1040 | # Trace command execution in shell environment |
| 1041 | # BUG: would be hard to reproduce a real shell behaviour since |
| 1042 | # the AST is not annotated with source lines/tokens. |
| 1043 | self._redirs.stdout().write(' '.join(args)) |
| 1044 | |
| 1045 | except ReturnSignal: |
| 1046 | raise |
| 1047 | except ShellError as e: |
| 1048 | if is_special or isinstance(e, (ExitSignal, |
| 1049 | ShellSyntaxError, ExpansionError)): |
| 1050 | raise e |
| 1051 | self._redirs.stderr().write(str(e)+'\n') |
| 1052 | return 1 |
| 1053 | |
| 1054 | return res |
| 1055 | |
| 1056 | def expand_token(self, word): |
| 1057 | """Expand a word as specified in [2.6 Word Expansions]. Return the list |
| 1058 | of expanded words. |
| 1059 | """ |
| 1060 | status, wtrees = self._expand_word(word) |
| 1061 | return map(pyshlex.wordtree_as_string, wtrees) |
| 1062 | |
| 1063 | def expand_variable(self, word): |
| 1064 | """Return a status code (or None if no command expansion occurred) |
| 1065 | and a single word. |
| 1066 | """ |
| 1067 | status, wtrees = self._expand_word(word, pathname=False, split=False) |
| 1068 | words = map(pyshlex.wordtree_as_string, wtrees) |
| 1069 | assert len(words)==1 |
| 1070 | return status, words[0] |
| 1071 | |
| 1072 | def expand_here_document(self, word): |
| 1073 | """Return the expanded document as a single word. The here document is |
| 1074 | assumed to be unquoted. |
| 1075 | """ |
| 1076 | status, wtrees = self._expand_word(word, pathname=False, |
| 1077 | split=False, here_document=True) |
| 1078 | words = map(pyshlex.wordtree_as_string, wtrees) |
| 1079 | assert len(words)==1 |
| 1080 | return words[0] |
| 1081 | |
| 1082 | def expand_redirection(self, word): |
| 1083 | """Return a single word.""" |
| 1084 | return self.expand_variable(word)[1] |
| 1085 | |
| 1086 | def get_env(self): |
| 1087 | return self._env |
| 1088 | |
| 1089 | def _expand_word(self, token, pathname=True, split=True, here_document=False): |
| 1090 | wtree = pyshlex.make_wordtree(token[1], here_document=here_document) |
| 1091 | |
| 1092 | # TODO: implement tilde expansion |
| 1093 | def expand(wtree): |
| 1094 | """Return a pseudo wordtree: the tree or its subelements can be empty |
| 1095 | lists when no value result from the expansion. |
| 1096 | """ |
| 1097 | status = None |
| 1098 | for part in wtree: |
| 1099 | if not isinstance(part, list): |
| 1100 | continue |
| 1101 | if part[0]in ("'", '\\'): |
| 1102 | continue |
| 1103 | elif part[0] in ('`', '$('): |
| 1104 | status, result = self._expand_command(part) |
| 1105 | part[:] = result |
| 1106 | elif part[0] in ('$', '${'): |
| 1107 | part[:] = self._expand_parameter(part, wtree[0]=='"', split) |
| 1108 | elif part[0] in ('', '"'): |
| 1109 | status, result = expand(part) |
| 1110 | part[:] = result |
| 1111 | else: |
| 1112 | raise NotImplementedError('%s expansion is not implemented' |
| 1113 | % part[0]) |
| 1114 | # [] is returned when an expansion result in no-field, |
| 1115 | # like an empty $@ |
| 1116 | wtree = [p for p in wtree if p != []] |
| 1117 | if len(wtree) < 3: |
| 1118 | return status, [] |
| 1119 | return status, wtree |
| 1120 | |
| 1121 | status, wtree = expand(wtree) |
| 1122 | if len(wtree) == 0: |
| 1123 | return status, wtree |
| 1124 | wtree = pyshlex.normalize_wordtree(wtree) |
| 1125 | |
| 1126 | if split: |
| 1127 | wtrees = self._split_fields(wtree) |
| 1128 | else: |
| 1129 | wtrees = [wtree] |
| 1130 | |
| 1131 | if pathname: |
| 1132 | wtrees = mappend(self._expand_pathname, wtrees) |
| 1133 | |
| 1134 | wtrees = map(self._remove_quotes, wtrees) |
| 1135 | return status, wtrees |
| 1136 | |
| 1137 | def _expand_command(self, wtree): |
| 1138 | # BUG: there is something to do with backslashes and quoted |
| 1139 | # characters here |
| 1140 | command = pyshlex.wordtree_as_string(wtree[1:-1]) |
| 1141 | status, output = self.subshell_output(command) |
| 1142 | return status, ['', output, ''] |
| 1143 | |
| 1144 | def _expand_parameter(self, wtree, quoted=False, split=False): |
| 1145 | """Return a valid wtree or an empty list when no parameter results.""" |
| 1146 | # Get the parameter name |
| 1147 | # TODO: implement weird expansion rules with ':' |
| 1148 | name = pyshlex.wordtree_as_string(wtree[1:-1]) |
| 1149 | if not is_name(name) and not is_special_param(name): |
| 1150 | raise ExpansionError('Bad substitution "%s"' % name) |
| 1151 | # TODO: implement special parameters |
| 1152 | if name in ('@', '*'): |
| 1153 | args = self._env.get_positional_args() |
| 1154 | if len(args) == 0: |
| 1155 | return [] |
| 1156 | if len(args)<2: |
| 1157 | return ['', ''.join(args), ''] |
| 1158 | |
| 1159 | sep = self._env.get('IFS', '')[:1] |
| 1160 | if split and quoted and name=='@': |
| 1161 | # Introduce a new token to tell the caller that these parameters |
| 1162 | # cause a split as specified in 2.5.2 |
| 1163 | return ['@'] + args + [''] |
| 1164 | else: |
| 1165 | return ['', sep.join(args), ''] |
| 1166 | |
| 1167 | return ['', self._env.get(name, ''), ''] |
| 1168 | |
| 1169 | def _split_fields(self, wtree): |
| 1170 | def is_empty(split): |
| 1171 | return split==['', '', ''] |
| 1172 | |
| 1173 | def split_positional(quoted): |
| 1174 | # Return a list of wtree split according positional parameters rules. |
| 1175 | # All remaining '@' groups are removed. |
| 1176 | assert quoted[0]=='"' |
| 1177 | |
| 1178 | splits = [[]] |
| 1179 | for part in quoted: |
| 1180 | if not isinstance(part, list) or part[0]!='@': |
| 1181 | splits[-1].append(part) |
| 1182 | else: |
| 1183 | # Empty or single argument list were dealt with already |
| 1184 | assert len(part)>3 |
| 1185 | # First argument must join with the beginning part of the original word |
| 1186 | splits[-1].append(part[1]) |
| 1187 | # Create double-quotes expressions for every argument after the first |
| 1188 | for arg in part[2:-1]: |
| 1189 | splits[-1].append('"') |
| 1190 | splits.append(['"', arg]) |
| 1191 | return splits |
| 1192 | |
| 1193 | # At this point, all expansions but pathnames have occured. Only quoted |
| 1194 | # and positional sequences remain. Thus, all candidates for field splitting |
| 1195 | # are in the tree root, or are positional splits ('@') and lie in root |
| 1196 | # children. |
| 1197 | if not wtree or wtree[0] not in ('', '"'): |
| 1198 | # The whole token is quoted or empty, nothing to split |
| 1199 | return [wtree] |
| 1200 | |
| 1201 | if wtree[0]=='"': |
| 1202 | wtree = ['', wtree, ''] |
| 1203 | |
| 1204 | result = [['', '']] |
| 1205 | for part in wtree[1:-1]: |
| 1206 | if isinstance(part, list): |
| 1207 | if part[0]=='"': |
| 1208 | splits = split_positional(part) |
| 1209 | if len(splits)<=1: |
| 1210 | result[-1] += [part, ''] |
| 1211 | else: |
| 1212 | # Terminate the current split |
| 1213 | result[-1] += [splits[0], ''] |
| 1214 | result += splits[1:-1] |
| 1215 | # Create a new split |
| 1216 | result += [['', splits[-1], '']] |
| 1217 | else: |
| 1218 | result[-1] += [part, ''] |
| 1219 | else: |
| 1220 | splits = self._env.split_fields(part) |
| 1221 | if len(splits)<=1: |
| 1222 | # No split |
| 1223 | result[-1][-1] += part |
| 1224 | else: |
| 1225 | # Terminate the current resulting part and create a new one |
| 1226 | result[-1][-1] += splits[0] |
| 1227 | result[-1].append('') |
| 1228 | result += [['', r, ''] for r in splits[1:-1]] |
| 1229 | result += [['', splits[-1]]] |
| 1230 | result[-1].append('') |
| 1231 | |
| 1232 | # Leading and trailing empty groups come from leading/trailing blanks |
| 1233 | if result and is_empty(result[-1]): |
| 1234 | result[-1:] = [] |
| 1235 | if result and is_empty(result[0]): |
| 1236 | result[:1] = [] |
| 1237 | return result |
| 1238 | |
| 1239 | def _expand_pathname(self, wtree): |
| 1240 | """See [2.6.6 Pathname Expansion].""" |
| 1241 | if self._env.has_opt('-f'): |
| 1242 | return [wtree] |
| 1243 | |
| 1244 | # All expansions have been performed, only quoted sequences should remain |
| 1245 | # in the tree. Generate the pattern by folding the tree, escaping special |
| 1246 | # characters when appear quoted |
| 1247 | special_chars = '*?[]' |
| 1248 | |
| 1249 | def make_pattern(wtree): |
| 1250 | subpattern = [] |
| 1251 | for part in wtree[1:-1]: |
| 1252 | if isinstance(part, list): |
| 1253 | part = make_pattern(part) |
| 1254 | elif wtree[0]!='': |
| 1255 | for c in part: |
| 1256 | # Meta-characters cannot be quoted |
| 1257 | if c in special_chars: |
| 1258 | raise GlobError() |
| 1259 | subpattern.append(part) |
| 1260 | return ''.join(subpattern) |
| 1261 | |
| 1262 | def pwd_glob(pattern): |
| 1263 | cwd = os.getcwd() |
| 1264 | os.chdir(self._env['PWD']) |
| 1265 | try: |
| 1266 | return glob.glob(pattern) |
| 1267 | finally: |
| 1268 | os.chdir(cwd) |
| 1269 | |
| 1270 | #TODO: check working directory issues here wrt relative patterns |
| 1271 | try: |
| 1272 | pattern = make_pattern(wtree) |
| 1273 | paths = pwd_glob(pattern) |
| 1274 | except GlobError: |
| 1275 | # BUG: Meta-characters were found in quoted sequences. The should |
| 1276 | # have been used literally but this is unsupported in current glob module. |
| 1277 | # Instead we consider the whole tree must be used literally and |
| 1278 | # therefore there is no point in globbing. This is wrong when meta |
| 1279 | # characters are mixed with quoted meta in the same pattern like: |
| 1280 | # < foo*"py*" > |
| 1281 | paths = [] |
| 1282 | |
| 1283 | if not paths: |
| 1284 | return [wtree] |
| 1285 | return [['', path, ''] for path in paths] |
| 1286 | |
| 1287 | def _remove_quotes(self, wtree): |
| 1288 | """See [2.6.7 Quote Removal].""" |
| 1289 | |
| 1290 | def unquote(wtree): |
| 1291 | unquoted = [] |
| 1292 | for part in wtree[1:-1]: |
| 1293 | if isinstance(part, list): |
| 1294 | part = unquote(part) |
| 1295 | unquoted.append(part) |
| 1296 | return ''.join(unquoted) |
| 1297 | |
| 1298 | return ['', unquote(wtree), ''] |
| 1299 | |
| 1300 | def subshell(self, script=None, ast=None, redirs=None): |
| 1301 | """Execute the script or AST in a subshell, with inherited redirections |
| 1302 | if redirs is not None. |
| 1303 | """ |
| 1304 | if redirs: |
| 1305 | sub_redirs = redirs |
| 1306 | else: |
| 1307 | sub_redirs = redirs.clone() |
| 1308 | |
| 1309 | subshell = None |
| 1310 | try: |
| 1311 | subshell = Interpreter(None, self._debugflags, self._env.clone(True), |
| 1312 | sub_redirs, opts=self._options) |
| 1313 | return subshell.execute_script(script, ast) |
| 1314 | finally: |
| 1315 | if not redirs: sub_redirs.close() |
| 1316 | if subshell: subshell.close() |
| 1317 | |
| 1318 | def subshell_output(self, script): |
| 1319 | """Execute the script in a subshell and return the captured output.""" |
| 1320 | # Create temporary file to capture subshell output |
| 1321 | tmpfd, tmppath = tempfile.mkstemp() |
| 1322 | try: |
| 1323 | tmpfile = os.fdopen(tmpfd, 'wb') |
| 1324 | stdout = FileWrapper('w', tmpfile) |
| 1325 | |
| 1326 | redirs = Redirections(self._redirs.stdin().dup(), |
| 1327 | stdout, |
| 1328 | self._redirs.stderr().dup()) |
| 1329 | try: |
| 1330 | status = self.subshell(script=script, redirs=redirs) |
| 1331 | finally: |
| 1332 | redirs.close() |
| 1333 | redirs = None |
| 1334 | |
| 1335 | # Extract subshell standard output |
| 1336 | tmpfile = open(tmppath, 'rb') |
| 1337 | try: |
| 1338 | output = tmpfile.read() |
| 1339 | return status, output.rstrip('\n') |
| 1340 | finally: |
| 1341 | tmpfile.close() |
| 1342 | finally: |
| 1343 | os.remove(tmppath) |
| 1344 | |
| 1345 | def _asynclist(self, cmd): |
| 1346 | args = (self._env.get_variables(), cmd) |
| 1347 | arg = encodeargs(args) |
| 1348 | assert len(args) < 30*1024 |
| 1349 | cmd = ['pysh.bat', '--ast', '-c', arg] |
| 1350 | p = subprocess.Popen(cmd, cwd=self._env['PWD']) |
| 1351 | self._children[p.pid] = p |
| 1352 | self._env['!'] = p.pid |
| 1353 | return 0 |
| 1354 | |
| 1355 | def wait(self, pids=None): |
| 1356 | if not pids: |
| 1357 | pids = self._children.keys() |
| 1358 | |
| 1359 | status = 127 |
| 1360 | for pid in pids: |
| 1361 | if pid not in self._children: |
| 1362 | continue |
| 1363 | p = self._children.pop(pid) |
| 1364 | status = p.wait() |
| 1365 | |
| 1366 | return status |
| 1367 | |