def Matches(self, comp): # TODO: Delete COMPREPLY here? It doesn't seem to be defined in bash by # default. argv, comp_words = comp.GetApiInput() state.SetGlobalArray(self.ex.mem, 'COMP_WORDS', comp_words) state.SetGlobalString(self.ex.mem, 'COMP_CWORD', str(comp.index)) state.SetGlobalString(self.ex.mem, 'COMP_LINE', comp.line) state.SetGlobalString(self.ex.mem, 'COMP_POINT', str(comp.end)) self.log('Running completion function %r with arguments %s', self.func.name, argv) status = self.ex.RunFuncForCompletion(self.func, argv) if status == 124: self.log('Got status 124 from %r', self.func.name) raise _RetryCompletion() # Lame: COMP_REPLY would follow the naming convention! val = state.GetGlobal(self.ex.mem, 'COMPREPLY') if val.tag == value_e.Undef: util.error('Ran function %s but COMPREPLY was not defined', self.func.name) return [] if val.tag != value_e.StrArray: log('ERROR: COMPREPLY should be an array, got %s', val) return [] self.log('COMPREPLY %s', val) # Return this all at once so we don't have a generator. COMPREPLY happens # all at once anyway. return val.strs
def ExecExternalProgram(argv, environ): """Execute a program and exit this process. Called by: ls / exec ls / ( ls / ) """ # TODO: If there is an error, like the file isn't executable, then we should # exit, and the parent will reap it. Should it capture stderr? try: os_.execvpe(argv[0], argv, environ) except OSError as e: util.error('%r: %s', argv[0], posix.strerror(e.errno)) # POSIX mentions 126 and 127 for two specific errors. The rest are # unspecified. # # http://pubs.opengroup.org/onlinepubs/9699919799.2016edition/utilities/V3_chap02.html#tag_18_08_02 if e.errno == errno.EACCES: status = 126 elif e.errno == errno.ENOENT: status = 127 # e.g. command not found should be 127. else: # dash uses 2, but we use that for parse errors. This seems to be # consistent with mksh and zsh. status = 127 sys.exit(status)
def _Source(self, argv): try: path = argv[0] except IndexError: # TODO: Should point to the source statement that failed. util.error('source: missing required argument') return 1 try: f = self.fd_state.Open(path) # Shell can't use descriptors 3-9 except OSError as e: # TODO: Should point to the source statement that failed. util.error('source %r failed: %s', path, os.strerror(e.errno)) return 1 try: line_reader = reader.FileLineReader(f, self.arena) _, c_parser = parse_lib.MakeParser(line_reader, self.arena) self.mem.PushSource(argv[1:]) try: status = self._EvalHelper(c_parser, path) return status finally: self.mem.PopSource() except _ControlFlow as e: if e.IsReturn(): return e.StatusCode() else: raise finally: f.close()
def _GetOpts(spec, argv, optind): optarg = '' # not set by default try: current = argv[optind - 1] # 1-based indexing except IndexError: return 1, '?', optarg, optind if not current.startswith('-'): # The next arg doesn't look like a flag. return 1, '?', optarg, optind # It looks like an argument. Stop iteration by returning 1. if current not in spec: # Invalid flag optind += 1 return 0, '?', optarg, optind optind += 1 opt_char = current[-1] needs_arg = spec[current] if needs_arg: try: optarg = argv[optind - 1] # 1-based indexing except IndexError: util.error('getopts: option %r requires an argument', current) # Hm doesn't cause status 1? return 0, '?', optarg, optind optind += 1 return 0, opt_char, optarg, optind
def Alias(argv, aliases): if not argv: for name in sorted(aliases): alias_exp = aliases[name] # This is somewhat like bash, except we use %r for ''. print('alias %s=%r' % (name, alias_exp)) return 0 status = 0 for arg in argv: parts = arg.split('=', 1) if len(parts) == 1: # if we get a plain word without, print alias name = parts[0] alias_exp = aliases.get(name) if alias_exp is None: util.error('alias %r is not defined', name) # TODO: error? status = 1 else: print('alias %s=%r' % (name, alias_exp)) else: name, alias_exp = parts aliases[name] = alias_exp #print(argv) #log('AFTER ALIAS %s', aliases) return status
def Help(argv, loader): # TODO: Need $VERSION inside all pages? try: topic = argv[0] except IndexError: topic = 'help' if topic == 'toc': # Just show the raw source. f = loader.open('doc/osh-quick-ref-toc.txt') else: try: section_id = osh_help.TOPIC_LOOKUP[topic] except KeyError: util.error('No help topics match %r', topic) return 1 else: try: f = loader.open('_devbuild/osh-quick-ref/%s' % section_id) except IOError as e: util.error(str(e)) raise AssertionError('Should have found %r' % section_id) for line in f: sys.stdout.write(line) f.close() return 0
def _GetOpts(spec, mem, optind): optarg = '' # not set by default v2 = mem.GetArgNum(optind) if v2.tag == value_e.Undef: # No more arguments. return 1, '?', optarg, optind assert v2.tag == value_e.Str current = v2.s if not current.startswith('-'): # The next arg doesn't look like a flag. return 1, '?', optarg, optind # It looks like an argument. Stop iteration by returning 1. if current not in spec: # Invalid flag optind += 1 return 0, '?', optarg, optind optind += 1 opt_char = current[-1] needs_arg = spec[current] if needs_arg: v3 = mem.GetArgNum(optind) if v3.tag == value_e.Undef: util.error('getopts: option %r requires an argument', current) # Hm doesn't cause status 1? return 0, '?', optarg, optind assert v3.tag == value_e.Str optarg = v3.s optind += 1 return 0, opt_char, optarg, optind
def _PushDup(self, fd1, fd2): """ Save fd2 and dup fd1 onto fd2. """ #log('---- _PushDup %s %s\n', fd1, fd2) try: fcntl.fcntl(fd2, fcntl.F_DUPFD, self.next_fd) except IOError as e: # Example program that causes this error: exec 4>&1. Descriptor 4 isn't # open. # This seems to be ignored in dash too in savefd()? if e.errno != errno.EBADF: raise else: os.close(fd2) fcntl.fcntl(self.next_fd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) #log('==== dup %s %s\n' % (fd1, fd2)) try: os.dup2(fd1, fd2) except OSError as e: # bash/dash give this error too, e.g. for 'echo hi 1>&3' util.error('%d: %s', fd1, os.strerror(e.errno)) # Restore and return error os.dup2(self.next_fd, fd2) os.close(self.next_fd) # Undo it return False # Oh this is wrong? #os.close(fd1) self.cur_frame.saved.append((self.next_fd, fd2)) self.next_fd += 1 return True
def _EvalRedirect(self, n): fd = REDIR_DEFAULT_FD[n.op_id] if n.fd == const.NO_INTEGER else n.fd if n.tag == redir_e.Redir: redir_type = REDIR_ARG_TYPES[n.op_id] # could be static in the LST? if redir_type == redir_arg_type_e.Path: # NOTE: no globbing. You can write to a file called '*.py'. val = self.word_ev.EvalWordToString(n.arg_word) if val.tag != value_e.Str: # TODO: This error never fires util.error("Redirect filename must be a string, got %s", val) return None filename = val.s if not filename: # Whether this is fatal depends on errexit. util.error("Redirect filename can't be empty") return None return runtime.PathRedirect(n.op_id, fd, filename) elif redir_type == redir_arg_type_e.Desc: # e.g. 1>&2 val = self.word_ev.EvalWordToString(n.arg_word) if val.tag != value_e.Str: # TODO: This error never fires util.error("Redirect descriptor should be a string, got %s", val) return None t = val.s if not t: util.error("Redirect descriptor can't be empty") return None try: target_fd = int(t) except ValueError: util.error( "Redirect descriptor should look like an integer, got %s", val) return None return runtime.DescRedirect(n.op_id, fd, target_fd) elif redir_type == redir_arg_type_e.Here: # here word # TODO: decay should be controlled by an option val = self.word_ev.EvalWordToString(n.arg_word, decay=True) if val.tag != value_e.Str: # TODO: This error never fires util.warn("Here word body should be a string, got %s", val) return None # NOTE: bash and mksh both add \n return runtime.HereRedirect(fd, val.s + '\n') else: raise AssertionError('Unknown redirect op') elif n.tag == redir_e.HereDoc: # TODO: decay shoudl be controlled by an option val = self.word_ev.EvalWordToString(n.body, decay=True) if val.tag != value_e.Str: # TODO: This error never fires util.warn("Here doc body should be a string, got %s", val) return None return runtime.HereRedirect(fd, val.s) else: raise AssertionError('Unknown redirect type')
def RunFuncForCompletion(self, func_node, argv): try: status = self._RunFunc(func_node, argv) except util.FatalRuntimeError as e: ui.PrettyPrintError(e, self.arena, sys.stderr) status = e.exit_status if e.exit_status is not None else 1 except _ControlFlow as e: # shouldn't be able to exit the shell from a completion hook! util.error('Attempted to exit from completion hook.') status = 1 return status
def _EvalRedirect(self, n): fd = REDIR_DEFAULT_FD[n.op.id] if n.fd == const.NO_INTEGER else n.fd if n.tag == redir_e.Redir: redir_type = REDIR_ARG_TYPES[n.op.id] # could be static in the LST? if redir_type == redir_arg_type_e.Path: # NOTE: no globbing. You can write to a file called '*.py'. val = self.word_ev.EvalWordToString(n.arg_word) if val.tag != value_e.Str: # TODO: This error never fires util.error("Redirect filename must be a string, got %s", val) return None filename = val.s if not filename: # Whether this is fatal depends on errexit. util.error("Redirect filename can't be empty") return None return redirect.PathRedirect(n.op.id, fd, filename) elif redir_type == redir_arg_type_e.Desc: # e.g. 1>&2 val = self.word_ev.EvalWordToString(n.arg_word) if val.tag != value_e.Str: # TODO: This error never fires util.error("Redirect descriptor should be a string, got %s", val) return None t = val.s if not t: util.error("Redirect descriptor can't be empty") return None try: target_fd = int(t) except ValueError: util.error( "Redirect descriptor should look like an integer, got %s", val) return None return redirect.DescRedirect(n.op.id, fd, target_fd) elif redir_type == redir_arg_type_e.Here: # here word val = self.word_ev.EvalWordToString(n.arg_word) assert val.tag == value_e.Str, val # NOTE: bash and mksh both add \n return redirect.HereRedirect(fd, val.s + '\n') else: raise AssertionError('Unknown redirect op') elif n.tag == redir_e.HereDoc: # HACK: Wrap it in a word to evaluate. w = osh_word.CompoundWord(n.stdin_parts) val = self.word_ev.EvalWordToString(w) assert val.tag == value_e.Str, val return redirect.HereRedirect(fd, val.s) else: raise AssertionError('Unknown redirect type')
def main(argv): arg, i = SPEC.Parse(argv) if not arg.f: util.error("-f must be passed") return 1 for path in argv[i:]: res = libc.realpath(path) if res is None: return 1 print(res) return 0
def UnAlias(argv, aliases): if not argv: raise args.UsageError('unalias NAME...') status = 0 for name in argv: try: del aliases[name] except KeyError: util.error('alias %r is not defined', name) status = 1 return status
def Exec(self, argv, environ): """Execute a program and exit this process. Called by: ls / exec ls / ( ls / ) """ if self.hijack_shebang: try: f = self.fd_state.Open(argv[0]) except OSError as e: pass else: try: line = f.read(40) if (line.startswith('#!/bin/sh') or line.startswith('#!/bin/bash') or line.startswith('#!/usr/bin/env bash')): self.debug_f.log('Hijacked: %s with %s', argv, self.hijack_shebang) argv = [self.hijack_shebang] + argv else: #self.debug_f.log('Not hijacking %s (%r)', argv, line) pass finally: f.close() # TODO: If there is an error, like the file isn't executable, then we should # exit, and the parent will reap it. Should it capture stderr? try: os_.execvpe(argv[0], argv, environ) except OSError as e: # TODO: Run with /bin/sh when ENOEXEC error (noshebang). Because all # shells do it. util.error('%r: %s', argv[0], posix.strerror(e.errno)) # POSIX mentions 126 and 127 for two specific errors. The rest are # unspecified. # # http://pubs.opengroup.org/onlinepubs/9699919799.2016edition/utilities/V3_chap02.html#tag_18_08_02 if e.errno == errno.EACCES: status = 126 elif e.errno == errno.ENOENT: status = 127 # e.g. command not found should be 127. else: # dash uses 2, but we use that for parse errors. This seems to be # consistent with mksh and zsh. status = 127 sys.exit(status) # raises SystemExit
def Exit(argv): if len(argv) > 1: util.error('exit: too many arguments') return 1 try: code = int(argv[0]) except IndexError: code = 0 except ValueError as e: print("Invalid argument %r" % argv[0], file=sys.stderr) code = 1 # Runtime Error # TODO: Should this be turned into our own SystemExit exception? sys.exit(code)
def Shift(argv, mem): if len(argv) > 1: util.error('shift: too many arguments') return 1 try: n = int(argv[0]) except IndexError: n = 1 except ValueError: print("Invalid shift argument %r" % argv[1], file=sys.stderr) return 1 # runtime error return mem.Shift(n)
def __call__(self, argv): arg_r = args.Reader(argv) arg = COMPOPT_SPEC.Parse(arg_r) if not self.comp_state.currently_completing: # bash checks this. util.error( 'compopt: not currently executing a completion function') return 1 self.comp_state.dynamic_opts.update(arg.opt_changes) #log('compopt: %s', arg) #log('compopt %s', base_opts) return 0
def Matches(self, comp): # Have to clear the response every time. TODO: Reuse the object? state.SetGlobalArray(self.ex.mem, 'COMPREPLY', []) # New completions should use COMP_ARGV, a construct specific to OSH> state.SetGlobalArray(self.ex.mem, 'COMP_ARGV', comp.partial_argv) # Old completions may use COMP_WORDS. It is split by : and = to emulate # bash's behavior. # More commonly, they will call _init_completion and use the 'words' output # of that, ignoring COMP_WORDS. comp_words = [] for a in comp.partial_argv: AdjustArg(a, [':', '='], comp_words) if comp.index == -1: # cmopgen comp_cword = comp.index else: comp_cword = len(comp_words) - 1 # weird invariant state.SetGlobalArray(self.ex.mem, 'COMP_WORDS', comp_words) state.SetGlobalString(self.ex.mem, 'COMP_CWORD', str(comp_cword)) state.SetGlobalString(self.ex.mem, 'COMP_LINE', comp.line) state.SetGlobalString(self.ex.mem, 'COMP_POINT', str(comp.end)) argv = [comp.first, comp.to_complete, comp.prev] self.log('Running completion function %r with arguments %s', self.func.name, argv) status = self.ex.RunFuncForCompletion(self.func, argv) if status == 124: self.log('Got status 124 from %r', self.func.name) raise _RetryCompletion() # Read the response. We set it above, so this error would only happen if # the user unset it. # NOTE: 'COMP_REPLY' would follow the naming convention! val = state.GetGlobal(self.ex.mem, 'COMPREPLY') if val.tag == value_e.Undef: util.error('Ran function %s but COMPREPLY was not defined', self.func.name) return [] if val.tag != value_e.StrArray: log('ERROR: COMPREPLY should be an array, got %s', val) return [] self.log('COMPREPLY %s', val) # Return this all at once so we don't have a generator. COMPREPLY happens # all at once anyway. return val.strs
def Popd(argv, home_dir, dir_stack): dest_dir = dir_stack.Pop() if dest_dir is None: util.error('popd: directory stack is empty') return 1 try: posix.chdir(dest_dir) except OSError as e: util.error("popd: %r: %s", dest_dir, posix.strerror(e.errno)) return 1 _PrintDirStack(dir_stack, SINGLE_LINE, home_dir) return 0
def Popd(argv, mem, dir_stack): dest_dir = dir_stack.Pop() if dest_dir is None: util.error('popd: directory stack is empty') return 1 try: posix.chdir(dest_dir) except OSError as e: util.error("popd: %r: %s", dest_dir, posix.strerror(e.errno)) return 1 _PrintDirStack(dir_stack, SINGLE_LINE, mem.GetVar('HOME')) state.SetGlobalString(mem, 'PWD', dest_dir) return 0
def _EvalHelper(self, c_parser, source_name): self.arena.PushSource(source_name) try: node = c_parser.ParseWholeFile() # NOTE: We could model a parse error as an exception, like Python, so we # get a traceback. (This won't be applicable for a static module system.) if not node: util.error('Parse error in %r:', source_name) err = c_parser.Error() ui.PrintErrorStack(err, self.arena, sys.stderr) return 1 status = self._Execute(node) return status finally: self.arena.PopSource()
def _Source(self, argv): try: path = argv[0] except IndexError: # TODO: Should point to the source statement that failed. util.error('source: missing required argument') return 1 try: with open(path) as f: line_reader = reader.FileLineReader(f, self.arena) _, c_parser = parse_lib.MakeParser(line_reader, self.arena) return self._EvalHelper(c_parser, path) except IOError as e: # TODO: Should point to the source statement that failed. util.error('source %r failed: %s', path, os.strerror(e.errno)) return 1
def __call__(self, argv): arg_r = args.Reader(argv) arg = COMPOPT_SPEC.Parse(arg_r) if not self.comp_state.currently_completing: # bash checks this. util.error( 'compopt: not currently executing a completion function') return 1 for name, b in arg.opt_changes: #log('setting %s = %s', name, b) self.comp_state.current_opts.Set(name, b) #log('compopt: %s', arg) #log('compopt %s', comp_opts) return 0
def _SetOption(self, opt_name, b): """Private version for synchronizing from SHELLOPTS.""" assert '_' not in opt_name if opt_name not in _SET_OPTION_NAMES: raise args.UsageError('Invalid option %r' % opt_name) if opt_name == 'errexit': self.errexit.Set(b) elif opt_name in ('vi', 'emacs'): if self.readline: self.readline.parse_and_bind("set editing-mode " + opt_name) else: # TODO error message copied from 'cmd_exec.py'; refactor? util.error('Oil was not built with readline/completion.') else: # strict-control-flow -> strict_control_flow opt_name = opt_name.replace('-', '_') setattr(self, opt_name, b)
def Repr(argv, mem): """Given a list of variable names, print their values. 'repr a' is a lot easier to type than 'argv.py "${a[@]}"'. """ status = 0 for name in argv: if not match.IsValidVarName(name): util.error('%r is not a valid variable name', name) return 1 # TODO: Should we print flags too? val = mem.GetVar(name) if val.tag == value_e.Undef: print('%r is not defined' % name) status = 1 else: print('%s = %s' % (name, val)) return status
def _PushDup(self, fd1, fd2): """Save fd2, and dup fd1 onto fd2. Mutates self.cur_frame.saved. Returns: success Bool """ new_fd = self._NextFreeFileDescriptor() #log('---- _PushDup %s %s', fd1, fd2) need_restore = True try: #log('DUPFD %s %s', fd2, self.next_fd) fcntl.fcntl(fd2, fcntl.F_DUPFD, new_fd) except IOError as e: # Example program that causes this error: exec 4>&1. Descriptor 4 isn't # open. # This seems to be ignored in dash too in savefd()? if e.errno == errno.EBADF: #log('ERROR %s', e) need_restore = False else: raise else: posix.close(fd2) fcntl.fcntl(new_fd, fcntl.F_SETFD, fcntl.FD_CLOEXEC) #log('==== dup %s %s\n' % (fd1, fd2)) try: posix.dup2(fd1, fd2) except OSError as e: # bash/dash give this error too, e.g. for 'echo hi 1>&3' util.error('%d: %s', fd1, posix.strerror(e.errno)) # Restore and return error posix.dup2(new_fd, fd2) posix.close(new_fd) # Undo it return False if need_restore: self.cur_frame.saved.append((new_fd, fd2)) return True
def ParseTrapCode(self, code_str): """ Returns: A node, or None if the code is invalid. """ line_reader = reader.StringLineReader(code_str, self.arena) _, c_parser = parse_lib.MakeParser(line_reader, self.arena) source_name = '<trap string>' self.arena.PushSource(source_name) try: node = c_parser.ParseWholeFile() if not node: util.error('Parse error in %r:', source_name) err = c_parser.Error() ui.PrintErrorStack(err, self.arena, sys.stderr) return None finally: self.arena.PopSource() return node
def _Source(self, argv): try: path = argv[0] except IndexError: # TODO: Should point to the source statement that failed. util.error('source: missing required argument') return 1 try: f = self.fd_state.Open(path) # Shell can't use descriptors 3-9 except OSError as e: # TODO: Should point to the source statement that failed. util.error('source %r failed: %s', path, posix.strerror(e.errno)) return 1 try: line_reader = reader.FileLineReader(f, self.arena) _, c_parser = self.parse_ctx.MakeParser(line_reader) # A sourced module CAN have a new arguments array, but it always shares # the same variable scope as the caller. The caller could be at either a # global or a local scope. source_argv = argv[1:] self.mem.PushSource(path, source_argv) try: status = self._EvalHelper(c_parser, path) finally: self.mem.PopSource(source_argv) return status except _ControlFlow as e: if e.IsReturn(): return e.StatusCode() else: raise finally: f.close()
def Cd(argv, mem, dir_stack): arg, i = CD_SPEC.Parse(argv) # TODO: error checking, etc. # TODO: ensure that if multiple flags are provided, the *last* one overrides # the others. try: dest_dir = argv[i] except IndexError: val = mem.GetVar('HOME') if val.tag == value_e.Undef: util.error("$HOME isn't defined") return 1 elif val.tag == value_e.Str: dest_dir = val.s elif val.tag == value_e.StrArray: util.error("$HOME shouldn't be an array.") return 1 if dest_dir == '-': old = mem.GetVar('OLDPWD', scope_e.GlobalOnly) if old.tag == value_e.Undef: log('OLDPWD not set') return 1 elif old.tag == value_e.Str: dest_dir = old.s print(dest_dir) # Shells print the directory elif old.tag == value_e.StrArray: # TODO: Prevent the user from setting OLDPWD to array (or maybe they # can't even set it at all.) raise AssertionError('Invalid OLDPWD') pwd = mem.GetVar('PWD') assert pwd.tag == value_e.Str, pwd # TODO: Need a general scheme to avoid # Calculate new directory, chdir() to it, then set PWD to it. NOTE: We can't # call posix.getcwd() because it can raise OSError if the directory was # removed (ENOENT.) abspath = os_path.join(pwd.s, dest_dir) # make it absolute, for cd .. if arg.P: # -P means resolve symbolic links, then process '..' real_dest_dir = libc.realpath(abspath) else: # -L means process '..' first. This just does string manipulation. (But # realpath afterward isn't correct?) real_dest_dir = os_path.normpath(abspath) try: posix.chdir(real_dest_dir) except OSError as e: # TODO: Add line number, etc. util.error("cd %r: %s", real_dest_dir, posix.strerror(e.errno)) return 1 state.ExportGlobalString(mem, 'OLDPWD', pwd.s) state.ExportGlobalString(mem, 'PWD', real_dest_dir) dir_stack.Reset() # for pushd/popd/dirs return 0
def ParseTrapCode(self, code_str): """ Returns: A node, or None if the code is invalid. """ line_reader = reader.StringLineReader(code_str, self.arena) _, c_parser = self.parse_ctx.MakeParser(line_reader) source_name = '<trap string>' self.arena.PushSource(source_name) try: try: node = main_loop.ParseWholeFile(c_parser) except util.ParseError as e: util.error('Parse error in %r:', source_name) ui.PrettyPrintError(e, self.arena, sys.stderr) return None finally: self.arena.PopSource() return node