xref: /OK3568_Linux_fs/yocto/poky/bitbake/lib/bb/codeparser.py (revision 4882a59341e53eb6f0b4789bf948001014eff981)
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