def ParseSubshell(self): left_spid = word.LeftMostSpanForWord(self.cur_word) self._Next() # skip past ( # Ensure that something $( (cd / && pwd) ) works. If ) is already on the # translation stack, we want to delay it. #print('ParseSubshell lexer.PushHint ) -> )') self.lexer.PushHint(Id.Op_RParen, Id.Right_Subshell) c_list = self.ParseCommandList() if not c_list: return None # Remove singleton CommandList as an optimization. if len(c_list.children) == 1: child = c_list.children[0] else: child = c_list node = ast.Subshell(child) right_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.Right_Subshell): return None node.spids.extend((left_spid, right_spid)) return node
def ParseIf(self): """ if_clause : If command_list Then command_list else_part? Fi ; """ if_node = ast.If() self._Next() # skip if cond = self.ParseCommandList() if not cond: return None then_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.KW_Then): return None body = self.ParseCommandList() if not body: return None arm = ast.if_arm(cond.children, body.children) arm.spids.extend((const.NO_INTEGER, then_spid)) # no if spid at first? if_node.arms.append(arm) if self.c_id in (Id.KW_Elif, Id.KW_Else): if not self._ParseElifElse(if_node): return None else: if_node.spids.append(const.NO_INTEGER) # no else spid fi_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.KW_Fi): return None if_node.spids.append(fi_spid) return if_node
def ParseKshFunctionDef(self): """ ksh_function_def : 'function' fname ( '(' ')' )? newline_ok function_body """ left_spid = word.LeftMostSpanForWord(self.cur_word) self._Next() # skip past 'function' if not self._Peek(): return None ok, name = word.AsFuncName(self.cur_word) if not ok: self.AddErrorContext("Invalid function name: %r", self.cur_word) return None after_name_spid = word.LeftMostSpanForWord(self.cur_word) + 1 self._Next() # skip past 'function name if not self._Peek(): return None if self.c_id == Id.Op_LParen: self.lexer.PushHint(Id.Op_RParen, Id.Right_FuncDef) self._Next() if not self._Eat(Id.Right_FuncDef): return None # Change it: after ) after_name_spid = word.LeftMostSpanForWord(self.cur_word) + 1 if not self._NewlineOk(): return None func = ast.FuncDef() func.name = name if not self.ParseFunctionBody(func): return None func.spids.append(left_spid) func.spids.append(after_name_spid) return func
def ParseCase(self): """ case_clause : Case WORD newline_ok in newline_ok case_list? Esac ; """ case_node = ast.Case() case_spid = word.LeftMostSpanForWord(self.cur_word) self._Next() # skip case if not self._Peek(): return None case_node.to_match = self.cur_word self._Next() if not self._NewlineOk(): return None in_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.KW_In): return None if not self._NewlineOk(): return None if self.c_id != Id.KW_Esac: # empty case list if not self.ParseCaseList(case_node.arms): self.AddErrorContext("ParseCase: error parsing case list") return None # TODO: should it return a list of nodes, and extend? if not self._Peek(): return None esac_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.KW_Esac): return None self._Next() case_node.spids.extend((case_spid, in_spid, esac_spid)) return case_node
def ParseCaseItem(self): """ case_item: '('? pattern ('|' pattern)* ')' newline_ok command_term? trailer? ; """ self.lexer.PushHint(Id.Op_RParen, Id.Right_CasePat) left_spid = word.LeftMostSpanForWord(self.cur_word) if self.c_id == Id.Op_LParen: self._Next() pat_words = [] while True: if not self._Peek(): return None pat_words.append(self.cur_word) self._Next() if not self._Peek(): return None if self.c_id == Id.Op_Pipe: self._Next() else: break rparen_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.Right_CasePat): return None if not self._NewlineOk(): return None if self.c_id not in (Id.Op_DSemi, Id.KW_Esac): c_list = self.ParseCommandTerm() if not c_list: return None action_children = c_list.children else: action_children = [] dsemi_spid = const.NO_INTEGER last_spid = const.NO_INTEGER if not self._Peek(): return None if self.c_id == Id.KW_Esac: last_spid = word.LeftMostSpanForWord(self.cur_word) elif self.c_id == Id.Op_DSemi: dsemi_spid = word.LeftMostSpanForWord(self.cur_word) self._Next() else: self.AddErrorContext('Expected DSEMI or ESAC, got %s', self.cur_word, word=self.cur_word) return None if not self._NewlineOk(): return None arm = ast.case_arm(pat_words, action_children) arm.spids.extend((left_spid, rparen_spid, dsemi_spid, last_spid)) return arm
def _SplitSimpleCommandPrefix(self, words): """ Second pass of SimpleCommand parsing: look for assignment words. """ prefix_bindings = [] suffix_words = [] done_prefix = False for w in words: if done_prefix: suffix_words.append(w) continue left_spid = word.LeftMostSpanForWord(w) kov = word.LooksLikeAssignment(w) if kov: k, op, v = kov t = word.TildeDetect(v) if t: # t is an unevaluated word with TildeSubPart prefix_bindings.append((k, op, t, left_spid)) else: prefix_bindings.append((k, op, v, left_spid)) # v is unevaluated word else: done_prefix = True suffix_words.append(w) return prefix_bindings, suffix_words
def _ValToArithOrError(self, val, int_coerce=True, blame_word=None, span_id=const.NO_INTEGER): if span_id == const.NO_INTEGER and blame_word: span_id = word.LeftMostSpanForWord(blame_word) #log('_ValToArithOrError span=%s blame=%s', span_id, blame_word) try: i = self._ValToArith(val, span_id, int_coerce=int_coerce) except util.FatalRuntimeError as e: if self.exec_opts.strict_arith: raise else: i = 0 span_id = dev.SpanIdFromError(e) if self.arena: # BoolEvaluator for test builtin doesn't have it. if span_id != const.NO_INTEGER: ui.PrintFilenameAndLine(span_id, self.arena) else: log('*** Warning has no location info ***') warn(e.UserErrorString()) return i
def PrettyPrintError(parse_error, arena, f): #print(parse_error) if parse_error.token: span_id = parse_error.token.span_id elif parse_error.word: # Can be -1 span_id = word.LeftMostSpanForWord(parse_error.word) else: span_id = -1 if span_id == -1: line = '<no position info for token>' path = '<unknown>' line_num = -1 col = -1 length = -1 else: line_span = arena.GetLineSpan(span_id) line_id = line_span.line_id line = arena.GetLine(line_id) path, line_num = arena.GetDebugInfo(line_id) col = line_span.col length = line_span.length print('Line %d of %r' % (line_num + 1, path), file=f) print(' ' + line.rstrip(), file=f) if col != -1: f.write(' ') # preserve tabs for c in line[:col]: f.write('\t' if c == '\t' else ' ') f.write('^') f.write('~' * (length - 1)) f.write('\n')
def ParseFunctionDef(self): """ function_header : fname '(' ')' function_def : function_header newline_ok function_body ; Precondition: Looking at the function name. Post condition: NOTE: There is an ambiguity with: function foo ( echo hi ) and function foo () ( echo hi ) Bash only accepts the latter, though it doesn't really follow a grammar. """ left_spid = word.LeftMostSpanForWord(self.cur_word) ok, name = word.AsFuncName(self.cur_word) if not ok: self.AddErrorContext("Invalid function name: %r", self.cur_word, word=self.cur_word) return None self._Next() # skip function name # Must be true beacuse of lookahead if not self._Peek(): return None assert self.c_id == Id.Op_LParen, self.cur_word self.lexer.PushHint(Id.Op_RParen, Id.Right_FuncDef) self._Next() if not self._Eat(Id.Right_FuncDef): return None after_name_spid = word.LeftMostSpanForWord(self.cur_word) + 1 if not self._NewlineOk(): return None func = ast.FuncDef() func.name = name if not self.ParseFunctionBody(func): return None func.spids.append(left_spid) func.spids.append(after_name_spid) return func
def ParseDoGroup(self): """ Used by ForEach, ForExpr, While, Until. Should this be a Do node? do_group : Do command_list Done ; /* Apply rule 6 */ """ if not self._Eat(Id.KW_Do): return None do_spid = word.LeftMostSpanForWord(self.cur_word) # after _Eat c_list = self.ParseCommandList() # could be any thing if not c_list: return None if not self._Eat(Id.KW_Done): return None done_spid = word.LeftMostSpanForWord(self.cur_word) # after _Eat node = ast.DoGroup(c_list.children) node.spids.extend((do_spid, done_spid)) return node
def _ParseForEachLoop(self): node = ast.ForEach() node.do_arg_iter = False ok, iter_name, quoted = word.StaticEval(self.cur_word) if not ok or quoted: self.AddErrorContext( "Invalid for loop variable", word=self.cur_word) return None if not VAR_NAME_RE.match(iter_name): self.AddErrorContext( "Invalid for loop variable name", word=self.cur_word) return None node.iter_name = iter_name self._Next() # skip past name if not self._NewlineOk(): return None in_spid = const.NO_INTEGER semi_spid = const.NO_INTEGER if not self._Peek(): return None if self.c_id == Id.KW_In: self._Next() # skip in in_spid = word.LeftMostSpanForWord(self.cur_word) + 1 x = self.ParseForWords() if x is None: return None iter_words, semi_spid = x words2 = braces.BraceDetectAll(iter_words) words3 = word.TildeDetectAll(words2) if iter_words is None: # empty list of words is OK return None node.iter_words = words3 elif self.c_id == Id.Op_Semi: node.do_arg_iter = True # implicit for loop self._Next() elif self.c_id == Id.KW_Do: node.do_arg_iter = True # implicit for loop # do not advance else: self.AddErrorContext("Unexpected word in for loop: %s", self.cur_word, word=self.cur_word) return None node.spids.extend((in_spid, semi_spid)) body_node = self.ParseDoGroup() if not body_node: return None node.body = body_node return node
def _assertSpanForWord(test, code_str): arena, w = _assertReadWordWithArena(test, code_str) span_id = word.LeftMostSpanForWord(w) print(code_str) print(span_id) if span_id != -1: span = arena.GetLineSpan(span_id) print(span)
def SpanIdFromError(error): #print(parse_error) if error.span_id != const.NO_INTEGER: return error.span_id if error.token: return error.token.span_id if error.part: return word.LeftMostSpanForPart(error.part) if error.word: return word.LeftMostSpanForWord(error.word) return const.NO_INTEGER
def ParseSubshell(self): left_spid = word.LeftMostSpanForWord(self.cur_word) self._Next() # skip past ( # Ensure that something $( (cd / && pwd) ) works. If ) is already on the # translation stack, we want to delay it. #print('ParseSubshell lexer.PushHint ) -> )') self.lexer.PushHint(Id.Op_RParen, Id.Right_Subshell) child = self.ParseCommandList() if not child: return None #print('SUB', self.cur_word) node = ast.Subshell() node.children.append(child) right_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.Right_Subshell): return None node.spids.extend((left_spid, right_spid)) return node
def _ParseElifElse(self, if_node): """ else_part: (Elif command_list Then command_list)* Else command_list ; """ arms = if_node.arms self._Peek() while self.c_id == Id.KW_Elif: elif_spid = word.LeftMostSpanForWord(self.cur_word) self._Next() # skip elif cond = self.ParseCommandList() if not cond: return None then_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.KW_Then): return None body = self.ParseCommandList() if not body: return None arm = ast.if_arm(cond.children, body.children) arm.spids.extend((elif_spid, then_spid)) arms.append(arm) if self.c_id == Id.KW_Else: else_spid = word.LeftMostSpanForWord(self.cur_word) self._Next() body = self.ParseCommandList() if not body: return None if_node.else_action = body.children else: else_spid = const.NO_INTEGER if_node.spids.append(else_spid) return True
def PrintError(error_stack, arena, f): """ NOTE: Parse errors always occur within a single arena. Runtime errors may span arenas (e.g. the function stack). """ # TODO: # - rename to PrintParseError() # - although parse errors happen at runtime because of 'source' # - should there be a distinction then? for parse_error in error_stack: print(parse_error) if parse_error.token: span_id = parse_error.token.span_id elif parse_error.word: # Can be -1 span_id = word.LeftMostSpanForWord(parse_error.word) else: span_id = -1 if span_id == -1: line = '<token had no position info>' path = '<unknown>' line_num = -1 col = -1 length = -1 else: line_span = arena.GetLineSpan(span_id) line_id = line_span.line_id line = arena.GetLine(line_id) path, line_num = arena.GetDebugInfo(line_id) col = line_span.col length = line_span.length print('Line %d of %r' % (line_num + 1, path)) print(' ' + line.rstrip()) if col == -1: print('NO COL') else: sys.stdout.write(' ') # preserve tabs for c in line[:col]: sys.stdout.write('\t' if c == '\t' else ' ') sys.stdout.write('^') sys.stdout.write('~' * (length - 1)) sys.stdout.write('\n') print(parse_error.UserErrorString(), file=f) print('---')
def _ParseForEachLoop(self): node = ast.ForEach() node.do_arg_iter = False ok, value, quoted = word.StaticEval(self.cur_word) if not ok or quoted: self.AddErrorContext( "Invalid for loop variable: %s", self.cur_word, word=self.cur_word) return None node.iter_name = value self._Next() # skip past name if not self._NewlineOk(): return None in_spid = -1 semi_spid = -1 if not self._Peek(): return None if self.c_id == Id.KW_In: self._Next() # skip in in_spid = word.LeftMostSpanForWord(self.cur_word) + 1 iter_words, semi_spid = self.ParseForWords() if iter_words is None: # empty list of words is OK return None node.iter_words = iter_words elif self.c_id == Id.Op_Semi: node.do_arg_iter = True # implicit for loop self._Next() elif self.c_id == Id.KW_Do: node.do_arg_iter = True # implicit for loop # do not advance else: self.AddErrorContext("Unexpected word in for loop: %s", self.cur_word, word=self.cur_word) return None node.spids.extend((in_spid, semi_spid)) body_node = self.ParseDoGroup() if not body_node: return None node.body = body_node return node
def ParseBraceGroup(self): """ brace_group : LBrace command_list RBrace ; """ left_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.Lit_LBrace): return None c_list = self.ParseCommandList() if not c_list: return None # Not needed #right_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.Lit_RBrace): return None node = ast.BraceGroup(c_list.children) node.spids.append(left_spid) return node
def _StringToIntegerOrError(self, s, blame_word=None, span_id=const.NO_INTEGER): """Used by both [[ $x -gt 3 ]] and (( $x )).""" if span_id == const.NO_INTEGER and blame_word: span_id = word.LeftMostSpanForWord(blame_word) try: i = _StringToInteger(s, span_id=span_id) except util.FatalRuntimeError as e: if self.exec_opts.strict_arith: raise else: i = 0 # TODO: Need the arena for printing this error? #ui.PrettyPrintError(e) warn(e.UserErrorString()) return i
def DoArithExpr(self, node, local_symbols): if node.tag == arith_expr_e.ArithBinary: # Maybe I should just write the left span and right span for each word? #self.f.write(str(node.left)) if node.op_id == Id.Arith_Plus: # NOTE: Right isn't necessarily a word! r_id = word.LeftMostSpanForWord(node.right.w) #self.cursor.SkipUntil(r_id) #self.f.write('PLUS') #self.f.write(str(node.right)) elif node.tag == arith_expr_e.ArithWord: self.DoWordInCommand(node.w, local_symbols) else: log("Unhandled: %s", node) raise AssertionError(node.tag)
def PrettyPrintError(parse_error, arena, f=sys.stderr): #print(parse_error) if parse_error.span_id != const.NO_INTEGER: span_id = parse_error.span_id elif parse_error.token: span_id = parse_error.token.span_id elif parse_error.part: span_id = word.LeftMostSpanForPart(parse_error.part) elif parse_error.word: span_id = word.LeftMostSpanForWord(parse_error.word) else: span_id = const.NO_INTEGER if span_id == const.NO_INTEGER: # Any clause above might return this. # This is usually a bug. print('*** Error has no source location info ***', file=f) return PrintFilenameAndLine(span_id, arena, f=f)
def ParseBraceGroup(self): """ brace_group : LBrace command_list RBrace ; """ left_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.Lit_LBrace): return None node = self.ParseCommandList() if not node: return None # Not needed #right_spid = word.LeftMostSpanForWord(self.cur_word) if not self._Eat(Id.Lit_RBrace): return None # OPTIMIZATION of AST. # CommandList has no redirects; BraceGroup may have redirects. if node.tag == command_e.CommandList: n = ast.BraceGroup(node.children) else: n = ast.BraceGroup([node]) n.spids.append(left_spid) return n
def _MakeAssignment(self, assign_kw, suffix_words): bindings = [] for i, w in enumerate(suffix_words): if i == 0: continue # skip over local, export, etc. left_spid = word.LeftMostSpanForWord(w) kv = word.LooksLikeAssignment(w) if kv: k, v = kv t = word.TildeDetect(v) if t: # t is an unevaluated word with TildeSubPart pair = (k, t, left_spid) else: pair = (k, v, left_spid) # v is unevaluated word else: # In aboriginal in variables/sources: export_if_blank does export "$1". # We should allow that. ok, value, quoted = word.StaticEval(w) if not ok or quoted: self.AddErrorContext( 'Variable names must be constant strings, got %s', w, word=w) return None pair = (value, None, left_spid) # No value is equivalent to '' bindings.append(pair) pairs = [] for lhs, rhs, spid in bindings: p = ast.assign_pair(ast.LeftVar(lhs), rhs) p.spids.append(spid) pairs.append(p) node = ast.Assignment(assign_kw, pairs) return node
def DoRedirect(self, node, local_symbols): #print(node, file=sys.stderr) op_spid = node.op.span_id op_id = node.op.id self.cursor.PrintUntil(op_spid) # TODO: # - Do < and <& the same way. # - How to handle here docs and here docs? # - >> becomes >+ or >-, or maybe >>> if node.tag == redir_e.Redir: if node.fd == const.NO_INTEGER: if op_id == Id.Redir_Great: self.f.write('>') # Allow us to replace the operator self.cursor.SkipUntil(op_spid + 1) elif op_id == Id.Redir_GreatAnd: self.f.write('> !') # Replace >& 2 with > !2 spid = word.LeftMostSpanForWord(node.arg_word) self.cursor.SkipUntil(spid) #self.DoWordInCommand(node.arg_word) else: # NOTE: Spacing like !2>err.txt vs !2 > err.txt can be done in the # formatter. self.f.write('!%d ' % node.fd) if op_id == Id.Redir_Great: self.f.write('>') self.cursor.SkipUntil(op_spid + 1) elif op_id == Id.Redir_GreatAnd: self.f.write('> !') # Replace 1>& 2 with !1 > !2 spid = word.LeftMostSpanForWord(node.arg_word) self.cursor.SkipUntil(spid) self.DoWordInCommand(node.arg_word, local_symbols) elif node.tag == redir_e.HereDoc: ok, delimiter, delim_quoted = word.StaticEval(node.here_begin) if not ok: p_die('Invalid here doc delimiter', word=node.here_begin) # Turn everything into <<. We just change the quotes self.f.write('<<') #here_begin_spid2 = word.RightMostSpanForWord(node.here_begin) if delim_quoted: self.f.write(" '''") else: self.f.write(' """') delim_end_spid = word.RightMostSpanForWord(node.here_begin) self.cursor.SkipUntil(delim_end_spid + 1) #self.cursor.SkipUntil(here_begin_spid + 1) # Now print the lines. TODO: Have a flag to indent these to the level of # the owning command, e.g. # cat <<EOF # EOF # Or since most here docs are the top level, you could just have a hack # for a fixed indent? TODO: Look at real use cases. for part in node.stdin_parts: self.DoWordPart(part, local_symbols) self.cursor.SkipUntil(node.here_end_span_id + 1) if delim_quoted: self.f.write("'''\n") else: self.f.write('"""\n') # Need #self.cursor.SkipUntil(here_end_spid2) else: raise AssertionError(node.__class__.__name__) # <<< 'here word' # << 'here word' # # 2> out.txt # !2 > out.txt # cat 1<< EOF # hello $name # EOF # cat !1 << """ # hello $name # """ # # cat << 'EOF' # no expansion # EOF # cat <<- 'EOF' # no expansion and indented # # cat << ''' # no expansion # ''' # cat << ''' # no expansion and indented # ''' # Warn about multiple here docs on a line. # As an obscure feature, allow # cat << \'ONE' << \"TWO" # 123 # ONE # 234 # TWO # The _ is an indicator that it's not a string to be piped in. pass
def ParseSimpleCommand(self): """ Fixed transcription of the POSIX grammar (TODO: port to grammar/Shell.g) io_file : '<' filename | LESSAND filename ... io_here : DLESS here_end | DLESSDASH here_end redirect : IO_NUMBER (io_redirect | io_here) prefix_part : ASSIGNMENT_WORD | redirect cmd_part : WORD | redirect assign_kw : Declare | Export | Local | Readonly # Without any words it is parsed as a command, not an assigment assign_listing : assign_kw # Now we have something to do (might be changing assignment flags too) # NOTE: any prefixes should be a warning, but they are allowed in shell. assignment : prefix_part* assign_kw (WORD | ASSIGNMENT_WORD)+ # an external command, a function call, or a builtin -- a "word_command" word_command : prefix_part* cmd_part+ simple_command : assign_listing | assignment | proc_command Simple imperative algorithm: 1) Read a list of words and redirects. Append them to separate lists. 2) Look for the first non-assignment word. If it's declare, etc., then keep parsing words AND assign words. Otherwise, just parse words. 3) If there are no non-assignment words, then it's a global assignment. { redirects, global assignments } OR { redirects, prefix_bindings, words } OR { redirects, ERROR_prefix_bindings, keyword, assignments, words } THEN CHECK that prefix bindings don't have any array literal parts! global assignment and keyword assignments can have the of course. well actually EXPORT shouldn't have them either -- WARNING 3 cases we want to warn: prefix_bindings for assignment, and array literal in prefix bindings, or export A command can be an assignment word, word, or redirect on its own. ls >out.txt >out.txt FOO=bar # this touches the file, and hten Or any sequence: ls foo bar <in.txt ls foo bar >out.txt <in.txt ls >out.txt foo bar Or add one or more environment bindings: VAR=val env >out.txt VAR=val env here_end vs filename is a matter of whether we test that it's quoted. e.g. <<EOF vs <<'EOF'. """ result = self._ScanSimpleCommand() if not result: return None redirects, words = result if not words: # e.g. >out.txt # redirect without words node = ast.SimpleCommand() node.redirects = redirects return node prefix_bindings, suffix_words = self._SplitSimpleCommandPrefix(words) if not suffix_words: # ONE=1 TWO=2 (with no other words) # TODO: Have a strict mode to prevent this? if redirects: # >out.txt g=foo print('WARNING: Got redirects in assignment: %s', redirects) pairs = [] for lhs, rhs, spid in prefix_bindings: p = ast.assign_pair(ast.LeftVar(lhs), rhs) p.spids.append(spid) pairs.append(p) node = ast.Assignment(Id.Assign_None, pairs) left_spid = word.LeftMostSpanForWord(words[0]) node.spids.append(left_spid) # no keyword spid to skip past return node assign_kw, keyword_spid = word.AssignmentBuiltinId(suffix_words[0]) if assign_kw == Id.Undefined_Tok: node = self._MakeSimpleCommand(prefix_bindings, suffix_words, redirects) return node if redirects: # TODO: Make it a warning, or do it in the second stage? print( 'WARNING: Got redirects in assignment: %s' % redirects, file=sys.stderr) if prefix_bindings: # FOO=bar local spam=eggs not allowed # Use the location of the first value. TODO: Use the whole word before # splitting. _, v0, _ = prefix_bindings[0] self.AddErrorContext( 'Invalid prefix bindings in assignment: %s', prefix_bindings, word=v0) return None node = self._MakeAssignment(assign_kw, suffix_words) if not node: return None node.spids.append(keyword_spid) return node
def ParseSimpleCommand(self): """ Fixed transcription of the POSIX grammar (TODO: port to grammar/Shell.g) io_file : '<' filename | LESSAND filename ... io_here : DLESS here_end | DLESSDASH here_end redirect : IO_NUMBER (io_redirect | io_here) prefix_part : ASSIGNMENT_WORD | redirect cmd_part : WORD | redirect assign_kw : Declare | Export | Local | Readonly # Without any words it is parsed as a command, not an assigment assign_listing : assign_kw # Now we have something to do (might be changing assignment flags too) # NOTE: any prefixes should be a warning, but they are allowed in shell. assignment : prefix_part* assign_kw (WORD | ASSIGNMENT_WORD)+ # an external command, a function call, or a builtin -- a "word_command" word_command : prefix_part* cmd_part+ simple_command : assign_listing | assignment | proc_command Simple imperative algorithm: 1) Read a list of words and redirects. Append them to separate lists. 2) Look for the first non-assignment word. If it's declare, etc., then keep parsing words AND assign words. Otherwise, just parse words. 3) If there are no non-assignment words, then it's a global assignment. { redirects, global assignments } OR { redirects, prefix_bindings, words } OR { redirects, ERROR_prefix_bindings, keyword, assignments, words } THEN CHECK that prefix bindings don't have any array literal parts! global assignment and keyword assignments can have the of course. well actually EXPORT shouldn't have them either -- WARNING 3 cases we want to warn: prefix_bindings for assignment, and array literal in prefix bindings, or export A command can be an assignment word, word, or redirect on its own. ls >out.txt >out.txt FOO=bar # this touches the file, and hten Or any sequence: ls foo bar <in.txt ls foo bar >out.txt <in.txt ls >out.txt foo bar Or add one or more environment bindings: VAR=val env >out.txt VAR=val env here_end vs filename is a matter of whether we test that it's quoted. e.g. <<EOF vs <<'EOF'. """ result = self._ScanSimpleCommand() if not result: return None redirects, words = result if not words: # e.g. >out.txt # redirect without words node = ast.SimpleCommand() node.redirects = redirects return node prefix_bindings, suffix_words = self._SplitSimpleCommandPrefix(words) if not suffix_words: # ONE=1 TWO=2 (with no other words) if redirects: binding1 = prefix_bindings[0] _, _, _, spid = binding1 self.AddErrorContext('Got redirects in global assignment', span_id=spid) return None pairs = [] for lhs, op, rhs, spid in prefix_bindings: p = ast.assign_pair(ast.LhsName(lhs), op, rhs) p.spids.append(spid) pairs.append(p) node = ast.Assignment(Id.Assign_None, [], pairs) left_spid = word.LeftMostSpanForWord(words[0]) node.spids.append(left_spid) # no keyword spid to skip past return node kind, kw_token = word.KeywordToken(suffix_words[0]) if kind == Kind.Assign: # Here we StaticEval suffix_words[1] to see if it's a command like # 'typeset -p'. Then it becomes a SimpleCommand node instead of an # Assignment. Note we're not handling duplicate flags like 'typeset # -pf'. I see this in bashdb (bash debugger) but it can just be changed # to 'typeset -p -f'. is_command = False if len(suffix_words) > 1: ok, val, _ = word.StaticEval(suffix_words[1]) if ok and (kw_token.id, val) in self._ASSIGN_COMMANDS: is_command = True if is_command: # declare -f, declare -p, typeset -p, etc. node = self._MakeSimpleCommand(prefix_bindings, suffix_words, redirects) return node else: # declare str='', declare -a array=() if redirects: # Attach the error location to the keyword. It would be more precise # to attach it to the self.AddErrorContext('Got redirects in assignment', token=kw_token) return None if prefix_bindings: # FOO=bar local spam=eggs not allowed # Use the location of the first value. TODO: Use the whole word before # splitting. _, _, v0, _ = prefix_bindings[0] self.AddErrorContext( 'Invalid prefix bindings in assignment: %s', prefix_bindings, word=v0) return None node = self._MakeAssignment(kw_token.id, suffix_words) if not node: return None node.spids.append(kw_token.span_id) return node elif kind == Kind.ControlFlow: if redirects: self.AddErrorContext('Got redirects in control flow: %s', redirects) return None if prefix_bindings: # FOO=bar local spam=eggs not allowed # Use the location of the first value. TODO: Use the whole word before # splitting. _, _, v0, _ = prefix_bindings[0] self.AddErrorContext( 'Invalid prefix bindings in control flow: %s', prefix_bindings, word=v0) return None # Attach the token for errors. (Assignment may not need it.) if len(suffix_words) == 1: arg_word = None elif len(suffix_words) == 2: arg_word = suffix_words[1] else: self.AddErrorContext('Too many arguments') return None return ast.ControlFlow(kw_token, arg_word) else: node = self._MakeSimpleCommand(prefix_bindings, suffix_words, redirects) return node
def _MakeAssignment(self, assign_kw, suffix_words): # First parse flags, e.g. -r -x -a -A. None of the flags have arguments. flags = [] n = len(suffix_words) i = 1 while i < n: w = suffix_words[i] ok, static_val, quoted = word.StaticEval(w) if not ok or quoted: break # can't statically evaluate if static_val.startswith('-'): flags.append(static_val) else: break # not a flag, rest are args i += 1 # Now parse bindings or variable names assignments = [] while i < n: w = suffix_words[i] left_spid = word.LeftMostSpanForWord(w) kov = word.LooksLikeAssignment(w) if kov: k, op, v = kov t = word.TildeDetect(v) if t: # t is an unevaluated word with TildeSubPart a = (k, op, t, left_spid) else: a = (k, op, v, left_spid) # v is unevaluated word else: # In aboriginal in variables/sources: export_if_blank does export "$1". # We should allow that. # Parse this differently then? # dynamic-export? # It sets global variables. ok, static_val, quoted = word.StaticEval(w) if not ok or quoted: self.AddErrorContext( 'Variable names must be constant strings, got %s', w, word=w) return None # No value is equivalent to '' m = VAR_NAME_RE.match(static_val) if not m: self.AddErrorContext('Invalid variable name %r', static_val, word=w) return None a = (static_val, assign_op_e.Equal, None, left_spid) assignments.append(a) i += 1 # TODO: Also make with LhsIndexedName pairs = [] for lhs, op, rhs, spid in assignments: p = ast.assign_pair(ast.LhsName(lhs), op, rhs) p.spids.append(spid) pairs.append(p) node = ast.Assignment(assign_kw, flags, pairs) return node
def DoRedirect(self, node, local_symbols): #print(node, file=sys.stderr) self.cursor.PrintUntil(node.spids[0]) # TODO: # - Do < and <& the same way. # - How to handle here docs and here docs? # - >> becomes >+ or >-, or maybe >>> if node.fd == -1: if node.op_id == Id.Redir_Great: self.f.write('>') # Allow us to replace the operator self.cursor.SkipUntil(node.spids[0] + 1) elif node.op_id == Id.Redir_GreatAnd: self.f.write('> !') # Replace >& 2 with > !2 spid = word.LeftMostSpanForWord(node.arg_word) self.cursor.SkipUntil(spid) #self.DoWordInCommand(node.arg_word) else: # NOTE: Spacing like !2>err.txt vs !2 > err.txt can be done in the # formatter. self.f.write('!%d ' % node.fd) if node.op_id == Id.Redir_Great: self.f.write('>') self.cursor.SkipUntil(node.spids[0] + 1) elif node.op_id == Id.Redir_GreatAnd: self.f.write('> !') # Replace 1>& 2 with !1 > !2 spid = word.LeftMostSpanForWord(node.arg_word) self.cursor.SkipUntil(spid) self.DoWordInCommand(node.arg_word, local_symbols) # <<< 'here word' # << 'here word' # # 2> out.txt # !2 > out.txt # cat 1<< EOF # hello $name # EOF # cat !1 << """ # hello $name # """ # # cat << 'EOF' # no expansion # EOF # cat <<- 'EOF' # no expansion and indented # # cat << ''' # no expansion # ''' # cat << ''' # no expansion and indented # ''' # Warn about multiple here docs on a line. # As an obscure feature, allow # cat << \'ONE' << \"TWO" # 123 # ONE # 234 # TWO # The _ is an indicator that it's not a string to be piped in. pass
def _Dispatch(self, node, fork_external): # If we call RunCommandSub in a recursive call to the executor, this will # be set true (if strict-errexit is false). But it only lasts for one # command. self.check_command_sub_status = False #argv0 = None # for error message check_errexit = False # for errexit if node.tag == command_e.SimpleCommand: check_errexit = True # Find span_id for a basic implementation of $LINENO, e.g. # PS4='+$SOURCE_NAME:$LINENO:' # NOTE: osh2oil uses node.more_env, but we don't need that. span_id = const.NO_INTEGER if node.words: first_word = node.words[0] span_id = word.LeftMostSpanForWord(first_word) self.mem.SetCurrentSpanId(span_id) # PROBLEM: We want to log argv in 'xtrace' mode, but we may have already # redirected here, which screws up logging. For example, 'echo hi # >/dev/null 2>&1'. We want to evaluate argv and log it BEFORE applying # redirects. # Another problem: # - tracing can be called concurrently from multiple processes, leading # to overlap. Maybe have a mode that creates a file per process. # xtrace-proc # - line numbers for every command would be very nice. But then you have # to print the filename too. words = braces.BraceExpandWords(node.words) argv = self.word_ev.EvalWordSequence(words) # This comes before evaluating env, in case there are problems evaluating # it. We could trace the env separately? Also trace unevaluated code # with set-o verbose? self.tracer.OnSimpleCommand(argv) if node.more_env: self.mem.PushTemp() try: for env_pair in node.more_env: val = self.word_ev.EvalWordToString(env_pair.val) # Set each var so the next one can reference it. Example: # FOO=1 BAR=$FOO ls / self.mem.SetVar(ast.LhsName(env_pair.name), val, (var_flags_e.Exported,), scope_e.TempEnv) # NOTE: This might never return! In the case of fork_external=False. status = self._RunSimpleCommand(argv, fork_external, span_id) finally: if node.more_env: self.mem.PopTemp() elif node.tag == command_e.Sentence: # Don't check_errexit since this isn't a real node! if node.terminator.id == Id.Op_Semi: status = self._Execute(node.child) else: status = self._RunJobInBackground(node.child) elif node.tag == command_e.Pipeline: check_errexit = True if node.stderr_indices: raise NotImplementedError('|&') if node.negated: self._PushErrExit() try: status2 = self._RunPipeline(node) finally: self._PopErrExit() # errexit is disabled for !. check_errexit = False status = 1 if status2 == 0 else 0 else: status = self._RunPipeline(node) elif node.tag == command_e.Subshell: check_errexit = True # This makes sure we don't waste a process if we'd launch one anyway. p = self._MakeProcess(node.child) status = p.Run(self.waiter) elif node.tag == command_e.DBracket: check_errexit = True result = self.bool_ev.Eval(node.expr) status = 0 if result else 1 elif node.tag == command_e.DParen: check_errexit = True i = self.arith_ev.Eval(node.child) status = 0 if i != 0 else 1 elif node.tag == command_e.Assignment: flags = word_compile.ParseAssignFlags(node.flags) if node.keyword == Id.Assign_Local: lookup_mode = scope_e.LocalOnly # typeset and declare are synonyms? I see typeset -a a=() the most. elif node.keyword in (Id.Assign_Declare, Id.Assign_Typeset): # declare is like local, except it can also be used outside functions? if var_flags_e.Global in flags: lookup_mode = scope_e.GlobalOnly else: lookup_mode = scope_e.LocalOnly elif node.keyword == Id.Assign_Readonly: lookup_mode = scope_e.Dynamic flags.append(var_flags_e.ReadOnly) elif node.keyword == Id.Assign_None: # mutate existing local or global lookup_mode = scope_e.Dynamic else: raise AssertionError(node.keyword) for pair in node.pairs: if pair.op == assign_op_e.PlusEqual: assert pair.rhs, pair.rhs # I don't think a+= is valid? val = self.word_ev.EvalRhsWord(pair.rhs) old_val, lval = expr_eval.EvalLhsAndLookup(pair.lhs, self.arith_ev, self.mem, self.exec_opts) sig = (old_val.tag, val.tag) if sig == (value_e.Undef, value_e.Str): pass # val is RHS elif sig == (value_e.Undef, value_e.StrArray): pass # val is RHS elif sig == (value_e.Str, value_e.Str): val = runtime.Str(old_val.s + val.s) elif sig == (value_e.Str, value_e.StrArray): e_die("Can't append array to string") elif sig == (value_e.StrArray, value_e.Str): e_die("Can't append string to array") elif sig == (value_e.StrArray, value_e.StrArray): val = runtime.StrArray(old_val.strs + val.strs) else: # plain assignment spid = pair.spids[0] # Source location for tracing lval = self._EvalLhs(pair.lhs, spid, lookup_mode) # RHS can be a string or array. if pair.rhs: val = self.word_ev.EvalRhsWord(pair.rhs) assert isinstance(val, runtime.value), val else: # e.g. 'readonly x' or 'local x' val = None # NOTE: In bash and mksh, declare -a myarray makes an empty cell with # Undef value, but the 'array' attribute. #log('setting %s to %s with flags %s', lval, val, flags) self.mem.SetVar(lval, val, flags, lookup_mode, strict_array=self.exec_opts.strict_array) # Assignment always appears to have a spid. if node.spids: current_spid = node.spids[0] else: current_spid = const.NO_INTEGER self.mem.SetCurrentSpanId(current_spid) self.tracer.OnAssignment(lval, pair.op, val, flags, lookup_mode) # PATCH to be compatible with existing shells: If the assignment had a # command sub like: # # s=$(echo one; false) # # then its status will be in mem.last_status, and we can check it here. # If there was NOT a command sub in the assignment, then we don't want to # check it. if node.keyword == Id.Assign_None: # mutate existing local or global # Only do this if there was a command sub? How? Look at node? # Set a flag in mem? self.mem.last_status or if self.check_command_sub_status: self._CheckStatus(self.mem.last_status, node) # A global assignment shouldn't clear $?. status = self.mem.last_status else: status = 0 else: # To be compatible with existing shells, local assignments DO clear # $?. Even in strict mode, we don't need to bother setting # check_errexit = True, because we would have already checked the # command sub in RunCommandSub. status = 0 # TODO: maybe we should have a "sane-status" that respects this: # false; echo $?; local f=x; echo $? elif node.tag == command_e.ControlFlow: if node.arg_word: # Evaluate the argument val = self.word_ev.EvalWordToString(node.arg_word) assert val.tag == value_e.Str arg = int(val.s) # They all take integers else: arg = 0 # return 0, exit 0, break 0 levels, etc. # NOTE: We don't do anything about a top-level 'return' here. Unlike in # bash, that is OK. If you can return from a sourced script, it makes # sense to return from a main script. ok = True tok = node.token if (tok.id in (Id.ControlFlow_Break, Id.ControlFlow_Continue) and self.loop_level == 0): ok = False msg = 'Invalid control flow at top level' if ok: raise _ControlFlow(tok, arg) if self.exec_opts.strict_control_flow: e_die(msg, token=tok) else: # Only print warnings, never fatal. # Bash oddly only exits 1 for 'return', but no other shell does. ui.PrintFilenameAndLine(tok.span_id, self.arena) util.warn(msg) status = 0 # The only difference between these two is that CommandList has no # redirects. We already took care of that above. elif node.tag in (command_e.CommandList, command_e.BraceGroup): status = self._ExecuteList(node.children) check_errexit = False elif node.tag == command_e.AndOr: # NOTE: && and || have EQUAL precedence in command mode. See case #13 # in dbracket.test.sh. left = node.children[0] # Suppress failure for every child except the last one. self._PushErrExit() try: status = self._Execute(left) finally: self._PopErrExit() i = 1 n = len(node.children) while i < n: #log('i %d status %d', i, status) child = node.children[i] op_id = node.ops[i-1] #log('child %s op_id %s', child, op_id) if op_id == Id.Op_DPipe and status == 0: i += 1 continue # short circuit elif op_id == Id.Op_DAmp and status != 0: i += 1 continue # short circuit if i == n - 1: # errexit handled differently for last child status = self._Execute(child) check_errexit = True else: self._PushErrExit() try: status = self._Execute(child) finally: self._PopErrExit() i += 1 elif node.tag == command_e.WhileUntil: if node.keyword.id == Id.KW_While: _DonePredicate = lambda status: status != 0 else: _DonePredicate = lambda status: status == 0 status = 0 self.loop_level += 1 try: while True: self._PushErrExit() try: cond_status = self._ExecuteList(node.cond) finally: self._PopErrExit() done = cond_status != 0 if _DonePredicate(cond_status): break try: status = self._Execute(node.body) # last one wins except _ControlFlow as e: if e.IsBreak(): status = 0 break elif e.IsContinue(): status = 0 continue else: # return needs to pop up more raise finally: self.loop_level -= 1 elif node.tag == command_e.ForEach: iter_name = node.iter_name if node.do_arg_iter: iter_list = self.mem.GetArgv() else: words = braces.BraceExpandWords(node.iter_words) iter_list = self.word_ev.EvalWordSequence(words) # We need word splitting and so forth # NOTE: This expands globs too. TODO: We should pass in a Globber() # object. status = 0 # in case we don't loop self.loop_level += 1 try: for x in iter_list: #log('> ForEach setting %r', x) state.SetLocalString(self.mem, iter_name, x) #log('<') try: status = self._Execute(node.body) # last one wins except _ControlFlow as e: if e.IsBreak(): status = 0 break elif e.IsContinue(): status = 0 else: # return needs to pop up more raise finally: self.loop_level -= 1 elif node.tag == command_e.ForExpr: status = 0 init, cond, body, update = node.init, node.cond, node.body, node.update if init: self.arith_ev.Eval(init) self.loop_level += 1 try: while True: if cond: b = self.arith_ev.Eval(cond) if not b: break try: status = self._Execute(body) except _ControlFlow as e: if e.IsBreak(): status = 0 break elif e.IsContinue(): status = 0 else: # return needs to pop up more raise if update: self.arith_ev.Eval(update) finally: self.loop_level -= 1 elif node.tag == command_e.DoGroup: status = self._ExecuteList(node.children) check_errexit = False # not real statements elif node.tag == command_e.FuncDef: # NOTE: Would it make sense to evaluate the redirects BEFORE entering? # It will save time on function calls. self.funcs[node.name] = node status = 0 elif node.tag == command_e.If: done = False for arm in node.arms: self._PushErrExit() try: status = self._ExecuteList(arm.cond) finally: self._PopErrExit() if status == 0: status = self._ExecuteList(arm.action) done = True break # TODO: The compiler should flatten this if not done and node.else_action is not None: status = self._ExecuteList(node.else_action) elif node.tag == command_e.NoOp: status = 0 # make it true elif node.tag == command_e.Case: val = self.word_ev.EvalWordToString(node.to_match) to_match = val.s status = 0 # If there are no arms, it should be zero? done = False for arm in node.arms: for pat_word in arm.pat_list: # NOTE: Is it OK that we're evaluating these as we go? # TODO: case "$@") shouldn't succeed? That's a type error? # That requires strict-array? pat_val = self.word_ev.EvalWordToString(pat_word, do_fnmatch=True) #log('Matching word %r against pattern %r', to_match, pat_val.s) if libc.fnmatch(pat_val.s, to_match): status = self._ExecuteList(arm.action) done = True # TODO: Parse ;;& and for fallthrough and such? break # Only execute action ONCE if done: break elif node.tag == command_e.TimeBlock: # TODO: # - When do we need RUSAGE_CHILDREN? # - Respect TIMEFORMAT environment variable. # "If this variable is not set, Bash acts as if it had the value" # $'\nreal\t%3lR\nuser\t%3lU\nsys\t%3lS' # "A trailing newline is added when the format string is displayed." start_t = time.time() # calls gettimeofday() under the hood start_u = resource.getrusage(resource.RUSAGE_SELF) status = self._Execute(node.pipeline) end_t = time.time() end_u = resource.getrusage(resource.RUSAGE_SELF) real = end_t - start_t user = end_u.ru_utime - start_u.ru_utime sys_ = end_u.ru_stime - start_u.ru_stime libc.print_time(real, user, sys_) else: raise NotImplementedError(node.__class__.__name__) return status, check_errexit
def DoCommand(self, node, local_symbols, at_top_level=False): if node.tag == command_e.CommandList: # TODO: How to distinguish between echo hi; echo bye; and on separate # lines for child in node.children: self.DoCommand(child, local_symbols, at_top_level=at_top_level) elif node.tag == command_e.SimpleCommand: # How to preserve spaces between words? Do you want to do it? # Well you need to test this: # # echo foo \ # bar # TODO: Need to print until the left most part of the phrase? the phrase # is a word, binding, redirect. #self.cursor.PrintUntil() if node.more_env: (left_spid,) = node.more_env[0].spids self.cursor.PrintUntil(left_spid) self.f.write('env ') # We only need to transform the right side, not left side. for pair in node.more_env: self.DoWordInCommand(pair.val, local_symbols) # More translations: # - . to source # - eval to sh-eval if node.words: first_word = node.words[0] ok, val, quoted = word.StaticEval(first_word) word0_spid = word.LeftMostSpanForWord(first_word) if ok and not quoted: if val == '[': last_word = node.words[-1] # Check if last word is ] ok, val, quoted = word.StaticEval(last_word) if ok and not quoted and val == ']': # Replace [ with 'test' self.cursor.PrintUntil(word0_spid) self.cursor.SkipUntil(word0_spid + 1) self.f.write('test') for w in node.words[1:-1]: self.DoWordInCommand(w, local_symbols) # Now omit ] last_spid = word.LeftMostSpanForWord(last_word) self.cursor.PrintUntil(last_spid - 1) # Get the space before self.cursor.SkipUntil(last_spid + 1) # ] takes one spid return else: raise RuntimeError('Got [ without ]') elif val == '.': self.cursor.PrintUntil(word0_spid) self.cursor.SkipUntil(word0_spid + 1) self.f.write('source') return for w in node.words: self.DoWordInCommand(w, local_symbols) # NOTE: This will change to "phrase"? Word or redirect. for r in node.redirects: self.DoRedirect(r, local_symbols) # TODO: Print the terminator. Could be \n or ; # Need to print env like PYTHONPATH = 'foo' && ls # Need to print redirects: # < > are the same. << is here string, and >> is assignment. # append is >+ # TODO: static_eval of simple command # - [ -> "test". Eliminate trailing ]. # - . -> source, etc. elif node.tag == command_e.Assignment: self.DoAssignment(node, at_top_level, local_symbols) elif node.tag == command_e.Pipeline: # Obscure: |& turns into |- or |+ for stderr. # TODO: # if ! true; then -> if not true { # if ! echo | grep; then -> if not { echo | grep } { # } # not is like do {}, but it negates the return value I guess. for child in node.children: self.DoCommand(child, local_symbols) elif node.tag == command_e.AndOr: for child in node.children: self.DoCommand(child, local_symbols) elif node.tag == command_e.Sentence: # 'ls &' to 'fork ls' # Keep ; the same. self.DoCommand(node.child, local_symbols) # This has to be different in the function case. elif node.tag == command_e.BraceGroup: # { echo hi; } -> do { echo hi } # For now it might be OK to keep 'do { echo hi; } #left_spid, right_spid = node.spids (left_spid,) = node.spids self.cursor.PrintUntil(left_spid) self.cursor.SkipUntil(left_spid + 1) self.f.write('do {') for child in node.children: self.DoCommand(child, local_symbols) elif node.tag == command_e.Subshell: # (echo hi) -> shell echo hi # (echo hi; echo bye) -> shell {echo hi; echo bye} (left_spid, right_spid) = node.spids self.cursor.PrintUntil(left_spid) self.cursor.SkipUntil(left_spid + 1) self.f.write('shell {') self.DoCommand(node.child, local_symbols) #self._DebugSpid(right_spid) #self._DebugSpid(right_spid + 1) #print('RIGHT SPID', right_spid) self.cursor.PrintUntil(right_spid) self.cursor.SkipUntil(right_spid + 1) self.f.write('}') elif node.tag == command_e.DParen: # (( a == 0 )) is sh-expr ' a == 0 ' # # NOTE: (( n++ )) is auto-translated to sh-expr 'n++', but could be set # n++. left_spid, right_spid = node.spids self.cursor.PrintUntil(left_spid) self.cursor.SkipUntil(left_spid + 1) self.f.write("sh-expr '") self.cursor.PrintUntil(right_spid - 1) # before )) self.cursor.SkipUntil(right_spid + 1) # after )) -- each one is a token self.f.write("'") elif node.tag == command_e.DBracket: # [[ 1 -eq 2 ]] to (1 == 2) self.DoBoolExpr(node.expr) elif node.tag == command_e.FuncDef: # TODO: skip name #self.f.write('proc %s' % node.name) # New symbol table for every function. new_local_symbols = {} # Should be the left most span, including 'function' self.cursor.PrintUntil(node.spids[0]) self.f.write('proc ') self.f.write(node.name) self.cursor.SkipUntil(node.spids[1]) if node.body.tag == command_e.BraceGroup: # Don't add "do" like a standalone brace group. Just use {}. for child in node.body.children: self.DoCommand(child, new_local_symbols) else: pass # Add {}. # proc foo { # shell {echo hi; echo bye} # } #self.DoCommand(node.body) elif node.tag == command_e.BraceGroup: for child in node.children: self.DoCommand(child, local_symbols) elif node.tag == command_e.DoGroup: do_spid, done_spid = node.spids self.cursor.PrintUntil(do_spid) self.cursor.SkipUntil(do_spid + 1) self.f.write('{') for child in node.children: self.DoCommand(child, local_symbols) self.cursor.PrintUntil(done_spid) self.cursor.SkipUntil(done_spid + 1) self.f.write('}') elif node.tag == command_e.ForEach: # Need to preserve spaces between words, because there can be line # wrapping. # for x in a b c \ # d e f; do in_spid, semi_spid = node.spids if in_spid == const.NO_INTEGER: #self.cursor.PrintUntil() # 'for x' and then space self.f.write('for %s in @Argv ' % node.iter_name) self.cursor.SkipUntil(node.body.spids[0]) else: self.cursor.PrintUntil(in_spid + 1) # 'for x in' and then space self.f.write('[') for w in node.iter_words: self.DoWordInCommand(w, local_symbols) self.f.write(']') #print("SKIPPING SEMI %d" % semi_spid, file=sys.stderr) if semi_spid != const.NO_INTEGER: self.cursor.PrintUntil(semi_spid) self.cursor.SkipUntil(semi_spid + 1) self.DoCommand(node.body, local_symbols) elif node.tag == command_e.ForExpr: # Change (( )) to ( ), and then _FixDoGroup pass elif node.tag == command_e.WhileUntil: # Skip 'until', and replace it with 'while not' if node.keyword.id == Id.KW_Until: kw_spid = node.keyword.span_id self.cursor.PrintUntil(kw_spid) self.f.write('while not') self.cursor.SkipUntil(kw_spid + 1) cond = node.cond # Skip the semi-colon in the condition, which is ususally a Sentence if len(cond) == 1 and cond[0].tag == command_e.Sentence: self.DoCommand(cond[0].child, local_symbols) semi_spid = cond[0].terminator.span_id self.cursor.SkipUntil(semi_spid + 1) self.DoCommand(node.body, local_symbols) elif node.tag == command_e.If: else_spid, fi_spid = node.spids # if foo; then -> if foo { # elif foo; then -> } elif foo { for arm in node.arms: elif_spid, then_spid = arm.spids if elif_spid != const.NO_INTEGER: self.cursor.PrintUntil(elif_spid) self.f.write('} ') cond = arm.cond if len(cond) == 1 and cond[0].tag == command_e.Sentence: sentence = cond[0] self.DoCommand(sentence, local_symbols) # Remove semi-colon semi_spid = sentence.terminator.span_id self.cursor.PrintUntil(semi_spid) self.cursor.SkipUntil(semi_spid + 1) else: for child in arm.cond: self.DoCommand(child, local_symbols) self.cursor.PrintUntil(then_spid) self.cursor.SkipUntil(then_spid + 1) self.f.write('{') for child in arm.action: self.DoCommand(child, local_symbols) # else -> } else { if node.else_action: self.cursor.PrintUntil(else_spid) self.f.write('} ') self.cursor.PrintUntil(else_spid + 1) self.f.write(' {') for child in node.else_action: self.DoCommand(child, local_symbols) # fi -> } self.cursor.PrintUntil(fi_spid) self.cursor.SkipUntil(fi_spid + 1) self.f.write('}') elif node.tag == command_e.Case: case_spid, in_spid, esac_spid = node.spids self.cursor.PrintUntil(case_spid) self.cursor.SkipUntil(case_spid + 1) self.f.write('match') # Reformat "$1" to $1 self.DoWordInCommand(node.to_match, local_symbols) self.cursor.PrintUntil(in_spid) self.cursor.SkipUntil(in_spid + 1) self.f.write('{') # matchstr $var { # each arm needs the ) and the ;; node to skip over? for arm in node.arms: left_spid, rparen_spid, dsemi_spid, last_spid = arm.spids #print(left_spid, rparen_spid, dsemi_spid) self.cursor.PrintUntil(left_spid) # Hm maybe keep | because it's semi-deprecated? You acn use # reload|force-relaod { # } # e/reload|force-reload/ { # } # / 'reload' or 'force-reload' / { # } # # Yeah it's the more abbreviated syntax. # change | to 'or' for pat in arm.pat_list: pass self.f.write('with ') # Remove the ) self.cursor.PrintUntil(rparen_spid) self.cursor.SkipUntil(rparen_spid + 1) for child in arm.action: self.DoCommand(child, local_symbols) if dsemi_spid != const.NO_INTEGER: # Remove ;; self.cursor.PrintUntil(dsemi_spid) self.cursor.SkipUntil(dsemi_spid + 1) elif last_spid != const.NO_INTEGER: self.cursor.PrintUntil(last_spid) else: raise AssertionError( "Expected with dsemi_spid or last_spid in case arm") self.cursor.PrintUntil(esac_spid) self.cursor.SkipUntil(esac_spid + 1) self.f.write('}') # strmatch $var { elif node.tag == command_e.NoOp: pass elif node.tag == command_e.ControlFlow: # No change for break / return / continue pass elif node.tag == command_e.TimeBlock: self.DoCommand(node.pipeline, local_symbols) else: #log('Command not handled: %s', node) raise AssertionError(node.__class__.__name__)