| # interp.py - shell interpreter for pysh. |
| # |
| # Copyright 2007 Patrick Mezard |
| # |
| # This software may be used and distributed according to the terms |
| # of the GNU General Public License, incorporated herein by reference. |
| |
| """Implement the shell interpreter. |
| |
| Most references are made to "The Open Group Base Specifications Issue 6". |
| <http://www.opengroup.org/onlinepubs/009695399/utilities/xcu_chap02.html> |
| """ |
| # TODO: document the fact input streams must implement fileno() so Popen will work correctly. |
| # it requires non-stdin stream to be implemented as files. Still to be tested... |
| # DOC: pathsep is used in PATH instead of ':'. Clearly, there are path syntax issues here. |
| # TODO: stop command execution upon error. |
| # TODO: sort out the filename/io_number mess. It should be possible to use filenames only. |
| # TODO: review subshell implementation |
| # TODO: test environment cloning for non-special builtins |
| # TODO: set -x should not rebuild commands from tokens, assignments/redirections are lost |
| # TODO: unit test for variable assignment |
| # TODO: test error management wrt error type/utility type |
| # TODO: test for binary output everywhere |
| # BUG: debug-parsing does not pass log file to PLY. Maybe a PLY upgrade is necessary. |
| import base64 |
| import cPickle as pickle |
| import errno |
| import glob |
| import os |
| import re |
| import subprocess |
| import sys |
| import tempfile |
| |
| try: |
| s = set() |
| del s |
| except NameError: |
| from Set import Set as set |
| |
| import builtin |
| from sherrors import * |
| import pyshlex |
| import pyshyacc |
| |
| def mappend(func, *args, **kargs): |
| """Like map but assume func returns a list. Returned lists are merged into |
| a single one. |
| """ |
| return reduce(lambda a,b: a+b, map(func, *args, **kargs), []) |
| |
| class FileWrapper: |
| """File object wrapper to ease debugging. |
| |
| Allow mode checking and implement file duplication through a simple |
| reference counting scheme. Not sure the latter is really useful since |
| only real file descriptors can be used. |
| """ |
| def __init__(self, mode, file, close=True): |
| if mode not in ('r', 'w', 'a'): |
| raise IOError('invalid mode: %s' % mode) |
| self._mode = mode |
| self._close = close |
| if isinstance(file, FileWrapper): |
| if file._refcount[0] <= 0: |
| raise IOError(0, 'Error') |
| self._refcount = file._refcount |
| self._refcount[0] += 1 |
| self._file = file._file |
| else: |
| self._refcount = [1] |
| self._file = file |
| |
| def dup(self): |
| return FileWrapper(self._mode, self, self._close) |
| |
| def fileno(self): |
| """fileno() should be only necessary for input streams.""" |
| return self._file.fileno() |
| |
| def read(self, size=-1): |
| if self._mode!='r': |
| raise IOError(0, 'Error') |
| return self._file.read(size) |
| |
| def readlines(self, *args, **kwargs): |
| return self._file.readlines(*args, **kwargs) |
| |
| def write(self, s): |
| if self._mode not in ('w', 'a'): |
| raise IOError(0, 'Error') |
| return self._file.write(s) |
| |
| def flush(self): |
| self._file.flush() |
| |
| def close(self): |
| if not self._refcount: |
| return |
| assert self._refcount[0] > 0 |
| |
| self._refcount[0] -= 1 |
| if self._refcount[0] == 0: |
| self._mode = 'c' |
| if self._close: |
| self._file.close() |
| self._refcount = None |
| |
| def mode(self): |
| return self._mode |
| |
| def __getattr__(self, name): |
| if name == 'name': |
| self.name = getattr(self._file, name) |
| return self.name |
| else: |
| raise AttributeError(name) |
| |
| def __del__(self): |
| self.close() |
| |
| |
| def win32_open_devnull(mode): |
| return open('NUL', mode) |
| |
| |
| class Redirections: |
| """Stores open files and their mapping to pseudo-sh file descriptor. |
| """ |
| # BUG: redirections are not handled correctly: 1>&3 2>&3 3>&4 does |
| # not make 1 to redirect to 4 |
| def __init__(self, stdin=None, stdout=None, stderr=None): |
| self._descriptors = {} |
| if stdin is not None: |
| self._add_descriptor(0, stdin) |
| if stdout is not None: |
| self._add_descriptor(1, stdout) |
| if stderr is not None: |
| self._add_descriptor(2, stderr) |
| |
| def add_here_document(self, interp, name, content, io_number=None): |
| if io_number is None: |
| io_number = 0 |
| |
| if name==pyshlex.unquote_wordtree(name): |
| content = interp.expand_here_document(('TOKEN', content)) |
| |
| # Write document content in a temporary file |
| tmp = tempfile.TemporaryFile() |
| try: |
| tmp.write(content) |
| tmp.flush() |
| tmp.seek(0) |
| self._add_descriptor(io_number, FileWrapper('r', tmp)) |
| except: |
| tmp.close() |
| raise |
| |
| def add(self, interp, op, filename, io_number=None): |
| if op not in ('<', '>', '>|', '>>', '>&'): |
| # TODO: add descriptor duplication and here_documents |
| raise RedirectionError('Unsupported redirection operator "%s"' % op) |
| |
| if io_number is not None: |
| io_number = int(io_number) |
| |
| if (op == '>&' and filename.isdigit()) or filename=='-': |
| # No expansion for file descriptors, quote them if you want a filename |
| fullname = filename |
| else: |
| if filename.startswith('/'): |
| # TODO: win32 kludge |
| if filename=='/dev/null': |
| fullname = 'NUL' |
| else: |
| # TODO: handle absolute pathnames, they are unlikely to exist on the |
| # current platform (win32 for instance). |
| raise NotImplementedError() |
| else: |
| fullname = interp.expand_redirection(('TOKEN', filename)) |
| if not fullname: |
| raise RedirectionError('%s: ambiguous redirect' % filename) |
| # Build absolute path based on PWD |
| fullname = os.path.join(interp.get_env()['PWD'], fullname) |
| |
| if op=='<': |
| return self._add_input_redirection(interp, fullname, io_number) |
| elif op in ('>', '>|'): |
| clobber = ('>|'==op) |
| return self._add_output_redirection(interp, fullname, io_number, clobber) |
| elif op=='>>': |
| return self._add_output_appending(interp, fullname, io_number) |
| elif op=='>&': |
| return self._dup_output_descriptor(fullname, io_number) |
| |
| def close(self): |
| if self._descriptors is not None: |
| for desc in self._descriptors.itervalues(): |
| desc.flush() |
| desc.close() |
| self._descriptors = None |
| |
| def stdin(self): |
| return self._descriptors[0] |
| |
| def stdout(self): |
| return self._descriptors[1] |
| |
| def stderr(self): |
| return self._descriptors[2] |
| |
| def clone(self): |
| clone = Redirections() |
| for desc, fileobj in self._descriptors.iteritems(): |
| clone._descriptors[desc] = fileobj.dup() |
| return clone |
| |
| def _add_output_redirection(self, interp, filename, io_number, clobber): |
| if io_number is None: |
| # io_number default to standard output |
| io_number = 1 |
| |
| if not clobber and interp.get_env().has_opt('-C') and os.path.isfile(filename): |
| # File already exist in no-clobber mode, bail out |
| raise RedirectionError('File "%s" already exists' % filename) |
| |
| # Open and register |
| self._add_file_descriptor(io_number, filename, 'w') |
| |
| def _add_output_appending(self, interp, filename, io_number): |
| if io_number is None: |
| io_number = 1 |
| self._add_file_descriptor(io_number, filename, 'a') |
| |
| def _add_input_redirection(self, interp, filename, io_number): |
| if io_number is None: |
| io_number = 0 |
| self._add_file_descriptor(io_number, filename, 'r') |
| |
| def _add_file_descriptor(self, io_number, filename, mode): |
| try: |
| if filename.startswith('/'): |
| if filename=='/dev/null': |
| f = win32_open_devnull(mode+'b') |
| else: |
| # TODO: handle absolute pathnames, they are unlikely to exist on the |
| # current platform (win32 for instance). |
| raise NotImplementedError('cannot open absolute path %s' % repr(filename)) |
| else: |
| f = file(filename, mode+'b') |
| except IOError as e: |
| raise RedirectionError(str(e)) |
| |
| wrapper = None |
| try: |
| wrapper = FileWrapper(mode, f) |
| f = None |
| self._add_descriptor(io_number, wrapper) |
| except: |
| if f: f.close() |
| if wrapper: wrapper.close() |
| raise |
| |
| def _dup_output_descriptor(self, source_fd, dest_fd): |
| if source_fd is None: |
| source_fd = 1 |
| self._dup_file_descriptor(source_fd, dest_fd, 'w') |
| |
| def _dup_file_descriptor(self, source_fd, dest_fd, mode): |
| source_fd = int(source_fd) |
| if source_fd not in self._descriptors: |
| raise RedirectionError('"%s" is not a valid file descriptor' % str(source_fd)) |
| source = self._descriptors[source_fd] |
| |
| if source.mode()!=mode: |
| raise RedirectionError('Descriptor %s cannot be duplicated in mode "%s"' % (str(source), mode)) |
| |
| if dest_fd=='-': |
| # Close the source descriptor |
| del self._descriptors[source_fd] |
| source.close() |
| else: |
| dest_fd = int(dest_fd) |
| if dest_fd not in self._descriptors: |
| raise RedirectionError('Cannot replace file descriptor %s' % str(dest_fd)) |
| |
| dest = self._descriptors[dest_fd] |
| if dest.mode()!=mode: |
| raise RedirectionError('Descriptor %s cannot be cannot be redirected in mode "%s"' % (str(dest), mode)) |
| |
| self._descriptors[dest_fd] = source.dup() |
| dest.close() |
| |
| def _add_descriptor(self, io_number, file): |
| io_number = int(io_number) |
| |
| if io_number in self._descriptors: |
| # Close the current descriptor |
| d = self._descriptors[io_number] |
| del self._descriptors[io_number] |
| d.close() |
| |
| self._descriptors[io_number] = file |
| |
| def __str__(self): |
| names = [('%d=%r' % (k, getattr(v, 'name', None))) for k,v |
| in self._descriptors.iteritems()] |
| names = ','.join(names) |
| return 'Redirections(%s)' % names |
| |
| def __del__(self): |
| self.close() |
| |
| def cygwin_to_windows_path(path): |
| """Turn /cygdrive/c/foo into c:/foo, or return path if it |
| is not a cygwin path. |
| """ |
| if not path.startswith('/cygdrive/'): |
| return path |
| path = path[len('/cygdrive/'):] |
| path = path[:1] + ':' + path[1:] |
| return path |
| |
| def win32_to_unix_path(path): |
| if path is not None: |
| path = path.replace('\\', '/') |
| return path |
| |
| _RE_SHEBANG = re.compile(r'^\#!\s?([^\s]+)(?:\s([^\s]+))?') |
| _SHEBANG_CMDS = { |
| '/usr/bin/env': 'env', |
| '/bin/sh': 'pysh', |
| 'python': 'python', |
| } |
| |
| def resolve_shebang(path, ignoreshell=False): |
| """Return a list of arguments as shebang interpreter call or an empty list |
| if path does not refer to an executable script. |
| See <http://www.opengroup.org/austin/docs/austin_51r2.txt>. |
| |
| ignoreshell - set to True to ignore sh shebangs. Return an empty list instead. |
| """ |
| try: |
| f = file(path) |
| try: |
| # At most 80 characters in the first line |
| header = f.read(80).splitlines()[0] |
| finally: |
| f.close() |
| |
| m = _RE_SHEBANG.search(header) |
| if not m: |
| return [] |
| cmd, arg = m.group(1,2) |
| if os.path.isfile(cmd): |
| # Keep this one, the hg script for instance contains a weird windows |
| # shebang referencing the current python install. |
| cmdfile = os.path.basename(cmd).lower() |
| if cmdfile == 'python.exe': |
| cmd = 'python' |
| pass |
| elif cmd not in _SHEBANG_CMDS: |
| raise CommandNotFound('Unknown interpreter "%s" referenced in '\ |
| 'shebang' % header) |
| cmd = _SHEBANG_CMDS.get(cmd) |
| if cmd is None or (ignoreshell and cmd == 'pysh'): |
| return [] |
| if arg is None: |
| return [cmd, win32_to_unix_path(path)] |
| return [cmd, arg, win32_to_unix_path(path)] |
| except IOError as e: |
| if e.errno!=errno.ENOENT and \ |
| (e.errno!=errno.EPERM and not os.path.isdir(path)): # Opening a directory raises EPERM |
| raise |
| return [] |
| |
| def win32_find_in_path(name, path): |
| if isinstance(path, str): |
| path = path.split(os.pathsep) |
| |
| exts = os.environ.get('PATHEXT', '').lower().split(os.pathsep) |
| for p in path: |
| p_name = os.path.join(p, name) |
| |
| prefix = resolve_shebang(p_name) |
| if prefix: |
| return prefix |
| |
| for ext in exts: |
| p_name_ext = p_name + ext |
| if os.path.exists(p_name_ext): |
| return [win32_to_unix_path(p_name_ext)] |
| return [] |
| |
| class Traps(dict): |
| def __setitem__(self, key, value): |
| if key not in ('EXIT',): |
| raise NotImplementedError() |
| super(Traps, self).__setitem__(key, value) |
| |
| # IFS white spaces character class |
| _IFS_WHITESPACES = (' ', '\t', '\n') |
| |
| class Environment: |
| """Environment holds environment variables, export table, function |
| definitions and whatever is defined in 2.12 "Shell Execution Environment", |
| redirection excepted. |
| """ |
| def __init__(self, pwd): |
| self._opt = set() #Shell options |
| |
| self._functions = {} |
| self._env = {'?': '0', '#': '0'} |
| self._exported = set([ |
| 'HOME', 'IFS', 'PATH' |
| ]) |
| |
| # Set environment vars with side-effects |
| self._ifs_ws = None # Set of IFS whitespace characters |
| self._ifs_re = None # Regular expression used to split between words using IFS classes |
| self['IFS'] = ''.join(_IFS_WHITESPACES) #Default environment values |
| self['PWD'] = pwd |
| self.traps = Traps() |
| |
| def clone(self, subshell=False): |
| env = Environment(self['PWD']) |
| env._opt = set(self._opt) |
| for k,v in self.get_variables().iteritems(): |
| if k in self._exported: |
| env.export(k,v) |
| elif subshell: |
| env[k] = v |
| |
| if subshell: |
| env._functions = dict(self._functions) |
| |
| return env |
| |
| def __getitem__(self, key): |
| if key in ('@', '*', '-', '$'): |
| raise NotImplementedError('%s is not implemented' % repr(key)) |
| return self._env[key] |
| |
| def get(self, key, defval=None): |
| try: |
| return self[key] |
| except KeyError: |
| return defval |
| |
| def __setitem__(self, key, value): |
| if key=='IFS': |
| # Update the whitespace/non-whitespace classes |
| self._update_ifs(value) |
| elif key=='PWD': |
| pwd = os.path.abspath(value) |
| if not os.path.isdir(pwd): |
| raise VarAssignmentError('Invalid directory %s' % value) |
| value = pwd |
| elif key in ('?', '!'): |
| value = str(int(value)) |
| self._env[key] = value |
| |
| def __delitem__(self, key): |
| if key in ('IFS', 'PWD', '?'): |
| raise VarAssignmentError('%s cannot be unset' % key) |
| del self._env[key] |
| |
| def __contains__(self, item): |
| return item in self._env |
| |
| def set_positional_args(self, args): |
| """Set the content of 'args' as positional argument from 1 to len(args). |
| Return previous argument as a list of strings. |
| """ |
| # Save and remove previous arguments |
| prevargs = [] |
| for i in range(int(self._env['#'])): |
| i = str(i+1) |
| prevargs.append(self._env[i]) |
| del self._env[i] |
| self._env['#'] = '0' |
| |
| #Set new ones |
| for i,arg in enumerate(args): |
| self._env[str(i+1)] = str(arg) |
| self._env['#'] = str(len(args)) |
| |
| return prevargs |
| |
| def get_positional_args(self): |
| return [self._env[str(i+1)] for i in range(int(self._env['#']))] |
| |
| def get_variables(self): |
| return dict(self._env) |
| |
| def export(self, key, value=None): |
| if value is not None: |
| self[key] = value |
| self._exported.add(key) |
| |
| def get_exported(self): |
| return [(k,self._env.get(k)) for k in self._exported] |
| |
| def split_fields(self, word): |
| if not self._ifs_ws or not word: |
| return [word] |
| return re.split(self._ifs_re, word) |
| |
| def _update_ifs(self, value): |
| """Update the split_fields related variables when IFS character set is |
| changed. |
| """ |
| # TODO: handle NULL IFS |
| |
| # Separate characters in whitespace and non-whitespace |
| chars = set(value) |
| ws = [c for c in chars if c in _IFS_WHITESPACES] |
| nws = [c for c in chars if c not in _IFS_WHITESPACES] |
| |
| # Keep whitespaces in a string for left and right stripping |
| self._ifs_ws = ''.join(ws) |
| |
| # Build a regexp to split fields |
| trailing = '[' + ''.join([re.escape(c) for c in ws]) + ']' |
| if nws: |
| # First, the single non-whitespace occurence. |
| nws = '[' + ''.join([re.escape(c) for c in nws]) + ']' |
| nws = '(?:' + trailing + '*' + nws + trailing + '*' + '|' + trailing + '+)' |
| else: |
| # Then mix all parts with quantifiers |
| nws = trailing + '+' |
| self._ifs_re = re.compile(nws) |
| |
| def has_opt(self, opt, val=None): |
| return (opt, val) in self._opt |
| |
| def set_opt(self, opt, val=None): |
| self._opt.add((opt, val)) |
| |
| def find_in_path(self, name, pwd=False): |
| path = self._env.get('PATH', '').split(os.pathsep) |
| if pwd: |
| path[:0] = [self['PWD']] |
| if os.name == 'nt': |
| return win32_find_in_path(name, self._env.get('PATH', '')) |
| else: |
| raise NotImplementedError() |
| |
| def define_function(self, name, body): |
| if not is_name(name): |
| raise ShellSyntaxError('%s is not a valid function name' % repr(name)) |
| self._functions[name] = body |
| |
| def remove_function(self, name): |
| del self._functions[name] |
| |
| def is_function(self, name): |
| return name in self._functions |
| |
| def get_function(self, name): |
| return self._functions.get(name) |
| |
| |
| name_charset = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_' |
| name_charset = dict(zip(name_charset,name_charset)) |
| |
| def match_name(s): |
| """Return the length in characters of the longest prefix made of name |
| allowed characters in s. |
| """ |
| for i,c in enumerate(s): |
| if c not in name_charset: |
| return s[:i] |
| return s |
| |
| def is_name(s): |
| return len([c for c in s if c not in name_charset])<=0 |
| |
| def is_special_param(c): |
| return len(c)==1 and c in ('@','*','#','?','-','$','!','0') |
| |
| def utility_not_implemented(name, *args, **kwargs): |
| raise NotImplementedError('%s utility is not implemented' % name) |
| |
| |
| class Utility: |
| """Define utilities properties: |
| func -- utility callable. See builtin module for utility samples. |
| is_special -- see XCU 2.8. |
| """ |
| def __init__(self, func, is_special=0): |
| self.func = func |
| self.is_special = bool(is_special) |
| |
| |
| def encodeargs(args): |
| def encodearg(s): |
| lines = base64.encodestring(s) |
| lines = [l.splitlines()[0] for l in lines] |
| return ''.join(lines) |
| |
| s = pickle.dumps(args) |
| return encodearg(s) |
| |
| def decodeargs(s): |
| s = base64.decodestring(s) |
| return pickle.loads(s) |
| |
| |
| class GlobError(Exception): |
| pass |
| |
| class Options: |
| def __init__(self): |
| # True if Mercurial operates with binary streams |
| self.hgbinary = True |
| |
| class Interpreter: |
| # Implementation is very basic: the execute() method just makes a DFS on the |
| # AST and execute nodes one by one. Nodes are tuple (name,obj) where name |
| # is a string identifier and obj the AST element returned by the parser. |
| # |
| # Handler are named after the node identifiers. |
| # TODO: check node names and remove the switch in execute with some |
| # dynamic getattr() call to find node handlers. |
| """Shell interpreter. |
| |
| The following debugging flags can be passed: |
| debug-parsing - enable PLY debugging. |
| debug-tree - print the generated AST. |
| debug-cmd - trace command execution before word expansion, plus exit status. |
| debug-utility - trace utility execution. |
| """ |
| |
| # List supported commands. |
| COMMANDS = { |
| 'cat': Utility(builtin.utility_cat,), |
| 'cd': Utility(builtin.utility_cd,), |
| ':': Utility(builtin.utility_colon,), |
| 'echo': Utility(builtin.utility_echo), |
| 'env': Utility(builtin.utility_env), |
| 'exit': Utility(builtin.utility_exit), |
| 'export': Utility(builtin.builtin_export, is_special=1), |
| 'egrep': Utility(builtin.utility_egrep), |
| 'fgrep': Utility(builtin.utility_fgrep), |
| 'gunzip': Utility(builtin.utility_gunzip), |
| 'kill': Utility(builtin.utility_kill), |
| 'mkdir': Utility(builtin.utility_mkdir), |
| 'netstat': Utility(builtin.utility_netstat), |
| 'printf': Utility(builtin.utility_printf), |
| 'pwd': Utility(builtin.utility_pwd), |
| 'return': Utility(builtin.builtin_return, is_special=1), |
| 'sed': Utility(builtin.utility_sed,), |
| 'set': Utility(builtin.builtin_set,), |
| 'shift': Utility(builtin.builtin_shift,), |
| 'sleep': Utility(builtin.utility_sleep,), |
| 'sort': Utility(builtin.utility_sort,), |
| 'trap': Utility(builtin.builtin_trap, is_special=1), |
| 'true': Utility(builtin.utility_true), |
| 'unset': Utility(builtin.builtin_unset, is_special=1), |
| 'wait': Utility(builtin.builtin_wait, is_special=1), |
| } |
| |
| def __init__(self, pwd, debugflags = [], env=None, redirs=None, stdin=None, |
| stdout=None, stderr=None, opts=Options()): |
| self._env = env |
| if self._env is None: |
| self._env = Environment(pwd) |
| self._children = {} |
| |
| self._redirs = redirs |
| self._close_redirs = False |
| |
| if self._redirs is None: |
| if stdin is None: |
| stdin = sys.stdin |
| if stdout is None: |
| stdout = sys.stdout |
| if stderr is None: |
| stderr = sys.stderr |
| stdin = FileWrapper('r', stdin, False) |
| stdout = FileWrapper('w', stdout, False) |
| stderr = FileWrapper('w', stderr, False) |
| self._redirs = Redirections(stdin, stdout, stderr) |
| self._close_redirs = True |
| |
| self._debugflags = list(debugflags) |
| self._logfile = sys.stderr |
| self._options = opts |
| |
| def close(self): |
| """Must be called when the interpreter is no longer used.""" |
| script = self._env.traps.get('EXIT') |
| if script: |
| try: |
| self.execute_script(script=script) |
| except: |
| pass |
| |
| if self._redirs is not None and self._close_redirs: |
| self._redirs.close() |
| self._redirs = None |
| |
| def log(self, s): |
| self._logfile.write(s) |
| self._logfile.flush() |
| |
| def __getitem__(self, key): |
| return self._env[key] |
| |
| def __setitem__(self, key, value): |
| self._env[key] = value |
| |
| def options(self): |
| return self._options |
| |
| def redirect(self, redirs, ios): |
| def add_redir(io): |
| if isinstance(io, pyshyacc.IORedirect): |
| redirs.add(self, io.op, io.filename, io.io_number) |
| else: |
| redirs.add_here_document(self, io.name, io.content, io.io_number) |
| |
| map(add_redir, ios) |
| return redirs |
| |
| def execute_script(self, script=None, ast=None, sourced=False, |
| scriptpath=None): |
| """If script is not None, parse the input. Otherwise takes the supplied |
| AST. Then execute the AST. |
| Return the script exit status. |
| """ |
| try: |
| if scriptpath is not None: |
| self._env['0'] = os.path.abspath(scriptpath) |
| |
| if script is not None: |
| debug_parsing = ('debug-parsing' in self._debugflags) |
| cmds, script = pyshyacc.parse(script, True, debug_parsing) |
| if 'debug-tree' in self._debugflags: |
| pyshyacc.print_commands(cmds, self._logfile) |
| self._logfile.flush() |
| else: |
| cmds, script = ast, '' |
| |
| status = 0 |
| for cmd in cmds: |
| try: |
| status = self.execute(cmd) |
| except ExitSignal as e: |
| if sourced: |
| raise |
| status = int(e.args[0]) |
| return status |
| except ShellError: |
| self._env['?'] = 1 |
| raise |
| if 'debug-utility' in self._debugflags or 'debug-cmd' in self._debugflags: |
| self.log('returncode ' + str(status)+ '\n') |
| return status |
| except CommandNotFound as e: |
| print >>self._redirs.stderr, str(e) |
| self._redirs.stderr.flush() |
| # Command not found by non-interactive shell |
| # return 127 |
| raise |
| except RedirectionError as e: |
| # TODO: should be handled depending on the utility status |
| print >>self._redirs.stderr, str(e) |
| self._redirs.stderr.flush() |
| # Command not found by non-interactive shell |
| # return 127 |
| raise |
| |
| def dotcommand(self, env, args): |
| if len(args) < 1: |
| raise ShellError('. expects at least one argument') |
| path = args[0] |
| if '/' not in path: |
| found = env.find_in_path(args[0], True) |
| if found: |
| path = found[0] |
| script = file(path).read() |
| return self.execute_script(script=script, sourced=True) |
| |
| def execute(self, token, redirs=None): |
| """Execute and AST subtree with supplied redirections overriding default |
| interpreter ones. |
| Return the exit status. |
| """ |
| if not token: |
| return 0 |
| |
| if redirs is None: |
| redirs = self._redirs |
| |
| if isinstance(token, list): |
| # Commands sequence |
| res = 0 |
| for t in token: |
| res = self.execute(t, redirs) |
| return res |
| |
| type, value = token |
| status = 0 |
| if type=='simple_command': |
| redirs_copy = redirs.clone() |
| try: |
| # TODO: define and handle command return values |
| # TODO: implement set -e |
| status = self._execute_simple_command(value, redirs_copy) |
| finally: |
| redirs_copy.close() |
| elif type=='pipeline': |
| status = self._execute_pipeline(value, redirs) |
| elif type=='and_or': |
| status = self._execute_and_or(value, redirs) |
| elif type=='for_clause': |
| status = self._execute_for_clause(value, redirs) |
| elif type=='while_clause': |
| status = self._execute_while_clause(value, redirs) |
| elif type=='function_definition': |
| status = self._execute_function_definition(value, redirs) |
| elif type=='brace_group': |
| status = self._execute_brace_group(value, redirs) |
| elif type=='if_clause': |
| status = self._execute_if_clause(value, redirs) |
| elif type=='subshell': |
| status = self.subshell(ast=value.cmds, redirs=redirs) |
| elif type=='async': |
| status = self._asynclist(value) |
| elif type=='redirect_list': |
| redirs_copy = self.redirect(redirs.clone(), value.redirs) |
| try: |
| status = self.execute(value.cmd, redirs_copy) |
| finally: |
| redirs_copy.close() |
| else: |
| raise NotImplementedError('Unsupported token type ' + type) |
| |
| if status < 0: |
| status = 255 |
| return status |
| |
| def _execute_if_clause(self, if_clause, redirs): |
| cond_status = self.execute(if_clause.cond, redirs) |
| if cond_status==0: |
| return self.execute(if_clause.if_cmds, redirs) |
| else: |
| return self.execute(if_clause.else_cmds, redirs) |
| |
| def _execute_brace_group(self, group, redirs): |
| status = 0 |
| for cmd in group.cmds: |
| status = self.execute(cmd, redirs) |
| return status |
| |
| def _execute_function_definition(self, fundef, redirs): |
| self._env.define_function(fundef.name, fundef.body) |
| return 0 |
| |
| def _execute_while_clause(self, while_clause, redirs): |
| status = 0 |
| while 1: |
| cond_status = 0 |
| for cond in while_clause.condition: |
| cond_status = self.execute(cond, redirs) |
| |
| if cond_status: |
| break |
| |
| for cmd in while_clause.cmds: |
| status = self.execute(cmd, redirs) |
| |
| return status |
| |
| def _execute_for_clause(self, for_clause, redirs): |
| if not is_name(for_clause.name): |
| raise ShellSyntaxError('%s is not a valid name' % repr(for_clause.name)) |
| items = mappend(self.expand_token, for_clause.items) |
| |
| status = 0 |
| for item in items: |
| self._env[for_clause.name] = item |
| for cmd in for_clause.cmds: |
| status = self.execute(cmd, redirs) |
| return status |
| |
| def _execute_and_or(self, or_and, redirs): |
| res = self.execute(or_and.left, redirs) |
| if (or_and.op=='&&' and res==0) or (or_and.op!='&&' and res!=0): |
| res = self.execute(or_and.right, redirs) |
| return res |
| |
| def _execute_pipeline(self, pipeline, redirs): |
| if len(pipeline.commands)==1: |
| status = self.execute(pipeline.commands[0], redirs) |
| else: |
| # Execute all commands one after the other |
| status = 0 |
| inpath, outpath = None, None |
| try: |
| # Commands inputs and outputs cannot really be plugged as done |
| # by a real shell. Run commands sequentially and chain their |
| # input/output throught temporary files. |
| tmpfd, inpath = tempfile.mkstemp() |
| os.close(tmpfd) |
| tmpfd, outpath = tempfile.mkstemp() |
| os.close(tmpfd) |
| |
| inpath = win32_to_unix_path(inpath) |
| outpath = win32_to_unix_path(outpath) |
| |
| for i, cmd in enumerate(pipeline.commands): |
| call_redirs = redirs.clone() |
| try: |
| if i!=0: |
| call_redirs.add(self, '<', inpath) |
| if i!=len(pipeline.commands)-1: |
| call_redirs.add(self, '>', outpath) |
| |
| status = self.execute(cmd, call_redirs) |
| |
| # Chain inputs/outputs |
| inpath, outpath = outpath, inpath |
| finally: |
| call_redirs.close() |
| finally: |
| if inpath: os.remove(inpath) |
| if outpath: os.remove(outpath) |
| |
| if pipeline.reverse_status: |
| status = int(not status) |
| self._env['?'] = status |
| return status |
| |
| def _execute_function(self, name, args, interp, env, stdin, stdout, stderr, *others): |
| assert interp is self |
| |
| func = env.get_function(name) |
| #Set positional parameters |
| prevargs = None |
| try: |
| prevargs = env.set_positional_args(args) |
| try: |
| redirs = Redirections(stdin.dup(), stdout.dup(), stderr.dup()) |
| try: |
| status = self.execute(func, redirs) |
| finally: |
| redirs.close() |
| except ReturnSignal as e: |
| status = int(e.args[0]) |
| env['?'] = status |
| return status |
| finally: |
| #Reset positional parameters |
| if prevargs is not None: |
| env.set_positional_args(prevargs) |
| |
| def _execute_simple_command(self, token, redirs): |
| """Can raise ReturnSignal when return builtin is called, ExitSignal when |
| exit is called, and other shell exceptions upon builtin failures. |
| """ |
| debug_command = 'debug-cmd' in self._debugflags |
| if debug_command: |
| self.log('word' + repr(token.words) + '\n') |
| self.log('assigns' + repr(token.assigns) + '\n') |
| self.log('redirs' + repr(token.redirs) + '\n') |
| |
| is_special = None |
| env = self._env |
| |
| try: |
| # Word expansion |
| args = [] |
| for word in token.words: |
| args += self.expand_token(word) |
| if is_special is None and args: |
| is_special = env.is_function(args[0]) or \ |
| (args[0] in self.COMMANDS and self.COMMANDS[args[0]].is_special) |
| |
| if debug_command: |
| self.log('_execute_simple_command' + str(args) + '\n') |
| |
| if not args: |
| # Redirections happen is a subshell |
| redirs = redirs.clone() |
| elif not is_special: |
| env = self._env.clone() |
| |
| # Redirections |
| self.redirect(redirs, token.redirs) |
| |
| # Variables assignments |
| res = 0 |
| for type,(k,v) in token.assigns: |
| status, expanded = self.expand_variable((k,v)) |
| if status is not None: |
| res = status |
| if args: |
| env.export(k, expanded) |
| else: |
| env[k] = expanded |
| |
| if args and args[0] in ('.', 'source'): |
| res = self.dotcommand(env, args[1:]) |
| elif args: |
| if args[0] in self.COMMANDS: |
| command = self.COMMANDS[args[0]] |
| elif env.is_function(args[0]): |
| command = Utility(self._execute_function, is_special=True) |
| else: |
| if not '/' in args[0].replace('\\', '/'): |
| cmd = env.find_in_path(args[0]) |
| if not cmd: |
| # TODO: test error code on unknown command => 127 |
| raise CommandNotFound('Unknown command: "%s"' % args[0]) |
| else: |
| # Handle commands like '/cygdrive/c/foo.bat' |
| cmd = cygwin_to_windows_path(args[0]) |
| if not os.path.exists(cmd): |
| raise CommandNotFound('%s: No such file or directory' % args[0]) |
| shebang = resolve_shebang(cmd) |
| if shebang: |
| cmd = shebang |
| else: |
| cmd = [cmd] |
| args[0:1] = cmd |
| command = Utility(builtin.run_command) |
| |
| # Command execution |
| if 'debug-cmd' in self._debugflags: |
| self.log('redirections ' + str(redirs) + '\n') |
| |
| res = command.func(args[0], args[1:], self, env, |
| redirs.stdin(), redirs.stdout(), |
| redirs.stderr(), self._debugflags) |
| |
| if self._env.has_opt('-x'): |
| # Trace command execution in shell environment |
| # BUG: would be hard to reproduce a real shell behaviour since |
| # the AST is not annotated with source lines/tokens. |
| self._redirs.stdout().write(' '.join(args)) |
| |
| except ReturnSignal: |
| raise |
| except ShellError as e: |
| if is_special or isinstance(e, (ExitSignal, |
| ShellSyntaxError, ExpansionError)): |
| raise e |
| self._redirs.stderr().write(str(e)+'\n') |
| return 1 |
| |
| return res |
| |
| def expand_token(self, word): |
| """Expand a word as specified in [2.6 Word Expansions]. Return the list |
| of expanded words. |
| """ |
| status, wtrees = self._expand_word(word) |
| return map(pyshlex.wordtree_as_string, wtrees) |
| |
| def expand_variable(self, word): |
| """Return a status code (or None if no command expansion occurred) |
| and a single word. |
| """ |
| status, wtrees = self._expand_word(word, pathname=False, split=False) |
| words = map(pyshlex.wordtree_as_string, wtrees) |
| assert len(words)==1 |
| return status, words[0] |
| |
| def expand_here_document(self, word): |
| """Return the expanded document as a single word. The here document is |
| assumed to be unquoted. |
| """ |
| status, wtrees = self._expand_word(word, pathname=False, |
| split=False, here_document=True) |
| words = map(pyshlex.wordtree_as_string, wtrees) |
| assert len(words)==1 |
| return words[0] |
| |
| def expand_redirection(self, word): |
| """Return a single word.""" |
| return self.expand_variable(word)[1] |
| |
| def get_env(self): |
| return self._env |
| |
| def _expand_word(self, token, pathname=True, split=True, here_document=False): |
| wtree = pyshlex.make_wordtree(token[1], here_document=here_document) |
| |
| # TODO: implement tilde expansion |
| def expand(wtree): |
| """Return a pseudo wordtree: the tree or its subelements can be empty |
| lists when no value result from the expansion. |
| """ |
| status = None |
| for part in wtree: |
| if not isinstance(part, list): |
| continue |
| if part[0]in ("'", '\\'): |
| continue |
| elif part[0] in ('`', '$('): |
| status, result = self._expand_command(part) |
| part[:] = result |
| elif part[0] in ('$', '${'): |
| part[:] = self._expand_parameter(part, wtree[0]=='"', split) |
| elif part[0] in ('', '"'): |
| status, result = expand(part) |
| part[:] = result |
| else: |
| raise NotImplementedError('%s expansion is not implemented' |
| % part[0]) |
| # [] is returned when an expansion result in no-field, |
| # like an empty $@ |
| wtree = [p for p in wtree if p != []] |
| if len(wtree) < 3: |
| return status, [] |
| return status, wtree |
| |
| status, wtree = expand(wtree) |
| if len(wtree) == 0: |
| return status, wtree |
| wtree = pyshlex.normalize_wordtree(wtree) |
| |
| if split: |
| wtrees = self._split_fields(wtree) |
| else: |
| wtrees = [wtree] |
| |
| if pathname: |
| wtrees = mappend(self._expand_pathname, wtrees) |
| |
| wtrees = map(self._remove_quotes, wtrees) |
| return status, wtrees |
| |
| def _expand_command(self, wtree): |
| # BUG: there is something to do with backslashes and quoted |
| # characters here |
| command = pyshlex.wordtree_as_string(wtree[1:-1]) |
| status, output = self.subshell_output(command) |
| return status, ['', output, ''] |
| |
| def _expand_parameter(self, wtree, quoted=False, split=False): |
| """Return a valid wtree or an empty list when no parameter results.""" |
| # Get the parameter name |
| # TODO: implement weird expansion rules with ':' |
| name = pyshlex.wordtree_as_string(wtree[1:-1]) |
| if not is_name(name) and not is_special_param(name): |
| raise ExpansionError('Bad substitution "%s"' % name) |
| # TODO: implement special parameters |
| if name in ('@', '*'): |
| args = self._env.get_positional_args() |
| if len(args) == 0: |
| return [] |
| if len(args)<2: |
| return ['', ''.join(args), ''] |
| |
| sep = self._env.get('IFS', '')[:1] |
| if split and quoted and name=='@': |
| # Introduce a new token to tell the caller that these parameters |
| # cause a split as specified in 2.5.2 |
| return ['@'] + args + [''] |
| else: |
| return ['', sep.join(args), ''] |
| |
| return ['', self._env.get(name, ''), ''] |
| |
| def _split_fields(self, wtree): |
| def is_empty(split): |
| return split==['', '', ''] |
| |
| def split_positional(quoted): |
| # Return a list of wtree split according positional parameters rules. |
| # All remaining '@' groups are removed. |
| assert quoted[0]=='"' |
| |
| splits = [[]] |
| for part in quoted: |
| if not isinstance(part, list) or part[0]!='@': |
| splits[-1].append(part) |
| else: |
| # Empty or single argument list were dealt with already |
| assert len(part)>3 |
| # First argument must join with the beginning part of the original word |
| splits[-1].append(part[1]) |
| # Create double-quotes expressions for every argument after the first |
| for arg in part[2:-1]: |
| splits[-1].append('"') |
| splits.append(['"', arg]) |
| return splits |
| |
| # At this point, all expansions but pathnames have occured. Only quoted |
| # and positional sequences remain. Thus, all candidates for field splitting |
| # are in the tree root, or are positional splits ('@') and lie in root |
| # children. |
| if not wtree or wtree[0] not in ('', '"'): |
| # The whole token is quoted or empty, nothing to split |
| return [wtree] |
| |
| if wtree[0]=='"': |
| wtree = ['', wtree, ''] |
| |
| result = [['', '']] |
| for part in wtree[1:-1]: |
| if isinstance(part, list): |
| if part[0]=='"': |
| splits = split_positional(part) |
| if len(splits)<=1: |
| result[-1] += [part, ''] |
| else: |
| # Terminate the current split |
| result[-1] += [splits[0], ''] |
| result += splits[1:-1] |
| # Create a new split |
| result += [['', splits[-1], '']] |
| else: |
| result[-1] += [part, ''] |
| else: |
| splits = self._env.split_fields(part) |
| if len(splits)<=1: |
| # No split |
| result[-1][-1] += part |
| else: |
| # Terminate the current resulting part and create a new one |
| result[-1][-1] += splits[0] |
| result[-1].append('') |
| result += [['', r, ''] for r in splits[1:-1]] |
| result += [['', splits[-1]]] |
| result[-1].append('') |
| |
| # Leading and trailing empty groups come from leading/trailing blanks |
| if result and is_empty(result[-1]): |
| result[-1:] = [] |
| if result and is_empty(result[0]): |
| result[:1] = [] |
| return result |
| |
| def _expand_pathname(self, wtree): |
| """See [2.6.6 Pathname Expansion].""" |
| if self._env.has_opt('-f'): |
| return [wtree] |
| |
| # All expansions have been performed, only quoted sequences should remain |
| # in the tree. Generate the pattern by folding the tree, escaping special |
| # characters when appear quoted |
| special_chars = '*?[]' |
| |
| def make_pattern(wtree): |
| subpattern = [] |
| for part in wtree[1:-1]: |
| if isinstance(part, list): |
| part = make_pattern(part) |
| elif wtree[0]!='': |
| for c in part: |
| # Meta-characters cannot be quoted |
| if c in special_chars: |
| raise GlobError() |
| subpattern.append(part) |
| return ''.join(subpattern) |
| |
| def pwd_glob(pattern): |
| cwd = os.getcwd() |
| os.chdir(self._env['PWD']) |
| try: |
| return glob.glob(pattern) |
| finally: |
| os.chdir(cwd) |
| |
| #TODO: check working directory issues here wrt relative patterns |
| try: |
| pattern = make_pattern(wtree) |
| paths = pwd_glob(pattern) |
| except GlobError: |
| # BUG: Meta-characters were found in quoted sequences. The should |
| # have been used literally but this is unsupported in current glob module. |
| # Instead we consider the whole tree must be used literally and |
| # therefore there is no point in globbing. This is wrong when meta |
| # characters are mixed with quoted meta in the same pattern like: |
| # < foo*"py*" > |
| paths = [] |
| |
| if not paths: |
| return [wtree] |
| return [['', path, ''] for path in paths] |
| |
| def _remove_quotes(self, wtree): |
| """See [2.6.7 Quote Removal].""" |
| |
| def unquote(wtree): |
| unquoted = [] |
| for part in wtree[1:-1]: |
| if isinstance(part, list): |
| part = unquote(part) |
| unquoted.append(part) |
| return ''.join(unquoted) |
| |
| return ['', unquote(wtree), ''] |
| |
| def subshell(self, script=None, ast=None, redirs=None): |
| """Execute the script or AST in a subshell, with inherited redirections |
| if redirs is not None. |
| """ |
| if redirs: |
| sub_redirs = redirs |
| else: |
| sub_redirs = redirs.clone() |
| |
| subshell = None |
| try: |
| subshell = Interpreter(None, self._debugflags, self._env.clone(True), |
| sub_redirs, opts=self._options) |
| return subshell.execute_script(script, ast) |
| finally: |
| if not redirs: sub_redirs.close() |
| if subshell: subshell.close() |
| |
| def subshell_output(self, script): |
| """Execute the script in a subshell and return the captured output.""" |
| # Create temporary file to capture subshell output |
| tmpfd, tmppath = tempfile.mkstemp() |
| try: |
| tmpfile = os.fdopen(tmpfd, 'wb') |
| stdout = FileWrapper('w', tmpfile) |
| |
| redirs = Redirections(self._redirs.stdin().dup(), |
| stdout, |
| self._redirs.stderr().dup()) |
| try: |
| status = self.subshell(script=script, redirs=redirs) |
| finally: |
| redirs.close() |
| redirs = None |
| |
| # Extract subshell standard output |
| tmpfile = open(tmppath, 'rb') |
| try: |
| output = tmpfile.read() |
| return status, output.rstrip('\n') |
| finally: |
| tmpfile.close() |
| finally: |
| os.remove(tmppath) |
| |
| def _asynclist(self, cmd): |
| args = (self._env.get_variables(), cmd) |
| arg = encodeargs(args) |
| assert len(args) < 30*1024 |
| cmd = ['pysh.bat', '--ast', '-c', arg] |
| p = subprocess.Popen(cmd, cwd=self._env['PWD']) |
| self._children[p.pid] = p |
| self._env['!'] = p.pid |
| return 0 |
| |
| def wait(self, pids=None): |
| if not pids: |
| pids = self._children.keys() |
| |
| status = 127 |
| for pid in pids: |
| if pid not in self._children: |
| continue |
| p = self._children.pop(pid) |
| status = p.wait() |
| |
| return status |
| |