Beispiel #1
0
    def Pop(self):
        # type: () -> None
        frame = self.stack.pop()
        #log('< Pop %s', frame)
        for rf in reversed(frame.saved):
            if rf.saved_fd == NO_FD:
                #log('Close %d', orig)
                try:
                    posix.close(rf.orig_fd)
                except OSError as e:
                    log('Error closing descriptor %d: %s', rf.orig_fd,
                        pyutil.strerror(e))
                    raise
            else:
                try:
                    posix.dup2(rf.saved_fd, rf.orig_fd)
                except OSError as e:
                    log('dup2(%d, %d) error: %s', rf.saved_fd, rf.orig_fd,
                        pyutil.strerror(e))
                    #log('fd state:')
                    #posix.system('ls -l /proc/%s/fd' % posix.getpid())
                    raise
                posix.close(rf.saved_fd)
                #log('dup2 %s %s', saved, orig)

        # Wait for here doc processes to finish.
        for proc, waiter in frame.need_wait:
            unused_status = proc.Wait(waiter)
Beispiel #2
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(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(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(e)))

                # Restore and return error
                if need_restore:
                    rf = self.cur_frame.saved.pop()
                    posix.dup2(rf.saved_fd, rf.orig_fd)
                    posix.close(rf.saved_fd)

                raise  # this redirect failed

            new_fd = fd2

        else:
            raise AssertionError()

        return new_fd
Beispiel #3
0
    def Run(self):
        # type: () -> None

        # NOTE: may NOT return due to exec().
        if not self.inherit_errexit:
            self.cmd_ev.mutable_opts.DisableErrExit()
        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, OSError) as e:
            stderr_line('osh I/O error: %s', pyutil.strerror(e))
            status = 2

        # Raises SystemExit, so we still have time to write a crash dump.
        exit(status)
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(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
Beispiel #5
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(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 #6
0
def main(argv):
  # type: (List[str]) -> int
  loader = pyutil.GetResourceLoader()
  login_shell = False

  environ = {}  # type: Dict[str, str]
  environ['PWD'] = posix.getcwd()
  environ['PATH'] = '/bin'  # stub
  # No getenv()!
  #environ['PATH'] = posix.getenv('PATH')
  #environ = posix.environ 

  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 (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', pyutil.strerror(e))
    return 2  # dash gives status 2
Beispiel #7
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(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(_RedirFrame(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 #8
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 = pyutil.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 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):
        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, parse_ctx, errfmt)

    builtins[builtin_i.help] = help_builtin

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

    assign_b = InitAssignmentBuiltins(mem, procs, 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[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

    AddMeta(builtins, shell_ex, mutable_opts, mem, procs, aliases, search_path,
            errfmt)
    AddBlock(builtins, mem, mutable_opts, 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(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 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:
            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)
        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