def _PushDup(self, fd1, loc): # type: (int, redir_loc_t) -> int """Save fd2 in a higher range, and dup fd1 onto fd2. Returns whether F_DUPFD/dup2 succeeded, and the new descriptor. """ UP_loc = loc if loc.tag_() == redir_loc_e.VarName: fd2_name = cast(redir_loc__VarName, UP_loc).name try: # F_DUPFD: GREATER than range new_fd = fcntl.fcntl(fd1, fcntl.F_DUPFD, _SHELL_MIN_FD) # type: int except IOError as e: if e.errno == errno.EBADF: self.errfmt.Print('%d: %s', fd1, posix.strerror(e.errno)) return NO_FD else: raise # this redirect failed self._WriteFdToMem(fd2_name, new_fd) elif loc.tag_() == redir_loc_e.Fd: fd2 = cast(redir_loc__Fd, UP_loc).fd if fd1 == fd2: # The user could have asked for it to be open on descrptor 3, but open() # already returned 3, e.g. echo 3>out.txt return NO_FD # Check the validity of fd1 before _PushSave(fd2) try: fcntl.fcntl(fd1, fcntl.F_GETFD) except IOError as e: self.errfmt.Print('%d: %s', fd1, posix.strerror(e.errno)) raise need_restore = self._PushSave(fd2) #log('==== dup2 %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' self.errfmt.Print('%d: %s', fd1, posix.strerror(e.errno)) # Restore and return error if need_restore: new_fd, fd2, _ = self.cur_frame.saved.pop() posix.dup2(new_fd, fd2) posix.close(new_fd) raise # this redirect failed new_fd = fd2 else: raise AssertionError() return new_fd
def Run(self): # type: () -> None # NOTE: may NOT return due to exec(). if not self.inherit_errexit: self.ex.mutable_opts.errexit.Disable() try: self.ex.ExecuteAndCatch(self.node, fork_external=False) status = self.ex.LastStatus() # NOTE: We ignore the is_fatal return value. The user should set -o # errexit so failures in subprocesses cause failures in the parent. except util.UserExit as e: status = e.status # Handle errors in a subshell. These two cases are repeated from main() # and the core/completion.py hook. except KeyboardInterrupt: print() status = 130 # 128 + 2 except (IOError, OSError) as e: ui.Stderr('osh I/O error: %s', posix.strerror(e.errno)) status = 2 # Raises SystemExit, so we still have time to write a crash dump. sys.exit(status)
def __call__(self, unused_word, state): # type: (str, int) -> Optional[str] """Return a single match.""" try: return self._GetNextCompletion(state) except util.UserExit as e: # TODO: Could use errfmt to show this stderr_line("osh: Ignoring 'exit' in completion plugin") except error.FatalRuntime as e: # From -W. TODO: -F is swallowed now. # We should have a nicer UI for displaying errors. Maybe they shouldn't # print it to stderr. That messes up the completion display. We could # print what WOULD have been COMPREPLY here. stderr_line('osh: Runtime error while completing: %s', e) self.debug_f.log('Runtime error while completing: %s', e) except (IOError, OSError) as e: # test this with prlimit --nproc=1 --pid=$$ stderr_line('osh: I/O error in completion: %s', posix.strerror(e.errno)) except KeyboardInterrupt: # It appears GNU readline handles Ctrl-C to cancel a long completion. # So this may never happen? stderr_line('Ctrl-C in completion') except Exception as e: # ESSENTIAL because readline swallows exceptions. if 1: import traceback traceback.print_exc() stderr_line('osh: Unhandled exception while completing: %s', e) self.debug_f.log('Unhandled exception while completing: %s', e) except SystemExit as e: # Because readline ignores SystemExit! posix._exit(e.code)
def _Exec(self, argv0_path, argv, argv0_spid, environ, should_retry): # type: (str, List[str], int, Dict[str, str], bool) -> None if self.hijack_shebang: try: f = self.fd_state.Open(argv0_path) except OSError as e: pass else: try: # Test if the shebang looks like a shell. The file might be binary # with no newlines, so read 80 bytes instead of readline(). line = f.read(80) # type: ignore # TODO: fix this if match.ShouldHijack(line): argv = [self.hijack_shebang, argv0_path] + argv[1:] argv0_path = self.hijack_shebang self.debug_f.log('Hijacked: %s', 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: posix.execve(argv0_path, argv, environ) except OSError as e: # Run with /bin/sh when ENOEXEC error (no shebang). All shells do this. if e.errno == errno.ENOEXEC and should_retry: new_argv = ['/bin/sh', argv0_path] new_argv.extend(argv[1:]) self._Exec('/bin/sh', new_argv, argv0_spid, environ, False) # NO RETURN # Would be nice: when the path is relative and ENOENT: print PWD and do # spelling correction? self.errfmt.Print("Can't execute %r: %s", argv0_path, posix.strerror(e.errno), span_id=argv0_spid) # 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: # TODO: most shells print 'command not found', rather than strerror() # == "No such file or directory". That's better because it's at the # end of the path search, and we're never searching for a directory. status = 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 Run(self, cmd_val): # type: (cmd_value__Argv) -> int num_args = len(cmd_val.argv) - 1 if num_args == 0: # TODO: It's suppose to try another dir before doing this? self.errfmt.Print('pushd: no other directory') return 1 elif num_args > 1: raise args.UsageError('got too many arguments') # TODO: 'cd' uses normpath? Is that inconsistent? dest_dir = os_path.abspath(cmd_val.argv[1]) try: posix.chdir(dest_dir) except OSError as e: self.errfmt.Print("pushd: %r: %s", dest_dir, posix.strerror(e.errno), span_id=cmd_val.arg_spids[1]) return 1 self.dir_stack.Push(dest_dir) _PrintDirStack(self.dir_stack, SINGLE_LINE, self.mem.GetVar('HOME')) state.ExportGlobalString(self.mem, 'PWD', dest_dir) self.mem.SetPwd(dest_dir) return 0
def main(argv): # type: (List[str]) -> int try: return AppBundleMain(argv) except error.Usage as e: #builtin.Help(['oil-usage'], util.GetResourceLoader()) log('oil: %s', e.msg) return 2 except RuntimeError as e: if 0: import traceback traceback.print_exc() # NOTE: The Python interpreter can cause this, e.g. on stack overflow. log('FATAL: %r', e) return 1 except KeyboardInterrupt: print() return 130 # 128 + 2 except (IOError, OSError) as e: if 0: import traceback traceback.print_exc() # test this with prlimit --nproc=1 --pid=$$ stderr_line('osh I/O error: %s', posix.strerror(e.errno)) return 2 # dash gives status 2 finally: _tlog('Exiting main()') if _trace_path: _tracer.Stop(_trace_path)
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int num_args = len(cmd_val.argv) - 1 if num_args == 0: # TODO: It's suppose to try another dir before doing this? self.errfmt.Print_('pushd: no other directory') return 1 elif num_args > 1: e_usage('got too many arguments') # TODO: 'cd' uses normpath? Is that inconsistent? dest_dir = os_path.abspath(cmd_val.argv[1]) err_num = pyos.Chdir(dest_dir) if err_num != 0: self.errfmt.Print_("pushd: %r: %s" % (dest_dir, posix.strerror(err_num)), span_id=cmd_val.arg_spids[1]) return 1 self.dir_stack.Push(dest_dir) _PrintDirStack(self.dir_stack, SINGLE_LINE, state.MaybeString(self.mem, 'HOME')) state.ExportGlobalString(self.mem, 'PWD', dest_dir) self.mem.SetPwd(dest_dir) return 0
def __call__(self, arg_vec): arg, i = CD_SPEC.ParseVec(arg_vec) # TODO: error checking, etc. # TODO: ensure that if multiple flags are provided, the *last* one overrides # the others. try: dest_dir = arg_vec.strs[i] except IndexError: val = self.mem.GetVar('HOME') if val.tag == value_e.Undef: self.errfmt.Print("$HOME isn't defined") return 1 elif val.tag == value_e.Str: dest_dir = val.s elif val.tag == value_e.StrArray: # User would have to unset $HOME to get rid of exported flag self.errfmt.Print("$HOME shouldn't be an array") return 1 if dest_dir == '-': old = self.mem.GetVar('OLDPWD', scope_e.GlobalOnly) if old.tag == value_e.Undef: self.errfmt.Print('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 = self.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: self.errfmt.Print("cd %r: %s", real_dest_dir, posix.strerror(e.errno), span_id=arg_vec.spids[i]) return 1 state.ExportGlobalString(self.mem, 'OLDPWD', pwd.s) state.ExportGlobalString(self.mem, 'PWD', real_dest_dir) self.dir_stack.Reset() # for pushd/popd/dirs return 0
def Exec(self, arg_vec, environ): """Execute a program and exit this process. Called by: ls / exec ls / ( ls / ) """ argv = arg_vec.strs if self.hijack_shebang: try: f = self.fd_state.Open(argv[0]) except OSError as e: pass else: try: line = f.read(40) if _ShouldHijack(line): 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. # Would be nice: when the path is relative and ENOENT: print PWD and do # spelling correction? self.errfmt.Print("Can't execute %r: %s", argv[0], posix.strerror(e.errno), span_id=arg_vec.spids[0]) # 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 _PopDirStack(mem, dir_stack, errfmt): """Helper for popd and cd { ... }.""" dest_dir = dir_stack.Pop() if dest_dir is None: errfmt.Print('popd: directory stack is empty') return 1 try: posix.chdir(dest_dir) except OSError as e: # Happens if a directory is deleted in pushing and popping errfmt.Print("popd: %r: %s", dest_dir, posix.strerror(e.errno)) return 1 state.SetGlobalString(mem, 'PWD', dest_dir) mem.SetPwd(dest_dir)
def __call__(self, arg_vec): dest_dir = self.dir_stack.Pop() if dest_dir is None: self.errfmt.Print('popd: directory stack is empty') return 1 try: posix.chdir(dest_dir) except OSError as e: # Happens if a directory is deleted in pushing and popping self.errfmt.Print("popd: %r: %s", dest_dir, posix.strerror(e.errno)) return 1 _PrintDirStack(self.dir_stack, SINGLE_LINE, self.mem.GetVar('HOME')) state.SetGlobalString(self.mem, 'PWD', dest_dir) return 0
def _PopDirStack(mem, dir_stack, errfmt): # type: (Mem, DirStack, ErrorFormatter) -> bool """Helper for popd and cd { ... }.""" dest_dir = dir_stack.Pop() if dest_dir is None: errfmt.Print_('popd: directory stack is empty') return False err_num = pyos.Chdir(dest_dir) if err_num != 0: # Happens if a directory is deleted in pushing and popping errfmt.Print_("popd: %r: %s" % (dest_dir, posix.strerror(err_num))) return False state.SetGlobalString(mem, 'PWD', dest_dir) mem.SetPwd(dest_dir) return True
def EvalForPlugin(self, w): """Wrapper around EvalWordToString that prevents errors. Runtime errors like $(( 1 / 0 )) and mutating $? like $(exit 42) are handled here. """ self.mem.PushStatusFrame() # to "sandbox" $? and $PIPESTATUS try: val = self.EvalWordToString(w) except util.FatalRuntimeError as e: val = value.Str("<Runtime error: %s>" % e.UserErrorString()) except (OSError, IOError) as e: # This is like the catch-all in Executor.ExecuteAndCatch(). val = value.Str("<I/O error: %s>" % posix.strerror(e.errno)) finally: self.mem.PopStatusFrame() return val
def _PushDup(self, fd1, fd2): # type: (int, int) -> bool """Save fd2, and dup fd1 onto fd2. Mutates self.cur_frame.saved. Returns: success Bool """ new_fd = self._GetFreeDescriptor() #log('---- _PushDup %s %s', fd1, fd2) need_restore = True try: 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' self.errfmt.Print('%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 __call__(self, arg_vec): arg, _ = PWD_SPEC.ParseVec(arg_vec) try: # This comes FIRST, even if you change $PWD. pwd = posix.getcwd() except OSError as e: # Happens when the directory is unlinked. self.errfmt.Print("Can't determine working directory: %s", posix.strerror(e.errno)) return 1 # '-L' is the default behavior; no need to check it # TODO: ensure that if multiple flags are provided, the *last* one overrides # the others if arg.P: pwd = libc.realpath(pwd) print(pwd) return 0
def _Source(self, arg_vec): argv = arg_vec.strs call_spid = arg_vec.spids[0] try: path = argv[1] except IndexError: raise args.UsageError('missing required argument') resolved = self.search_path.Lookup(path, exec_required=False) if resolved is None: resolved = path try: f = self.fd_state.Open(resolved) # Shell can't use descriptors 3-9 except OSError as e: self.errfmt.Print('source %r failed: %s', path, posix.strerror(e.errno), span_id=arg_vec.spids[1]) return 1 try: line_reader = reader.FileLineReader(f, self.arena) c_parser = self.parse_ctx.MakeOshParser(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[2:] self.mem.PushSource(path, source_argv) try: status = self._EvalHelper(c_parser, source.SourcedFile(path, call_spid)) finally: self.mem.PopSource(source_argv) return status except _ControlFlow as e: if e.IsReturn(): return e.StatusCode() else: raise finally: f.close()
def __call__(self, arg_vec): num_args = len(arg_vec.strs) - 1 if num_args == 0: # TODO: It's suppose to try another dir before doing this? self.errfmt.Print('pushd: no other directory') return 1 elif num_args > 1: raise args.UsageError('got too many arguments') dest_dir = os_path.abspath(arg_vec.strs[1]) try: posix.chdir(dest_dir) except OSError as e: self.errfmt.Print("pushd: %r: %s", dest_dir, posix.strerror(e.errno), span_id=arg_vec.spids[1]) return 1 self.dir_stack.Push(dest_dir) _PrintDirStack(self.dir_stack, SINGLE_LINE, self.mem.GetVar('HOME')) state.SetGlobalString(self.mem, 'PWD', dest_dir) return 0
def __call__(self, unused_word, state): """Return a single match.""" try: return self._GetNextCompletion(state) except util.FatalRuntimeError as e: # From -W. TODO: -F is swallowed now. # We should have a nicer UI for displaying errors. Maybe they shouldn't # print it to stderr. That messes up the completion display. We could # print what WOULD have been COMPREPLY here. log('Runtime error while completing: %s', e) self.debug_f.log('Runtime error while completing: %s', e) except (IOError, OSError) as e: # test this with prlimit --nproc=1 --pid=$$ ui.Stderr('I/O error in osh completion: %s', posix.strerror(e.errno)) except Exception as e: # ESSENTIAL because readline swallows exceptions. import traceback traceback.print_exc() log('Unhandled exception while completing: %s', e) self.debug_f.log('Unhandled exception while completing: %s', e) except SystemExit as e: # Because readline ignores SystemExit! posix._exit(e.code)
def TeaMain(argv): # type: (str, List[str]) -> int arena = alloc.Arena() try: script_name = argv[1] arena.PushSource(source.MainFile(script_name)) except IndexError: arena.PushSource(source.Stdin()) f = sys.stdin else: try: f = open(script_name) except IOError as e: stderr_line("tea: Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 2 aliases = {} # Dummy value; not respecting aliases! loader = pyutil.GetResourceLoader() oil_grammar = pyutil.LoadOilGrammar(loader) # Not used in tea, but OK... opt0_array = state.InitOpts() parse_opts = optview.Parse(opt0_array) # parse `` and a[x+1]=bar differently parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar) line_reader = reader.FileLineReader(f, arena) try: parse_ctx.ParseTeaModule(line_reader) status = 0 except error.Parse as e: ui.PrettyPrintError(e, arena) status = 2 return status
def _GetWorkingDir(): """Fallback for pwd and $PWD when there's no 'cd' and no inherited $PWD.""" try: return posix.getcwd() except OSError as e: e_die("Can't determine working directory: %s", posix.strerror(e.errno))
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int arg, i = CD_SPEC.ParseCmdVal(cmd_val) try: dest_dir = cmd_val.argv[i] except IndexError: val = self.mem.GetVar('HOME') if val.tag == value_e.Undef: self.errfmt.Print("$HOME isn't defined") return 1 elif val.tag == value_e.Str: dest_dir = val.s elif val.tag == value_e.MaybeStrArray: # User would have to unset $HOME to get rid of exported flag self.errfmt.Print("$HOME shouldn't be an array") return 1 if dest_dir == '-': old = self.mem.GetVar('OLDPWD', scope_e.GlobalOnly) if old.tag == value_e.Undef: self.errfmt.Print('$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.MaybeStrArray: # TODO: Prevent the user from setting OLDPWD to array (or maybe they # can't even set it at all.) raise AssertionError('Invalid $OLDPWD') pwd = self.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: self.errfmt.Print("cd %r: %s", real_dest_dir, posix.strerror(e.errno), span_id=cmd_val.arg_spids[i]) return 1 state.ExportGlobalString(self.mem, 'PWD', real_dest_dir) # WEIRD: We need a copy that is NOT PWD, because the user could mutate PWD. # Other shells use global variables. self.mem.SetPwd(real_dest_dir) if cmd_val.block: self.dir_stack.Push(real_dest_dir) try: unused = self.ex.EvalBlock(cmd_val.block) finally: # TODO: Change this to a context manager. # note: it might be more consistent to use an exception here. if not _PopDirStack(self.mem, self.dir_stack, self.errfmt): return 1 else: # No block state.ExportGlobalString(self.mem, 'OLDPWD', pwd.s) self.dir_stack.Reset() # for pushd/popd/dirs return 0
def _ApplyRedirect(self, r, waiter): # type: (redirect, Waiter) -> None arg = r.arg UP_arg = arg with tagswitch(arg) as case: if case(redirect_arg_e.Path): arg = cast(redirect_arg__Path, UP_arg) if r.op_id in (Id.Redir_Great, Id.Redir_AndGreat): # > &> # NOTE: This is different than >| because it respects noclobber, but # that option is almost never used. See test/wild.sh. mode = posix.O_CREAT | posix.O_WRONLY | posix.O_TRUNC elif r.op_id == Id.Redir_Clobber: # >| mode = posix.O_CREAT | posix.O_WRONLY | posix.O_TRUNC elif r.op_id in (Id.Redir_DGreat, Id.Redir_AndDGreat): # >> &>> mode = posix.O_CREAT | posix.O_WRONLY | posix.O_APPEND elif r.op_id == Id.Redir_Less: # < mode = posix.O_RDONLY elif r.op_id == Id.Redir_LessGreat: # <> mode = posix.O_CREAT | posix.O_RDWR else: raise NotImplementedError(r.op_id) # NOTE: 0666 is affected by umask, all shells use it. try: open_fd = posix.open(arg.filename, mode, 0o666) except OSError as e: self.errfmt.Print("Can't open %r: %s", arg.filename, posix.strerror(e.errno), span_id=r.op_spid) raise # redirect failed new_fd = self._PushDup(open_fd, r.loc) if new_fd != NO_FD: posix.close(open_fd) # Now handle &> and &>> and their variants. These pairs are the same: # # stdout_stderr.py &> out-err.txt # stdout_stderr.py > out-err.txt 2>&1 # # stdout_stderr.py 3&> out-err.txt # stdout_stderr.py 3> out-err.txt 2>&3 # # Ditto for {fd}> and {fd}&> if r.op_id in (Id.Redir_AndGreat, Id.Redir_AndDGreat): self._PushDup(new_fd, redir_loc.Fd(2)) elif case(redirect_arg_e.CopyFd): # e.g. echo hi 1>&2 arg = cast(redirect_arg__CopyFd, UP_arg) if r.op_id == Id.Redir_GreatAnd: # 1>&2 self._PushDup(arg.target_fd, r.loc) elif r.op_id == Id.Redir_LessAnd: # 0<&5 # The only difference between >& and <& is the default file # descriptor argument. self._PushDup(arg.target_fd, r.loc) else: raise NotImplementedError() elif case(redirect_arg_e.MoveFd): # e.g. echo hi 5>&6- arg = cast(redirect_arg__MoveFd, UP_arg) new_fd = self._PushDup(arg.target_fd, r.loc) if new_fd != NO_FD: posix.close(arg.target_fd) UP_loc = r.loc if r.loc.tag_() == redir_loc_e.Fd: fd = cast(redir_loc__Fd, UP_loc).fd else: fd = NO_FD self.cur_frame.saved.append((new_fd, fd, False)) elif case(redirect_arg_e.CloseFd): # e.g. echo hi 5>&- self._PushCloseFd(r.loc) elif case(redirect_arg_e.HereDoc): arg = cast(redirect_arg__HereDoc, UP_arg) # NOTE: Do these descriptors have to be moved out of the range 0-9? read_fd, write_fd = posix.pipe() self._PushDup(read_fd, r.loc) # stdin is now the pipe # We can't close like we do in the filename case above? The writer can # get a "broken pipe". self._PushClose(read_fd) thunk = _HereDocWriterThunk(write_fd, arg.body) # TODO: Use PIPE_SIZE to save a process in the case of small here docs, # which are the common case. (dash does this.) start_process = True #start_process = False if start_process: here_proc = Process(thunk, self.job_state) # NOTE: we could close the read pipe here, but it doesn't really # matter because we control the code. _ = here_proc.Start() #log('Started %s as %d', here_proc, pid) self._PushWait(here_proc, waiter) # Now that we've started the child, close it in the parent. posix.close(write_fd) else: posix.write(write_fd, arg.body) posix.close(write_fd)
def OshCommandMain(argv): """Run an 'oshc' tool. 'osh' is short for "osh compiler" or "osh command". TODO: - oshc --help oshc deps --path: the $PATH to use to find executables. What about libraries? NOTE: we're leaving out su -c, find, xargs, etc.? Those should generally run functions using the $0 pattern. --chained-command sudo """ try: action = argv[0] except IndexError: raise error.Usage('Missing required subcommand.') if action not in SUBCOMMANDS: raise error.Usage('Invalid subcommand %r.' % action) if action == 'parse-glob': # Pretty-print the AST produced by osh/glob_.py print('TODO:parse-glob') return 0 if action == 'parse-printf': # Pretty-print the AST produced by osh/builtin_printf.py print('TODO:parse-printf') return 0 arena = alloc.Arena() try: script_name = argv[1] arena.PushSource(source.MainFile(script_name)) except IndexError: arena.PushSource(source.Stdin()) f = sys.stdin else: try: f = open(script_name) except IOError as e: stderr_line("oshc: Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 2 aliases = {} # Dummy value; not respecting aliases! loader = pyutil.GetResourceLoader() oil_grammar = pyutil.LoadOilGrammar(loader) opt0_array = state.InitOpts() no_stack = None # type: List[bool] # for mycpp opt_stacks = [no_stack] * option_i.ARRAY_SIZE # type: List[List[bool]] parse_opts = optview.Parse(opt0_array, opt_stacks) # parse `` and a[x+1]=bar differently parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar) parse_ctx.Init_OnePassParse(True) line_reader = reader.FileLineReader(f, arena) c_parser = parse_ctx.MakeOshParser(line_reader) try: node = main_loop.ParseWholeFile(c_parser) except error.Parse as e: ui.PrettyPrintError(e, arena) return 2 assert node is not None f.close() # Columns for list-* # path line name # where name is the binary path, variable name, or library path. # bin-deps and lib-deps can be used to make an app bundle. # Maybe I should list them together? 'deps' can show 4 columns? # # path, line, type, name # # --pretty can show the LST location. # stderr: show how we're following imports? if action == 'translate': osh2oil.PrintAsOil(arena, node) elif action == 'arena': # for debugging osh2oil.PrintArena(arena) elif action == 'spans': # for debugging osh2oil.PrintSpans(arena) elif action == 'format': # TODO: autoformat code raise NotImplementedError(action) elif action == 'deps': deps.Deps(node) elif action == 'undefined-vars': # could be environment variables raise NotImplementedError() else: raise AssertionError # Checked above return 0
def ShellMain(lang, argv0, argv, login_shell): """Used by bin/osh and bin/oil. Args: lang: 'osh' or 'oil' argv0, argv: So we can also invoke bin/osh as 'oil.ovm osh'. Like busybox. login_shell: Was - on the front? """ # Differences between osh and oil: # - --help? I guess Oil has a SUPERSET of OSH options. # - oshrc vs oilrc # - the parser and executor # - Change the prompt in the interactive shell? assert lang in ('osh', 'oil'), lang arg_r = args.Reader(argv) try: opts = OSH_SPEC.Parse(arg_r) except args.UsageError as e: ui.Stderr('osh usage error: %s', e.msg) return 2 # NOTE: This has a side effect of deleting _OVM_* from the environment! # TODO: Thread this throughout the program, and get rid of the global # variable in core/util.py. Rename to InitResourceLaoder(). It's now only # used for the 'help' builtin and --version. loader = pyutil.GetResourceLoader() if opts.help: builtin.Help(['%s-usage' % lang], loader) return 0 if opts.version: # OSH version is the only binary in Oil right now, so it's all one version. _ShowVersion() return 0 if arg_r.AtEnd(): dollar0 = argv0 has_main = False else: dollar0 = arg_r.Peek() # the script name, or the arg after -c has_main = True arena = alloc.Arena() errfmt = ui.ErrorFormatter(arena) # NOTE: has_main is only for ${BASH_SOURCE[@} and family. Could be a # required arg. mem = state.Mem(dollar0, argv[arg_r.i + 1:], posix.environ, arena, has_main=has_main) builtin_funcs.Init(mem) procs = {} job_state = process.JobState() fd_state = process.FdState(errfmt, job_state) parse_opts = parse_lib.OilParseOptions() exec_opts = state.ExecOpts(mem, parse_opts, line_input) if opts.show_options: # special case: sh -o exec_opts.ShowOptions([]) return 0 builtin_pure.SetExecOpts(exec_opts, opts.opt_changes, opts.shopt_changes) aliases = {} # feedback between runtime and parser oil_grammar = meta.LoadOilGrammar(loader) if opts.one_pass_parse and not exec_opts.noexec: raise args.UsageError('--one-pass-parse requires noexec (-n)') parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar, one_pass_parse=opts.one_pass_parse) # Three ParseContext instances SHARE aliases. comp_arena = alloc.Arena() comp_arena.PushSource(source.Unused('completion')) trail1 = parse_lib.Trail() # one_pass_parse needs to be turned on to complete inside backticks. TODO: # fix the issue where ` gets erased because it's not part of # set_completer_delims(). comp_ctx = parse_lib.ParseContext(comp_arena, parse_opts, aliases, oil_grammar, trail=trail1, one_pass_parse=True) hist_arena = alloc.Arena() hist_arena.PushSource(source.Unused('history')) trail2 = parse_lib.Trail() hist_ctx = parse_lib.ParseContext(hist_arena, parse_opts, aliases, oil_grammar, trail=trail2) # Deps helps manages dependencies. These dependencies are circular: # - ex and word_ev, arith_ev -- for command sub, arith sub # - arith_ev and word_ev -- for $(( ${a} )) and $x$(( 1 )) # - ex and builtins (which execute code, like eval) # - prompt_ev needs word_ev for $PS1, which needs prompt_ev for @P exec_deps = cmd_exec.Deps() # TODO: In general, exec_deps are shared between the mutually recursive # evaluators. Some of the four below are only shared between a builtin and # the Executor, so we could put them somewhere else. exec_deps.traps = {} exec_deps.trap_nodes = [] # TODO: Clear on fork() to avoid duplicates exec_deps.job_state = job_state # note: exec_opts.interactive set later exec_deps.waiter = process.Waiter(job_state, exec_opts) exec_deps.errfmt = errfmt my_pid = posix.getpid() debug_path = '' debug_dir = posix.environ.get('OSH_DEBUG_DIR') if opts.debug_file: # --debug-file takes precedence over OSH_DEBUG_DIR debug_path = opts.debug_file elif debug_dir: debug_path = os_path.join(debug_dir, '%d-osh.log' % my_pid) if debug_path: # This will be created as an empty file if it doesn't exist, or it could be # a pipe. try: debug_f = util.DebugFile(fd_state.Open(debug_path, mode='w')) except OSError as e: ui.Stderr("osh: Couldn't open %r: %s", debug_path, posix.strerror(e.errno)) return 2 else: debug_f = util.NullDebugFile() exec_deps.debug_f = debug_f # Not using datetime for dependency reasons. TODO: maybe show the date at # the beginning of the log, and then only show time afterward? To save # space, and make space for microseconds. (datetime supports microseconds # but time.strftime doesn't). iso_stamp = time.strftime("%Y-%m-%d %H:%M:%S") debug_f.log('%s [%d] OSH started with argv %s', iso_stamp, my_pid, argv) if debug_path: debug_f.log('Writing logs to %r', debug_path) interp = posix.environ.get('OSH_HIJACK_SHEBANG', '') exec_deps.search_path = state.SearchPath(mem) exec_deps.ext_prog = process.ExternalProgram(interp, fd_state, exec_deps.search_path, errfmt, debug_f) splitter = split.SplitContext(mem) exec_deps.splitter = splitter # split() builtin builtin_funcs.SetGlobalFunc(mem, 'split', lambda s: splitter.SplitForWordEval(s)) # This could just be OSH_DEBUG_STREAMS='debug crash' ? That might be # stuffing too much into one, since a .json crash dump isn't a stream. crash_dump_dir = posix.environ.get('OSH_CRASH_DUMP_DIR', '') exec_deps.dumper = dev.CrashDumper(crash_dump_dir) if opts.xtrace_to_debug_file: trace_f = debug_f else: trace_f = util.DebugFile(sys.stderr) exec_deps.trace_f = trace_f comp_lookup = completion.Lookup() # Various Global State objects to work around readline interfaces compopt_state = completion.OptionState() comp_ui_state = comp_ui.State() prompt_state = comp_ui.PromptState() dir_stack = state.DirStack() new_var = builtin_assign.NewVar(mem, procs, errfmt) builtins = { # Lookup builtin_e.ECHO: builtin_pure.Echo(exec_opts), builtin_e.PRINTF: builtin_printf.Printf(mem, parse_ctx, errfmt), builtin_e.PUSHD: builtin.Pushd(mem, dir_stack, errfmt), builtin_e.POPD: builtin.Popd(mem, dir_stack, errfmt), builtin_e.DIRS: builtin.Dirs(mem, dir_stack, errfmt), builtin_e.PWD: builtin.Pwd(mem, errfmt), builtin_e.READ: builtin.Read(splitter, mem), builtin_e.HELP: builtin.Help(loader, errfmt), builtin_e.HISTORY: builtin.History(line_input), # Completion (more added below) builtin_e.COMPOPT: builtin_comp.CompOpt(compopt_state, errfmt), builtin_e.COMPADJUST: builtin_comp.CompAdjust(mem), # test / [ differ by need_right_bracket builtin_e.TEST: builtin_bracket.Test(False, errfmt), builtin_e.BRACKET: builtin_bracket.Test(True, errfmt), # Assignment (which are pure) builtin_e.DECLARE: new_var, builtin_e.TYPESET: new_var, builtin_e.LOCAL: new_var, builtin_e.EXPORT: builtin_assign.Export(mem, errfmt), builtin_e.READONLY: builtin_assign.Readonly(mem, errfmt), builtin_e.UNSET: builtin_assign.Unset(mem, procs, errfmt), builtin_e.SHIFT: builtin_assign.Shift(mem), # Pure builtin_e.SET: builtin_pure.Set(exec_opts, mem), builtin_e.SHOPT: builtin_pure.Shopt(exec_opts), builtin_e.ALIAS: builtin_pure.Alias(aliases, errfmt), builtin_e.UNALIAS: builtin_pure.UnAlias(aliases, errfmt), builtin_e.TYPE: builtin_pure.Type(procs, aliases, exec_deps.search_path), builtin_e.HASH: builtin_pure.Hash(exec_deps.search_path), builtin_e.GETOPTS: builtin_pure.GetOpts(mem, errfmt), builtin_e.COLON: lambda arg_vec: 0, # a "special" builtin builtin_e.TRUE: lambda arg_vec: 0, builtin_e.FALSE: lambda arg_vec: 1, # Process builtin_e.WAIT: builtin_process.Wait(exec_deps.waiter, exec_deps.job_state, mem, errfmt), builtin_e.JOBS: builtin_process.Jobs(exec_deps.job_state), builtin_e.FG: builtin_process.Fg(exec_deps.job_state, exec_deps.waiter), builtin_e.BG: builtin_process.Bg(exec_deps.job_state), builtin_e.UMASK: builtin_process.Umask, # Oil builtin_e.REPR: builtin_oil.Repr(mem, errfmt), builtin_e.PUSH: builtin_oil.Push(mem, errfmt), builtin_e.USE: builtin_oil.Use(mem, errfmt), } ex = cmd_exec.Executor(mem, fd_state, procs, builtins, exec_opts, parse_ctx, exec_deps) exec_deps.ex = ex word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, exec_deps, arena) exec_deps.word_ev = word_ev arith_ev = osh_expr_eval.ArithEvaluator(mem, exec_opts, word_ev, errfmt) exec_deps.arith_ev = arith_ev word_ev.arith_ev = arith_ev # Another circular dependency bool_ev = osh_expr_eval.BoolEvaluator(mem, exec_opts, word_ev, errfmt) exec_deps.bool_ev = bool_ev expr_ev = expr_eval.OilEvaluator(mem, procs, ex, word_ev, errfmt) exec_deps.expr_ev = expr_ev tracer = dev.Tracer(parse_ctx, exec_opts, mem, word_ev, trace_f) exec_deps.tracer = tracer # HACK for circular deps ex.word_ev = word_ev ex.arith_ev = arith_ev ex.bool_ev = bool_ev ex.expr_ev = expr_ev ex.tracer = tracer word_ev.expr_ev = expr_ev spec_builder = builtin_comp.SpecBuilder(ex, parse_ctx, word_ev, splitter, comp_lookup) # Add some builtins that depend on the executor! complete_builtin = builtin_comp.Complete(spec_builder, comp_lookup) builtins[builtin_e.COMPLETE] = complete_builtin builtins[builtin_e.COMPGEN] = builtin_comp.CompGen(spec_builder) builtins[builtin_e.CD] = builtin.Cd(mem, dir_stack, ex, errfmt) builtins[builtin_e.JSON] = builtin_oil.Json(mem, ex, errfmt) sig_state = process.SignalState() sig_state.InitShell() builtins[builtin_e.TRAP] = builtin_process.Trap(sig_state, exec_deps.traps, exec_deps.trap_nodes, ex, errfmt) if lang == 'oil': # The Oil executor wraps an OSH executor? It needs to be able to source # it. ex = oil_cmd_exec.OilExecutor(ex) # PromptEvaluator rendering is needed in non-interactive shells for @P. prompt_ev = prompt.Evaluator(lang, parse_ctx, ex, mem) exec_deps.prompt_ev = prompt_ev word_ev.prompt_ev = prompt_ev # HACK for circular deps # History evaluation is a no-op if line_input is None. hist_ev = history.Evaluator(line_input, hist_ctx, debug_f) if opts.c is not None: arena.PushSource(source.CFlag()) line_reader = reader.StringLineReader(opts.c, arena) if opts.i: # -c and -i can be combined exec_opts.interactive = True elif opts.i: # force interactive arena.PushSource(source.Stdin(' -i')) line_reader = reader.InteractiveLineReader(arena, prompt_ev, hist_ev, line_input, prompt_state, sig_state) exec_opts.interactive = True else: try: script_name = arg_r.Peek() except IndexError: if sys.stdin.isatty(): arena.PushSource(source.Interactive()) line_reader = reader.InteractiveLineReader( arena, prompt_ev, hist_ev, line_input, prompt_state, sig_state) exec_opts.interactive = True else: arena.PushSource(source.Stdin('')) line_reader = reader.FileLineReader(sys.stdin, arena) else: arena.PushSource(source.MainFile(script_name)) try: f = fd_state.Open(script_name) except OSError as e: ui.Stderr("osh: Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 1 line_reader = reader.FileLineReader(f, arena) # TODO: assert arena.NumSourcePaths() == 1 # TODO: .rc file needs its own arena. if lang == 'osh': c_parser = parse_ctx.MakeOshParser(line_reader) else: c_parser = parse_ctx.MakeOilParser(line_reader) if exec_opts.interactive: # Calculate ~/.config/oil/oshrc or oilrc # Use ~/.config/oil to avoid cluttering the user's home directory. Some # users may want to ln -s ~/.config/oil/oshrc ~/oshrc or ~/.oshrc. # https://unix.stackexchange.com/questions/24347/why-do-some-applications-use-config-appname-for-their-config-data-while-other home_dir = process.GetHomeDir() assert home_dir is not None rc_path = opts.rcfile or os_path.join(home_dir, '.config/oil', lang + 'rc') history_filename = os_path.join(home_dir, '.config/oil', 'history_' + lang) if line_input: # NOTE: We're using a different WordEvaluator here. ev = word_eval.CompletionWordEvaluator(mem, exec_opts, exec_deps, arena) root_comp = completion.RootCompleter(ev, mem, comp_lookup, compopt_state, comp_ui_state, comp_ctx, debug_f) term_width = 0 if opts.completion_display == 'nice': try: term_width = libc.get_terminal_width() except IOError: # stdin not a terminal pass if term_width != 0: display = comp_ui.NiceDisplay(term_width, comp_ui_state, prompt_state, debug_f, line_input) else: display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state, debug_f) _InitReadline(line_input, history_filename, root_comp, display, debug_f) _InitDefaultCompletions(ex, complete_builtin, comp_lookup) else: # Without readline module display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state, debug_f) sig_state.InitInteractiveShell(display) # NOTE: Call this AFTER _InitDefaultCompletions. try: SourceStartupFile(rc_path, lang, parse_ctx, ex) except util.UserExit as e: return e.status line_reader.Reset() # After sourcing startup file, render $PS1 prompt_plugin = prompt.UserPlugin(mem, parse_ctx, ex) try: status = main_loop.Interactive(opts, ex, c_parser, display, prompt_plugin, errfmt) if ex.MaybeRunExitTrap(): status = ex.LastStatus() except util.UserExit as e: status = e.status return status nodes_out = [] if exec_opts.noexec else None if nodes_out is None and opts.parser_mem_dump: raise args.UsageError('--parser-mem-dump can only be used with -n') _tlog('Execute(node)') try: status = main_loop.Batch(ex, c_parser, arena, nodes_out=nodes_out) if ex.MaybeRunExitTrap(): status = ex.LastStatus() except util.UserExit as e: status = e.status # Only print nodes if the whole parse succeeded. if nodes_out is not None and status == 0: if opts.parser_mem_dump: # only valid in -n mode # This might be superstition, but we want to let the value stabilize # after parsing. bash -c 'cat /proc/$$/status' gives different results # with a sleep. time.sleep(0.001) input_path = '/proc/%d/status' % posix.getpid() with open(input_path) as f, open(opts.parser_mem_dump, 'w') as f2: contents = f.read() f2.write(contents) log('Wrote %s to %s (--parser-mem-dump)', input_path, opts.parser_mem_dump) ui.PrintAst(nodes_out, opts) # NOTE: 'exit 1' is ControlFlow and gets here, but subshell/commandsub # don't because they call sys.exit(). if opts.runtime_mem_dump: # This might be superstition, but we want to let the value stabilize # after parsing. bash -c 'cat /proc/$$/status' gives different results # with a sleep. time.sleep(0.001) input_path = '/proc/%d/status' % posix.getpid() with open(input_path) as f, open(opts.runtime_mem_dump, 'w') as f2: contents = f.read() f2.write(contents) log('Wrote %s to %s (--runtime-mem-dump)', input_path, opts.runtime_mem_dump) # NOTE: We haven't closed the file opened with fd_state.Open return status
def Main(lang, arg_r, environ, login_shell, loader, line_input): # type: (str, args.Reader, Dict[str, str], bool, pyutil._ResourceLoader, Any) -> int """The full shell lifecycle. Used by bin/osh and bin/oil. Args: lang: 'osh' or 'oil' argv0, arg_r: command line arguments environ: environment login_shell: Was - on the front? loader: to get help, version, grammar, etc. line_input: optional GNU readline """ # Differences between osh and oil: # - --help? I guess Oil has a SUPERSET of OSH options. # - oshrc vs oilrc # - shopt -s oil:all # - Change the prompt in the interactive shell? # osh-pure: # - no oil grammar # - no expression evaluator # - no interactive shell, or line_input # - no process.* # process.{ExternalProgram,Waiter,FdState,JobState,SignalState} -- we want # to evaluate config files without any of these # Modules not translated yet: completion, comp_ui, builtin_comp, process # - word evaluator # - shouldn't glob? set -o noglob? or hard failure? # - ~ shouldn't read from the file system # - I guess it can just be the HOME=HOME? # Builtin: # shellvm -c 'echo hi' # shellvm <<< 'echo hi' argv0 = arg_r.Peek() assert argv0 is not None arg_r.Next() assert lang in ('osh', 'oil'), lang try: attrs = flag_spec.ParseMore('main', arg_r) except error.Usage as e: stderr_line('osh usage error: %s', e.msg) return 2 flag = arg_types.main(attrs.attrs) arena = alloc.Arena() errfmt = ui.ErrorFormatter(arena) help_builtin = builtin_misc.Help(loader, errfmt) if flag.help: help_builtin.Run(pure.MakeBuiltinArgv(['%s-usage' % lang])) return 0 if flag.version: # OSH version is the only binary in Oil right now, so it's all one version. pyutil.ShowAppVersion('Oil', loader) return 0 no_str = None # type: str debug_stack = [] # type: List[state.DebugFrame] if arg_r.AtEnd(): dollar0 = argv0 else: dollar0 = arg_r.Peek() # the script name, or the arg after -c # Copy quirky bash behavior. frame0 = state.DebugFrame(dollar0, 'main', no_str, state.LINE_ZERO, 0, 0) debug_stack.append(frame0) # Copy quirky bash behavior. frame1 = state.DebugFrame(no_str, no_str, no_str, runtime.NO_SPID, 0, 0) debug_stack.append(frame1) script_name = arg_r.Peek() # type: Optional[str] arg_r.Next() mem = state.Mem(dollar0, arg_r.Rest(), arena, debug_stack) version_str = pyutil.GetVersion(loader) state.InitMem(mem, environ, version_str) builtin_funcs.Init(mem) procs = {} # type: Dict[str, command__ShFunction] job_state = process.JobState() fd_state = process.FdState(errfmt, job_state, mem) opt_hook = ShellOptHook(line_input) parse_opts, exec_opts, mutable_opts = state.MakeOpts(mem, opt_hook) # TODO: only MutableOpts needs mem, so it's not a true circular dep. mem.exec_opts = exec_opts # circular dep if attrs.show_options: # special case: sh -o mutable_opts.ShowOptions([]) return 0 # Set these BEFORE processing flags, so they can be overridden. if lang == 'oil': mutable_opts.SetShoptOption('oil:all', True) builtin_pure.SetShellOpts(mutable_opts, attrs.opt_changes, attrs.shopt_changes) # feedback between runtime and parser aliases = {} # type: Dict[str, str] oil_grammar = pyutil.LoadOilGrammar(loader) if flag.one_pass_parse and not exec_opts.noexec(): raise error.Usage('--one-pass-parse requires noexec (-n)') parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar) parse_ctx.Init_OnePassParse(flag.one_pass_parse) # Three ParseContext instances SHARE aliases. comp_arena = alloc.Arena() comp_arena.PushSource(source.Unused('completion')) trail1 = parse_lib.Trail() # one_pass_parse needs to be turned on to complete inside backticks. TODO: # fix the issue where ` gets erased because it's not part of # set_completer_delims(). comp_ctx = parse_lib.ParseContext(comp_arena, parse_opts, aliases, oil_grammar) comp_ctx.Init_Trail(trail1) comp_ctx.Init_OnePassParse(True) hist_arena = alloc.Arena() hist_arena.PushSource(source.Unused('history')) trail2 = parse_lib.Trail() hist_ctx = parse_lib.ParseContext(hist_arena, parse_opts, aliases, oil_grammar) hist_ctx.Init_Trail(trail2) # Deps helps manages dependencies. These dependencies are circular: # - cmd_ev and word_ev, arith_ev -- for command sub, arith sub # - arith_ev and word_ev -- for $(( ${a} )) and $x$(( 1 )) # - cmd_ev and builtins (which execute code, like eval) # - prompt_ev needs word_ev for $PS1, which needs prompt_ev for @P cmd_deps = cmd_eval.Deps() cmd_deps.mutable_opts = mutable_opts # TODO: In general, cmd_deps are shared between the mutually recursive # evaluators. Some of the four below are only shared between a builtin and # the CommandEvaluator, so we could put them somewhere else. cmd_deps.traps = {} cmd_deps.trap_nodes = [] # TODO: Clear on fork() to avoid duplicates waiter = process.Waiter(job_state, exec_opts) my_pid = posix.getpid() debug_path = '' debug_dir = environ.get('OSH_DEBUG_DIR') if flag.debug_file is not None: # --debug-file takes precedence over OSH_DEBUG_DIR debug_path = flag.debug_file elif debug_dir is not None: debug_path = os_path.join(debug_dir, '%d-osh.log' % my_pid) if len(debug_path): # This will be created as an empty file if it doesn't exist, or it could be # a pipe. try: debug_f = util.DebugFile( fd_state.OpenForWrite(debug_path)) # type: util._DebugFile except OSError as e: stderr_line("osh: Couldn't open %r: %s", debug_path, posix.strerror(e.errno)) return 2 else: debug_f = util.NullDebugFile() cmd_deps.debug_f = debug_f # Not using datetime for dependency reasons. TODO: maybe show the date at # the beginning of the log, and then only show time afterward? To save # space, and make space for microseconds. (datetime supports microseconds # but time.strftime doesn't). if mylib.PYTHON: iso_stamp = time.strftime("%Y-%m-%d %H:%M:%S") debug_f.log('%s [%d] OSH started with argv %s', iso_stamp, my_pid, arg_r.argv) if len(debug_path): debug_f.log('Writing logs to %r', debug_path) interp = environ.get('OSH_HIJACK_SHEBANG', '') search_path = state.SearchPath(mem) ext_prog = process.ExternalProgram(interp, fd_state, errfmt, debug_f) splitter = split.SplitContext(mem) # split() builtin # TODO: Accept IFS as a named arg? split('a b', IFS=' ') builtin_funcs.SetGlobalFunc( mem, 'split', lambda s, ifs=None: splitter.SplitForWordEval(s, ifs=ifs)) # glob() builtin # TODO: This is instantiation is duplicated in osh/word_eval.py globber = glob_.Globber(exec_opts) builtin_funcs.SetGlobalFunc(mem, 'glob', lambda s: globber.OilFuncCall(s)) # This could just be OSH_DEBUG_STREAMS='debug crash' ? That might be # stuffing too much into one, since a .json crash dump isn't a stream. crash_dump_dir = environ.get('OSH_CRASH_DUMP_DIR', '') cmd_deps.dumper = dev.CrashDumper(crash_dump_dir) if flag.xtrace_to_debug_file: trace_f = debug_f else: trace_f = util.DebugFile(mylib.Stderr()) comp_lookup = completion.Lookup() # Various Global State objects to work around readline interfaces compopt_state = completion.OptionState() comp_ui_state = comp_ui.State() prompt_state = comp_ui.PromptState() dir_stack = state.DirStack() # # Initialize builtins that don't depend on evaluators # builtins = {} # type: Dict[int, vm._Builtin] pure.AddPure(builtins, mem, procs, mutable_opts, aliases, search_path, errfmt) pure.AddIO(builtins, mem, dir_stack, exec_opts, splitter, errfmt) AddProcess(builtins, mem, ext_prog, fd_state, job_state, waiter, search_path, errfmt) AddOil(builtins, mem, errfmt) builtins[builtin_i.help] = help_builtin # Interactive, depend on line_input builtins[builtin_i.bind] = builtin_lib.Bind(line_input, errfmt) builtins[builtin_i.history] = builtin_lib.History(line_input, mylib.Stdout()) # # Assignment builtins # assign_b = {} # type: Dict[int, vm._AssignBuiltin] new_var = builtin_assign.NewVar(mem, procs, errfmt) assign_b[builtin_i.declare] = new_var assign_b[builtin_i.typeset] = new_var assign_b[builtin_i.local] = new_var assign_b[builtin_i.export_] = builtin_assign.Export(mem, errfmt) assign_b[builtin_i.readonly] = builtin_assign.Readonly(mem, errfmt) # # Initialize Evaluators # arith_ev = sh_expr_eval.ArithEvaluator(mem, exec_opts, parse_ctx, errfmt) bool_ev = sh_expr_eval.BoolEvaluator(mem, exec_opts, parse_ctx, errfmt) expr_ev = expr_eval.OilEvaluator(mem, procs, splitter, errfmt) word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, splitter, errfmt) cmd_ev = cmd_eval.CommandEvaluator(mem, exec_opts, errfmt, procs, assign_b, arena, cmd_deps) shell_ex = executor.ShellExecutor(mem, exec_opts, mutable_opts, procs, builtins, search_path, ext_prog, waiter, job_state, fd_state, errfmt) # PromptEvaluator rendering is needed in non-interactive shells for @P. prompt_ev = prompt.Evaluator(lang, parse_ctx, mem) tracer = dev.Tracer(parse_ctx, exec_opts, mutable_opts, mem, word_ev, trace_f) # Wire up circular dependencies. vm.InitCircularDeps(arith_ev, bool_ev, expr_ev, word_ev, cmd_ev, shell_ex, prompt_ev, tracer) # # Initialize builtins that depend on evaluators # # note: 'printf -v a[i]' and 'unset a[i]' require same deps builtins[builtin_i.printf] = builtin_printf.Printf(mem, exec_opts, parse_ctx, arith_ev, errfmt) builtins[builtin_i.unset] = builtin_assign.Unset(mem, exec_opts, procs, parse_ctx, arith_ev, errfmt) builtins[builtin_i.eval] = builtin_meta.Eval(parse_ctx, exec_opts, cmd_ev) source_builtin = builtin_meta.Source(parse_ctx, search_path, cmd_ev, fd_state, errfmt) builtins[builtin_i.source] = source_builtin builtins[builtin_i.dot] = source_builtin builtins[builtin_i.builtin] = builtin_meta.Builtin(shell_ex, errfmt) builtins[builtin_i.command] = builtin_meta.Command(shell_ex, procs, aliases, search_path) spec_builder = builtin_comp.SpecBuilder(cmd_ev, parse_ctx, word_ev, splitter, comp_lookup) complete_builtin = builtin_comp.Complete(spec_builder, comp_lookup) builtins[builtin_i.complete] = complete_builtin builtins[builtin_i.compgen] = builtin_comp.CompGen(spec_builder) builtins[builtin_i.compopt] = builtin_comp.CompOpt(compopt_state, errfmt) builtins[builtin_i.compadjust] = builtin_comp.CompAdjust(mem) # These builtins take blocks, and thus need cmd_ev. builtins[builtin_i.cd] = builtin_misc.Cd(mem, dir_stack, cmd_ev, errfmt) builtins[builtin_i.json] = builtin_oil.Json(mem, cmd_ev, errfmt) sig_state = pyos.SignalState() sig_state.InitShell() builtins[builtin_i.trap] = builtin_process.Trap(sig_state, cmd_deps.traps, cmd_deps.trap_nodes, parse_ctx, errfmt) # History evaluation is a no-op if line_input is None. hist_ev = history.Evaluator(line_input, hist_ctx, debug_f) if flag.c is not None: arena.PushSource(source.CFlag()) line_reader = reader.StringLineReader(flag.c, arena) # type: reader._Reader if flag.i: # -c and -i can be combined mutable_opts.set_interactive() elif flag.i: # force interactive arena.PushSource(source.Stdin(' -i')) line_reader = py_reader.InteractiveLineReader(arena, prompt_ev, hist_ev, line_input, prompt_state) mutable_opts.set_interactive() else: if script_name is None: stdin = mylib.Stdin() if stdin.isatty(): arena.PushSource(source.Interactive()) line_reader = py_reader.InteractiveLineReader( arena, prompt_ev, hist_ev, line_input, prompt_state) mutable_opts.set_interactive() else: arena.PushSource(source.Stdin('')) line_reader = reader.FileLineReader(stdin, arena) else: arena.PushSource(source.MainFile(script_name)) try: f = fd_state.Open(script_name) except OSError as e: stderr_line("osh: Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 1 line_reader = reader.FileLineReader(f, arena) # TODO: assert arena.NumSourcePaths() == 1 # TODO: .rc file needs its own arena. c_parser = parse_ctx.MakeOshParser(line_reader) if exec_opts.interactive(): # bash: 'set -o emacs' is the default only in the interactive shell mutable_opts.set_emacs() # Calculate ~/.config/oil/oshrc or oilrc # Use ~/.config/oil to avoid cluttering the user's home directory. Some # users may want to ln -s ~/.config/oil/oshrc ~/oshrc or ~/.oshrc. # https://unix.stackexchange.com/questions/24347/why-do-some-applications-use-config-appname-for-their-config-data-while-other home_dir = pyos.GetMyHomeDir() assert home_dir is not None history_filename = os_path.join(home_dir, '.config/oil/history_%s' % lang) if line_input: # NOTE: We're using a different WordEvaluator here. ev = word_eval.CompletionWordEvaluator(mem, exec_opts, splitter, errfmt) ev.arith_ev = arith_ev ev.expr_ev = expr_ev ev.prompt_ev = prompt_ev ev.CheckCircularDeps() root_comp = completion.RootCompleter(ev, mem, comp_lookup, compopt_state, comp_ui_state, comp_ctx, debug_f) term_width = 0 if flag.completion_display == 'nice': try: term_width = libc.get_terminal_width() except IOError: # stdin not a terminal pass if term_width != 0: display = comp_ui.NiceDisplay( term_width, comp_ui_state, prompt_state, debug_f, line_input) # type: comp_ui._IDisplay else: display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state, debug_f) comp_ui.InitReadline(line_input, history_filename, root_comp, display, debug_f) _InitDefaultCompletions(cmd_ev, complete_builtin, comp_lookup) else: # Without readline module display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state, debug_f) sig_state.InitInteractiveShell(display) rc_path = flag.rcfile or os_path.join(home_dir, '.config/oil/%src' % lang) try: # NOTE: Should be called AFTER _InitDefaultCompletions. SourceStartupFile(fd_state, rc_path, lang, parse_ctx, cmd_ev) except util.UserExit as e: return e.status line_reader.Reset() # After sourcing startup file, render $PS1 prompt_plugin = prompt.UserPlugin(mem, parse_ctx, cmd_ev) try: status = main_loop.Interactive(flag, cmd_ev, c_parser, display, prompt_plugin, errfmt) except util.UserExit as e: status = e.status box = [status] cmd_ev.MaybeRunExitTrap(box) status = box[0] return status if flag.rcfile: # bash doesn't have this warning, but it's useful stderr_line('osh warning: --rcfile ignored in non-interactive shell') if exec_opts.noexec(): status = 0 try: node = main_loop.ParseWholeFile(c_parser) except error.Parse as e: ui.PrettyPrintError(e, arena) status = 2 if status == 0: if flag.parser_mem_dump is not None: # only valid in -n mode input_path = '/proc/%d/status' % posix.getpid() pyutil.CopyFile(input_path, flag.parser_mem_dump) ui.PrintAst(node, flag) else: if flag.parser_mem_dump is not None: raise error.Usage('--parser-mem-dump can only be used with -n') try: status = main_loop.Batch(cmd_ev, c_parser, arena, cmd_flags=cmd_eval.IsMainProgram) except util.UserExit as e: status = e.status box = [status] cmd_ev.MaybeRunExitTrap(box) status = box[0] # NOTE: 'exit 1' is ControlFlow and gets here, but subshell/commandsub # don't because they call sys.exit(). if flag.runtime_mem_dump is not None: input_path = '/proc/%d/status' % posix.getpid() pyutil.CopyFile(input_path, flag.runtime_mem_dump) # NOTE: We haven't closed the file opened with fd_state.Open return status
def strerror_OS(e): # type: (OSError) -> str return posix.strerror(e.errno)
def strerror_IO(e): # type: (IOError) -> str return posix.strerror(e.errno)
def _ApplyRedirect(self, r, waiter): # type: (redirect_t, Waiter) -> bool ok = True UP_r = r with tagswitch(r) as case: if case(redirect_e.Path): r = cast(redirect__Path, UP_r) if r.op_id in (Id.Redir_Great, Id.Redir_AndGreat): # > &> # NOTE: This is different than >| because it respects noclobber, but # that option is almost never used. See test/wild.sh. mode = posix.O_CREAT | posix.O_WRONLY | posix.O_TRUNC elif r.op_id == Id.Redir_Clobber: # >| mode = posix.O_CREAT | posix.O_WRONLY | posix.O_TRUNC elif r.op_id in (Id.Redir_DGreat, Id.Redir_AndDGreat): # >> &>> mode = posix.O_CREAT | posix.O_WRONLY | posix.O_APPEND elif r.op_id == Id.Redir_Less: # < mode = posix.O_RDONLY else: raise NotImplementedError(r.op_id) # NOTE: 0666 is affected by umask, all shells use it. try: target_fd = posix.open(r.filename, mode, 0o666) except OSError as e: self.errfmt.Print( "Can't open %r: %s", r.filename, posix.strerror(e.errno), span_id=r.op_spid) return False # Apply redirect if not self._PushDup(target_fd, r.fd): ok = False # Now handle the extra redirects for aliases &> and &>>. # # We can rewrite # stdout_stderr.py &> out-err.txt # as # stdout_stderr.py > out-err.txt 2>&1 # # And rewrite # stdout_stderr.py 3&> out-err.txt # as # stdout_stderr.py 3> out-err.txt 2>&3 if ok: if r.op_id == Id.Redir_AndGreat: if not self._PushDup(r.fd, 2): ok = False elif r.op_id == Id.Redir_AndDGreat: if not self._PushDup(r.fd, 2): ok = False posix.close(target_fd) # We already made a copy of it. # I don't think we need to close(0) because it will be restored from its # saved position (10), which closes it. #self._PushClose(r.fd) elif case(redirect_e.FileDesc): # e.g. echo hi 1>&2 r = cast(redirect__FileDesc, UP_r) if r.op_id == Id.Redir_GreatAnd: # 1>&2 if not self._PushDup(r.target_fd, r.fd): ok = False elif r.op_id == Id.Redir_LessAnd: # 0<&5 # The only difference between >& and <& is the default file # descriptor argument. if not self._PushDup(r.target_fd, r.fd): ok = False else: raise NotImplementedError() elif case(redirect_e.HereDoc): r = cast(redirect__HereDoc, UP_r) # NOTE: Do these descriptors have to be moved out of the range 0-9? read_fd, write_fd = posix.pipe() if not self._PushDup(read_fd, r.fd): # stdin is now the pipe ok = False # We can't close like we do in the filename case above? The writer can # get a "broken pipe". self._PushClose(read_fd) thunk = _HereDocWriterThunk(write_fd, r.body) # TODO: Use PIPE_SIZE to save a process in the case of small here docs, # which are the common case. (dash does this.) start_process = True #start_process = False if start_process: here_proc = Process(thunk, self.job_state) # NOTE: we could close the read pipe here, but it doesn't really # matter because we control the code. _ = here_proc.Start() #log('Started %s as %d', here_proc, pid) self._PushWait(here_proc, waiter) # Now that we've started the child, close it in the parent. posix.close(write_fd) else: posix.write(write_fd, r.body) posix.close(write_fd) return ok
def strerror(e): # type: (Union[IOError, OSError]) -> str return posix.strerror(e.errno)
def ShellMain(lang, argv0, argv, login_shell): # type: (str, str, List[str], bool) -> int """Used by bin/osh and bin/oil. Args: lang: 'osh' or 'oil' argv0, argv: So we can also invoke bin/osh as 'oil.ovm osh'. Like busybox. login_shell: Was - on the front? """ # Differences between osh and oil: # - --help? I guess Oil has a SUPERSET of OSH options. # - oshrc vs oilrc # - the parser and executor # - Change the prompt in the interactive shell? assert lang in ('osh', 'oil'), lang arg_r = args.Reader(argv) try: opts = OSH_SPEC.Parse(arg_r) except args.UsageError as e: ui.Stderr('osh usage error: %s', e.msg) return 2 # NOTE: This has a side effect of deleting _OVM_* from the environment! # TODO: Thread this throughout the program, and get rid of the global # variable in core/util.py. Rename to InitResourceLaoder(). It's now only # used for the 'help' builtin and --version. loader = pyutil.GetResourceLoader() if opts.help: builtin_misc.Help(['%s-usage' % lang], loader) return 0 if opts.version: # OSH version is the only binary in Oil right now, so it's all one version. _ShowVersion() return 0 no_str = None # type: str debug_stack = [] if arg_r.AtEnd(): dollar0 = argv0 else: dollar0 = arg_r.Peek() # the script name, or the arg after -c # Copy quirky bash behavior. frame0 = state.DebugFrame(dollar0, 'main', no_str, state.LINE_ZERO, 0, 0) debug_stack.append(frame0) # Copy quirky bash behavior. frame1 = state.DebugFrame(no_str, no_str, no_str, runtime.NO_SPID, 0, 0) debug_stack.append(frame1) arena = alloc.Arena() errfmt = ui.ErrorFormatter(arena) mem = state.Mem(dollar0, argv[arg_r.i + 1:], arena, debug_stack) state.InitMem(mem, posix.environ) builtin_funcs.Init(mem) procs = {} job_state = process.JobState() fd_state = process.FdState(errfmt, job_state) opt_hook = ShellOptHook(line_input) parse_opts, exec_opts, mutable_opts = state.MakeOpts(mem, opt_hook) # TODO: only MutableOpts needs mem, so it's not a true circular dep. mem.exec_opts = exec_opts # circular dep if opts.show_options: # special case: sh -o mutable_opts.ShowOptions([]) return 0 # Set these BEFORE processing flags, so they can be overridden. if lang == 'oil': mutable_opts.SetShoptOption('oil:all', True) builtin_pure.SetShellOpts(mutable_opts, opts.opt_changes, opts.shopt_changes) aliases = {} # feedback between runtime and parser oil_grammar = meta.LoadOilGrammar(loader) if opts.one_pass_parse and not exec_opts.noexec(): raise args.UsageError('--one-pass-parse requires noexec (-n)') parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar) parse_ctx.Init_OnePassParse(opts.one_pass_parse) # Three ParseContext instances SHARE aliases. comp_arena = alloc.Arena() comp_arena.PushSource(source.Unused('completion')) trail1 = parse_lib.Trail() # one_pass_parse needs to be turned on to complete inside backticks. TODO: # fix the issue where ` gets erased because it's not part of # set_completer_delims(). comp_ctx = parse_lib.ParseContext(comp_arena, parse_opts, aliases, oil_grammar) comp_ctx.Init_Trail(trail1) comp_ctx.Init_OnePassParse(True) hist_arena = alloc.Arena() hist_arena.PushSource(source.Unused('history')) trail2 = parse_lib.Trail() hist_ctx = parse_lib.ParseContext(hist_arena, parse_opts, aliases, oil_grammar) hist_ctx.Init_Trail(trail2) # Deps helps manages dependencies. These dependencies are circular: # - ex and word_ev, arith_ev -- for command sub, arith sub # - arith_ev and word_ev -- for $(( ${a} )) and $x$(( 1 )) # - ex and builtins (which execute code, like eval) # - prompt_ev needs word_ev for $PS1, which needs prompt_ev for @P exec_deps = cmd_exec.Deps() exec_deps.mutable_opts = mutable_opts # TODO: In general, exec_deps are shared between the mutually recursive # evaluators. Some of the four below are only shared between a builtin and # the Executor, so we could put them somewhere else. exec_deps.traps = {} exec_deps.trap_nodes = [] # TODO: Clear on fork() to avoid duplicates exec_deps.job_state = job_state exec_deps.waiter = process.Waiter(job_state, exec_opts) exec_deps.errfmt = errfmt my_pid = posix.getpid() debug_path = '' debug_dir = posix.environ.get('OSH_DEBUG_DIR') if opts.debug_file: # --debug-file takes precedence over OSH_DEBUG_DIR debug_path = opts.debug_file elif debug_dir: debug_path = os_path.join(debug_dir, '%d-osh.log' % my_pid) if debug_path: # This will be created as an empty file if it doesn't exist, or it could be # a pipe. try: debug_f = util.DebugFile(fd_state.Open(debug_path, mode='w')) except OSError as e: ui.Stderr("osh: Couldn't open %r: %s", debug_path, posix.strerror(e.errno)) return 2 else: debug_f = util.NullDebugFile() exec_deps.debug_f = debug_f # Not using datetime for dependency reasons. TODO: maybe show the date at # the beginning of the log, and then only show time afterward? To save # space, and make space for microseconds. (datetime supports microseconds # but time.strftime doesn't). iso_stamp = time.strftime("%Y-%m-%d %H:%M:%S") debug_f.log('%s [%d] OSH started with argv %s', iso_stamp, my_pid, argv) if debug_path: debug_f.log('Writing logs to %r', debug_path) interp = posix.environ.get('OSH_HIJACK_SHEBANG', '') exec_deps.search_path = state.SearchPath(mem) exec_deps.ext_prog = process.ExternalProgram(interp, fd_state, exec_deps.search_path, errfmt, debug_f) splitter = split.SplitContext(mem) # split() builtin # TODO: Accept IFS as a named arg? split('a b', IFS=' ') builtin_funcs.SetGlobalFunc( mem, 'split', lambda s, ifs=None: splitter.SplitForWordEval(s, ifs=ifs)) # glob() builtin # TODO: This is instantiation is duplicated in osh/word_eval.py globber = glob_.Globber(exec_opts) builtin_funcs.SetGlobalFunc(mem, 'glob', lambda s: globber.OilFuncCall(s)) # This could just be OSH_DEBUG_STREAMS='debug crash' ? That might be # stuffing too much into one, since a .json crash dump isn't a stream. crash_dump_dir = posix.environ.get('OSH_CRASH_DUMP_DIR', '') exec_deps.dumper = dev.CrashDumper(crash_dump_dir) if opts.xtrace_to_debug_file: trace_f = debug_f else: trace_f = util.DebugFile(sys.stderr) exec_deps.trace_f = trace_f comp_lookup = completion.Lookup() # Various Global State objects to work around readline interfaces compopt_state = completion.OptionState() comp_ui_state = comp_ui.State() prompt_state = comp_ui.PromptState() dir_stack = state.DirStack() new_var = builtin_assign.NewVar(mem, procs, errfmt) builtins = { # Lookup builtin_i.echo: builtin_pure.Echo(exec_opts), builtin_i.printf: builtin_printf.Printf(mem, parse_ctx, errfmt), builtin_i.pushd: builtin_misc.Pushd(mem, dir_stack, errfmt), builtin_i.popd: builtin_misc.Popd(mem, dir_stack, errfmt), builtin_i.dirs: builtin_misc.Dirs(mem, dir_stack, errfmt), builtin_i.pwd: builtin_misc.Pwd(mem, errfmt), builtin_i.times: builtin_misc.Times(), builtin_i.read: builtin_misc.Read(splitter, mem), builtin_i.help: builtin_misc.Help(loader, errfmt), builtin_i.history: builtin_misc.History(line_input), # Completion (more added below) builtin_i.compopt: builtin_comp.CompOpt(compopt_state, errfmt), builtin_i.compadjust: builtin_comp.CompAdjust(mem), # test / [ differ by need_right_bracket builtin_i.test: builtin_bracket.Test(False, exec_opts, errfmt), builtin_i.bracket: builtin_bracket.Test(True, exec_opts, errfmt), # ShAssignment (which are pure) builtin_i.declare: new_var, builtin_i.typeset: new_var, builtin_i.local: new_var, builtin_i.export_: builtin_assign.Export(mem, errfmt), builtin_i.readonly: builtin_assign.Readonly(mem, errfmt), builtin_i.unset: builtin_assign.Unset(mem, procs, errfmt), builtin_i.shift: builtin_assign.Shift(mem), # Pure builtin_i.set: builtin_pure.Set(mutable_opts, mem), builtin_i.shopt: builtin_pure.Shopt(mutable_opts), builtin_i.alias: builtin_pure.Alias(aliases, errfmt), builtin_i.unalias: builtin_pure.UnAlias(aliases, errfmt), builtin_i.type: builtin_pure.Type(procs, aliases, exec_deps.search_path), builtin_i.hash: builtin_pure.Hash(exec_deps.search_path), builtin_i.getopts: builtin_pure.GetOpts(mem, errfmt), builtin_i.colon: builtin_pure.Boolean(0), # a "special" builtin builtin_i.true_: builtin_pure.Boolean(0), builtin_i.false_: builtin_pure.Boolean(1), # Process builtin_i.wait: builtin_process.Wait(exec_deps.waiter, exec_deps.job_state, mem, errfmt), builtin_i.jobs: builtin_process.Jobs(exec_deps.job_state), builtin_i.fg: builtin_process.Fg(exec_deps.job_state, exec_deps.waiter), builtin_i.bg: builtin_process.Bg(exec_deps.job_state), builtin_i.umask: builtin_process.Umask(), # Oil builtin_i.push: builtin_oil.Push(mem, errfmt), builtin_i.append: builtin_oil.Append(mem, errfmt), builtin_i.write: builtin_oil.Write(mem, errfmt), builtin_i.getline: builtin_oil.Getline(mem, errfmt), builtin_i.repr: builtin_oil.Repr(mem, errfmt), builtin_i.use: builtin_oil.Use(mem, errfmt), builtin_i.opts: builtin_oil.Opts(mem, errfmt), } arith_ev = sh_expr_eval.ArithEvaluator(mem, exec_opts, errfmt) bool_ev = sh_expr_eval.BoolEvaluator(mem, exec_opts, errfmt) expr_ev = expr_eval.OilEvaluator(mem, procs, errfmt) word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, splitter, errfmt) ex = cmd_exec.Executor(mem, fd_state, procs, builtins, exec_opts, parse_ctx, exec_deps) # PromptEvaluator rendering is needed in non-interactive shells for @P. prompt_ev = prompt.Evaluator(lang, parse_ctx, mem) tracer = dev.Tracer(parse_ctx, exec_opts, mutable_opts, mem, word_ev, trace_f) # Wire up circular dependencies. vm.InitCircularDeps(arith_ev, bool_ev, expr_ev, word_ev, ex, prompt_ev, tracer) spec_builder = builtin_comp.SpecBuilder(ex, parse_ctx, word_ev, splitter, comp_lookup) # Add some builtins that depend on the executor! complete_builtin = builtin_comp.Complete(spec_builder, comp_lookup) builtins[builtin_i.complete] = complete_builtin builtins[builtin_i.compgen] = builtin_comp.CompGen(spec_builder) builtins[builtin_i.cd] = builtin_misc.Cd(mem, dir_stack, ex, errfmt) builtins[builtin_i.json] = builtin_oil.Json(mem, ex, errfmt) sig_state = process.SignalState() sig_state.InitShell() builtins[builtin_i.trap] = builtin_process.Trap(sig_state, exec_deps.traps, exec_deps.trap_nodes, ex, errfmt) # History evaluation is a no-op if line_input is None. hist_ev = history.Evaluator(line_input, hist_ctx, debug_f) if opts.c is not None: arena.PushSource(source.CFlag()) line_reader = reader.StringLineReader(opts.c, arena) if opts.i: # -c and -i can be combined mutable_opts.set_interactive() elif opts.i: # force interactive arena.PushSource(source.Stdin(' -i')) line_reader = py_reader.InteractiveLineReader(arena, prompt_ev, hist_ev, line_input, prompt_state) mutable_opts.set_interactive() else: script_name = arg_r.Peek() if script_name is None: if sys.stdin.isatty(): arena.PushSource(source.Interactive()) line_reader = py_reader.InteractiveLineReader( arena, prompt_ev, hist_ev, line_input, prompt_state) mutable_opts.set_interactive() else: arena.PushSource(source.Stdin('')) line_reader = reader.FileLineReader(sys.stdin, arena) else: arena.PushSource(source.MainFile(script_name)) try: f = fd_state.Open(script_name) except OSError as e: ui.Stderr("osh: Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 1 line_reader = reader.FileLineReader(f, arena) # TODO: assert arena.NumSourcePaths() == 1 # TODO: .rc file needs its own arena. c_parser = parse_ctx.MakeOshParser(line_reader) if exec_opts.interactive(): # Calculate ~/.config/oil/oshrc or oilrc # Use ~/.config/oil to avoid cluttering the user's home directory. Some # users may want to ln -s ~/.config/oil/oshrc ~/oshrc or ~/.oshrc. # https://unix.stackexchange.com/questions/24347/why-do-some-applications-use-config-appname-for-their-config-data-while-other home_dir = passwd.GetMyHomeDir() assert home_dir is not None rc_path = opts.rcfile or os_path.join(home_dir, '.config/oil', lang + 'rc') history_filename = os_path.join(home_dir, '.config/oil', 'history_' + lang) if line_input: # NOTE: We're using a different WordEvaluator here. ev = word_eval.CompletionWordEvaluator(mem, exec_opts, splitter, errfmt) ev.arith_ev = arith_ev ev.expr_ev = expr_ev ev.prompt_ev = prompt_ev ev.CheckCircularDeps() root_comp = completion.RootCompleter(ev, mem, comp_lookup, compopt_state, comp_ui_state, comp_ctx, debug_f) term_width = 0 if opts.completion_display == 'nice': try: term_width = libc.get_terminal_width() except IOError: # stdin not a terminal pass if term_width != 0: display = comp_ui.NiceDisplay(term_width, comp_ui_state, prompt_state, debug_f, line_input) else: display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state, debug_f) _InitReadline(line_input, history_filename, root_comp, display, debug_f) _InitDefaultCompletions(ex, complete_builtin, comp_lookup) else: # Without readline module display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state, debug_f) sig_state.InitInteractiveShell(display) # NOTE: Call this AFTER _InitDefaultCompletions. try: SourceStartupFile(rc_path, lang, parse_ctx, ex) except util.UserExit as e: return e.status line_reader.Reset() # After sourcing startup file, render $PS1 prompt_plugin = prompt.UserPlugin(mem, parse_ctx, ex) try: status = main_loop.Interactive(opts, ex, c_parser, display, prompt_plugin, errfmt) if ex.MaybeRunExitTrap(): status = ex.LastStatus() except util.UserExit as e: status = e.status return status nodes_out = [] if exec_opts.noexec() else None if nodes_out is None and opts.parser_mem_dump: raise args.UsageError('--parser-mem-dump can only be used with -n') _tlog('Execute(node)') try: status = main_loop.Batch(ex, c_parser, arena, nodes_out=nodes_out) if ex.MaybeRunExitTrap(): status = ex.LastStatus() except util.UserExit as e: status = e.status # Only print nodes if the whole parse succeeded. if nodes_out is not None and status == 0: if opts.parser_mem_dump: # only valid in -n mode # This might be superstition, but we want to let the value stabilize # after parsing. bash -c 'cat /proc/$$/status' gives different results # with a sleep. time.sleep(0.001) input_path = '/proc/%d/status' % posix.getpid() with open(input_path) as f, open(opts.parser_mem_dump, 'w') as f2: contents = f.read() f2.write(contents) log('Wrote %s to %s (--parser-mem-dump)', input_path, opts.parser_mem_dump) ui.PrintAst(nodes_out, opts) # NOTE: 'exit 1' is ControlFlow and gets here, but subshell/commandsub # don't because they call sys.exit(). if opts.runtime_mem_dump: # This might be superstition, but we want to let the value stabilize # after parsing. bash -c 'cat /proc/$$/status' gives different results # with a sleep. time.sleep(0.001) input_path = '/proc/%d/status' % posix.getpid() with open(input_path) as f, open(opts.runtime_mem_dump, 'w') as f2: contents = f.read() f2.write(contents) log('Wrote %s to %s (--runtime-mem-dump)', input_path, opts.runtime_mem_dump) # NOTE: We haven't closed the file opened with fd_state.Open return status