Exemplo n.º 1
0
Arquivo: shell.py Projeto: dpercy/oil
def Main(lang, arg_r, environ, login_shell, loader, line_input):
    # type: (str, args.Reader, Dict[str, str], bool, pyutil._ResourceLoader, Any) -> int
    """The full shell lifecycle.  Used by bin/osh and bin/oil.

  Args:
    lang: 'osh' or 'oil'
    argv0, arg_r: command line arguments
    environ: environment
    login_shell: Was - on the front?
    loader: to get help, version, grammar, etc.
    line_input: optional GNU readline
  """
    # Differences between osh and oil:
    # - --help?  I guess Oil has a SUPERSET of OSH options.
    # - oshrc vs oilrc
    # - shopt -s oil:all
    # - Change the prompt in the interactive shell?

    # osh-pure:
    # - no oil grammar
    # - no expression evaluator
    # - no interactive shell, or line_input
    # - no process.*
    #   process.{ExternalProgram,Waiter,FdState,JobState,SignalState} -- we want
    #   to evaluate config files without any of these
    # Modules not translated yet: completion, comp_ui, builtin_comp, process
    # - word evaluator
    #   - shouldn't glob?  set -o noglob?  or hard failure?
    #   - ~ shouldn't read from the file system
    #     - I guess it can just be the HOME=HOME?
    # Builtin:
    #   shellvm -c 'echo hi'
    #   shellvm <<< 'echo hi'

    argv0 = arg_r.Peek()
    assert argv0 is not None
    arg_r.Next()

    assert lang in ('osh', 'oil'), lang

    try:
        attrs = flag_spec.ParseMore('main', arg_r)
    except error.Usage as e:
        stderr_line('osh usage error: %s', e.msg)
        return 2
    flag = arg_types.main(attrs.attrs)

    arena = alloc.Arena()
    errfmt = ui.ErrorFormatter(arena)

    help_builtin = builtin_misc.Help(loader, errfmt)
    if flag.help:
        help_builtin.Run(pure.MakeBuiltinArgv(['%s-usage' % lang]))
        return 0
    if flag.version:
        # OSH version is the only binary in Oil right now, so it's all one version.
        pyutil.ShowAppVersion('Oil', loader)
        return 0

    no_str = None  # type: str

    debug_stack = []  # type: List[state.DebugFrame]
    if arg_r.AtEnd():
        dollar0 = argv0
    else:
        dollar0 = arg_r.Peek()  # the script name, or the arg after -c

        # Copy quirky bash behavior.
        frame0 = state.DebugFrame(dollar0, 'main', no_str, state.LINE_ZERO, 0,
                                  0)
        debug_stack.append(frame0)

    # Copy quirky bash behavior.
    frame1 = state.DebugFrame(no_str, no_str, no_str, runtime.NO_SPID, 0, 0)
    debug_stack.append(frame1)

    script_name = arg_r.Peek()  # type: Optional[str]
    arg_r.Next()
    mem = state.Mem(dollar0, arg_r.Rest(), arena, debug_stack)

    version_str = pyutil.GetVersion(loader)
    state.InitMem(mem, environ, version_str)

    builtin_funcs.Init(mem)

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

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

    opt_hook = ShellOptHook(line_input)
    parse_opts, exec_opts, mutable_opts = state.MakeOpts(mem, opt_hook)
    # TODO: only MutableOpts needs mem, so it's not a true circular dep.
    mem.exec_opts = exec_opts  # circular dep

    if attrs.show_options:  # special case: sh -o
        mutable_opts.ShowOptions([])
        return 0

    # Set these BEFORE processing flags, so they can be overridden.
    if lang == 'oil':
        mutable_opts.SetShoptOption('oil:all', True)

    builtin_pure.SetShellOpts(mutable_opts, attrs.opt_changes,
                              attrs.shopt_changes)
    # feedback between runtime and parser
    aliases = {}  # type: Dict[str, str]

    oil_grammar = pyutil.LoadOilGrammar(loader)

    if flag.one_pass_parse and not exec_opts.noexec():
        raise error.Usage('--one-pass-parse requires noexec (-n)')
    parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar)
    parse_ctx.Init_OnePassParse(flag.one_pass_parse)

    # Three ParseContext instances SHARE aliases.
    comp_arena = alloc.Arena()
    comp_arena.PushSource(source.Unused('completion'))
    trail1 = parse_lib.Trail()
    # one_pass_parse needs to be turned on to complete inside backticks.  TODO:
    # fix the issue where ` gets erased because it's not part of
    # set_completer_delims().
    comp_ctx = parse_lib.ParseContext(comp_arena, parse_opts, aliases,
                                      oil_grammar)
    comp_ctx.Init_Trail(trail1)
    comp_ctx.Init_OnePassParse(True)

    hist_arena = alloc.Arena()
    hist_arena.PushSource(source.Unused('history'))
    trail2 = parse_lib.Trail()
    hist_ctx = parse_lib.ParseContext(hist_arena, parse_opts, aliases,
                                      oil_grammar)
    hist_ctx.Init_Trail(trail2)

    # Deps helps manages dependencies.  These dependencies are circular:
    # - cmd_ev and word_ev, arith_ev -- for command sub, arith sub
    # - arith_ev and word_ev -- for $(( ${a} )) and $x$(( 1 ))
    # - cmd_ev and builtins (which execute code, like eval)
    # - prompt_ev needs word_ev for $PS1, which needs prompt_ev for @P
    cmd_deps = cmd_eval.Deps()
    cmd_deps.mutable_opts = mutable_opts

    # TODO: In general, cmd_deps are shared between the mutually recursive
    # evaluators.  Some of the four below are only shared between a builtin and
    # the CommandEvaluator, so we could put them somewhere else.
    cmd_deps.traps = {}
    cmd_deps.trap_nodes = []  # TODO: Clear on fork() to avoid duplicates

    waiter = process.Waiter(job_state, exec_opts)

    my_pid = posix.getpid()

    debug_path = ''
    debug_dir = environ.get('OSH_DEBUG_DIR')
    if flag.debug_file is not None:
        # --debug-file takes precedence over OSH_DEBUG_DIR
        debug_path = flag.debug_file
    elif debug_dir is not None:
        debug_path = os_path.join(debug_dir, '%d-osh.log' % my_pid)

    if len(debug_path):
        # This will be created as an empty file if it doesn't exist, or it could be
        # a pipe.
        try:
            debug_f = util.DebugFile(
                fd_state.OpenForWrite(debug_path))  # type: util._DebugFile
        except OSError as e:
            stderr_line("osh: Couldn't open %r: %s", debug_path,
                        posix.strerror(e.errno))
            return 2
    else:
        debug_f = util.NullDebugFile()

    cmd_deps.debug_f = debug_f

    # Not using datetime for dependency reasons.  TODO: maybe show the date at
    # the beginning of the log, and then only show time afterward?  To save
    # space, and make space for microseconds.  (datetime supports microseconds
    # but time.strftime doesn't).
    if mylib.PYTHON:
        iso_stamp = time.strftime("%Y-%m-%d %H:%M:%S")
        debug_f.log('%s [%d] OSH started with argv %s', iso_stamp, my_pid,
                    arg_r.argv)
    if len(debug_path):
        debug_f.log('Writing logs to %r', debug_path)

    interp = environ.get('OSH_HIJACK_SHEBANG', '')
    search_path = state.SearchPath(mem)
    ext_prog = process.ExternalProgram(interp, fd_state, errfmt, debug_f)

    splitter = split.SplitContext(mem)

    # split() builtin
    # TODO: Accept IFS as a named arg?  split('a b', IFS=' ')
    builtin_funcs.SetGlobalFunc(
        mem,
        'split',
        lambda s, ifs=None: splitter.SplitForWordEval(s, ifs=ifs))

    # glob() builtin
    # TODO: This is instantiation is duplicated in osh/word_eval.py
    globber = glob_.Globber(exec_opts)
    builtin_funcs.SetGlobalFunc(mem, 'glob', lambda s: globber.OilFuncCall(s))

    # This could just be OSH_DEBUG_STREAMS='debug crash' ?  That might be
    # stuffing too much into one, since a .json crash dump isn't a stream.
    crash_dump_dir = environ.get('OSH_CRASH_DUMP_DIR', '')
    cmd_deps.dumper = dev.CrashDumper(crash_dump_dir)

    if flag.xtrace_to_debug_file:
        trace_f = debug_f
    else:
        trace_f = util.DebugFile(mylib.Stderr())

    comp_lookup = completion.Lookup()

    # Various Global State objects to work around readline interfaces
    compopt_state = completion.OptionState()
    comp_ui_state = comp_ui.State()
    prompt_state = comp_ui.PromptState()

    dir_stack = state.DirStack()

    #
    # Initialize builtins that don't depend on evaluators
    #

    builtins = {}  # type: Dict[int, vm._Builtin]

    pure.AddPure(builtins, mem, procs, mutable_opts, aliases, search_path,
                 errfmt)
    pure.AddIO(builtins, mem, dir_stack, exec_opts, splitter, errfmt)
    AddProcess(builtins, mem, ext_prog, fd_state, job_state, waiter,
               search_path, errfmt)
    AddOil(builtins, mem, errfmt)

    builtins[builtin_i.help] = help_builtin

    # Interactive, depend on line_input
    builtins[builtin_i.bind] = builtin_lib.Bind(line_input, errfmt)
    builtins[builtin_i.history] = builtin_lib.History(line_input,
                                                      mylib.Stdout())

    #
    # Assignment builtins
    #

    assign_b = {}  # type: Dict[int, vm._AssignBuiltin]

    new_var = builtin_assign.NewVar(mem, procs, errfmt)
    assign_b[builtin_i.declare] = new_var
    assign_b[builtin_i.typeset] = new_var
    assign_b[builtin_i.local] = new_var

    assign_b[builtin_i.export_] = builtin_assign.Export(mem, errfmt)
    assign_b[builtin_i.readonly] = builtin_assign.Readonly(mem, errfmt)

    #
    # Initialize Evaluators
    #

    arith_ev = sh_expr_eval.ArithEvaluator(mem, exec_opts, parse_ctx, errfmt)
    bool_ev = sh_expr_eval.BoolEvaluator(mem, exec_opts, parse_ctx, errfmt)
    expr_ev = expr_eval.OilEvaluator(mem, procs, splitter, errfmt)
    word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, splitter, errfmt)
    cmd_ev = cmd_eval.CommandEvaluator(mem, exec_opts, errfmt, procs, assign_b,
                                       arena, cmd_deps)

    shell_ex = executor.ShellExecutor(mem, exec_opts, mutable_opts, procs,
                                      builtins, search_path, ext_prog, waiter,
                                      job_state, fd_state, errfmt)

    # PromptEvaluator rendering is needed in non-interactive shells for @P.
    prompt_ev = prompt.Evaluator(lang, parse_ctx, mem)
    tracer = dev.Tracer(parse_ctx, exec_opts, mutable_opts, mem, word_ev,
                        trace_f)

    # Wire up circular dependencies.
    vm.InitCircularDeps(arith_ev, bool_ev, expr_ev, word_ev, cmd_ev, shell_ex,
                        prompt_ev, tracer)

    #
    # Initialize builtins that depend on evaluators
    #

    # note: 'printf -v a[i]' and 'unset a[i]' require same deps
    builtins[builtin_i.printf] = builtin_printf.Printf(mem, exec_opts,
                                                       parse_ctx, arith_ev,
                                                       errfmt)
    builtins[builtin_i.unset] = builtin_assign.Unset(mem, exec_opts, procs,
                                                     parse_ctx, arith_ev,
                                                     errfmt)
    builtins[builtin_i.eval] = builtin_meta.Eval(parse_ctx, exec_opts, cmd_ev)

    source_builtin = builtin_meta.Source(parse_ctx, search_path, cmd_ev,
                                         fd_state, errfmt)
    builtins[builtin_i.source] = source_builtin
    builtins[builtin_i.dot] = source_builtin

    builtins[builtin_i.builtin] = builtin_meta.Builtin(shell_ex, errfmt)
    builtins[builtin_i.command] = builtin_meta.Command(shell_ex, procs,
                                                       aliases, search_path)

    spec_builder = builtin_comp.SpecBuilder(cmd_ev, parse_ctx, word_ev,
                                            splitter, comp_lookup)
    complete_builtin = builtin_comp.Complete(spec_builder, comp_lookup)
    builtins[builtin_i.complete] = complete_builtin
    builtins[builtin_i.compgen] = builtin_comp.CompGen(spec_builder)
    builtins[builtin_i.compopt] = builtin_comp.CompOpt(compopt_state, errfmt)
    builtins[builtin_i.compadjust] = builtin_comp.CompAdjust(mem)

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

    sig_state = pyos.SignalState()
    sig_state.InitShell()

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

    # History evaluation is a no-op if line_input is None.
    hist_ev = history.Evaluator(line_input, hist_ctx, debug_f)

    if flag.c is not None:
        arena.PushSource(source.CFlag())
        line_reader = reader.StringLineReader(flag.c,
                                              arena)  # type: reader._Reader
        if flag.i:  # -c and -i can be combined
            mutable_opts.set_interactive()

    elif flag.i:  # force interactive
        arena.PushSource(source.Stdin(' -i'))
        line_reader = py_reader.InteractiveLineReader(arena, prompt_ev,
                                                      hist_ev, line_input,
                                                      prompt_state)
        mutable_opts.set_interactive()

    else:
        if script_name is None:
            stdin = mylib.Stdin()
            if stdin.isatty():
                arena.PushSource(source.Interactive())
                line_reader = py_reader.InteractiveLineReader(
                    arena, prompt_ev, hist_ev, line_input, prompt_state)
                mutable_opts.set_interactive()
            else:
                arena.PushSource(source.Stdin(''))
                line_reader = reader.FileLineReader(stdin, arena)
        else:
            arena.PushSource(source.MainFile(script_name))
            try:
                f = fd_state.Open(script_name)
            except OSError as e:
                stderr_line("osh: Couldn't open %r: %s", script_name,
                            posix.strerror(e.errno))
                return 1
            line_reader = reader.FileLineReader(f, arena)

    # TODO: assert arena.NumSourcePaths() == 1
    # TODO: .rc file needs its own arena.
    c_parser = parse_ctx.MakeOshParser(line_reader)

    if exec_opts.interactive():
        # bash: 'set -o emacs' is the default only in the interactive shell
        mutable_opts.set_emacs()

        # Calculate ~/.config/oil/oshrc or oilrc
        # Use ~/.config/oil to avoid cluttering the user's home directory.  Some
        # users may want to ln -s ~/.config/oil/oshrc ~/oshrc or ~/.oshrc.

        # https://unix.stackexchange.com/questions/24347/why-do-some-applications-use-config-appname-for-their-config-data-while-other
        home_dir = pyos.GetMyHomeDir()
        assert home_dir is not None
        history_filename = os_path.join(home_dir,
                                        '.config/oil/history_%s' % lang)

        if line_input:
            # NOTE: We're using a different WordEvaluator here.
            ev = word_eval.CompletionWordEvaluator(mem, exec_opts, splitter,
                                                   errfmt)

            ev.arith_ev = arith_ev
            ev.expr_ev = expr_ev
            ev.prompt_ev = prompt_ev
            ev.CheckCircularDeps()

            root_comp = completion.RootCompleter(ev, mem, comp_lookup,
                                                 compopt_state, comp_ui_state,
                                                 comp_ctx, debug_f)

            term_width = 0
            if flag.completion_display == 'nice':
                try:
                    term_width = libc.get_terminal_width()
                except IOError:  # stdin not a terminal
                    pass

            if term_width != 0:
                display = comp_ui.NiceDisplay(
                    term_width, comp_ui_state, prompt_state, debug_f,
                    line_input)  # type: comp_ui._IDisplay
            else:
                display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state,
                                                 debug_f)

            comp_ui.InitReadline(line_input, history_filename, root_comp,
                                 display, debug_f)
            _InitDefaultCompletions(cmd_ev, complete_builtin, comp_lookup)

        else:  # Without readline module
            display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state,
                                             debug_f)

        sig_state.InitInteractiveShell(display)

        rc_path = flag.rcfile or os_path.join(home_dir,
                                              '.config/oil/%src' % lang)
        try:
            # NOTE: Should be called AFTER _InitDefaultCompletions.
            SourceStartupFile(fd_state, rc_path, lang, parse_ctx, cmd_ev)
        except util.UserExit as e:
            return e.status

        line_reader.Reset()  # After sourcing startup file, render $PS1

        prompt_plugin = prompt.UserPlugin(mem, parse_ctx, cmd_ev)
        try:
            status = main_loop.Interactive(flag, cmd_ev, c_parser, display,
                                           prompt_plugin, errfmt)
        except util.UserExit as e:
            status = e.status
        box = [status]
        cmd_ev.MaybeRunExitTrap(box)
        status = box[0]

        return status

    if flag.rcfile:  # bash doesn't have this warning, but it's useful
        stderr_line('osh warning: --rcfile ignored in non-interactive shell')

    if exec_opts.noexec():
        status = 0
        try:
            node = main_loop.ParseWholeFile(c_parser)
        except error.Parse as e:
            ui.PrettyPrintError(e, arena)
            status = 2

        if status == 0:
            if flag.parser_mem_dump is not None:  # only valid in -n mode
                input_path = '/proc/%d/status' % posix.getpid()
                pyutil.CopyFile(input_path, flag.parser_mem_dump)

            ui.PrintAst(node, flag)
    else:
        if flag.parser_mem_dump is not None:
            raise error.Usage('--parser-mem-dump can only be used with -n')

        try:
            status = main_loop.Batch(cmd_ev,
                                     c_parser,
                                     arena,
                                     cmd_flags=cmd_eval.IsMainProgram)
        except util.UserExit as e:
            status = e.status
        box = [status]
        cmd_ev.MaybeRunExitTrap(box)
        status = box[0]

    # NOTE: 'exit 1' is ControlFlow and gets here, but subshell/commandsub
    # don't because they call sys.exit().
    if flag.runtime_mem_dump is not None:
        input_path = '/proc/%d/status' % posix.getpid()
        pyutil.CopyFile(input_path, flag.runtime_mem_dump)

    # NOTE: We haven't closed the file opened with fd_state.Open
    return status
def 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 error.Usage 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
    version_str = pyutil.GetVersion()
    if opts.version:
        # OSH version is the only binary in Oil right now, so it's all one version.
        _ShowVersion(version_str)
        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, 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 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 error.Usage('--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:
    # - 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 = 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()

    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).
    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', '')
    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 = posix.environ.get('OSH_CRASH_DUMP_DIR', '')
    cmd_deps.dumper = dev.CrashDumper(crash_dump_dir)

    if opts.xtrace_to_debug_file:
        trace_f = debug_f
    else:
        trace_f = util.DebugFile(sys.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()

    new_var = builtin_assign.NewVar(mem, procs, errfmt)
    assign_builtins = {
        # 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),
    }

    true_ = builtin_pure.Boolean(0)

    builtins = {
        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),
        builtin_i.cat:
        builtin_misc.Cat(),  # for $(<file)

        # Completion (more added below)
        builtin_i.compopt:
        builtin_comp.CompOpt(compopt_state, errfmt),
        builtin_i.compadjust:
        builtin_comp.CompAdjust(mem),

        # interactive
        builtin_i.bind:
        builtin_lib.Bind(line_input, errfmt),

        # test / [ differ by need_right_bracket
        builtin_i.test:
        builtin_bracket.Test(False, exec_opts, mem, errfmt),
        builtin_i.bracket:
        builtin_bracket.Test(True, exec_opts, mem, 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, search_path),
        builtin_i.hash:
        builtin_pure.Hash(search_path),
        builtin_i.getopts:
        builtin_pure.GetOpts(mem, errfmt),
        builtin_i.colon:
        true_,  # a "special" builtin 
        builtin_i.true_:
        true_,
        builtin_i.false_:
        builtin_pure.Boolean(1),

        # Process
        builtin_i.exec_:
        builtin_process.Exec(mem, ext_prog, fd_state, search_path, errfmt),
        builtin_i.wait:
        builtin_process.Wait(waiter, job_state, mem, errfmt),
        builtin_i.jobs:
        builtin_process.Jobs(job_state),
        builtin_i.fg:
        builtin_process.Fg(job_state, waiter),
        builtin_i.bg:
        builtin_process.Bg(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, parse_ctx, errfmt)
    bool_ev = sh_expr_eval.BoolEvaluator(mem, exec_opts, parse_ctx, errfmt)
    expr_ev = expr_eval.OilEvaluator(mem, procs, errfmt)
    word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, splitter, errfmt)
    cmd_ev = cmd_eval.CommandEvaluator(mem, exec_opts, errfmt, procs,
                                       assign_builtins, 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)

    #
    # Add builtins that depend on various evaluators
    #

    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)

    # 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 = process.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 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():
        # 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 = 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(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)

        # NOTE: Call this AFTER _InitDefaultCompletions.
        try:
            SourceStartupFile(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(opts, cmd_ev, c_parser, display,
                                           prompt_plugin, errfmt)
            if cmd_ev.MaybeRunExitTrap():
                status = cmd_ev.LastStatus()
        except util.UserExit as e:
            status = e.status
        return status

    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 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(node, opts)
    else:
        if opts.parser_mem_dump:
            raise error.Usage('--parser-mem-dump can only be used with -n')

        _tlog('Execute(node)')
        try:
            status = main_loop.Batch(cmd_ev, c_parser, arena, is_main=True)
            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 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