Beispiel #1
0
    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, pyutil.strerror_IO(e)))
                    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, pyutil.strerror_IO(e)))
                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, pyutil.strerror_OS(e)))

                # 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
Beispiel #2
0
    def Run(self):
        # type: () -> None

        # NOTE: may NOT return due to exec().
        if not self.inherit_errexit:
            self.cmd_ev.mutable_opts.errexit.Disable()

        try:
            # optimize to eliminate redundant subshells like ( echo hi ) | wc -l etc.
            self.cmd_ev.ExecuteAndCatch(self.node, cmd_flags=cmd_eval.Optimize)
            status = self.cmd_ev.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 as e:
            stderr_line('osh I/O error: %s', pyutil.strerror_IO(e))
            status = 2
        except OSError as e:  # mycpp: duplicated for translation
            stderr_line('osh I/O error: %s', pyutil.strerror_OS(e))
            status = 2

        # Raises SystemExit, so we still have time to write a crash dump.
        exit(status)
Beispiel #3
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])
        try:
            posix.chdir(dest_dir)
        except OSError as e:
            self.errfmt.Print_("pushd: %r: %s" %
                               (dest_dir, pyutil.strerror_OS(e)),
                               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
Beispiel #4
0
    def _Exec(self, argv0_path, argv, argv0_spid, environ, should_retry):
        # type: (str, List[str], int, Dict[str, str], bool) -> None
        if len(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, pyutil.strerror_OS(e)),
                               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

            exit(status)  # raises SystemExit
  def Run(self, cmd_val):
    # type: (cmd_value__Argv) -> int
    argv = cmd_val.argv
    call_spid = cmd_val.arg_spids[0]

    try:
      path = argv[1]
    except IndexError:
      raise error.Usage('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, pyutil.strerror_OS(e),
                        span_id=cmd_val.arg_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)

      src = source.SourcedFile(path, call_spid)
      self.arena.PushSource(src)
      try:
        status = main_loop.Batch(self.cmd_ev, c_parser, self.arena)
      finally:
        self.arena.PopSource()
        self.mem.PopSource(source_argv)

      return status

    except _ControlFlow as e:
      if e.IsReturn():
        return e.StatusCode()
      else:
        raise
    finally:
      f.close()
Beispiel #6
0
  def Run(self, cmd_val):
    # type: (cmd_value__Argv) -> int
    call_spid = cmd_val.arg_spids[0]
    _, arg_r = flag_spec.ParseCmdVal('source', cmd_val)

    path = arg_r.Peek()
    if path is None:
      e_usage('missing required argument')
    arg_r.Next()

    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, pyutil.strerror_OS(e)),
                        span_id=cmd_val.arg_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 = arg_r.Rest()
      self.mem.PushSource(path, source_argv)

      src = source.SourcedFile(path, call_spid)
      try:
        with alloc.ctx_Location(self.arena, src):
          status = main_loop.Batch(self.cmd_ev, c_parser, self.arena,
                                   cmd_flags=cmd_eval.IsEvalSource)
      finally:
        self.mem.PopSource(source_argv)

      return status

    except _ControlFlow as e:
      if e.IsReturn():
        return e.StatusCode()
      else:
        raise
    finally:
      f.close()
Beispiel #7
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

    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, pyutil.strerror_OS(e)))
        return False

    state.SetGlobalString(mem, 'PWD', dest_dir)
    mem.SetPwd(dest_dir)
    return True
Beispiel #8
0
def main(argv):
    # type: (List[str]) -> int
    loader = pyutil.GetResourceLoader()
    login_shell = False

    environ = {}  # type: Dict[str, str]
    environ['PWD'] = posix.getcwd()

    arg_r = args.Reader(argv, spids=[runtime.NO_SPID] * len(argv))

    try:
        status = pure.Main('osh', arg_r, environ, login_shell, loader, None)
        return status
    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.
        # f() { f; }; f will cause this
        msg = e.message  # type: str
        stderr_line('osh fatal error: %s', msg)
        return 1

    # Note: This doesn't happen in C++.
    except KeyboardInterrupt:
        print('')
        return 130  # 128 + 2

    except OSError as e:
        if 0:
            import traceback
            traceback.print_exc()

        # test this with prlimit --nproc=1 --pid=$$
        stderr_line('osh I/O error: %s', pyutil.strerror_OS(e))
        return 2  # dash gives status 2

    except IOError as e:  # duplicate of above because CPython is inconsistent
        stderr_line('osh I/O error: %s', pyutil.strerror_IO(e))
        return 2
Beispiel #9
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, pyutil.strerror_OS(e)),
                                       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)
Beispiel #10
0
    def Run(self, cmd_val):
        # type: (cmd_value__Argv) -> int
        attrs, arg_r = flag_spec.ParseCmdVal('cd', cmd_val)
        arg = arg_types.cd(attrs.attrs)

        dest_dir, arg_spid = arg_r.Peek2()
        if dest_dir is None:
            val = self.mem.GetVar('HOME')
            try:
                dest_dir = state.GetString(self.mem, 'HOME')
            except error.Runtime as e:
                self.errfmt.Print_(e.UserErrorString())
                return 1

        if dest_dir == '-':
            try:
                dest_dir = state.GetString(self.mem, 'OLDPWD')
                print(dest_dir)  # Shells print the directory
            except error.Runtime as e:
                self.errfmt.Print_(e.UserErrorString())
                return 1

        try:
            pwd = state.GetString(self.mem, 'PWD')
        except error.Runtime as e:
            self.errfmt.Print_(e.UserErrorString())
            return 1

        # 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, 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, pyutil.strerror_OS(e)),
                               span_id=arg_spid)
            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.cmd_ev.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)
            self.dir_stack.Reset()  # for pushd/popd/dirs

        return 0
Beispiel #11
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(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)

  procs = {}  # type: Dict[str, command__ShFunction]

  #job_state = process.JobState()
  #fd_state = process.FdState(errfmt, job_state, mem)

  opt_hook = state.OptHook()
  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 = None  # type: grammar.Grammar
  #oil_grammar = meta.LoadOilGrammar(loader)

  if flag.one_pass_parse and not exec_opts.noexec():
    e_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:  # --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):
    raise NotImplementedError()
  else:
    debug_f = util.NullDebugFile()  # type: util._DebugFile

  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)

  # 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]

  AddPure(builtins, mem, procs, mutable_opts, aliases, search_path, errfmt)
  AddIO(builtins, mem, dir_stack, exec_opts, splitter, errfmt)

  builtins[builtin_i.help] = help_builtin

  #
  # 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 = None  # type: expr_eval.OilEvaluator
  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)
  shell_ex = NullExecutor(exec_opts, mutable_opts, procs, builtins)

  # 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[0] will give it same deps as 'unset'
  builtins[builtin_i.printf] = builtin_printf.Printf(mem, parse_ctx, 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)

  # These builtins take blocks, and thus need cmd_ev.
  builtins[builtin_i.cd] = builtin_misc.Cd(mem, dir_stack, cmd_ev, errfmt)

  #sig_state = process.SignalState()
  #sig_state.InitShell()

  #builtins[builtin_i.trap] = builtin_process.Trap(sig_state, cmd_deps.traps,
  #                                                cmd_deps.trap_nodes,
  #                                                parse_ctx, errfmt)

  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
    raise NotImplementedError()

  else:
    if script_name is None:
      stdin = mylib.Stdin()
      arena.PushSource(source.Stdin(''))
      line_reader = reader.FileLineReader(stdin, arena)
    else:
      arena.PushSource(source.MainFile(script_name))
      try:
        #f = fd_state.Open(script_name)
        f = mylib.open(script_name)
      except OSError as e:
        stderr_line("osh: Couldn't open %r: %s", script_name,
                    pyutil.strerror_OS(e))
        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():
    raise NotImplementedError()

  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:  # 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:
      e_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)
      if cmd_ev.MaybeRunExitTrap():
        status = cmd_ev.LastStatus()
    except util.UserExit as e:
      status = e.status

  # NOTE: 'exit 1' is ControlFlow and gets here, but subshell/commandsub
  # don't because they call sys.exit().
  if flag.runtime_mem_dump:
    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