1*4882a593Smuzhiyun# 2*4882a593Smuzhiyun# Copyright BitBake Contributors 3*4882a593Smuzhiyun# 4*4882a593Smuzhiyun# SPDX-License-Identifier: GPL-2.0-only 5*4882a593Smuzhiyun# 6*4882a593Smuzhiyun 7*4882a593Smuzhiyun""" 8*4882a593SmuzhiyunBitBake code parser 9*4882a593Smuzhiyun 10*4882a593SmuzhiyunParses actual code (i.e. python and shell) for functions and in-line 11*4882a593Smuzhiyunexpressions. Used mainly to determine dependencies on other functions 12*4882a593Smuzhiyunand variables within the BitBake metadata. Also provides a cache for 13*4882a593Smuzhiyunthis information in order to speed up processing. 14*4882a593Smuzhiyun 15*4882a593Smuzhiyun(Not to be confused with the code that parses the metadata itself, 16*4882a593Smuzhiyunsee lib/bb/parse/ for that). 17*4882a593Smuzhiyun 18*4882a593SmuzhiyunNOTE: if you change how the parsers gather information you will almost 19*4882a593Smuzhiyuncertainly need to increment CodeParserCache.CACHE_VERSION below so that 20*4882a593Smuzhiyunany existing codeparser cache gets invalidated. Additionally you'll need 21*4882a593Smuzhiyunto increment __cache_version__ in cache.py in order to ensure that old 22*4882a593Smuzhiyunrecipe caches don't trigger "Taskhash mismatch" errors. 23*4882a593Smuzhiyun 24*4882a593Smuzhiyun""" 25*4882a593Smuzhiyun 26*4882a593Smuzhiyunimport ast 27*4882a593Smuzhiyunimport sys 28*4882a593Smuzhiyunimport codegen 29*4882a593Smuzhiyunimport logging 30*4882a593Smuzhiyunimport bb.pysh as pysh 31*4882a593Smuzhiyunimport bb.utils, bb.data 32*4882a593Smuzhiyunimport hashlib 33*4882a593Smuzhiyunfrom itertools import chain 34*4882a593Smuzhiyunfrom bb.pysh import pyshyacc, pyshlex 35*4882a593Smuzhiyunfrom bb.cache import MultiProcessCache 36*4882a593Smuzhiyun 37*4882a593Smuzhiyunlogger = logging.getLogger('BitBake.CodeParser') 38*4882a593Smuzhiyun 39*4882a593Smuzhiyundef bbhash(s): 40*4882a593Smuzhiyun return hashlib.sha256(s.encode("utf-8")).hexdigest() 41*4882a593Smuzhiyun 42*4882a593Smuzhiyundef check_indent(codestr): 43*4882a593Smuzhiyun """If the code is indented, add a top level piece of code to 'remove' the indentation""" 44*4882a593Smuzhiyun 45*4882a593Smuzhiyun i = 0 46*4882a593Smuzhiyun while codestr[i] in ["\n", "\t", " "]: 47*4882a593Smuzhiyun i = i + 1 48*4882a593Smuzhiyun 49*4882a593Smuzhiyun if i == 0: 50*4882a593Smuzhiyun return codestr 51*4882a593Smuzhiyun 52*4882a593Smuzhiyun if codestr[i-1] == "\t" or codestr[i-1] == " ": 53*4882a593Smuzhiyun if codestr[0] == "\n": 54*4882a593Smuzhiyun # Since we're adding a line, we need to remove one line of any empty padding 55*4882a593Smuzhiyun # to ensure line numbers are correct 56*4882a593Smuzhiyun codestr = codestr[1:] 57*4882a593Smuzhiyun return "if 1:\n" + codestr 58*4882a593Smuzhiyun 59*4882a593Smuzhiyun return codestr 60*4882a593Smuzhiyun 61*4882a593Smuzhiyun# A custom getstate/setstate using tuples is actually worth 15% cachesize by 62*4882a593Smuzhiyun# avoiding duplication of the attribute names! 63*4882a593Smuzhiyun 64*4882a593Smuzhiyun 65*4882a593Smuzhiyunclass SetCache(object): 66*4882a593Smuzhiyun def __init__(self): 67*4882a593Smuzhiyun self.setcache = {} 68*4882a593Smuzhiyun 69*4882a593Smuzhiyun def internSet(self, items): 70*4882a593Smuzhiyun 71*4882a593Smuzhiyun new = [] 72*4882a593Smuzhiyun for i in items: 73*4882a593Smuzhiyun new.append(sys.intern(i)) 74*4882a593Smuzhiyun s = frozenset(new) 75*4882a593Smuzhiyun h = hash(s) 76*4882a593Smuzhiyun if h in self.setcache: 77*4882a593Smuzhiyun return self.setcache[h] 78*4882a593Smuzhiyun self.setcache[h] = s 79*4882a593Smuzhiyun return s 80*4882a593Smuzhiyun 81*4882a593Smuzhiyuncodecache = SetCache() 82*4882a593Smuzhiyun 83*4882a593Smuzhiyunclass pythonCacheLine(object): 84*4882a593Smuzhiyun def __init__(self, refs, execs, contains): 85*4882a593Smuzhiyun self.refs = codecache.internSet(refs) 86*4882a593Smuzhiyun self.execs = codecache.internSet(execs) 87*4882a593Smuzhiyun self.contains = {} 88*4882a593Smuzhiyun for c in contains: 89*4882a593Smuzhiyun self.contains[c] = codecache.internSet(contains[c]) 90*4882a593Smuzhiyun 91*4882a593Smuzhiyun def __getstate__(self): 92*4882a593Smuzhiyun return (self.refs, self.execs, self.contains) 93*4882a593Smuzhiyun 94*4882a593Smuzhiyun def __setstate__(self, state): 95*4882a593Smuzhiyun (refs, execs, contains) = state 96*4882a593Smuzhiyun self.__init__(refs, execs, contains) 97*4882a593Smuzhiyun def __hash__(self): 98*4882a593Smuzhiyun l = (hash(self.refs), hash(self.execs)) 99*4882a593Smuzhiyun for c in sorted(self.contains.keys()): 100*4882a593Smuzhiyun l = l + (c, hash(self.contains[c])) 101*4882a593Smuzhiyun return hash(l) 102*4882a593Smuzhiyun def __repr__(self): 103*4882a593Smuzhiyun return " ".join([str(self.refs), str(self.execs), str(self.contains)]) 104*4882a593Smuzhiyun 105*4882a593Smuzhiyun 106*4882a593Smuzhiyunclass shellCacheLine(object): 107*4882a593Smuzhiyun def __init__(self, execs): 108*4882a593Smuzhiyun self.execs = codecache.internSet(execs) 109*4882a593Smuzhiyun 110*4882a593Smuzhiyun def __getstate__(self): 111*4882a593Smuzhiyun return (self.execs) 112*4882a593Smuzhiyun 113*4882a593Smuzhiyun def __setstate__(self, state): 114*4882a593Smuzhiyun (execs) = state 115*4882a593Smuzhiyun self.__init__(execs) 116*4882a593Smuzhiyun def __hash__(self): 117*4882a593Smuzhiyun return hash(self.execs) 118*4882a593Smuzhiyun def __repr__(self): 119*4882a593Smuzhiyun return str(self.execs) 120*4882a593Smuzhiyun 121*4882a593Smuzhiyunclass CodeParserCache(MultiProcessCache): 122*4882a593Smuzhiyun cache_file_name = "bb_codeparser.dat" 123*4882a593Smuzhiyun # NOTE: you must increment this if you change how the parsers gather information, 124*4882a593Smuzhiyun # so that an existing cache gets invalidated. Additionally you'll need 125*4882a593Smuzhiyun # to increment __cache_version__ in cache.py in order to ensure that old 126*4882a593Smuzhiyun # recipe caches don't trigger "Taskhash mismatch" errors. 127*4882a593Smuzhiyun CACHE_VERSION = 11 128*4882a593Smuzhiyun 129*4882a593Smuzhiyun def __init__(self): 130*4882a593Smuzhiyun MultiProcessCache.__init__(self) 131*4882a593Smuzhiyun self.pythoncache = self.cachedata[0] 132*4882a593Smuzhiyun self.shellcache = self.cachedata[1] 133*4882a593Smuzhiyun self.pythoncacheextras = self.cachedata_extras[0] 134*4882a593Smuzhiyun self.shellcacheextras = self.cachedata_extras[1] 135*4882a593Smuzhiyun 136*4882a593Smuzhiyun # To avoid duplication in the codeparser cache, keep 137*4882a593Smuzhiyun # a lookup of hashes of objects we already have 138*4882a593Smuzhiyun self.pythoncachelines = {} 139*4882a593Smuzhiyun self.shellcachelines = {} 140*4882a593Smuzhiyun 141*4882a593Smuzhiyun def newPythonCacheLine(self, refs, execs, contains): 142*4882a593Smuzhiyun cacheline = pythonCacheLine(refs, execs, contains) 143*4882a593Smuzhiyun h = hash(cacheline) 144*4882a593Smuzhiyun if h in self.pythoncachelines: 145*4882a593Smuzhiyun return self.pythoncachelines[h] 146*4882a593Smuzhiyun self.pythoncachelines[h] = cacheline 147*4882a593Smuzhiyun return cacheline 148*4882a593Smuzhiyun 149*4882a593Smuzhiyun def newShellCacheLine(self, execs): 150*4882a593Smuzhiyun cacheline = shellCacheLine(execs) 151*4882a593Smuzhiyun h = hash(cacheline) 152*4882a593Smuzhiyun if h in self.shellcachelines: 153*4882a593Smuzhiyun return self.shellcachelines[h] 154*4882a593Smuzhiyun self.shellcachelines[h] = cacheline 155*4882a593Smuzhiyun return cacheline 156*4882a593Smuzhiyun 157*4882a593Smuzhiyun def init_cache(self, d): 158*4882a593Smuzhiyun # Check if we already have the caches 159*4882a593Smuzhiyun if self.pythoncache: 160*4882a593Smuzhiyun return 161*4882a593Smuzhiyun 162*4882a593Smuzhiyun MultiProcessCache.init_cache(self, d) 163*4882a593Smuzhiyun 164*4882a593Smuzhiyun # cachedata gets re-assigned in the parent 165*4882a593Smuzhiyun self.pythoncache = self.cachedata[0] 166*4882a593Smuzhiyun self.shellcache = self.cachedata[1] 167*4882a593Smuzhiyun 168*4882a593Smuzhiyun def create_cachedata(self): 169*4882a593Smuzhiyun data = [{}, {}] 170*4882a593Smuzhiyun return data 171*4882a593Smuzhiyun 172*4882a593Smuzhiyuncodeparsercache = CodeParserCache() 173*4882a593Smuzhiyun 174*4882a593Smuzhiyundef parser_cache_init(d): 175*4882a593Smuzhiyun codeparsercache.init_cache(d) 176*4882a593Smuzhiyun 177*4882a593Smuzhiyundef parser_cache_save(): 178*4882a593Smuzhiyun codeparsercache.save_extras() 179*4882a593Smuzhiyun 180*4882a593Smuzhiyundef parser_cache_savemerge(): 181*4882a593Smuzhiyun codeparsercache.save_merge() 182*4882a593Smuzhiyun 183*4882a593SmuzhiyunLogger = logging.getLoggerClass() 184*4882a593Smuzhiyunclass BufferedLogger(Logger): 185*4882a593Smuzhiyun def __init__(self, name, level=0, target=None): 186*4882a593Smuzhiyun Logger.__init__(self, name) 187*4882a593Smuzhiyun self.setLevel(level) 188*4882a593Smuzhiyun self.buffer = [] 189*4882a593Smuzhiyun self.target = target 190*4882a593Smuzhiyun 191*4882a593Smuzhiyun def handle(self, record): 192*4882a593Smuzhiyun self.buffer.append(record) 193*4882a593Smuzhiyun 194*4882a593Smuzhiyun def flush(self): 195*4882a593Smuzhiyun for record in self.buffer: 196*4882a593Smuzhiyun if self.target.isEnabledFor(record.levelno): 197*4882a593Smuzhiyun self.target.handle(record) 198*4882a593Smuzhiyun self.buffer = [] 199*4882a593Smuzhiyun 200*4882a593Smuzhiyunclass DummyLogger(): 201*4882a593Smuzhiyun def flush(self): 202*4882a593Smuzhiyun return 203*4882a593Smuzhiyun 204*4882a593Smuzhiyunclass PythonParser(): 205*4882a593Smuzhiyun getvars = (".getVar", ".appendVar", ".prependVar", "oe.utils.conditional") 206*4882a593Smuzhiyun getvarflags = (".getVarFlag", ".appendVarFlag", ".prependVarFlag") 207*4882a593Smuzhiyun containsfuncs = ("bb.utils.contains", "base_contains") 208*4882a593Smuzhiyun containsanyfuncs = ("bb.utils.contains_any", "bb.utils.filter") 209*4882a593Smuzhiyun execfuncs = ("bb.build.exec_func", "bb.build.exec_task") 210*4882a593Smuzhiyun 211*4882a593Smuzhiyun def warn(self, func, arg): 212*4882a593Smuzhiyun """Warn about calls of bitbake APIs which pass a non-literal 213*4882a593Smuzhiyun argument for the variable name, as we're not able to track such 214*4882a593Smuzhiyun a reference. 215*4882a593Smuzhiyun """ 216*4882a593Smuzhiyun 217*4882a593Smuzhiyun try: 218*4882a593Smuzhiyun funcstr = codegen.to_source(func) 219*4882a593Smuzhiyun argstr = codegen.to_source(arg) 220*4882a593Smuzhiyun except TypeError: 221*4882a593Smuzhiyun self.log.debug2('Failed to convert function and argument to source form') 222*4882a593Smuzhiyun else: 223*4882a593Smuzhiyun self.log.debug(self.unhandled_message % (funcstr, argstr)) 224*4882a593Smuzhiyun 225*4882a593Smuzhiyun def visit_Call(self, node): 226*4882a593Smuzhiyun name = self.called_node_name(node.func) 227*4882a593Smuzhiyun if name and (name.endswith(self.getvars) or name.endswith(self.getvarflags) or name in self.containsfuncs or name in self.containsanyfuncs): 228*4882a593Smuzhiyun if isinstance(node.args[0], ast.Str): 229*4882a593Smuzhiyun varname = node.args[0].s 230*4882a593Smuzhiyun if name in self.containsfuncs and isinstance(node.args[1], ast.Str): 231*4882a593Smuzhiyun if varname not in self.contains: 232*4882a593Smuzhiyun self.contains[varname] = set() 233*4882a593Smuzhiyun self.contains[varname].add(node.args[1].s) 234*4882a593Smuzhiyun elif name in self.containsanyfuncs and isinstance(node.args[1], ast.Str): 235*4882a593Smuzhiyun if varname not in self.contains: 236*4882a593Smuzhiyun self.contains[varname] = set() 237*4882a593Smuzhiyun self.contains[varname].update(node.args[1].s.split()) 238*4882a593Smuzhiyun elif name.endswith(self.getvarflags): 239*4882a593Smuzhiyun if isinstance(node.args[1], ast.Str): 240*4882a593Smuzhiyun self.references.add('%s[%s]' % (varname, node.args[1].s)) 241*4882a593Smuzhiyun else: 242*4882a593Smuzhiyun self.warn(node.func, node.args[1]) 243*4882a593Smuzhiyun else: 244*4882a593Smuzhiyun self.references.add(varname) 245*4882a593Smuzhiyun else: 246*4882a593Smuzhiyun self.warn(node.func, node.args[0]) 247*4882a593Smuzhiyun elif name and name.endswith(".expand"): 248*4882a593Smuzhiyun if isinstance(node.args[0], ast.Str): 249*4882a593Smuzhiyun value = node.args[0].s 250*4882a593Smuzhiyun d = bb.data.init() 251*4882a593Smuzhiyun parser = d.expandWithRefs(value, self.name) 252*4882a593Smuzhiyun self.references |= parser.references 253*4882a593Smuzhiyun self.execs |= parser.execs 254*4882a593Smuzhiyun for varname in parser.contains: 255*4882a593Smuzhiyun if varname not in self.contains: 256*4882a593Smuzhiyun self.contains[varname] = set() 257*4882a593Smuzhiyun self.contains[varname] |= parser.contains[varname] 258*4882a593Smuzhiyun elif name in self.execfuncs: 259*4882a593Smuzhiyun if isinstance(node.args[0], ast.Str): 260*4882a593Smuzhiyun self.var_execs.add(node.args[0].s) 261*4882a593Smuzhiyun else: 262*4882a593Smuzhiyun self.warn(node.func, node.args[0]) 263*4882a593Smuzhiyun elif name and isinstance(node.func, (ast.Name, ast.Attribute)): 264*4882a593Smuzhiyun self.execs.add(name) 265*4882a593Smuzhiyun 266*4882a593Smuzhiyun def called_node_name(self, node): 267*4882a593Smuzhiyun """Given a called node, return its original string form""" 268*4882a593Smuzhiyun components = [] 269*4882a593Smuzhiyun while node: 270*4882a593Smuzhiyun if isinstance(node, ast.Attribute): 271*4882a593Smuzhiyun components.append(node.attr) 272*4882a593Smuzhiyun node = node.value 273*4882a593Smuzhiyun elif isinstance(node, ast.Name): 274*4882a593Smuzhiyun components.append(node.id) 275*4882a593Smuzhiyun return '.'.join(reversed(components)) 276*4882a593Smuzhiyun else: 277*4882a593Smuzhiyun break 278*4882a593Smuzhiyun 279*4882a593Smuzhiyun def __init__(self, name, log): 280*4882a593Smuzhiyun self.name = name 281*4882a593Smuzhiyun self.var_execs = set() 282*4882a593Smuzhiyun self.contains = {} 283*4882a593Smuzhiyun self.execs = set() 284*4882a593Smuzhiyun self.references = set() 285*4882a593Smuzhiyun self._log = log 286*4882a593Smuzhiyun # Defer init as expensive 287*4882a593Smuzhiyun self.log = DummyLogger() 288*4882a593Smuzhiyun 289*4882a593Smuzhiyun self.unhandled_message = "in call of %s, argument '%s' is not a string literal" 290*4882a593Smuzhiyun self.unhandled_message = "while parsing %s, %s" % (name, self.unhandled_message) 291*4882a593Smuzhiyun 292*4882a593Smuzhiyun def parse_python(self, node, lineno=0, filename="<string>"): 293*4882a593Smuzhiyun if not node or not node.strip(): 294*4882a593Smuzhiyun return 295*4882a593Smuzhiyun 296*4882a593Smuzhiyun h = bbhash(str(node)) 297*4882a593Smuzhiyun 298*4882a593Smuzhiyun if h in codeparsercache.pythoncache: 299*4882a593Smuzhiyun self.references = set(codeparsercache.pythoncache[h].refs) 300*4882a593Smuzhiyun self.execs = set(codeparsercache.pythoncache[h].execs) 301*4882a593Smuzhiyun self.contains = {} 302*4882a593Smuzhiyun for i in codeparsercache.pythoncache[h].contains: 303*4882a593Smuzhiyun self.contains[i] = set(codeparsercache.pythoncache[h].contains[i]) 304*4882a593Smuzhiyun return 305*4882a593Smuzhiyun 306*4882a593Smuzhiyun if h in codeparsercache.pythoncacheextras: 307*4882a593Smuzhiyun self.references = set(codeparsercache.pythoncacheextras[h].refs) 308*4882a593Smuzhiyun self.execs = set(codeparsercache.pythoncacheextras[h].execs) 309*4882a593Smuzhiyun self.contains = {} 310*4882a593Smuzhiyun for i in codeparsercache.pythoncacheextras[h].contains: 311*4882a593Smuzhiyun self.contains[i] = set(codeparsercache.pythoncacheextras[h].contains[i]) 312*4882a593Smuzhiyun return 313*4882a593Smuzhiyun 314*4882a593Smuzhiyun # Need to parse so take the hit on the real log buffer 315*4882a593Smuzhiyun self.log = BufferedLogger('BitBake.Data.PythonParser', logging.DEBUG, self._log) 316*4882a593Smuzhiyun 317*4882a593Smuzhiyun # We can't add to the linenumbers for compile, we can pad to the correct number of blank lines though 318*4882a593Smuzhiyun node = "\n" * int(lineno) + node 319*4882a593Smuzhiyun code = compile(check_indent(str(node)), filename, "exec", 320*4882a593Smuzhiyun ast.PyCF_ONLY_AST) 321*4882a593Smuzhiyun 322*4882a593Smuzhiyun for n in ast.walk(code): 323*4882a593Smuzhiyun if n.__class__.__name__ == "Call": 324*4882a593Smuzhiyun self.visit_Call(n) 325*4882a593Smuzhiyun 326*4882a593Smuzhiyun self.execs.update(self.var_execs) 327*4882a593Smuzhiyun 328*4882a593Smuzhiyun codeparsercache.pythoncacheextras[h] = codeparsercache.newPythonCacheLine(self.references, self.execs, self.contains) 329*4882a593Smuzhiyun 330*4882a593Smuzhiyunclass ShellParser(): 331*4882a593Smuzhiyun def __init__(self, name, log): 332*4882a593Smuzhiyun self.funcdefs = set() 333*4882a593Smuzhiyun self.allexecs = set() 334*4882a593Smuzhiyun self.execs = set() 335*4882a593Smuzhiyun self._name = name 336*4882a593Smuzhiyun self._log = log 337*4882a593Smuzhiyun # Defer init as expensive 338*4882a593Smuzhiyun self.log = DummyLogger() 339*4882a593Smuzhiyun 340*4882a593Smuzhiyun self.unhandled_template = "unable to handle non-literal command '%s'" 341*4882a593Smuzhiyun self.unhandled_template = "while parsing %s, %s" % (name, self.unhandled_template) 342*4882a593Smuzhiyun 343*4882a593Smuzhiyun def parse_shell(self, value): 344*4882a593Smuzhiyun """Parse the supplied shell code in a string, returning the external 345*4882a593Smuzhiyun commands it executes. 346*4882a593Smuzhiyun """ 347*4882a593Smuzhiyun 348*4882a593Smuzhiyun h = bbhash(str(value)) 349*4882a593Smuzhiyun 350*4882a593Smuzhiyun if h in codeparsercache.shellcache: 351*4882a593Smuzhiyun self.execs = set(codeparsercache.shellcache[h].execs) 352*4882a593Smuzhiyun return self.execs 353*4882a593Smuzhiyun 354*4882a593Smuzhiyun if h in codeparsercache.shellcacheextras: 355*4882a593Smuzhiyun self.execs = set(codeparsercache.shellcacheextras[h].execs) 356*4882a593Smuzhiyun return self.execs 357*4882a593Smuzhiyun 358*4882a593Smuzhiyun # Need to parse so take the hit on the real log buffer 359*4882a593Smuzhiyun self.log = BufferedLogger('BitBake.Data.%s' % self._name, logging.DEBUG, self._log) 360*4882a593Smuzhiyun 361*4882a593Smuzhiyun self._parse_shell(value) 362*4882a593Smuzhiyun self.execs = set(cmd for cmd in self.allexecs if cmd not in self.funcdefs) 363*4882a593Smuzhiyun 364*4882a593Smuzhiyun codeparsercache.shellcacheextras[h] = codeparsercache.newShellCacheLine(self.execs) 365*4882a593Smuzhiyun 366*4882a593Smuzhiyun return self.execs 367*4882a593Smuzhiyun 368*4882a593Smuzhiyun def _parse_shell(self, value): 369*4882a593Smuzhiyun try: 370*4882a593Smuzhiyun tokens, _ = pyshyacc.parse(value, eof=True, debug=False) 371*4882a593Smuzhiyun except Exception: 372*4882a593Smuzhiyun bb.error('Error during parse shell code, the last 5 lines are:\n%s' % '\n'.join(value.split('\n')[-5:])) 373*4882a593Smuzhiyun raise 374*4882a593Smuzhiyun 375*4882a593Smuzhiyun self.process_tokens(tokens) 376*4882a593Smuzhiyun 377*4882a593Smuzhiyun def process_tokens(self, tokens): 378*4882a593Smuzhiyun """Process a supplied portion of the syntax tree as returned by 379*4882a593Smuzhiyun pyshyacc.parse. 380*4882a593Smuzhiyun """ 381*4882a593Smuzhiyun 382*4882a593Smuzhiyun def function_definition(value): 383*4882a593Smuzhiyun self.funcdefs.add(value.name) 384*4882a593Smuzhiyun return [value.body], None 385*4882a593Smuzhiyun 386*4882a593Smuzhiyun def case_clause(value): 387*4882a593Smuzhiyun # Element 0 of each item in the case is the list of patterns, and 388*4882a593Smuzhiyun # Element 1 of each item in the case is the list of commands to be 389*4882a593Smuzhiyun # executed when that pattern matches. 390*4882a593Smuzhiyun words = chain(*[item[0] for item in value.items]) 391*4882a593Smuzhiyun cmds = chain(*[item[1] for item in value.items]) 392*4882a593Smuzhiyun return cmds, words 393*4882a593Smuzhiyun 394*4882a593Smuzhiyun def if_clause(value): 395*4882a593Smuzhiyun main = chain(value.cond, value.if_cmds) 396*4882a593Smuzhiyun rest = value.else_cmds 397*4882a593Smuzhiyun if isinstance(rest, tuple) and rest[0] == "elif": 398*4882a593Smuzhiyun return chain(main, if_clause(rest[1])) 399*4882a593Smuzhiyun else: 400*4882a593Smuzhiyun return chain(main, rest) 401*4882a593Smuzhiyun 402*4882a593Smuzhiyun def simple_command(value): 403*4882a593Smuzhiyun return None, chain(value.words, (assign[1] for assign in value.assigns)) 404*4882a593Smuzhiyun 405*4882a593Smuzhiyun token_handlers = { 406*4882a593Smuzhiyun "and_or": lambda x: ((x.left, x.right), None), 407*4882a593Smuzhiyun "async": lambda x: ([x], None), 408*4882a593Smuzhiyun "brace_group": lambda x: (x.cmds, None), 409*4882a593Smuzhiyun "for_clause": lambda x: (x.cmds, x.items), 410*4882a593Smuzhiyun "function_definition": function_definition, 411*4882a593Smuzhiyun "if_clause": lambda x: (if_clause(x), None), 412*4882a593Smuzhiyun "pipeline": lambda x: (x.commands, None), 413*4882a593Smuzhiyun "redirect_list": lambda x: ([x.cmd], None), 414*4882a593Smuzhiyun "subshell": lambda x: (x.cmds, None), 415*4882a593Smuzhiyun "while_clause": lambda x: (chain(x.condition, x.cmds), None), 416*4882a593Smuzhiyun "until_clause": lambda x: (chain(x.condition, x.cmds), None), 417*4882a593Smuzhiyun "simple_command": simple_command, 418*4882a593Smuzhiyun "case_clause": case_clause, 419*4882a593Smuzhiyun } 420*4882a593Smuzhiyun 421*4882a593Smuzhiyun def process_token_list(tokens): 422*4882a593Smuzhiyun for token in tokens: 423*4882a593Smuzhiyun if isinstance(token, list): 424*4882a593Smuzhiyun process_token_list(token) 425*4882a593Smuzhiyun continue 426*4882a593Smuzhiyun name, value = token 427*4882a593Smuzhiyun try: 428*4882a593Smuzhiyun more_tokens, words = token_handlers[name](value) 429*4882a593Smuzhiyun except KeyError: 430*4882a593Smuzhiyun raise NotImplementedError("Unsupported token type " + name) 431*4882a593Smuzhiyun 432*4882a593Smuzhiyun if more_tokens: 433*4882a593Smuzhiyun self.process_tokens(more_tokens) 434*4882a593Smuzhiyun 435*4882a593Smuzhiyun if words: 436*4882a593Smuzhiyun self.process_words(words) 437*4882a593Smuzhiyun 438*4882a593Smuzhiyun process_token_list(tokens) 439*4882a593Smuzhiyun 440*4882a593Smuzhiyun def process_words(self, words): 441*4882a593Smuzhiyun """Process a set of 'words' in pyshyacc parlance, which includes 442*4882a593Smuzhiyun extraction of executed commands from $() blocks, as well as grabbing 443*4882a593Smuzhiyun the command name argument. 444*4882a593Smuzhiyun """ 445*4882a593Smuzhiyun 446*4882a593Smuzhiyun words = list(words) 447*4882a593Smuzhiyun for word in list(words): 448*4882a593Smuzhiyun wtree = pyshlex.make_wordtree(word[1]) 449*4882a593Smuzhiyun for part in wtree: 450*4882a593Smuzhiyun if not isinstance(part, list): 451*4882a593Smuzhiyun continue 452*4882a593Smuzhiyun 453*4882a593Smuzhiyun if part[0] in ('`', '$('): 454*4882a593Smuzhiyun command = pyshlex.wordtree_as_string(part[1:-1]) 455*4882a593Smuzhiyun self._parse_shell(command) 456*4882a593Smuzhiyun 457*4882a593Smuzhiyun if word[0] in ("cmd_name", "cmd_word"): 458*4882a593Smuzhiyun if word in words: 459*4882a593Smuzhiyun words.remove(word) 460*4882a593Smuzhiyun 461*4882a593Smuzhiyun usetoken = False 462*4882a593Smuzhiyun for word in words: 463*4882a593Smuzhiyun if word[0] in ("cmd_name", "cmd_word") or \ 464*4882a593Smuzhiyun (usetoken and word[0] == "TOKEN"): 465*4882a593Smuzhiyun if "=" in word[1]: 466*4882a593Smuzhiyun usetoken = True 467*4882a593Smuzhiyun continue 468*4882a593Smuzhiyun 469*4882a593Smuzhiyun cmd = word[1] 470*4882a593Smuzhiyun if cmd.startswith("$"): 471*4882a593Smuzhiyun self.log.debug(self.unhandled_template % cmd) 472*4882a593Smuzhiyun elif cmd == "eval": 473*4882a593Smuzhiyun command = " ".join(word for _, word in words[1:]) 474*4882a593Smuzhiyun self._parse_shell(command) 475*4882a593Smuzhiyun else: 476*4882a593Smuzhiyun self.allexecs.add(cmd) 477*4882a593Smuzhiyun break 478