def _GetOpts(spec, argv, optind, errfmt): # type: (Dict[str, bool], List[str], int, ErrorFormatter) -> Tuple[int, str, str, int] optarg = '' # not set by default try: current = argv[optind - 1] # 1-based indexing except IndexError: return 1, '?', optarg, optind if not current.startswith('-'): # The next arg doesn't look like a flag. return 1, '?', optarg, optind # It looks like an argument. Stop iteration by returning 1. if current not in spec: # Invalid flag optind += 1 return 0, '?', optarg, optind optind += 1 opt_char = current[-1] needs_arg = spec[current] if needs_arg: try: optarg = argv[optind - 1] # 1-based indexing except IndexError: # TODO: Add location info errfmt.Print_('getopts: option %r requires an argument.' % current) tmp = [qsn.maybe_shell_encode(a) for a in argv] stderr_line('(getopts argv: %s)', ' '.join(tmp)) # Hm doesn't cause status 1? return 0, '?', optarg, optind optind += 1 return 0, opt_char, optarg, optind
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int argv = cmd_val.argv[1:] if len(argv) == 0: # umask() has a dumb API: you can't get it without modifying it first! # NOTE: dash disables interrupts around the two umask() calls, but that # shouldn't be a concern for us. Signal handlers won't call umask(). mask = posix.umask(0) posix.umask(mask) # print('0%03o' % mask) # octal format return 0 if len(argv) == 1: a = argv[0] try: new_mask = int(a, 8) except ValueError: # NOTE: This happens if we have '8' or '9' in the input too. stderr_line("osh warning: umask with symbolic input isn't implemented") return 1 else: posix.umask(new_mask) return 0 raise error.Usage('umask: unexpected arguments')
def main(argv): # type: (List[str]) -> int try: return AppBundleMain(argv) except error.Usage as e: #builtin.Help(['oil-usage'], util.GetResourceLoader()) log('oil: %s', e.msg) return 2 except RuntimeError as e: if 0: import traceback traceback.print_exc() # NOTE: The Python interpreter can cause this, e.g. on stack overflow. log('FATAL: %r', e) return 1 except KeyboardInterrupt: print() return 130 # 128 + 2 except (IOError, OSError) as e: if 0: import traceback traceback.print_exc() # test this with prlimit --nproc=1 --pid=$$ stderr_line('osh I/O error: %s', posix.strerror(e.errno)) return 2 # dash gives status 2 finally: _tlog('Exiting main()') if _trace_path: _tracer.Stop(_trace_path)
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int attrs, arg_r = flag_spec.ParseCmdVal('hash', cmd_val) arg = arg_types.hash(attrs.attrs) rest = arg_r.Rest() if arg.r: if len(rest): e_usage('got extra arguments after -r') self.search_path.ClearCache() return 0 status = 0 if len(rest): for cmd in rest: # enter in cache full_path = self.search_path.CachedLookup(cmd) if full_path is None: stderr_line('hash: %r not found', cmd) status = 1 else: # print cache commands = self.search_path.CachedCommands() commands.sort() for cmd in commands: print(cmd) return status
def __call__(self, unused_word, state): # type: (str, int) -> Optional[str] """Return a single match.""" try: return self._GetNextCompletion(state) except util.UserExit as e: # TODO: Could use errfmt to show this stderr_line("osh: Ignoring 'exit' in completion plugin") except error.FatalRuntime as e: # From -W. TODO: -F is swallowed now. # We should have a nicer UI for displaying errors. Maybe they shouldn't # print it to stderr. That messes up the completion display. We could # print what WOULD have been COMPREPLY here. stderr_line('osh: Runtime error while completing: %s', e) self.debug_f.log('Runtime error while completing: %s', e) except (IOError, OSError) as e: # test this with prlimit --nproc=1 --pid=$$ stderr_line('osh: I/O error in completion: %s', posix.strerror(e.errno)) except KeyboardInterrupt: # It appears GNU readline handles Ctrl-C to cancel a long completion. # So this may never happen? stderr_line('Ctrl-C in completion') except Exception as e: # ESSENTIAL because readline swallows exceptions. if 1: import traceback traceback.print_exc() stderr_line('osh: Unhandled exception while completing: %s', e) self.debug_f.log('Unhandled exception while completing: %s', e) except SystemExit as e: # Because readline ignores SystemExit! posix._exit(e.code)
def PrintAst(node, flag): # type: (command_t, arg_types.main) -> None if flag.ast_format == 'none': stderr_line('AST not printed.') if 0: from _devbuild.gen.id_kind_asdl import Id_str from frontend.lexer import ID_HIST for id_, count in ID_HIST.most_common(10): print('%8d %s' % (count, Id_str(id_))) print() total = sum(ID_HIST.values()) print('%8d total tokens returned' % total) else: # text output f = mylib.Stdout() afmt = flag.ast_format # note: mycpp rewrite to avoid 'in' if afmt in ('text', 'abbrev-text'): ast_f = fmt.DetectConsoleOutput(f) elif afmt in ('html', 'abbrev-html'): ast_f = fmt.HtmlOutput(f) else: raise AssertionError() if 'abbrev-' in afmt: tree = node.AbbreviatedTree() else: tree = node.PrettyTree() ast_f.FileHeader() fmt.PrintTree(tree, ast_f) ast_f.FileFooter() ast_f.write('\n')
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)
def EvalCStringToken(tok): # type: (Token) -> Optional[str] """ This function is shared between echo -e and $''. $'' could use it at compile time, much like brace expansion in braces.py. """ id_ = tok.id value = tok.val if id_ == Id.Char_Literals: return value elif id_ == Id.Char_BadBackslash: if 1: # Either \A or trailing \ (A is not a valid backslash escape) # TODO: add location info with tok.span_id (errfmt), and make it an rror # when strict_backslash is on. I USED this to fix a refactored regex! # Extract from [[ ]] and fix backslashes. stderr_line( 'warning: Invalid backslash escape in C-style string: %r' % value) #from core.pyerror import e_die #e_die('Invalid backslash escape %r', value, span_id=tok.span_id) return value elif id_ == Id.Char_OneChar: c = value[1] return consts.LookupCharC(c) elif id_ == Id.Char_Stop: # \c returns a special sentinel return None elif id_ in (Id.Char_Octal3, Id.Char_Octal4): if id_ == Id.Char_Octal3: # $'\377' s = value[1:] else: # echo -e '\0377' s = value[2:] i = int(s, 8) if i >= 256: i = i % 256 # NOTE: This is for strict mode #raise AssertionError('Out of range') return chr(i) elif id_ == Id.Char_Hex: s = value[2:] i = int(s, 16) return chr(i) elif id_ in (Id.Char_Unicode4, Id.Char_Unicode8): s = value[2:] i = int(s, 16) #util.log('i = %d', i) return string_ops.Utf8Encode(i) else: raise AssertionError()
def main(argv): arg, i = SPEC.ParseArgv(argv) if not arg.f: stderr_line("readlink: -f must be passed") return 1 for path in argv[i:]: res = libc.realpath(path) if res is None: return 1 print(res) return 0
def EvalCStringToken(id_, value): # type: (Id_t, str) -> Optional[str] """ This function is shared between echo -e and $''. $'' could use it at compile time, much like brace expansion in braces.py. """ if id_ == Id.Char_Literals: return value elif id_ == Id.Char_BadBackslash: if 1: # TODO: # - make this an error in strict mode # - improve the error message. We don't have a span_id! # Either \A or trailing \ (A is not a valid backslash escape) stderr_line('warning: Invalid backslash escape in C-style string') return value elif id_ == Id.Char_OneChar: c = value[1] return _ONE_CHAR[c] elif id_ == Id.Char_Stop: # \c returns a special sentinel return None elif id_ in (Id.Char_Octal3, Id.Char_Octal4): if id_ == Id.Char_Octal3: # $'\377' s = value[1:] else: # echo -e '\0377' s = value[2:] i = int(s, 8) if i >= 256: i = i % 256 # NOTE: This is for strict mode #raise AssertionError('Out of range') return chr(i) elif id_ == Id.Char_Hex: s = value[2:] i = int(s, 16) return chr(i) elif id_ in (Id.Char_Unicode4, Id.Char_Unicode8): s = value[2:] i = int(s, 16) #util.log('i = %d', i) return string_ops.Utf8Encode(i) else: raise AssertionError()
def Expand(self, arg, out): # type: (str, List[str]) -> int """Given a string that could be a glob, append a list of strings to 'out'. Returns: Number of items appended. """ if not LooksLikeGlob(arg): # e.g. don't glob 'echo' because it doesn't look like a glob out.append(GlobUnescape(arg)) return 1 if self.exec_opts.noglob(): # we didn't glob escape it in osh/word_eval.py out.append(arg) return 1 try: results = libc.glob(arg) except RuntimeError as e: # These errors should be rare: I/O error, out of memory, or unknown # There are no syntax errors. (But see comment about globerr() in # native/libc.c.) # note: MyPy doesn't know RuntimeError has e.message (and e.args) msg = e.message # type: str stderr_line("Error expanding glob %r: %s", arg, msg) raise #log('glob %r -> %r', arg, g) n = len(results) if n: # Something matched for name in results: # Omit files starting with - to solve the --. # dash_glob turned OFF with shopt -s oil:basic. if name.startswith('-') and not self.exec_opts.dashglob(): n -= 1 continue out.append(name) return n # Nothing matched #if self.exec_opts.failglob(): # note: to match bash, the whole command has to return 1. But this also # happens in for loop and array literal contexts. It might not be worth # it? # raise NotImplementedError() if self.exec_opts.nullglob(): return 0 # Return the original string out.append(GlobUnescape(arg)) return 1
def main(argv): arg_r = args.Reader(argv) arg_r.Next() # skip argv[0] arg = args.Parse(SPEC, arg_r) if not arg.f: stderr_line("readlink: -f must be passed") return 1 for path in arg_r.Rest(): res = libc.realpath(path) if res is None: return 1 print(res) return 0
def _GetOpts(spec, argv, my_state, errfmt): # type: (Dict[str, bool], List[str], GetOptsState, ErrorFormatter) -> Tuple[int, str] current = my_state.GetArg(argv) #log('current %s', current) if current is None: # out of range, etc. my_state.Fail() return 1, '?' if not current.startswith('-') or current == '-': my_state.Fail() return 1, '?' flag_char = current[my_state.flag_pos] if my_state.flag_pos < len(current) - 1: my_state.flag_pos += 1 # don't move past this arg yet more_chars = True else: my_state.IncIndex() my_state.flag_pos = 1 more_chars = False if flag_char not in spec: # Invalid flag return 0, '?' if spec[flag_char]: # does it need an argument? if more_chars: optarg = current[my_state.flag_pos:] else: optarg = my_state.GetArg(argv) if optarg is None: my_state.Fail() # TODO: Add location info errfmt.Print_('getopts: option %r requires an argument.' % current) tmp = [qsn.maybe_shell_encode(a) for a in argv] stderr_line('(getopts argv: %s)', ' '.join(tmp)) # Hm doesn't cause status 1? return 0, '?' my_state.IncIndex() my_state.SetArg(optarg) else: my_state.SetArg('') return 0, flag_char
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
def OnChange(self, opt_array, opt_name, b): # type: (List[bool], str, bool) -> bool """This method is called whenever an option is changed. Returns success or failure. """ if opt_name == 'vi' or opt_name == 'emacs': # TODO: Replace with a hook? Just like setting LANG= can have a hook. if self.line_input: self.line_input.parse_and_bind("set editing-mode " + opt_name); else: stderr_line( "Warning: Can't set option %r because Oil wasn't built with line editing (e.g. GNU readline)", opt_name) return False # Invert: they are mutually exclusive! if opt_name == 'vi': opt_array[option_i.emacs] = not b elif opt_name == 'emacs': opt_array[option_i.vi] = not b return True
def TeaMain(argv): # type: (str, List[str]) -> int arena = alloc.Arena() try: script_name = argv[1] arena.PushSource(source.MainFile(script_name)) except IndexError: arena.PushSource(source.Stdin()) f = sys.stdin else: try: f = open(script_name) except IOError as e: stderr_line("tea: Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 2 aliases = {} # Dummy value; not respecting aliases! loader = pyutil.GetResourceLoader() oil_grammar = pyutil.LoadOilGrammar(loader) # Not used in tea, but OK... opt0_array = state.InitOpts() parse_opts = optview.Parse(opt0_array) # parse `` and a[x+1]=bar differently parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar) line_reader = reader.FileLineReader(f, arena) try: parse_ctx.ParseTeaModule(line_reader) status = 0 except error.Parse as e: ui.PrettyPrintError(e, arena) status = 2 return status
def Matches(self, comp): # type: (Api) -> List[str] # Have to clear the response every time. TODO: Reuse the object? state.SetGlobalArray(self.cmd_ev.mem, 'COMPREPLY', []) # New completions should use COMP_ARGV, a construct specific to OSH> state.SetGlobalArray(self.cmd_ev.mem, 'COMP_ARGV', comp.partial_argv) # Old completions may use COMP_WORDS. It is split by : and = to emulate # bash's behavior. # More commonly, they will call _init_completion and use the 'words' output # of that, ignoring COMP_WORDS. comp_words = [] for a in comp.partial_argv: AdjustArg(a, [':', '='], comp_words) if comp.index == -1: # cmopgen comp_cword = comp.index else: comp_cword = len(comp_words) - 1 # weird invariant state.SetGlobalArray(self.cmd_ev.mem, 'COMP_WORDS', comp_words) state.SetGlobalString(self.cmd_ev.mem, 'COMP_CWORD', str(comp_cword)) state.SetGlobalString(self.cmd_ev.mem, 'COMP_LINE', comp.line) state.SetGlobalString(self.cmd_ev.mem, 'COMP_POINT', str(comp.end)) argv = [comp.first, comp.to_complete, comp.prev] self.log('Running completion function %r with arguments %s', self.func.name, argv) self.comp_lookup.ClearCommandsChanged() status = self.cmd_ev.RunFuncForCompletion(self.func, argv) commands_changed = self.comp_lookup.GetCommandsChanged() self.log('comp.first %s, commands_changed: %s', comp.first, commands_changed) if status == 124: cmd = os_path.basename(comp.first) if cmd in commands_changed: self.log('Got status 124 from %r and %s commands changed', self.func.name, commands_changed) raise _RetryCompletion() else: # This happens with my own completion scripts. bash doesn't show an # error. self.log( "Function %r returned 124, but the completion spec for %r wasn't " "changed", self.func.name, cmd) return [] # Read the response. # NOTE: 'COMP_REPLY' would follow the naming convention! val = state.GetGlobal(self.cmd_ev.mem, 'COMPREPLY') if val.tag_() == value_e.Undef: # We set it above, so this error would only happen if the user unset it. # Not changing it means there were no completions. # TODO: This writes over the command line; it would be better to use an # error object. stderr_line('osh: Ran function %r but COMPREPLY was unset', self.func.name) return [] if val.tag_() != value_e.MaybeStrArray: log('ERROR: COMPREPLY should be an array, got %s', val) return [] self.log('COMPREPLY %s', val) # Return this all at once so we don't have a generator. COMPREPLY happens # all at once anyway. return val.strs
def WaitForOne(self, eintr_retry): # type: (bool) -> int """Wait until the next process returns (or maybe Ctrl-C). Args: Should be True to prevent zombies Returns: -1 Nothing to wait for 0 OK, job mutated >= 128 Interrupted with signal In the interactive shell, we return 0 if we get a Ctrl-C, so the caller will try again. Callers: wait -n -- WaitForOne() just once wait -- WaitForOne() in a loop wait $! -- job.JobWait() Comparisons: bash: jobs.c waitchld() Has a special case macro(!) CHECK_WAIT_INTR for the wait builtin dash: jobs.c waitproc() uses sigfillset(), sigprocmask(), etc. Runs in a loop while (gotsigchld), but that might be a hack for System V! You could imagine a clean API like posix::wait_for_one() wait_result = ECHILD -- nothing to wait for | Done(int pid, int status) -- process done | EINTR(bool sigint) -- may or may not retry But I think we want to keep KeyboardInterrupt as an exception for now. """ # This is a list of async jobs try: # Notes: # - The arg -1 makes it like wait(), which waits for any process. # - WUNTRACED is necessary to get stopped jobs. What about WCONTINUED? # - Unlike other syscalls, we do NOT try on EINTR, because the 'wait' # builtin should be interruptable. This doesn't appear to cause any # problems for other WaitForOne callers? pid, status = posix.waitpid(-1, WUNTRACED) except OSError as e: #log('wait() error: %s', e) if e.errno == errno_.ECHILD: return -1 # nothing to wait for caller should stop elif e.errno == errno_.EINTR: # Bug #858 fix # Examples: # - 128 + SIGUSR1 = 128 + 10 = 138 # - 128 + SIGUSR2 = 128 + 12 = 140 return 0 if eintr_retry else (128 + self.sig_state.last_sig_num) else: # The signature of waitpid() means this shouldn't happen raise AssertionError() except KeyboardInterrupt: # NOTE: Another way to handle this is to disable SIGINT when a process is # running. Not sure if there's any real difference. bash and dash # handle SIGINT pretty differently. if self.exec_opts.interactive(): # Caller should keep waiting. If we run 'sleep 3' and hit Ctrl-C, both # processes will get SIGINT, but the shell has to wait again to get the # exit code. return 0 else: raise # abort a batch script # All child processes are supposed to be in this dict. But this may # legitimately happen if a grandchild outlives the child (its parent). # Then it is reparented under this process, so we might receive # notification of its exit, even though we didn't start it. We can't have # any knowledge of such processes, so print a warning. if pid not in self.job_state.child_procs: stderr_line("osh: PID %d stopped, but osh didn't start it", pid) return True # caller should keep waiting proc = self.job_state.child_procs[pid] if WIFSIGNALED(status): term_sig = WTERMSIG(status) status = 128 + term_sig # Print newline after Ctrl-C. if term_sig == signal_.SIGINT: print('') proc.WhenDone(pid, status) elif WIFEXITED(status): status = WEXITSTATUS(status) #log('exit status: %s', status) proc.WhenDone(pid, status) elif WIFSTOPPED(status): #sig = posix.WSTOPSIG(status) # TODO: Do something nicer here. Implement 'fg' command. # Show in jobs list. log('') log('[PID %d] Stopped', pid) self.job_state.NotifyStopped( pid) # show in 'jobs' list, enable 'fg' proc.WhenStopped() else: raise AssertionError(status) self.last_status = status # for wait -n self.tracer.OnProcessEnd(pid, status) return 0 # caller should keep waiting
def AppBundleMain(argv): # type: (List[str]) -> int # NOTE: This has a side effect of deleting _OVM_* from the environment! loader = pyutil.GetResourceLoader() b = os_path.basename(argv[0]) main_name, ext = os_path.splitext(b) arg_r = args.Reader(argv) if main_name == 'oil' and ext: # oil.py or oil.ovm arg_r.Next() first_arg = arg_r.Peek() if first_arg is None: raise error.Usage('Missing required applet name.') if first_arg in ('-h', '--help'): errfmt = None # not needed here help_builtin = builtin_misc.Help(loader, errfmt) help_builtin.Run(pure.MakeBuiltinArgv(['bundle-usage'])) sys.exit(0) if first_arg in ('-V', '--version'): pyutil.ShowAppVersion('Oil', loader) sys.exit(0) main_name = first_arg login_shell = False if main_name.startswith('-'): login_shell = True main_name = main_name[1:] if main_name in ('osh', 'sh'): # TODO: # - Initialize a different shell if line_input isn't present status = shell.Main('osh', arg_r, posix.environ, login_shell, loader, line_input) _tlog('done osh main') return status elif main_name == 'osh-pure': # TODO: pure.Main() pass elif main_name == 'oshc': arg_r.Next() main_argv = arg_r.Rest() try: return OshCommandMain(main_argv) except error.Usage as e: stderr_line('oshc usage error: %s', e.msg) return 2 elif main_name == 'oil': return shell.Main('oil', arg_r, posix.environ, login_shell, loader, line_input) elif main_name == 'tea': main_argv = arg_r.Rest() return TeaMain(main_argv) # For testing latency elif main_name == 'true': return 0 elif main_name == 'false': return 1 elif main_name == 'readlink': main_argv = arg_r.Rest() return readlink.main(main_argv) else: raise error.Usage('Invalid applet name %r.' % main_name)
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
def OshCommandMain(argv): """Run an 'oshc' tool. 'osh' is short for "osh compiler" or "osh command". TODO: - oshc --help oshc deps --path: the $PATH to use to find executables. What about libraries? NOTE: we're leaving out su -c, find, xargs, etc.? Those should generally run functions using the $0 pattern. --chained-command sudo """ try: action = argv[0] except IndexError: raise error.Usage('Missing required subcommand.') if action not in SUBCOMMANDS: raise error.Usage('Invalid subcommand %r.' % action) if action == 'parse-glob': # Pretty-print the AST produced by osh/glob_.py print('TODO:parse-glob') return 0 if action == 'parse-printf': # Pretty-print the AST produced by osh/builtin_printf.py print('TODO:parse-printf') return 0 arena = alloc.Arena() try: script_name = argv[1] arena.PushSource(source.MainFile(script_name)) except IndexError: arena.PushSource(source.Stdin()) f = sys.stdin else: try: f = open(script_name) except IOError as e: stderr_line("oshc: Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 2 aliases = {} # Dummy value; not respecting aliases! loader = pyutil.GetResourceLoader() oil_grammar = pyutil.LoadOilGrammar(loader) opt0_array = state.InitOpts() no_stack = None # type: List[bool] # for mycpp opt_stacks = [no_stack] * option_i.ARRAY_SIZE # type: List[List[bool]] parse_opts = optview.Parse(opt0_array, opt_stacks) # parse `` and a[x+1]=bar differently parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar) parse_ctx.Init_OnePassParse(True) line_reader = reader.FileLineReader(f, arena) c_parser = parse_ctx.MakeOshParser(line_reader) try: node = main_loop.ParseWholeFile(c_parser) except error.Parse as e: ui.PrettyPrintError(e, arena) return 2 assert node is not None f.close() # Columns for list-* # path line name # where name is the binary path, variable name, or library path. # bin-deps and lib-deps can be used to make an app bundle. # Maybe I should list them together? 'deps' can show 4 columns? # # path, line, type, name # # --pretty can show the LST location. # stderr: show how we're following imports? if action == 'translate': osh2oil.PrintAsOil(arena, node) elif action == 'arena': # for debugging osh2oil.PrintArena(arena) elif action == 'spans': # for debugging osh2oil.PrintSpans(arena) elif action == 'format': # TODO: autoformat code raise NotImplementedError(action) elif action == 'deps': deps.Deps(node) elif action == 'undefined-vars': # could be environment variables raise NotImplementedError() else: raise AssertionError # Checked above return 0
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 Run(self, cmd_val): # type: (cmd_value__Argv) -> int attrs, arg_r = flag_spec.ParseCmdVal('trap', cmd_val) arg = arg_types.trap(attrs.attrs) if arg.p: # Print registered handlers for name, value in self.traps.iteritems(): # The unit tests rely on this being one line. # bash prints a line that can be re-parsed. print('%s %s' % (name, value.__class__.__name__)) return 0 if arg.l: # List valid signals and hooks ordered = _SIGNAL_NAMES.items() ordered.sort(key=lambda x: x[1]) for name in _HOOK_NAMES: print(' %s' % name) for name, int_val in ordered: print('%2d %s' % (int_val, name)) return 0 code_str = arg_r.ReadRequired('requires a code string') sig_spec, sig_spid = arg_r.ReadRequired2('requires a signal or hook name') # sig_key is NORMALIZED sig_spec: a signal number string or string hook # name. sig_key = None # type: Optional[str] sig_num = None if sig_spec in _HOOK_NAMES: sig_key = sig_spec elif sig_spec == '0': # Special case sig_key = 'EXIT' else: sig_num = _GetSignalNumber(sig_spec) if sig_num is not None: sig_key = str(sig_num) if sig_key is None: self.errfmt.Print("Invalid signal or hook %r", sig_spec, span_id=cmd_val.arg_spids[2]) return 1 # NOTE: sig_spec isn't validated when removing handlers. if code_str == '-': if sig_key in _HOOK_NAMES: try: del self.traps[sig_key] except KeyError: pass return 0 if sig_num is not None: try: del self.traps[sig_key] except KeyError: pass self.sig_state.RemoveUserTrap(sig_num) return 0 raise AssertionError('Signal or trap') # Try parsing the code first. node = self._ParseTrapCode(code_str) if node is None: return 1 # ParseTrapCode() prints an error for us. # Register a hook. if sig_key in _HOOK_NAMES: if sig_key in ('ERR', 'RETURN', 'DEBUG'): stderr_line("osh warning: The %r hook isn't implemented", sig_spec) self.traps[sig_key] = _TrapHandler(node, self.nodes_to_run) return 0 # Register a signal. if sig_num is not None: handler = _TrapHandler(node, self.nodes_to_run) # For signal handlers, the traps dictionary is used only for debugging. self.traps[sig_key] = handler if sig_num in (signal.SIGKILL, signal.SIGSTOP): self.errfmt.Print("Signal %r can't be handled", sig_spec, span_id=sig_spid) # Other shells return 0, but this seems like an obvious error return 1 self.sig_state.AddUserTrap(sig_num, handler) return 0 raise AssertionError('Signal or trap')
def WaitForOne(self): # type: () -> bool """Wait until the next process returns (or maybe Ctrl-C). Returns: True if we got a notification, or False if there was nothing to wait for. In the interactive shell, we return True if we get a Ctrl-C, so the caller will try again. """ # This is a list of async jobs try: # -1 makes it like wait(), which waits for any process. # NOTE: WUNTRACED is necessary to get stopped jobs. What about # WCONTINUED? pid, status = posix.waitpid(-1, WUNTRACED) except OSError as e: #log('wait() error: %s', e) if e.errno == errno_.ECHILD: return False # nothing to wait for caller should stop else: # We should never get here. EINTR was handled by the 'posix' # module. The only other error is EINVAL, which doesn't apply to # this call. raise except KeyboardInterrupt: # NOTE: Another way to handle this is to disable SIGINT when a process is # running. Not sure if there's any real difference. bash and dash # handle SIGINT pretty differently. if self.exec_opts.interactive(): # Caller should keep waiting. If we run 'sleep 3' and hit Ctrl-C, both # processes will get SIGINT, but the shell has to wait again to get the # exit code. return True else: raise # abort a batch script #log('WAIT got %s %s', pid, status) # All child processes are supposed to be in this dict. But this may # legitimately happen if a grandchild outlives the child (its parent). # Then it is reparented under this process, so we might receive # notification of its exit, even though we didn't start it. We can't have # any knowledge of such processes, so print a warning. if pid not in self.job_state.child_procs: stderr_line("osh: PID %d stopped, but osh didn't start it", pid) return True # caller should keep waiting proc = self.job_state.child_procs[pid] if WIFSIGNALED(status): term_sig = WTERMSIG(status) status = 128 + term_sig # Print newline after Ctrl-C. if term_sig == signal_.SIGINT: print('') proc.WhenDone(pid, status) elif WIFEXITED(status): status = WEXITSTATUS(status) #log('exit status: %s', status) proc.WhenDone(pid, status) elif WIFSTOPPED(status): #sig = posix.WSTOPSIG(status) # TODO: Do something nicer here. Implement 'fg' command. # Show in jobs list. log('') log('[PID %d] Stopped', pid) self.job_state.NotifyStopped( pid) # show in 'jobs' list, enable 'fg' proc.WhenStopped() self.last_status = status # for wait -n return True # caller should keep waiting
def Main(arg_r): # type: (args.Reader) -> int """ Usage: tea myprog.tea # Run the script tea -c 'func main() { echo "hi" }' # Run this snippet. Not common since # there's no top level statementes! # Use bin/oil for that. tea -n -c 'var x = 1' # Parse it # Compile to C++. Produce myprog.cc and myprog.h. tea --cpp-out myprog myprog.tea lib.tea """ try: attrs = flag_spec.ParseOil('tea_main', arg_r) except error.Usage as e: stderr_line('tea usage error: %s', e.msg) return 2 arg = arg_types.tea_main(attrs.attrs) arena = alloc.Arena() if arg.c is not None: arena.PushSource(source.CFlag()) line_reader = reader.StringLineReader(arg.c, arena) # type: reader._Reader else: script_name = arg_r.Peek() if script_name is None: arena.PushSource(source.Stdin()) f = mylib.Stdin() # type: mylib.LineReader else: arena.PushSource(source.MainFile(script_name)) try: # Hack for type checking. core/shell.py uses fd_state.Open(). f = cast('mylib.LineReader', open(script_name)) except IOError as e: stderr_line("tea: Couldn't open %r: %s", script_name, posix.strerror(e.errno)) return 2 line_reader = reader.FileLineReader(f, arena) # Dummy value; not respecting aliases! aliases = {} # type: Dict[str, str] loader = pyutil.GetResourceLoader() oil_grammar = pyutil.LoadOilGrammar(loader) # Not used in tea, but OK... opt0_array = state.InitOpts() no_stack = None # type: List[bool] # for mycpp opt_stacks = [no_stack] * option_i.ARRAY_SIZE # type: List[List[bool]] parse_opts = optview.Parse(opt0_array, opt_stacks) # parse `` and a[x+1]=bar differently parse_ctx = parse_lib.ParseContext(arena, parse_opts, aliases, oil_grammar) if arg.n: try: parse_ctx.ParseTeaModule(line_reader) status = 0 except error.Parse as e: ui.PrettyPrintError(e, arena) status = 2 else: e_usage("Tea doesn't run anything yet. Pass -n to parse.") return status
def StderrLine(self, msg): # type: (str) -> None stderr_line(msg)