def _Visit(self, node): """ """ #log('VISIT %s', node.__class__.__name__) # NOTE: The tags are not unique!!! We would need this: # if isinstance(node, ast.command) and node.tag == command_e.Simple: # But it's easier to check the __class__ attribute. cls = node.__class__ if cls is command.Simple: #log('SimpleCommand %s', node.words) #log('--') #node.PrettyPrint() # Things to consider: # - source and . # - DONE builtins: get a list from builtin.py # - DONE functions: have to enter function definitions into a dictionary # - Commands that call others: sudo, su, find, xargs, etc. # - builtins that call others: exec, command # - except not command -v! if not node.words: return w = node.words[0] ok, argv0, _ = word_.StaticEval(w) if not ok: log("Couldn't statically evaluate %r", w) return if (builtin.ResolveSpecial(argv0) == builtin_e.NONE and builtin.ResolveAssign(argv0) == builtin_e.NONE and builtin.Resolve(argv0) == builtin_e.NONE): self.progs_used[argv0] = True # NOTE: If argv1 is $0, then we do NOT print a warning! if argv0 == 'sudo': if len(node.words) < 2: return w1 = node.words[1] ok, argv1, _ = word_.StaticEval(w1) if not ok: log("Couldn't statically evaluate %r", w) return # Should we mark them behind 'sudo'? e.g. "sudo apt install"? self.progs_used[argv1] = True elif cls is command.ShFunction: self.funcs_defined[node.name] = True
def testStaticEvalWord(self): expr = r'\EOF' # Quoted here doc delimiter w_parser = test_lib.InitWordParser(expr) w = w_parser.ReadWord(lex_mode_e.ShCommand) ok, s, quoted = word_.StaticEval(w) self.assertEqual(True, ok) self.assertEqual('EOF', s) self.assertEqual(True, quoted)
def _ReadPatSubVarOp(self): # type: () -> suffix_op__PatSub """ Match = ('/' | '#' | '%') WORD VarSub = ... | VarOf '/' Match '/' WORD """ # Exception: VSub_ArgUnquoted even if it's quoted # stop at eof_type=Lit_Slash, empty_ok=False UP_pat = self._ReadVarOpArg3(lex_mode_e.VSub_ArgUnquoted, Id.Lit_Slash, False) assert UP_pat.tag_() == word_e.Compound, UP_pat # Because empty_ok=False pat = cast(compound_word, UP_pat) if len(pat.parts) == 1: ok, s, quoted = word_.StaticEval(pat) if ok and s == '/' and not quoted: # Looks like ${a////c}, read again self._Next(lex_mode_e.VSub_ArgUnquoted) self._Peek() pat.parts.append(self.cur_token) if len(pat.parts) == 0: p_die('Pattern in ${x/pat/replace} must not be empty', token=self.cur_token) replace_mode = Id.Undefined_Tok # Check for / # % modifier on pattern. UP_first_part = pat.parts[0] if UP_first_part.tag_() == word_part_e.Literal: lit_id = cast(Token, UP_first_part).id if lit_id in (Id.Lit_Slash, Id.Lit_Pound, Id.Lit_Percent): pat.parts.pop(0) replace_mode = lit_id # NOTE: If there is a modifier, the pattern can be empty, e.g. # ${s/#/foo} and ${a/%/foo}. if self.token_type == Id.Right_DollarBrace: # e.g. ${v/a} is the same as ${v/a/} -- empty replacement string return suffix_op.PatSub(pat, None, replace_mode) if self.token_type == Id.Lit_Slash: replace = self._ReadVarOpArg(lex_mode_e.VSub_ArgUnquoted) # do not stop at / self._Peek() if self.token_type != Id.Right_DollarBrace: # NOTE: I think this never happens. # We're either in the VS_ARG_UNQ or VS_ARG_DQ lex state, and everything # there is Lit_ or Left_, except for }. p_die("Expected } after replacement string, got %s", ui.PrettyId(self.token_type), token=self.cur_token) return suffix_op.PatSub(pat, replace, replace_mode) # Happens with ${x//} and ${x///foo}, see test/parse-errors.sh p_die('Expected } or / to close pattern', token=self.cur_token)
def _ReadPatSubVarOp(self, lex_mode): # type: (lex_mode_t) -> suffix_op__PatSub """ Match = ('/' | '#' | '%') WORD VarSub = ... | VarOf '/' Match '/' WORD """ pat = self._ReadVarOpArg(lex_mode, eof_type=Id.Lit_Slash, empty_ok=False) assert isinstance(pat, word__Compound) # Because empty_ok=False if len(pat.parts) == 1: ok, s, quoted = word_.StaticEval(pat) if ok and s == '/' and not quoted: # Looks like ${a////c}, read again self._Next(lex_mode) self._Peek() p = word_part.Literal(self.cur_token) pat.parts.append(p) if len(pat.parts) == 0: p_die('Pattern in ${x/pat/replace} must not be empty', token=self.cur_token) replace_mode = Id.Undefined_Tok # Check for / # % modifier on pattern. first_part = pat.parts[0] if isinstance(first_part, word_part__Literal): lit_id = first_part.token.id if lit_id in (Id.Lit_Slash, Id.Lit_Pound, Id.Lit_Percent): pat.parts.pop(0) replace_mode = lit_id # NOTE: If there is a modifier, the pattern can be empty, e.g. # ${s/#/foo} and ${a/%/foo}. if self.token_type == Id.Right_DollarBrace: # e.g. ${v/a} is the same as ${v/a/} -- empty replacement string return suffix_op.PatSub(pat, None, replace_mode) if self.token_type == Id.Lit_Slash: replace = self._ReadVarOpArg(lex_mode) # do not stop at / self._Peek() if self.token_type != Id.Right_DollarBrace: # NOTE: I think this never happens. # We're either in the VS_ARG_UNQ or VS_ARG_DQ lex state, and everything # there is Lit_ or Left_, except for }. p_die("Expected } after replacement string, got %s", self.cur_token, token=self.cur_token) return suffix_op.PatSub(pat, replace, replace_mode) # Happens with ${x//} and ${x///foo}, see test/parse-errors.sh p_die("Expected } after pat sub, got %r", self.cur_token.val, token=self.cur_token)
def testGitComment(self): # ;# is a comment! Gah. # Conclusion: Comments are NOT LEXICAL. They are part of word parsing. node = assert_ParseCommandList( self, """\ . "$TEST_DIRECTORY"/diff-lib.sh ;# test-lib chdir's into trash """) self.assertEqual(command_e.Sentence, node.tag) self.assertEqual(2, len(node.child.words)) # This is NOT a comment node = assert_ParseCommandList(self, """\ echo foo#bar """) self.assertEqual(command_e.Simple, node.tag) self.assertEqual(2, len(node.words)) _, s, _ = word_.StaticEval(node.words[1]) self.assertEqual('foo#bar', s) # This is a comment node = assert_ParseCommandList(self, """\ echo foo #comment """) self.assertEqual(command_e.Simple, node.tag) self.assertEqual(2, len(node.words)) _, s, _ = word_.StaticEval(node.words[1]) self.assertEqual('foo', s) # Empty comment node = assert_ParseCommandList(self, """\ echo foo # """) self.assertEqual(command_e.Simple, node.tag) self.assertEqual(2, len(node.words)) _, s, _ = word_.StaticEval(node.words[1]) self.assertEqual('foo', s)
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.Simple: # 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.ShAssignment: self.DoShAssignment(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.ShFunction: # 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[2]) 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 == runtime.NO_SPID: #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 != runtime.NO_SPID: 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) if node.cond.tag_() == condition_e.Shell: commands = node.cond.commands # Skip the semi-colon in the condition, which is ususally a Sentence if len(commands) == 1 and commands[0].tag_( ) == command_e.Sentence: self.DoCommand(commands[0].child, local_symbols) semi_spid = commands[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 i, arm in enumerate(node.arms): elif_spid, then_spid = arm.spids if i != 0: # 'if' not 'elif' on the first arm self.cursor.PrintUntil(elif_spid) self.f.write('} ') cond = arm.cond if cond.tag_() == condition_e.Shell: if len( cond.commands ) == 1 and cond.commands[0].tag == command_e.Sentence: sentence = cond.commands[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 cond.commands: 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 != runtime.NO_SPID: # Remove ;; self.cursor.PrintUntil(dsemi_spid) self.cursor.SkipUntil(dsemi_spid + 1) elif last_spid != runtime.NO_SPID: 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__)
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 False: if node.fd == runtime.NO_SPID: 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: elif False: 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 testPatSub(self): w = _assertReadWord(self, '${var/pat/replace}') op = _GetSuffixOp(self, w) self.assertUnquoted('pat', op.pat) self.assertUnquoted('replace', op.replace) self.assertEqual(Id.Undefined_Tok, op.replace_mode) w = _assertReadWord(self, '${var//pat/replace}') # sub all op = _GetSuffixOp(self, w) self.assertUnquoted('pat', op.pat) self.assertUnquoted('replace', op.replace) self.assertEqual(Id.Lit_Slash, op.replace_mode) w = _assertReadWord(self, '${var/%pat/replace}') # prefix op = _GetSuffixOp(self, w) self.assertUnquoted('pat', op.pat) self.assertUnquoted('replace', op.replace) self.assertEqual(Id.Lit_Percent, op.replace_mode) w = _assertReadWord(self, '${var/#pat/replace}') # suffix op = _GetSuffixOp(self, w) self.assertUnquoted('pat', op.pat) self.assertUnquoted('replace', op.replace) self.assertEqual(Id.Lit_Pound, op.replace_mode) w = _assertReadWord(self, '${var/pat}') # no replacement w = _assertReadWord(self, '${var//pat}') # no replacement op = _GetSuffixOp(self, w) self.assertUnquoted('pat', op.pat) self.assertEqual(None, op.replace) self.assertEqual(Id.Lit_Slash, op.replace_mode) # replace with slash w = _assertReadWord(self, '${var/pat//}') op = _GetSuffixOp(self, w) self.assertUnquoted('pat', op.pat) self.assertUnquoted('/', op.replace) # replace with two slashes unquoted w = _assertReadWord(self, '${var/pat///}') op = _GetSuffixOp(self, w) self.assertUnquoted('pat', op.pat) self.assertUnquoted('//', op.replace) # replace with two slashes quoted w = _assertReadWord(self, '${var/pat/"//"}') op = _GetSuffixOp(self, w) self.assertUnquoted('pat', op.pat) ok, s, quoted = word_.StaticEval(op.replace) self.assertTrue(ok) self.assertEqual('//', s) self.assertTrue(quoted) # Real example found in the wild! # http://www.oilshell.org/blog/2016/11/07.html w = _assertReadWord(self, r'${var////\\/}') op = _GetSuffixOp(self, w) self.assertEqual(Id.Lit_Slash, op.replace_mode) self.assertUnquoted('/', op.pat) ok, s, quoted = word_.StaticEval(op.replace) self.assertTrue(ok) self.assertEqual(r'\/', s)
def assertUnquoted(self, expected, w): ok, s, quoted = word_.StaticEval(w) self.assertTrue(ok) self.assertEqual(expected, s) self.assertFalse(quoted)
def _visit_command_Simple(self, node): if not node.words: return w_ob1 = node.words[0] ok1, word1, _ = word_.StaticEval(w_ob1) if not ok1: logger.debug( "Couldn't statically evaluate 1st word object of command: %r", w_ob1) return else: self.record_word(w_ob1, word1) # If there's a second word, let's go ahead and pop it off. # We don't know we need it, but enough cases do that it's easier. if len(node.words) > 1: w_ob2 = node.words[1] ok2, word2, _ = word_.StaticEval(w_ob2) # we don't care if it succeeded, yet else: if word1 in WATCH_FIRSTWORDS: # just a chance to bail out on a broken script # may not be worth the code... raise Exception( "Trying to handle {:} but it lacks a required argument". format(word1), node, ) # CAUTION: some prefixable commands/builtins are ~infinitely nestable. # "builtin builtin builtin builtin builtin command whoami" is perfectly valid. # The current code won't see the dep on an external command 'whoami'. # TODO: Does it make sense to use the presence buildin/command etc # as smells that trigger extra scrutinty? i.e., "builtin source" may # be a reasonable smell that the script or something it sources overrides # source? # TODO: should builtin be here? Currently not because we don't want to replace them... if word1 in (".", "source", "sudo", "command", "eval", "exec", "alias"): logger.info("Visiting command: %s %s", word1, word2) if not ok2: logger.info(" Command is dynamic") # DEBUG: print(node) for part in w_ob2.parts: bad_token = find_dynamic_token(part) if bad_token: # Letting $1-style subs through for now. There are # In practice, these could be paths we want to # resolve, *or* be perfectly fine as is. if bad_token.id == Id.VSub_Number: # TODO: if there's a good way to walk back out # could record the outer context as sourcing # its arguments. But the naive version doesn't # give us much more than outright rejecting here. # A non-naive version would need to figure out # which numbered argument it was, be able to figure # out if set/shift were used to fiddle with it, # and walk it all the way back out to the string # passed into the original function call to treat # *that* as the token to resolve. return # TODO: practice below mostly considers part vars like # $HOME/blah or $PREFIX/blah, but there are other # patterns a more sophisticated version could address. # At the moment those would need to be manually # patched. I'd like to follow this definition back to # the vardef and register it for substitution if it's # a simple string, flatten here and reconsider. # Letting ${name}-style subs through only if they're in # a list of allowed names. (goal: require conscious # exceptions, but make them easy to add) elif (bad_token.id == Id.VSub_Name and bad_token.val in allowed_executable_varsubs): return # Letting $name-style subs through only if they're in # a list of allowed names. (goal: require conscious # exceptions, but make them easy to add) elif (bad_token.id == Id.VSub_DollarName # [1:] to leave off the $ and bad_token.val[1:] in allowed_executable_varsubs): return else: raise ResolutionError( # TODO: crap phrasing "Can't resolve %r with an argument that can't be statically parsed", w_ob1.parts[0].val, word=w_ob2, token=bad_token, status=2, arena=self.arena, ) raise Exception( "Not sure. I thought 'ok' was only False when we hit a dynamic token, but we just searched for a dynamic token and didn't find one. Reconsider everything you know.", part, w_ob2, ) else: logger.info(" Command is static") # No magic if word1 in ("sudo", "command", "eval", "exec"): self.record_word(w_ob2, word2) elif word1 in (".", "source"): # CAUTION: in a multi-module library, we'll have to think very carefully about how to look up targets in order to parse them, but *avoid* translating the source statement into an absolute URI. (If this is sticky, another option might be a post-substitute to replace the build-path with the output path?) target = lookup(word2) logger.debug("Looked up source: %r -> %r", word2, target) # it was already a valid absolute path if target == word2 and target[0] == "/": # TODO: I'm not sure if we should do anything about absolute paths # but if so, this is where we'd do it. # raise ResolutionError( # "Do we want to object to absolute paths like %r ?", # word2, # word=w_ob2, # status=2, # ) self.record_source(w_ob2, word2, target) # it seems to resolve relative filenames for files in the current # directory, no matter what path is set to... elif target == word2: # TODO: I guess these all need to get prefxed with $out? self.record_source(w_ob2, word2, target) # it resolved to a new location elif target: self.record_source(w_ob2, word2, target) # It didn't resolve, or it was an invalid absolute path else: # self.unresolved_source.add(target) # I was recording this, but maybe we should just raise an exception raise ResolutionError( "Unable to resolve source target %r to a known file", word2, word=w_ob2, status=2, arena=self.arena, ) elif word1 == "alias": # try to handle all observed alias representations alias = word2.strip("\"='").split("=")[0] self.aliases.add(alias)