| 1 | #!/usr/bin/python
 | 
| 2 | from __future__ import print_function
 | 
| 3 | """
 | 
| 4 | deps.py
 | 
| 5 | """
 | 
| 6 | 
 | 
| 7 | import sys
 | 
| 8 | 
 | 
| 9 | from asdl import asdl_ as asdl
 | 
| 10 | from asdl import py_meta
 | 
| 11 | from core import builtin
 | 
| 12 | from core import util
 | 
| 13 | from core import word
 | 
| 14 | from osh.meta import ast, runtime
 | 
| 15 | from osh import ast_lib
 | 
| 16 | 
 | 
| 17 | command_e = ast.command_e
 | 
| 18 | builtin_e = runtime.builtin_e
 | 
| 19 | log = util.log
 | 
| 20 | 
 | 
| 21 | # TODO: Move to asdl/visitor.py?
 | 
| 22 | class Visitor(object):
 | 
| 23 |   # Python does introspection on method names:
 | 
| 24 |   # method = 'visit_' + node.__class__.__name__
 | 
| 25 |   # I'm using ASDL metaprogramming instead.
 | 
| 26 | 
 | 
| 27 |   def Visit(self, node):
 | 
| 28 |     raise NotImplementedError
 | 
| 29 | 
 | 
| 30 |   # Like ast.NodeVisitor().generic_visit!
 | 
| 31 |   def VisitChildren(self, node):
 | 
| 32 |     """
 | 
| 33 |     Args:
 | 
| 34 |       node: an ASDL node.
 | 
| 35 |     """
 | 
| 36 |     #print 'CHILD', node.ASDL_TYPE
 | 
| 37 | 
 | 
| 38 |     for name, _ in node.ASDL_TYPE.GetFields():
 | 
| 39 |       child = getattr(node, name)
 | 
| 40 |       #log('Considering child %s', name)
 | 
| 41 | 
 | 
| 42 |       if isinstance(child, list):
 | 
| 43 |         #log('Visiting child array %s', name)
 | 
| 44 |         for item in child:
 | 
| 45 |           # We have to check for compound objects on an INSTANCE basis, not a
 | 
| 46 |           # type basis, because sums can look like this:
 | 
| 47 |           # iterable = IterArgv | IterArray(word* words)
 | 
| 48 |           # We visit the latter but not the former.
 | 
| 49 |           if isinstance(item, py_meta.CompoundObj):
 | 
| 50 |             self.Visit(item)
 | 
| 51 |         continue
 | 
| 52 | 
 | 
| 53 |       if isinstance(child, py_meta.CompoundObj):
 | 
| 54 |         #log('Visiting child %s', name)
 | 
| 55 |         self.Visit(child)
 | 
| 56 |         continue
 | 
| 57 | 
 | 
| 58 | 
 | 
| 59 | class DepsVisitor(Visitor):
 | 
| 60 |   """
 | 
| 61 |   Output:
 | 
| 62 | 
 | 
| 63 |   type      name          resolved_name             source_path line_num
 | 
| 64 |   bin       cp            /usr/bin/cp               prog.sh     22
 | 
| 65 |   lib       functions.sh  /home/andy/src/functions  prog.sh     22
 | 
| 66 | 
 | 
| 67 |   TODO:
 | 
| 68 |   - Make this TSV2
 | 
| 69 |   - handle source and .
 | 
| 70 |   - flags like --path and --special exec
 | 
| 71 |   - need some knowledge of function scope.
 | 
| 72 |     f; f() { true; }  -- f is an exeternal binary!
 | 
| 73 |     g() { f; }; f() { true; }   -- f is a function!
 | 
| 74 | 
 | 
| 75 |   """
 | 
| 76 |   def __init__(self, f):
 | 
| 77 |     Visitor.__init__(self)
 | 
| 78 |     self.funcs_defined = {}
 | 
| 79 |     self.progs_used = {}
 | 
| 80 |     self.f = f
 | 
| 81 | 
 | 
| 82 |   def _Visit(self, node):
 | 
| 83 |     """
 | 
| 84 |     """
 | 
| 85 |     #log('VISIT %s', node.__class__.__name__)
 | 
| 86 | 
 | 
| 87 |     # NOTE: The tags are not unique!!!  We would need this:
 | 
| 88 |     # if isinstance(node, ast.command) and node.tag == command_e.SimpleCommand:
 | 
| 89 |     # But it's easier to check the __class__ attribute.
 | 
| 90 | 
 | 
| 91 |     cls = node.__class__
 | 
| 92 |     if cls is ast.SimpleCommand:
 | 
| 93 |       #log('SimpleCommand %s', node.words)
 | 
| 94 |       #log('--')
 | 
| 95 |       #ast_lib.PrettyPrint(node)
 | 
| 96 | 
 | 
| 97 |       # Things to consider:
 | 
| 98 |       # - source and .
 | 
| 99 |       # - DONE builtins: get a list from builtin.py
 | 
| 100 |       # - DONE functions: have to enter function definitions into a dictionary
 | 
| 101 |       # - Commands that call others: sudo, su, find, xargs, etc.
 | 
| 102 |       # - builtins that call others: exec, command
 | 
| 103 |       #   - except not command -v!
 | 
| 104 | 
 | 
| 105 |       if not node.words:
 | 
| 106 |         return
 | 
| 107 | 
 | 
| 108 |       w = node.words[0]
 | 
| 109 |       ok, argv0, _ = word.StaticEval(w)
 | 
| 110 |       if not ok:
 | 
| 111 |         log("Couldn't statically evaluate %r", w)
 | 
| 112 |         return
 | 
| 113 | 
 | 
| 114 |       if (builtin.ResolveSpecial(argv0) == builtin_e.NONE and
 | 
| 115 |           builtin.Resolve(argv0) == builtin_e.NONE):
 | 
| 116 |         self.progs_used[argv0] = True
 | 
| 117 | 
 | 
| 118 |       # NOTE: If argv1 is $0, then we do NOT print a warning!
 | 
| 119 |       if argv0 == 'sudo':
 | 
| 120 |         if len(node.words) < 2:
 | 
| 121 |           return
 | 
| 122 |         w1 = node.words[1]
 | 
| 123 |         ok, argv1, _ = word.StaticEval(w1)
 | 
| 124 |         if not ok:
 | 
| 125 |           log("Couldn't statically evaluate %r", w)
 | 
| 126 |           return
 | 
| 127 | 
 | 
| 128 |         # Should we mark them behind 'sudo'?  e.g. "sudo apt install"?
 | 
| 129 |         self.progs_used[argv1] = True
 | 
| 130 |         
 | 
| 131 |     elif cls is ast.FuncDef:
 | 
| 132 |       self.funcs_defined[node.name] = True
 | 
| 133 | 
 | 
| 134 |   def Visit(self, node):
 | 
| 135 |     self._Visit(node)
 | 
| 136 | 
 | 
| 137 |     # We always need to visit children, even for SimpleCommand, etc.  There
 | 
| 138 |     # could be command sub, e.g. even in redirect.  echo hi > $(cat out)
 | 
| 139 |     self.VisitChildren(node)
 | 
| 140 | 
 | 
| 141 |   def Emit(self, row):
 | 
| 142 |     # TSV-like format
 | 
| 143 |     self.f.write('\t'.join(row))
 | 
| 144 |     self.f.write('\n')
 | 
| 145 | 
 | 
| 146 |   def Done(self):
 | 
| 147 |     """Write a report."""
 | 
| 148 |     # TODO: Use self.Emit(), make it TSV.
 | 
| 149 |     for name in self.progs_used:
 | 
| 150 |       if name not in self.funcs_defined:
 | 
| 151 |         print(name)
 | 
| 152 | 
 | 
| 153 | 
 | 
| 154 | def Deps(node):
 | 
| 155 |   v = DepsVisitor(sys.stdout)
 | 
| 156 |   v.Visit(node)
 | 
| 157 |   v.Done()
 |