def Run(self): # type: () -> None val = self.mem.GetValue('PROMPT_COMMAND') if val.tag_() != value_e.Str: return # PROMPT_COMMAND almost never changes, so we try to cache its parsing. # This avoids memory allocations. prompt_cmd = cast(value__Str, val).s node = self.parse_cache.get(prompt_cmd) if node is None: line_reader = reader.StringLineReader(prompt_cmd, self.arena) c_parser = self.parse_ctx.MakeOshParser(line_reader) # NOTE: This is similar to CommandEvaluator.ParseTrapCode(). # TODO: Add spid with alloc.ctx_Location(self.arena, source.PromptCommand(runtime.NO_SPID)): try: node = main_loop.ParseWholeFile(c_parser) except error.Parse as e: ui.PrettyPrintError(e, self.arena) return # don't execute self.parse_cache[prompt_cmd] = node # Save this so PROMPT_COMMAND can't set $? with state.ctx_Status(self.mem): # Catches fatal execution error self.cmd_ev.ExecuteAndCatch(node)
def SourceStartupFile(fd_state, rc_path, lang, parse_ctx, cmd_ev): # type: (process.FdState, str, str, parse_lib.ParseContext, cmd_eval.CommandEvaluator) -> None # Right now this is called when the shell is interactive. (Maybe it should # be called on login_shel too.) # # Terms: # - interactive shell: Roughly speaking, no args or -c, and isatty() is true # for stdin and stdout. # - login shell: Started from the top level, e.g. from init or ssh. # # We're not going to copy everything bash does because it's too complex, but # for reference: # https://www.gnu.org/software/bash/manual/bash.html#Bash-Startup-Files # Bash also has --login. try: f = fd_state.Open(rc_path) except OSError as e: # TODO: Could warn about nonexistent explicit --rcfile? if e.errno != errno.ENOENT: raise # Goes to top level. Handle this better? return arena = parse_ctx.arena rc_line_reader = reader.FileLineReader(f, arena) rc_c_parser = parse_ctx.MakeOshParser(rc_line_reader) with alloc.ctx_Location(arena, source.SourcedFile(rc_path)): # TODO: handle status, e.g. 2 for ParseError status = main_loop.Batch(cmd_ev, rc_c_parser, arena) f.close()
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int # There are no flags, but we need it to respect -- _, arg_r = flag_spec.ParseCmdVal('eval', cmd_val) if self.exec_opts.simple_eval_builtin(): code_str, eval_spid = arg_r.ReadRequired2('requires code string') if not arg_r.AtEnd(): e_usage('requires exactly 1 argument') else: code_str = ' '.join(arg_r.Rest()) # code_str could be EMPTY, so just use the first one eval_spid = cmd_val.arg_spids[0] line_reader = reader.StringLineReader(code_str, self.arena) c_parser = self.parse_ctx.MakeOshParser(line_reader) src = source.EvalArg(eval_spid) with dev.ctx_Tracer(self.tracer, 'eval', None): with alloc.ctx_Location(self.arena, src): return main_loop.Batch(self.cmd_ev, c_parser, self.arena, cmd_flags=cmd_eval.IsEvalSource)
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int call_spid = cmd_val.arg_spids[0] _, arg_r = flag_spec.ParseCmdVal('source', cmd_val) path = arg_r.Peek() if path is None: e_usage('missing required argument') arg_r.Next() resolved = self.search_path.Lookup(path, exec_required=False) if resolved is None: resolved = path try: f = self.fd_state.Open(resolved) # Shell can't use descriptors 3-9 except OSError as e: self.errfmt.Print_('source %r failed: %s' % (path, pyutil.strerror(e)), span_id=cmd_val.arg_spids[1]) return 1 try: line_reader = reader.FileLineReader(f, self.arena) c_parser = self.parse_ctx.MakeOshParser(line_reader) # A sourced module CAN have a new arguments array, but it always shares # the same variable scope as the caller. The caller could be at either a # global or a local scope. source_argv = arg_r.Rest() self.mem.PushSource(path, source_argv) src = source.SourcedFile(path, call_spid) try: with alloc.ctx_Location(self.arena, src): status = main_loop.Batch(self.cmd_ev, c_parser, self.arena, cmd_flags=cmd_eval.IsEvalSource) finally: self.mem.PopSource(source_argv) return status except _ControlFlow as e: if e.IsReturn(): return e.StatusCode() else: raise finally: f.close()
def _UnsetVar(self, arg, spid, proc_fallback): # type: (str, int, bool) -> bool """ Returns: bool: whether the 'unset' builtin should succeed with code 0. """ arena = self.parse_ctx.arena a_parser = self.parse_ctx.MakeArithParser(arg) with alloc.ctx_Location(arena, source.ArgvWord(spid)): try: anode = a_parser.Parse() except error.Parse as e: ui.PrettyPrintError(e, arena) # show parse error e_usage('Invalid unset expression', span_id=spid) lval = self.arith_ev.EvalArithLhs(anode, spid) # Prevent attacks like these by default: # # unset -v 'A["$(echo K; rm *)"]' if not self.exec_opts.eval_unsafe_arith( ) and lval.tag_() != lvalue_e.Named: e_usage( 'expected a variable name. shopt -s eval_unsafe_arith allows expressions', span_id=spid) #log('lval %s', lval) found = False try: # Note: This has 'setvar' semantics. It could be 'setref' too? # So it composes? found = self.mem.Unset(lval, False) except error.Runtime as e: # note: in bash, myreadonly=X fails, but declare myreadonly=X doens't # fail because it's a builtin. So I guess the same is true of 'unset'. e.span_id = spid ui.PrettyPrintError(e, arena) return False if proc_fallback and not found: if arg in self.funcs: del self.funcs[arg] return True
def _ParseTrapCode(self, code_str): # type: (str) -> command_t """ Returns: A node, or None if the code is invalid. """ line_reader = reader.StringLineReader(code_str, self.arena) c_parser = self.parse_ctx.MakeOshParser(line_reader) # TODO: the SPID should be passed through argv. Use ArgvWord? with alloc.ctx_Location(self.arena, source.Trap(runtime.NO_SPID)): try: node = main_loop.ParseWholeFile(c_parser) except error.Parse as e: ui.PrettyPrintError(e, self.arena) return None return node
def _Line(self, arg, var_name): # type: (arg_types.read, str) -> int line = _ReadLine() if len(line) == 0: # EOF return 1 if not arg.with_eol: if line.endswith('\r\n'): line = line[:-2] elif line.endswith('\n'): line = line[:-1] # Lines that don't start with a single quote aren't QSN. They may contain # a single quote internally, like: # # Fool's Gold if arg.q and line.startswith("'"): arena = self.parse_ctx.arena line_reader = reader.StringLineReader(line, arena) lexer = self.parse_ctx._MakeLexer(line_reader) # The parser only yields valid tokens: # Char_Literals, Char_OneChar, Char_Hex, Char_UBraced # So we can use word_compile.EvalCStringToken, which is also used for # $''. # Important: we don't generate Id.Unknown_Backslash because that is valid # in echo -e. We just make it Id.Unknown_Tok? try: # TODO: read should know about stdin, and redirects, and pipelines? with alloc.ctx_Location(arena, source.Stdin('')): tokens = qsn_native.Parse(lexer) except error.Parse as e: ui.PrettyPrintError(e, arena) return 1 tmp = [word_compile.EvalCStringToken(t) for t in tokens] line = ''.join(tmp) lhs = lvalue.Named(var_name) self.mem.SetValue(lhs, value.Str(line), scope_e.LocalOnly) return 0
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int """ printf: printf [-v var] format [argument ...] """ attrs, arg_r = flag_spec.ParseCmdVal('printf', cmd_val) arg = arg_types.printf(attrs.attrs) fmt, fmt_spid = arg_r.ReadRequired2('requires a format string') varargs, spids = arg_r.Rest2() #log('fmt %s', fmt) #log('vals %s', vals) arena = self.parse_ctx.arena if fmt in self.parse_cache: parts = self.parse_cache[fmt] else: line_reader = reader.StringLineReader(fmt, arena) # TODO: Make public lexer = self.parse_ctx._MakeLexer(line_reader) parser = _FormatStringParser(lexer) with alloc.ctx_Location(arena, source.ArgvWord(fmt_spid)): try: parts = parser.Parse() except error.Parse as e: self.errfmt.PrettyPrintError(e) return 2 # parse error self.parse_cache[fmt] = parts if 0: print() for part in parts: part.PrettyPrint() print() out = [] # type: List[str] arg_index = 0 num_args = len(varargs) backslash_c = False while True: for part in parts: UP_part = part if part.tag_() == printf_part_e.Literal: part = cast(printf_part__Literal, UP_part) token = part.token if token.id == Id.Format_EscapedPercent: s = '%' else: s = word_compile.EvalCStringToken(token) out.append(s) elif part.tag_() == printf_part_e.Percent: part = cast(printf_part__Percent, UP_part) flags = [] # type: List[str] if len(part.flags) > 0: for flag_token in part.flags: flags.append(flag_token.val) width = -1 # nonexistent if part.width: if part.width.id in (Id.Format_Num, Id.Format_Zero): width_str = part.width.val width_spid = part.width.span_id elif part.width.id == Id.Format_Star: if arg_index < num_args: width_str = varargs[arg_index] width_spid = spids[arg_index] arg_index += 1 else: width_str = '' # invalid width_spid = runtime.NO_SPID else: raise AssertionError() try: width = int(width_str) except ValueError: if width_spid == runtime.NO_SPID: width_spid = part.width.span_id self.errfmt.Print_("printf got invalid width %r" % width_str, span_id=width_spid) return 1 precision = -1 # nonexistent if part.precision: if part.precision.id == Id.Format_Dot: precision_str = '0' precision_spid = part.precision.span_id elif part.precision.id in (Id.Format_Num, Id.Format_Zero): precision_str = part.precision.val precision_spid = part.precision.span_id elif part.precision.id == Id.Format_Star: if arg_index < num_args: precision_str = varargs[arg_index] precision_spid = spids[arg_index] arg_index += 1 else: precision_str = '' precision_spid = runtime.NO_SPID else: raise AssertionError() try: precision = int(precision_str) except ValueError: if precision_spid == runtime.NO_SPID: precision_spid = part.precision.span_id self.errfmt.Print_( 'printf got invalid precision %r' % precision_str, span_id=precision_spid) return 1 #log('index=%d n=%d', arg_index, num_args) if arg_index < num_args: s = varargs[arg_index] word_spid = spids[arg_index] arg_index += 1 else: s = '' word_spid = runtime.NO_SPID typ = part.type.val if typ == 's': if precision >= 0: s = s[:precision] # truncate elif typ == 'q': s = qsn.maybe_shell_encode(s) elif typ == 'b': # Process just like echo -e, except \c handling is simpler. c_parts = [] # type: List[str] lex = match.EchoLexer(s) while True: id_, tok_val = lex.Next() if id_ == Id.Eol_Tok: # Note: This is really a NUL terminator break # TODO: add span_id from argv tok = Token(id_, runtime.NO_SPID, tok_val) p = word_compile.EvalCStringToken(tok) # Unusual behavior: '\c' aborts processing! if p is None: backslash_c = True break c_parts.append(p) s = ''.join(c_parts) elif typ in 'diouxX' or part.type.id == Id.Format_Time: try: d = int(s) except ValueError: if len(s) >= 1 and s[0] in '\'"': # TODO: utf-8 decode s[1:] to be more correct. Probably # depends on issue #366, a utf-8 library. # Note: len(s) == 1 means there is a NUL (0) after the quote.. d = ord(s[1]) if len(s) >= 2 else 0 elif part.type.id == Id.Format_Time and len( s) == 0 and word_spid == runtime.NO_SPID: # Note: No argument means -1 for %(...)T as in Bash Reference # Manual 4.2 "If no argument is specified, conversion behaves # as if -1 had been given." d = -1 else: if word_spid == runtime.NO_SPID: # Blame the format string blame_spid = part.type.span_id else: blame_spid = word_spid self.errfmt.Print_( 'printf expected an integer, got %r' % s, span_id=blame_spid) return 1 if typ in 'di': s = str(d) elif typ in 'ouxX': if d < 0: e_die( "Can't format negative number %d with %%%s", d, typ, span_id=part.type.span_id) if typ == 'u': s = str(d) elif typ == 'o': s = mylib.octal(d) elif typ == 'x': s = mylib.hex_lower(d) elif typ == 'X': s = mylib.hex_upper(d) elif part.type.id == Id.Format_Time: # %(...)T # Initialize timezone: # `localtime' uses the current timezone information initialized # by `tzset'. The function `tzset' refers to the environment # variable `TZ'. When the exported variable `TZ' is present, # its value should be reflected in the real environment # variable `TZ' before call of `tzset'. # # Note: unlike LANG, TZ doesn't seem to change behavior if it's # not exported. # # TODO: In Oil, provide an API that doesn't rely on libc's # global state. tzcell = self.mem.GetCell('TZ') if tzcell and tzcell.exported and tzcell.val.tag_( ) == value_e.Str: tzval = cast(value__Str, tzcell.val) posix.putenv('TZ', tzval.s) time_.tzset() # Handle special values: # User can specify two special values -1 and -2 as in Bash # Reference Manual 4.2: "Two special argument values may be # used: -1 represents the current time, and -2 represents the # time the shell was invoked." from # https://www.gnu.org/software/bash/manual/html_node/Bash-Builtins.html#index-printf if d == -1: # the current time ts = time_.time() elif d == -2: # the shell start time ts = self.shell_start_time else: ts = d s = time_.strftime(typ[1:-2], time_.localtime(ts)) if precision >= 0: s = s[:precision] # truncate else: raise AssertionError() else: raise AssertionError() if width >= 0: if len(flags): if '-' in flags: s = s.ljust(width, ' ') elif '0' in flags: s = s.rjust(width, '0') else: pass else: s = s.rjust(width, ' ') out.append(s) else: raise AssertionError() if backslash_c: # 'printf %b a\cb xx' - \c terminates processing! break if arg_index >= num_args: break # Otherwise there are more args. So cycle through the loop once more to # implement the 'arg recycling' behavior. result = ''.join(out) if arg.v is not None: # TODO: get the span_id for arg.v! v_spid = runtime.NO_SPID arena = self.parse_ctx.arena a_parser = self.parse_ctx.MakeArithParser(arg.v) with alloc.ctx_Location(arena, source.ArgvWord(v_spid)): try: anode = a_parser.Parse() except error.Parse as e: ui.PrettyPrintError(e, arena) # show parse error e_usage('Invalid -v expression', span_id=v_spid) lval = self.arith_ev.EvalArithLhs(anode, v_spid) if not self.exec_opts.eval_unsafe_arith( ) and lval.tag_() != lvalue_e.Named: e_usage( '-v expected a variable name. shopt -s eval_unsafe_arith allows expressions', span_id=v_spid) state.SetRef(self.mem, lval, value.Str(result)) else: mylib.Stdout().write(result) return 0
def _StringToInteger(self, s, span_id=runtime.NO_SPID): # type: (str, int) -> int """Use bash-like rules to coerce a string to an integer. Runtime parsing enables silly stuff like $(( $(echo 1)$(echo 2) + 1 )) => 13 0xAB -- hex constant 042 -- octal constant 42 -- decimal constant 64#z -- arbitary base constant bare word: variable quoted word: string (not done?) """ if s.startswith('0x'): try: integer = int(s, 16) except ValueError: e_strict('Invalid hex constant %r', s, span_id=span_id) return integer if s.startswith('0'): try: integer = int(s, 8) except ValueError: e_strict('Invalid octal constant %r', s, span_id=span_id) return integer if '#' in s: b, digits = mylib.split_once(s, '#') try: base = int(b) except ValueError: e_strict('Invalid base for numeric constant %r', b, span_id=span_id) integer = 0 for ch in digits: if IsLower(ch): digit = ord(ch) - ord('a') + 10 elif IsUpper(ch): digit = ord(ch) - ord('A') + 36 elif ch == '@': # horrible syntax digit = 62 elif ch == '_': digit = 63 elif ch.isdigit(): digit = int(ch) else: e_strict('Invalid digits for numeric constant %r', digits, span_id=span_id) if digit >= base: e_strict('Digits %r out of range for base %d', digits, base, span_id=span_id) integer = integer * base + digit return integer try: # Normal base 10 integer. This includes negative numbers like '-42'. integer = int(s) except ValueError: # doesn't look like an integer # note: 'test' and '[' never evaluate recursively if self.exec_opts.eval_unsafe_arith() and self.parse_ctx: # Special case so we don't get EOF error if len(s.strip()) == 0: return 0 # For compatibility: Try to parse it as an expression and evaluate it. arena = self.parse_ctx.arena a_parser = self.parse_ctx.MakeArithParser(s) with alloc.ctx_Location(arena, source.Variable(span_id)): try: node2 = a_parser.Parse() # may raise error.Parse except error.Parse as e: ui.PrettyPrintError(e, arena) e_die('Parse error in recursive arithmetic', span_id=e.span_id) # Prevent infinite recursion of $(( 1x )) -- it's a word that evaluates # to itself, and you don't want to reparse it as a word. if node2.tag_() == arith_expr_e.Word: e_die("Invalid integer constant %r", s, span_id=span_id) else: integer = self.EvalToInt(node2) else: if len(s.strip()) == 0 or match.IsValidVarName(s): # x42 could evaluate to 0 e_strict("Invalid integer constant %r", s, span_id=span_id) else: # 42x is always fatal! e_die("Invalid integer constant %r", s, span_id=span_id) return integer
def Run(self, cmd_val): # type: (cmd_value__Argv) -> int """ printf: printf [-v var] format [argument ...] """ attrs, arg_r = flag_spec.ParseCmdVal('printf', cmd_val) arg = arg_types.printf(attrs.attrs) fmt, fmt_spid = arg_r.ReadRequired2('requires a format string') varargs, spids = arg_r.Rest2() #log('fmt %s', fmt) #log('vals %s', vals) arena = self.parse_ctx.arena if fmt in self.parse_cache: parts = self.parse_cache[fmt] else: line_reader = reader.StringLineReader(fmt, arena) # TODO: Make public lexer = self.parse_ctx._MakeLexer(line_reader) parser = _FormatStringParser(lexer) with alloc.ctx_Location(arena, source.ArgvWord(fmt_spid)): try: parts = parser.Parse() except error.Parse as e: self.errfmt.PrettyPrintError(e) return 2 # parse error self.parse_cache[fmt] = parts if 0: print() for part in parts: part.PrettyPrint() print() out = [] # type: List[str] status = self._Format(parts, varargs, spids, out) if status != 0: return status # failure result = ''.join(out) if arg.v is not None: # TODO: get the span_id for arg.v! v_spid = runtime.NO_SPID arena = self.parse_ctx.arena a_parser = self.parse_ctx.MakeArithParser(arg.v) with alloc.ctx_Location(arena, source.ArgvWord(v_spid)): try: anode = a_parser.Parse() except error.Parse as e: ui.PrettyPrintError(e, arena) # show parse error e_usage('Invalid -v expression', span_id=v_spid) lval = self.arith_ev.EvalArithLhs(anode, v_spid) if not self.exec_opts.eval_unsafe_arith( ) and lval.tag_() != lvalue_e.Named: e_usage( '-v expected a variable name. shopt -s eval_unsafe_arith allows expressions', span_id=v_spid) state.SetRef(self.mem, lval, value.Str(result)) else: mylib.Stdout().write(result) return 0