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
Exemple #2
0
  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)
Exemple #3
0
 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
Exemple #5
0
    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
Exemple #6
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)
Exemple #7
0
    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
Exemple #8
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
Exemple #9
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
Exemple #10
0
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)
Exemple #11
0
  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
Exemple #12
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
Exemple #13
0
 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
Exemple #14
0
  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
Exemple #15
0
  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
Exemple #16
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()
Exemple #17
0
  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
Exemple #18
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)
Exemple #19
0
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
Exemple #20
0
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))
Exemple #21
0
    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)
Exemple #23
0
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
Exemple #24
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
Exemple #25
0
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
Exemple #26
0
def strerror_OS(e):
    # type: (OSError) -> str
    return posix.strerror(e.errno)
Exemple #27
0
def strerror_IO(e):
    # type: (IOError) -> str
    return posix.strerror(e.errno)
Exemple #28
0
  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
Exemple #29
0
def strerror(e):
  # type: (Union[IOError, OSError]) -> str
  return posix.strerror(e.errno)
Exemple #30
0
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