def InitExecutor(parse_ctx=None, comp_lookup=None, arena=None, mem=None): if parse_ctx: arena = parse_ctx.arena else: arena or MakeArena('<InitExecutor>') parse_ctx = parse_lib.ParseContext(arena, {}) mem = mem or state.Mem('', [], {}, arena) fd_state = process.FdState() funcs = {} comp_state = completion.State() comp_lookup = comp_lookup or completion.Lookup() readline = None # simulate not having it builtins = { # Lookup builtin_e.HISTORY: builtin.History(readline), builtin_e.COMPOPT: builtin_comp.CompOpt(comp_state), builtin_e.COMPADJUST: builtin_comp.CompAdjust(mem), } # For the tests, we do not use 'readline'. exec_opts = state.ExecOpts(mem, None) debug_f = util.DebugFile(sys.stderr) exec_deps = cmd_exec.Deps() exec_deps.dumper = dev.CrashDumper('') exec_deps.debug_f = debug_f exec_deps.trace_f = debug_f splitter = split.SplitContext(mem) exec_deps.splitter = splitter word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, exec_deps, arena) exec_deps.word_ev = word_ev arith_ev = expr_eval.ArithEvaluator(mem, exec_opts, word_ev, arena) exec_deps.arith_ev = arith_ev word_ev.arith_ev = arith_ev # Circular bool_ev = expr_eval.BoolEvaluator(mem, exec_opts, word_ev, arena) exec_deps.bool_ev = bool_ev tracer = cmd_exec.Tracer(parse_ctx, exec_opts, mem, word_ev, debug_f) exec_deps.tracer = tracer ex = cmd_exec.Executor(mem, fd_state, funcs, builtins, exec_opts, parse_ctx, exec_deps) spec_builder = builtin_comp.SpecBuilder(ex, parse_ctx, word_ev, splitter, comp_lookup) # Add some builtins that depend on the executor! complete_builtin = builtin_comp.Complete(spec_builder, comp_lookup) # used later builtins[builtin_e.COMPLETE] = complete_builtin builtins[builtin_e.COMPGEN] = builtin_comp.CompGen(spec_builder) return ex
def ShellMain(lang, argv0, argv, login_shell): """Used by bin/osh and bin/oil. Args: lang: 'osh' or 'oil' argv0, argv: So we can also invoke bin/osh as 'oil.ovm osh'. Like busybox. login_shell: Was - on the front? """ # Differences between osh and oil: # - --help? I guess Oil has a SUPERSET of OSH options. # - oshrc vs oilrc # - the parser and executor # - Change the prompt in the interactive shell? assert lang in ('osh', 'oil'), lang arg_r = args.Reader(argv) try: opts = OSH_SPEC.Parse(arg_r) except args.UsageError as e: ui.usage('osh usage error: %s', e) return 2 if opts.help: loader = util.GetResourceLoader() builtin.Help(['%s-usage' % lang], loader) return 0 if opts.version: # OSH version is the only binary in Oil right now, so it's all one version. _ShowVersion() return 0 # TODO: This should be in interactive mode only? builtin.RegisterSigIntHandler() if arg_r.AtEnd(): dollar0 = argv0 has_main = False else: dollar0 = arg_r.Peek() # the script name, or the arg after -c has_main = True pool = alloc.Pool() arena = pool.NewArena() # NOTE: has_main is only for ${BASH_SOURCE[@} and family. Could be a # required arg. mem = state.Mem(dollar0, argv[arg_r.i + 1:], posix.environ, arena, has_main=has_main) funcs = {} fd_state = process.FdState() exec_opts = state.ExecOpts(mem, readline) builtin.SetExecOpts(exec_opts, opts.opt_changes) aliases = {} # feedback between runtime and parser parse_ctx = parse_lib.ParseContext(arena, aliases) # For main_loop # Three ParseContext instances SHARE aliases. TODO: Complete aliases. comp_arena = pool.NewArena() comp_arena.PushSource('<completion>') trail1 = parse_lib.Trail() comp_ctx = parse_lib.ParseContext(comp_arena, aliases, trail=trail1) hist_arena = pool.NewArena() hist_arena.PushSource('<history>') trail2 = parse_lib.Trail() hist_ctx = parse_lib.ParseContext(hist_arena, aliases, trail=trail2) # Deps helps manages dependencies. These dependencies are circular: # - ex and word_ev, arith_ev -- for command sub, arith sub # - arith_ev and word_ev -- for $(( ${a} )) and $x$(( 1 )) # - ex and builtins (which execute code, like eval) # - prompt_ev needs word_ev for $PS1, which needs prompt_ev for @P exec_deps = cmd_exec.Deps() if opts.debug_file: debug_f = util.DebugFile(fd_state.Open(opts.debug_file, mode='w')) else: debug_f = util.NullDebugFile() exec_deps.debug_f = debug_f debug_f.log('Debug file is %s', opts.debug_file) splitter = split.SplitContext(mem) exec_deps.splitter = splitter # Controlled by env variable, flag, or hook? exec_deps.dumper = dev.CrashDumper( posix.environ.get('OSH_CRASH_DUMP_DIR', '')) if opts.xtrace_to_debug_file: trace_f = debug_f else: trace_f = util.DebugFile(sys.stderr) exec_deps.trace_f = trace_f # TODO: Separate comp_state and comp_lookup. comp_state = completion.State() comp_lookup = completion.Lookup() builtins = { # Lookup builtin_e.HISTORY: builtin.History(readline), builtin_e.COMPOPT: builtin_comp.CompOpt(comp_state), builtin_e.COMPADJUST: builtin_comp.CompAdjust(mem), } ex = cmd_exec.Executor(mem, fd_state, funcs, builtins, exec_opts, parse_ctx, exec_deps) exec_deps.ex = ex word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, exec_deps, arena) exec_deps.word_ev = word_ev arith_ev = expr_eval.ArithEvaluator(mem, exec_opts, word_ev, arena) exec_deps.arith_ev = arith_ev word_ev.arith_ev = arith_ev # Another circular dependency bool_ev = expr_eval.BoolEvaluator(mem, exec_opts, word_ev, arena) exec_deps.bool_ev = bool_ev tracer = cmd_exec.Tracer(parse_ctx, exec_opts, mem, word_ev, trace_f) exec_deps.tracer = tracer # HACK for circular deps ex.word_ev = word_ev ex.arith_ev = arith_ev ex.bool_ev = bool_ev ex.tracer = tracer spec_builder = builtin_comp.SpecBuilder(ex, parse_ctx, word_ev, splitter) # Add some builtins that depend on the executor! complete_builtin = builtin_comp.Complete(spec_builder, comp_lookup) # used later builtins[builtin_e.COMPLETE] = complete_builtin builtins[builtin_e.COMPGEN] = builtin_comp.CompGen(spec_builder) if lang == 'oil': # The Oil executor wraps an OSH executor? It needs to be able to source # it. ex = oil_cmd_exec.OilExecutor(ex) # PromptEvaluator rendering is needed in non-interactive shells for @P. prompt_ev = ui.PromptEvaluator(lang, arena, parse_ctx, ex, mem) exec_deps.prompt_ev = prompt_ev word_ev.prompt_ev = prompt_ev # HACK for circular deps # History evaluation is a no-op if readline is None. hist_ev = reader.HistoryEvaluator(readline, hist_ctx, debug_f) # 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 = mem.GetVar('HOME') assert home_dir.tag == value_e.Str, home_dir rc_path = opts.rcfile or os_path.join(home_dir.s, '.config/oil', lang + 'rc') history_filename = os_path.join(home_dir.s, '.config/oil', 'history_' + lang) if opts.c is not None: arena.PushSource('<command string>') line_reader = reader.StringLineReader(opts.c, arena) if opts.i: # -c and -i can be combined exec_opts.interactive = True elif opts.i: # force interactive arena.PushSource('<stdin -i>') # interactive shell only line_reader = reader.InteractiveLineReader(arena, prompt_ev, hist_ev) exec_opts.interactive = True else: try: script_name = arg_r.Peek() except IndexError: if sys.stdin.isatty(): arena.PushSource('<interactive>') # interactive shell only line_reader = reader.InteractiveLineReader( arena, prompt_ev, hist_ev) exec_opts.interactive = True else: arena.PushSource('<stdin>') line_reader = reader.FileLineReader(sys.stdin, arena) else: arena.PushSource(script_name) try: f = fd_state.Open(script_name) except OSError as e: util.error("Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 1 line_reader = reader.FileLineReader(f, arena) # TODO: assert arena.NumSourcePaths() == 1 # TODO: .rc file needs its own arena. if lang == 'osh': c_parser = parse_ctx.MakeOshParser(line_reader) else: c_parser = parse_ctx.MakeOilParser(line_reader) if exec_opts.interactive: # NOTE: We're using a different evaluator here. The completion system can # also run functions... it gets the Executor through Executor._Complete. if readline: ev = word_eval.CompletionWordEvaluator(mem, exec_opts, exec_deps, arena) progress_f = ui.StatusLine() root_comp = completion.RootCompleter(ev, mem, comp_lookup, comp_state, comp_ctx, progress_f, debug_f) _InitReadline(readline, history_filename, root_comp, debug_f) _InitDefaultCompletions(ex, complete_builtin, comp_lookup) # NOTE: Call this AFTER _InitDefaultCompletions. SourceStartupFile(rc_path, lang, parse_ctx, ex) return main_loop.Interactive(opts, ex, c_parser, arena) # TODO: Remove this after removing it from benchmarks/osh-runtime. It's no # longer relevant with main_loop. if opts.parser_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.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) nodes_out = [] if exec_opts.noexec else None _tlog('Execute(node)') status = main_loop.Batch(ex, c_parser, arena, nodes_out=nodes_out) # Only print nodes if the whole parse succeeded. if nodes_out is not None and status == 0: ui.PrintAst(nodes_out, opts) # NOTE: 'exit 1' is ControlFlow and gets here, but subshell/commandsub # don't because they call sys.exit(). if opts.runtime_mem_dump: # This might be superstition, but we want to let the value stabilize # after parsing. bash -c 'cat /proc/$$/status' gives different results # with a sleep. time.sleep(0.001) input_path = '/proc/%d/status' % posix.getpid() with open(input_path) as f, open(opts.runtime_mem_dump, 'w') as f2: contents = f.read() f2.write(contents) log('Wrote %s to %s (--runtime-mem-dump)', input_path, opts.runtime_mem_dump) # NOTE: This doesn't cause any spec tests to fail, but it could. if posix.environ.get('ASDL_TYPE_CHECK'): log('NOTE: Performed %d ASDL_TYPE_CHECKs.', runtime.NUM_TYPE_CHECKS) # NOTE: We haven't closed the file opened with fd_state.Open return status
def ShellMain(lang, argv0, argv, login_shell): """Used by bin/osh and bin/oil. Args: lang: 'osh' or 'oil' argv0, argv: So we can also invoke bin/osh as 'oil.ovm osh'. Like busybox. login_shell: Was - on the front? """ # Differences between osh and oil: # - --help? I guess Oil has a SUPERSET of OSH options. # - oshrc vs oilrc # - the parser and executor # - Change the prompt in the interactive shell? assert lang in ('osh', 'oil'), lang arg_r = args.Reader(argv) try: opts = OSH_SPEC.Parse(arg_r) except args.UsageError as e: ui.usage('osh usage error: %s', e) return 2 # NOTE: This has a side effect of deleting _OVM_* from the environment! # TODO: Thread this throughout the program, and get rid of the global # variable in core/util.py. Rename to InitResourceLaoder(). It's now only # used for the 'help' builtin and --version. loader = pyutil.GetResourceLoader() if opts.help: builtin.Help(['%s-usage' % lang], loader) return 0 if opts.version: # OSH version is the only binary in Oil right now, so it's all one version. _ShowVersion() return 0 if arg_r.AtEnd(): dollar0 = argv0 has_main = False else: dollar0 = arg_r.Peek() # the script name, or the arg after -c has_main = True pool = alloc.Pool() arena = pool.NewArena() # NOTE: has_main is only for ${BASH_SOURCE[@} and family. Could be a # required arg. mem = state.Mem(dollar0, argv[arg_r.i + 1:], posix.environ, arena, has_main=has_main) funcs = {} fd_state = process.FdState() exec_opts = state.ExecOpts(mem, line_input) builtin.SetExecOpts(exec_opts, opts.opt_changes) aliases = {} # feedback between runtime and parser if opts.one_pass_parse and not exec_opts.noexec: raise args.UsageError('--one-pass-parse requires noexec (-n)') parse_ctx = parse_lib.ParseContext(arena, aliases, one_pass_parse=opts.one_pass_parse) # Three ParseContext instances SHARE aliases. comp_arena = pool.NewArena() comp_arena.PushSource('<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, aliases, trail=trail1, one_pass_parse=True) hist_arena = pool.NewArena() hist_arena.PushSource('<history>') trail2 = parse_lib.Trail() hist_ctx = parse_lib.ParseContext(hist_arena, aliases, trail=trail2) # Deps helps manages dependencies. These dependencies are circular: # - ex and word_ev, arith_ev -- for command sub, arith sub # - arith_ev and word_ev -- for $(( ${a} )) and $x$(( 1 )) # - ex and builtins (which execute code, like eval) # - prompt_ev needs word_ev for $PS1, which needs prompt_ev for @P exec_deps = cmd_exec.Deps() 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: util.error("Couldn't open %r: %s", debug_path, posix.strerror(e.errno)) return 2 else: debug_f = util.NullDebugFile() exec_deps.debug_f = debug_f # Not using datetime for dependency reasons. TODO: maybe show the date at # the beginning of the log, and then only show time afterward? To save # space, and make space for microseconds. (datetime supports microseconds # but time.strftime doesn't). iso_stamp = time.strftime("%Y-%m-%d %H:%M:%S") debug_f.log('%s [%d] OSH started with argv %s', iso_stamp, my_pid, argv) if debug_path: debug_f.log('Writing logs to %r', debug_path) interp = posix.environ.get('OSH_HIJACK_SHEBANG', '') exec_deps.ext_prog = process.ExternalProgram(interp, fd_state, debug_f) splitter = split.SplitContext(mem) exec_deps.splitter = splitter # This could just be OSH_DEBUG_STREAMS='debug crash' ? That might be # stuffing too much into one, since a .json crash dump isn't a stream. crash_dump_dir = posix.environ.get('OSH_CRASH_DUMP_DIR', '') exec_deps.dumper = dev.CrashDumper(crash_dump_dir) if opts.xtrace_to_debug_file: trace_f = debug_f else: trace_f = util.DebugFile(sys.stderr) exec_deps.trace_f = trace_f comp_lookup = completion.Lookup() # # Various Global State objects to work around readline interfaces # compopt_state = completion.OptionState() comp_ui_state = comp_ui.State() prompt_state = comp_ui.PromptState() builtins = { # Lookup builtin_e.HISTORY: builtin.History(line_input), builtin_e.COMPOPT: builtin_comp.CompOpt(compopt_state), builtin_e.COMPADJUST: builtin_comp.CompAdjust(mem), } ex = cmd_exec.Executor(mem, fd_state, funcs, builtins, exec_opts, parse_ctx, exec_deps) exec_deps.ex = ex word_ev = word_eval.NormalWordEvaluator(mem, exec_opts, exec_deps, arena) exec_deps.word_ev = word_ev arith_ev = expr_eval.ArithEvaluator(mem, exec_opts, word_ev, arena) exec_deps.arith_ev = arith_ev word_ev.arith_ev = arith_ev # Another circular dependency bool_ev = expr_eval.BoolEvaluator(mem, exec_opts, word_ev, arena) exec_deps.bool_ev = bool_ev tracer = cmd_exec.Tracer(parse_ctx, exec_opts, mem, word_ev, trace_f) exec_deps.tracer = tracer # HACK for circular deps ex.word_ev = word_ev ex.arith_ev = arith_ev ex.bool_ev = bool_ev ex.tracer = tracer spec_builder = builtin_comp.SpecBuilder(ex, parse_ctx, word_ev, splitter, comp_lookup) # Add some builtins that depend on the executor! complete_builtin = builtin_comp.Complete(spec_builder, comp_lookup) builtins[builtin_e.COMPLETE] = complete_builtin builtins[builtin_e.COMPGEN] = builtin_comp.CompGen(spec_builder) if lang == 'oil': # The Oil executor wraps an OSH executor? It needs to be able to source # it. ex = oil_cmd_exec.OilExecutor(ex) # PromptEvaluator rendering is needed in non-interactive shells for @P. prompt_ev = prompt.Evaluator(lang, arena, parse_ctx, ex, mem) exec_deps.prompt_ev = prompt_ev word_ev.prompt_ev = prompt_ev # HACK for circular deps # History evaluation is a no-op if line_input is None. hist_ev = history.Evaluator(line_input, hist_ctx, debug_f) # Calculate ~/.config/oil/oshrc or oilrc # Use ~/.config/oil to avoid cluttering the user's home directory. Some # users may want to ln -s ~/.config/oil/oshrc ~/oshrc or ~/.oshrc. # https://unix.stackexchange.com/questions/24347/why-do-some-applications-use-config-appname-for-their-config-data-while-other home_dir = process.GetHomeDir() assert home_dir is not None rc_path = opts.rcfile or os_path.join(home_dir, '.config/oil', lang + 'rc') history_filename = os_path.join(home_dir, '.config/oil', 'history_' + lang) if opts.c is not None: arena.PushSource('<command string>') line_reader = reader.StringLineReader(opts.c, arena) if opts.i: # -c and -i can be combined exec_opts.interactive = True elif opts.i: # force interactive arena.PushSource('<stdin -i>') # interactive shell only line_reader = reader.InteractiveLineReader(arena, prompt_ev, hist_ev, line_input, prompt_state) exec_opts.interactive = True else: try: script_name = arg_r.Peek() except IndexError: if sys.stdin.isatty(): arena.PushSource('<interactive>') # interactive shell only line_reader = reader.InteractiveLineReader( arena, prompt_ev, hist_ev, line_input, prompt_state) exec_opts.interactive = True else: arena.PushSource('<stdin>') line_reader = reader.FileLineReader(sys.stdin, arena) else: arena.PushSource(script_name) try: f = fd_state.Open(script_name) except OSError as e: util.error("Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 1 line_reader = reader.FileLineReader(f, arena) # TODO: assert arena.NumSourcePaths() == 1 # TODO: .rc file needs its own arena. if lang == 'osh': c_parser = parse_ctx.MakeOshParser(line_reader) else: c_parser = parse_ctx.MakeOilParser(line_reader) # NOTE: SIGINT is temporarily enabled during readline() by # frontend/reader.py. # It's treated differently than SIGQUIT and SIGTSTP because Python handles it # with KeyboardInterrupt. We don't want KeyboardInterrupt at arbitrary # points in a non-interactive shell. (e.g. osh -c 'sleep 5' then Ctrl-C) signal.signal(signal.SIGINT, signal.SIG_IGN) if exec_opts.interactive: if line_input: # NOTE: We're using a different WordEvaluator here. ev = word_eval.CompletionWordEvaluator(mem, exec_opts, exec_deps, arena) root_comp = completion.RootCompleter(ev, mem, comp_lookup, compopt_state, comp_ui_state, comp_ctx, debug_f) term_width = 0 if opts.completion_display == 'nice': try: term_width = libc.get_terminal_width() except IOError: # stdin not a terminal pass if term_width != 0: display = comp_ui.NiceDisplay(term_width, comp_ui_state, prompt_state, debug_f, line_input) else: display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state, debug_f) _InitReadline(line_input, history_filename, root_comp, display, debug_f) _InitDefaultCompletions(ex, complete_builtin, comp_lookup) else: # Without readline module display = comp_ui.MinimalDisplay(comp_ui_state, prompt_state, debug_f) # The shell itself should ignore Ctrl-\. signal.signal(signal.SIGQUIT, signal.SIG_IGN) # This prevents Ctrl-Z from suspending OSH in interactive mode. But we're # not getting notification via wait() that the child stopped? signal.signal(signal.SIGTSTP, signal.SIG_IGN) # Register a callback to receive terminal width changes. signal.signal(signal.SIGWINCH, lambda x, y: display.OnWindowChange()) # NOTE: Call this AFTER _InitDefaultCompletions. SourceStartupFile(rc_path, lang, parse_ctx, ex) line_reader.Reset() # After sourcing startup file, render $PS1 return main_loop.Interactive(opts, ex, c_parser, display, arena) # TODO: This doesn't do anything interesting. # - Remove the column from osh-runtime, since it doesn't work with main_loop. # - http://www.oilshell.org/release/0.6.pre6/benchmarks.wwz/osh-runtime/ # - Move it to right before ui.PrintAst(), so we can use it in -n mode. # This benchmark has been broken since 0.6.pre4. # http://www.oilshell.org/release/0.6.pre3/benchmarks.wwz/osh-parser/ if opts.parser_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.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) nodes_out = [] if exec_opts.noexec else None _tlog('Execute(node)') status = main_loop.Batch(ex, c_parser, arena, nodes_out=nodes_out) # Only print nodes if the whole parse succeeded. if nodes_out is not None and status == 0: ui.PrintAst(nodes_out, opts) # NOTE: 'exit 1' is ControlFlow and gets here, but subshell/commandsub # don't because they call sys.exit(). if opts.runtime_mem_dump: # This might be superstition, but we want to let the value stabilize # after parsing. bash -c 'cat /proc/$$/status' gives different results # with a sleep. time.sleep(0.001) input_path = '/proc/%d/status' % posix.getpid() with open(input_path) as f, open(opts.runtime_mem_dump, 'w') as f2: contents = f.read() f2.write(contents) log('Wrote %s to %s (--runtime-mem-dump)', input_path, opts.runtime_mem_dump) # NOTE: We haven't closed the file opened with fd_state.Open return status