| 1 | from __future__ import print_function
 | 
| 2 | """
 | 
| 3 | fix.py -- Do source transformations.  Somewhat like 'go fix'.
 | 
| 4 | 
 | 
| 5 | TODO: Change := to =, and var/const/set
 | 
| 6 | """
 | 
| 7 | 
 | 
| 8 | import sys
 | 
| 9 | 
 | 
| 10 | from asdl import const
 | 
| 11 | from core import util
 | 
| 12 | from core import word
 | 
| 13 | from osh.meta import ast, Id
 | 
| 14 | 
 | 
| 15 | from _devbuild.gen import runtime_asdl
 | 
| 16 | 
 | 
| 17 | word_style_e = runtime_asdl.word_style_e
 | 
| 18 | 
 | 
| 19 | log = util.log
 | 
| 20 | 
 | 
| 21 | command_e = ast.command_e
 | 
| 22 | redir_e = ast.redir_e
 | 
| 23 | word_e = ast.word_e
 | 
| 24 | word_part_e = ast.word_part_e
 | 
| 25 | arith_expr_e = ast.arith_expr_e
 | 
| 26 | bool_expr_e = ast.bool_expr_e
 | 
| 27 | lhs_expr_e = ast.lhs_expr_e
 | 
| 28 | 
 | 
| 29 | 
 | 
| 30 | class Cursor(object):
 | 
| 31 |   """
 | 
| 32 |   Wrapper for printing/transforming a complete source file stored in a single
 | 
| 33 |   arena.
 | 
| 34 |   """
 | 
| 35 |   def __init__(self, arena, f):
 | 
| 36 |     self.arena = arena
 | 
| 37 |     self.f = f
 | 
| 38 |     self.next_span_id = 0
 | 
| 39 | 
 | 
| 40 |   def PrintUntil(self, until_span_id):
 | 
| 41 |     # Sometimes we add +1
 | 
| 42 |     assert until_span_id < const.NO_INTEGER, 'Missing span ID, got %d' % until_span_id
 | 
| 43 |     #log('PrintUntil %d', until_span_id)
 | 
| 44 |     for span_id in range(self.next_span_id, until_span_id):
 | 
| 45 |       #log('Looking up span id %d', span_id)
 | 
| 46 |       span = self.arena.GetLineSpan(span_id)
 | 
| 47 |       #log('SPAN %s', span)
 | 
| 48 | 
 | 
| 49 |       line = self.arena.GetLine(span.line_id)
 | 
| 50 |       piece = line[span.col : span.col + span.length]
 | 
| 51 |       self.f.write(piece)
 | 
| 52 |       # Spacing
 | 
| 53 |       #self.f.write('%r' % piece)
 | 
| 54 |     #self.f.write('__')
 | 
| 55 | 
 | 
| 56 |     self.next_span_id = until_span_id
 | 
| 57 | 
 | 
| 58 |   def SkipUntil(self, next_span_id):
 | 
| 59 |     """Skip everything before next_span_id.
 | 
| 60 |     Printing will start at next_span_id
 | 
| 61 |     """
 | 
| 62 |     assert next_span_id != const.NO_INTEGER, next_span_id
 | 
| 63 |     self.next_span_id = next_span_id
 | 
| 64 | 
 | 
| 65 | 
 | 
| 66 | def PrintAsOil(arena, node, debug_spans):
 | 
| 67 |   #print node
 | 
| 68 |   #print(spans)
 | 
| 69 |   if debug_spans:
 | 
| 70 |     for i, span in enumerate(arena.spans):
 | 
| 71 |       line = arena.GetLine(span.line_id)
 | 
| 72 |       piece = line[span.col : span.col + span.length]
 | 
| 73 |       print('%5d %r' % (i, piece), file=sys.stderr)
 | 
| 74 |     print('(%d spans)' % len(arena.spans), file=sys.stderr)
 | 
| 75 | 
 | 
| 76 |   cursor = Cursor(arena, sys.stdout)
 | 
| 77 |   fixer = OilPrinter(cursor, arena, sys.stdout)
 | 
| 78 |   fixer.DoCommand(node, None, at_top_level=True)  # no local symbols yet
 | 
| 79 |   fixer.End()
 | 
| 80 | 
 | 
| 81 | 
 | 
| 82 |     # Cases:
 | 
| 83 |     #
 | 
| 84 |     # - Does it look like $foo?
 | 
| 85 |     #   - Pedantic mode, then:
 | 
| 86 |     #     x = @split(foo)          No globbing here!
 | 
| 87 |     #                              @split($1) or @1 ?
 | 
| 88 |     #     @-foo @-1 in expression mode
 | 
| 89 |     #     And then for command mode, you will have *@1 and *@foo.  Split first
 | 
| 90 |     #     then glob.
 | 
| 91 |     #
 | 
| 92 |     #   - Nice mode, then foo
 | 
| 93 |     #     --assume no-word-splitting
 | 
| 94 |     # - Does it look like $(( 1 + 2 )) ?  or $(echo hi)
 | 
| 95 |     #   pedantic mode:  $(1 + 2) or @[echo hi]   ?
 | 
| 96 |     #   nice mode: $(1 + 2) or $[echo hi]
 | 
| 97 |     #
 | 
| 98 |     # - Does it look like "$foo" or "${foo:-}"?  Then it's just x = foo
 | 
| 99 |     #   x = foo or 'default'
 | 
| 100 |     # - Does it contain any substitutions?  Then whole thing is double quoted
 | 
| 101 |     # - Otherwise single quoted
 | 
| 102 |     #
 | 
| 103 |     # PROBLEM: ~ substitution.  That is disabled by "".
 | 
| 104 |     # You can turn it into $HOME I guess
 | 
| 105 |     # const foo = $HOME/hello
 | 
| 106 |     # const foo = $~/bar  # hm I kind of don't like this but OK
 | 
| 107 |     # const foo = "$~/bar"
 | 
| 108 |     # const foo = [ ~/bar ][0]  # does this make sense?
 | 
| 109 |     # const foo = `~/bar`
 | 
| 110 | 
 | 
| 111 |     # I think ~ should be like $ -- special.  Maybe even inside double quotes?
 | 
| 112 |     # Or only at the front?
 | 
| 113 | 
 | 
| 114 | 
 | 
| 115 | # QEFS is wrong?  Because RHS never gets split!  It can always be foo=$1/foo.
 | 
| 116 | # Not used because RHS not split:
 | 
| 117 | # $x -> @-x  and  ${x} -> @-x
 | 
| 118 | # ${x:-default}  ->  @-(x or 'default')
 | 
| 119 | 
 | 
| 120 | def _GetRhsStyle(w):
 | 
| 121 |   # NOTE: Pattern matching style would be a lot nicer for this...
 | 
| 122 | 
 | 
| 123 |   # Arith and command sub both retain $() and $[], so they are not pure
 | 
| 124 |   # "expressions".
 | 
| 125 |   VAR_SUBS = (word_part_e.SimpleVarSub, word_part_e.BracedVarSub,
 | 
| 126 |               word_part_e.TildeSubPart)
 | 
| 127 |   OTHER_SUBS = (word_part_e.CommandSubPart, word_part_e.ArithSubPart)
 | 
| 128 | 
 | 
| 129 |   ALL_SUBS = VAR_SUBS + OTHER_SUBS
 | 
| 130 | 
 | 
| 131 |   # Actually splitting NEVER HAPPENS ON ASSIGNMENT.  LEAVE IT OFF.
 | 
| 132 | 
 | 
| 133 |   if len(w.parts) == 0:
 | 
| 134 |     raise AssertionError(w)
 | 
| 135 | 
 | 
| 136 |   elif len(w.parts) == 1:
 | 
| 137 |     part0 = w.parts[0]
 | 
| 138 |     if part0.tag in VAR_SUBS:
 | 
| 139 |       # $x -> x  and  ${x} -> x  and ${x:-default} -> x or 'default'
 | 
| 140 |       # ~ -> homedir()
 | 
| 141 |       # ~andy -> homedir('andy')
 | 
| 142 |       # tilde()
 | 
| 143 |       # tilde('andy') ?
 | 
| 144 |       return word_style_e.Expr
 | 
| 145 |     elif part0.tag in OTHER_SUBS:
 | 
| 146 |       return word_style_e.Unquoted
 | 
| 147 | 
 | 
| 148 |     elif part0.tag == word_part_e.DoubleQuotedPart:
 | 
| 149 |       if len(part0.parts) == 1:
 | 
| 150 |         dq_part0 = part0.parts[0]
 | 
| 151 |         # "$x" -> x  and  "${x}" -> x  and "${x:-default}" -> x or 'default'
 | 
| 152 |         if dq_part0.tag in VAR_SUBS:
 | 
| 153 |           return word_style_e.Expr
 | 
| 154 |         elif dq_part0.tag in OTHER_SUBS:
 | 
| 155 |           return word_style_e.Unquoted
 | 
| 156 | 
 | 
| 157 |   # Tilde subs also cause double quoted style.
 | 
| 158 |   for part in w.parts:
 | 
| 159 |     if part.tag == word_part_e.DoubleQuotedPart:
 | 
| 160 |       for dq_part in part.parts:
 | 
| 161 |         if dq_part.tag in ALL_SUBS:
 | 
| 162 |           return word_style_e.DQ
 | 
| 163 |     elif part.tag in ALL_SUBS:
 | 
| 164 |       return word_style_e.DQ
 | 
| 165 | 
 | 
| 166 |   return word_style_e.SQ
 | 
| 167 | 
 | 
| 168 | 
 | 
| 169 | # TODO: Change to --assume, and have a default for each one?
 | 
| 170 | #
 | 
| 171 | # NICE mode: Assume that the user isn't relying on word splitting.  A lot of
 | 
| 172 | # users want this!
 | 
| 173 | #
 | 
| 174 | # Problem cases:
 | 
| 175 | #
 | 
| 176 | # for name in $(find ...); do echo $name; done
 | 
| 177 | #
 | 
| 178 | # This doesn't split.  Heuristic:
 | 
| 179 | #
 | 
| 180 | # This should be a bunch of flags:
 | 
| 181 | #
 | 
| 182 | # --assume 'no-word-splitting no-undefined' etc.
 | 
| 183 | #   globals-defined-first-outside-func (then we can generated := vs. ::=)
 | 
| 184 | # --split-output-from-commands 'find ls'  # tokenize these
 | 
| 185 | 
 | 
| 186 | # Special case: "find" is assumed to produce multiple things that you will want
 | 
| 187 | # to split?  But that doesn't go within function calls.  Hm.
 | 
| 188 | #
 | 
| 189 | # $(find -type f) -> @[find -type f]
 | 
| 190 | 
 | 
| 191 | NICE = 0
 | 
| 192 | 
 | 
| 193 | # Try to convert with pedantic correctness.  Not sure if users will want this
 | 
| 194 | # though.  Most people are not super principled about their shell programs.
 | 
| 195 | # But experts might want it.  Experts might want to run ShellCheck first and
 | 
| 196 | # quote everything, and then everything will be unquoted.
 | 
| 197 | #
 | 
| 198 | # "$foo" "${foo}" -> $foo $foo
 | 
| 199 | # $foo -> @-foo   -> split then glob?
 | 
| 200 | #         *@foo    maybe
 | 
| 201 | # $(find -type f) -> @[find -type f]
 | 
| 202 | 
 | 
| 203 | PEDANTIC = 1
 | 
| 204 | 
 | 
| 205 | 
 | 
| 206 | class OilPrinter(object):
 | 
| 207 |   """
 | 
| 208 |   Convert osh code to oil.
 | 
| 209 | 
 | 
| 210 |   - command invocations
 | 
| 211 |     - find invocations
 | 
| 212 |     - xargs
 | 
| 213 |   """
 | 
| 214 |   def __init__(self, cursor, arena, f, mode=NICE):
 | 
| 215 |     self.cursor = cursor
 | 
| 216 |     self.arena = arena
 | 
| 217 |     self.f = f
 | 
| 218 |     # In PEDANTIC mode, we translate unquoted $foo to @-foo, which means it will
 | 
| 219 |     # be split and globbed?
 | 
| 220 |     self.mode = mode
 | 
| 221 | 
 | 
| 222 |   def _DebugSpid(self, spid):
 | 
| 223 |     span = self.arena.GetLineSpan(spid)
 | 
| 224 |     line = self.arena.GetLine(span.line_id)
 | 
| 225 |     # TODO: This should be factored out
 | 
| 226 |     s = line[span.col : span.col + span.length]
 | 
| 227 |     print('SPID %d = %r' % (spid, s), file=sys.stderr)
 | 
| 228 | 
 | 
| 229 |   def End(self):
 | 
| 230 |     """Make sure we print until the end of the file."""
 | 
| 231 |     end_id = len(self.arena.spans)
 | 
| 232 |     self.cursor.PrintUntil(end_id)
 | 
| 233 | 
 | 
| 234 |   def DoRedirect(self, node, local_symbols):
 | 
| 235 |     #print(node, file=sys.stderr)
 | 
| 236 |     self.cursor.PrintUntil(node.spids[0])
 | 
| 237 | 
 | 
| 238 |     # TODO:
 | 
| 239 |     # - Do < and <& the same way.
 | 
| 240 |     # - How to handle here docs and here docs?
 | 
| 241 |     # - >> becomes >+ or >-, or maybe >>>
 | 
| 242 | 
 | 
| 243 |     if node.tag == redir_e.Redir:
 | 
| 244 |       if node.fd == const.NO_INTEGER:
 | 
| 245 |         if node.op_id == Id.Redir_Great:
 | 
| 246 |           self.f.write('>')  # Allow us to replace the operator
 | 
| 247 |           self.cursor.SkipUntil(node.spids[0] + 1)
 | 
| 248 |         elif node.op_id == Id.Redir_GreatAnd:
 | 
| 249 |           self.f.write('> !')  # Replace >& 2 with > !2
 | 
| 250 |           spid = word.LeftMostSpanForWord(node.arg_word)
 | 
| 251 |           self.cursor.SkipUntil(spid)
 | 
| 252 |           #self.DoWordInCommand(node.arg_word)
 | 
| 253 | 
 | 
| 254 |       else:
 | 
| 255 |         # NOTE: Spacing like !2>err.txt vs !2 > err.txt can be done in the
 | 
| 256 |         # formatter.
 | 
| 257 |         self.f.write('!%d ' % node.fd)
 | 
| 258 |         if node.op_id == Id.Redir_Great:
 | 
| 259 |           self.f.write('>')
 | 
| 260 |           self.cursor.SkipUntil(node.spids[0] + 1)
 | 
| 261 |         elif node.op_id == Id.Redir_GreatAnd:
 | 
| 262 |           self.f.write('> !')  # Replace 1>& 2 with !1 > !2
 | 
| 263 |           spid = word.LeftMostSpanForWord(node.arg_word)
 | 
| 264 |           self.cursor.SkipUntil(spid)
 | 
| 265 | 
 | 
| 266 |       self.DoWordInCommand(node.arg_word, local_symbols)
 | 
| 267 | 
 | 
| 268 |     elif node.tag == redir_e.HereDoc:
 | 
| 269 |       # TODO:
 | 
| 270 |       # If do_expansion, then """, else '''
 | 
| 271 |       # HereDoc LST node needs spids for both opening and closing delimiter.
 | 
| 272 |       raise NotImplementedError(node.__class__.__name__)
 | 
| 273 | 
 | 
| 274 |     else:
 | 
| 275 |       raise AssertionError(node.__class__.__name__)
 | 
| 276 | 
 | 
| 277 |     # <<< 'here word'
 | 
| 278 |     # << 'here word'
 | 
| 279 |     #
 | 
| 280 |     # 2> out.txt
 | 
| 281 |     # !2 > out.txt
 | 
| 282 | 
 | 
| 283 |     # cat 1<< EOF
 | 
| 284 |     # hello $name
 | 
| 285 |     # EOF
 | 
| 286 |     # cat !1 << """
 | 
| 287 |     # hello $name
 | 
| 288 |     # """
 | 
| 289 |     #
 | 
| 290 |     # cat << 'EOF'
 | 
| 291 |     # no expansion
 | 
| 292 |     # EOF
 | 
| 293 |     #   cat <<- 'EOF'
 | 
| 294 |     #   no expansion and indented
 | 
| 295 |     #
 | 
| 296 |     # cat << '''
 | 
| 297 |     # no expansion
 | 
| 298 |     # '''
 | 
| 299 |     #   cat << '''
 | 
| 300 |     #   no expansion and indented
 | 
| 301 |     #   '''
 | 
| 302 | 
 | 
| 303 |     # Warn about multiple here docs on a line.
 | 
| 304 |     # As an obscure feature, allow
 | 
| 305 |     # cat << \'ONE' << \"TWO"
 | 
| 306 |     # 123
 | 
| 307 |     # ONE
 | 
| 308 |     # 234
 | 
| 309 |     # TWO
 | 
| 310 |     # The _ is an indicator that it's not a string to be piped in.
 | 
| 311 |     pass
 | 
| 312 | 
 | 
| 313 |   def DoAssignment(self, node, at_top_level, local_symbols):
 | 
| 314 |     """
 | 
| 315 |     local_symbols:
 | 
| 316 |       - Add every 'local' declaration to it
 | 
| 317 |         - problem: what if you have local in an "if" ?
 | 
| 318 |         - we could treat it like nested scope and see what happens?  Do any
 | 
| 319 |           programs have a problem with it?
 | 
| 320 |           case/if/for/while/BraceGroup all define scopes or what?
 | 
| 321 |           You don't want inconsistency of variables that could be defined at
 | 
| 322 |           any point.
 | 
| 323 |           - or maybe you only need it within "if / case" ?  Well I guess
 | 
| 324 |             for/while can break out of the loop and cause problems.  A break is
 | 
| 325 |               an "if".
 | 
| 326 | 
 | 
| 327 |       - for subsequent
 | 
| 328 |     """
 | 
| 329 |     # Change RHS to expression language.  Bare words not allowed.  foo -> 'foo'
 | 
| 330 | 
 | 
| 331 |     has_rhs = False  # TODO: This is on a per-variable basis.
 | 
| 332 |                      # local foo -> var foo = ''
 | 
| 333 |                      # readonly foo -> setconst foo
 | 
| 334 |                      # export foo -> export foo
 | 
| 335 | 
 | 
| 336 |     # TODO:
 | 
| 337 |     # - This depends on self.mode.
 | 
| 338 |     # - And we also need the enclosing FuncDef node to analyze.
 | 
| 339 |     #   - or we need a symbol table for the current function.  Forget about
 | 
| 340 |     #
 | 
| 341 |     # Oil keywords:
 | 
| 342 |     # - global : scope qualifier
 | 
| 343 |     # - var, const : mutability
 | 
| 344 |     # - setconst, export : state mutation
 | 
| 345 |     #
 | 
| 346 |     # Operators:
 | 
| 347 |     # = and :=
 | 
| 348 |     #
 | 
| 349 |     # NOTE: Bash also has "unset".  Does anyone use it?
 | 
| 350 |     # You can use "delete" like Python I guess.  It's not the opposite of
 | 
| 351 |     # set.
 | 
| 352 | 
 | 
| 353 |     # NOTE:
 | 
| 354 |     # - We CAN tell if a variable has been defined locally.
 | 
| 355 |     # - We CANNOT tell if it's been defined globally, because different files
 | 
| 356 |     # share the same global namespace, and we can't statically figure out what
 | 
| 357 |     # files are in the program.
 | 
| 358 |     defined_locally = False  # is it a local variable in this function?
 | 
| 359 |                              # can't tell if global
 | 
| 360 |     # We can change it from = to := or ::= (in pedantic mode)
 | 
| 361 |     new_assign_op_e = None
 | 
| 362 | 
 | 
| 363 |     if node.keyword == Id.Assign_Local:
 | 
| 364 |       # Assume that 'local' it's a declaration.  In osh, it's an error if
 | 
| 365 |       # locals are redefined.  In bash, it's OK to do 'local f=1; local f=2'.
 | 
| 366 |       # Could have a flag if enough people do this.
 | 
| 367 |       if at_top_level:
 | 
| 368 |         raise RuntimeError('local at top level is invalid')
 | 
| 369 | 
 | 
| 370 |       if defined_locally:
 | 
| 371 |         raise RuntimeError("Can't redefine local")
 | 
| 372 | 
 | 
| 373 |       keyword_spid = node.spids[0]
 | 
| 374 |       self.cursor.PrintUntil(keyword_spid)
 | 
| 375 |       self.cursor.SkipUntil(keyword_spid + 1)
 | 
| 376 |       self.f.write('var')
 | 
| 377 | 
 | 
| 378 |       if local_symbols is not None:
 | 
| 379 |         for pair in node.pairs:
 | 
| 380 |           # NOTE: Not handling local a[b]=c
 | 
| 381 |           if pair.lhs.tag == lhs_expr_e.LhsName:
 | 
| 382 |             #print("REGISTERED %s" % pair.lhs.name)
 | 
| 383 |             local_symbols[pair.lhs.name] = True
 | 
| 384 | 
 | 
| 385 |     elif node.keyword == Id.Assign_None:
 | 
| 386 |       self.cursor.PrintUntil(node.spids[0])
 | 
| 387 | 
 | 
| 388 |       # For now, just detect whether the FIRST assignment on the line has been
 | 
| 389 |       # declared locally.  We might want to split every line into separate
 | 
| 390 |       # statements.
 | 
| 391 |       if local_symbols is not None:
 | 
| 392 |         lhs0 = node.pairs[0].lhs
 | 
| 393 |         if lhs0.tag == lhs_expr_e.LhsName and lhs0.name in local_symbols:
 | 
| 394 |           defined_locally = True
 | 
| 395 |         #print("CHECKING NAME", lhs0.name, defined_locally, local_symbols)
 | 
| 396 | 
 | 
| 397 |       # need semantic analysis.
 | 
| 398 |       # Would be nice to assume that it's a local though.
 | 
| 399 |       if at_top_level:
 | 
| 400 |         self.f.write('global ')  # can't be redefined
 | 
| 401 |         new_assign_op_e = '::='
 | 
| 402 |         #self.f.write('global TODO := TODO')  # mutate global or define it
 | 
| 403 |       elif defined_locally:
 | 
| 404 |         new_assign_op_e = ':='  # assume mutation of local
 | 
| 405 |         #self.f.write('[local mutated]')
 | 
| 406 |       else:
 | 
| 407 |         # we're in a function, but it's not defined locally.
 | 
| 408 |         self.f.write('global ')  # assume mutation of local
 | 
| 409 |         if self.mode == PEDANTIC:  # assume globals defined
 | 
| 410 |           new_assign_op_e = '::='
 | 
| 411 |         else:
 | 
| 412 |           new_assign_op_e = ':='
 | 
| 413 | 
 | 
| 414 |     elif node.keyword == Id.Assign_Readonly:
 | 
| 415 |       # Explicit const.  Assume it can't be redefined.
 | 
| 416 |       # Verb.
 | 
| 417 |       #
 | 
| 418 |       # Top level;
 | 
| 419 |       #   readonly FOO=bar  -> const FOO = 'bar'
 | 
| 420 |       #   readonly FOO -> freeze FOO
 | 
| 421 |       # function level:
 | 
| 422 |       #   readonly FOO=bar  -> const global FOO ::= 'bar'
 | 
| 423 |       #   readonly FOO  -> freeze FOO
 | 
| 424 |       keyword_spid = node.spids[0]
 | 
| 425 |       if at_top_level:
 | 
| 426 |         self.cursor.PrintUntil(keyword_spid)
 | 
| 427 |         self.cursor.SkipUntil(keyword_spid + 1)
 | 
| 428 |         self.f.write('const')  # can't be redefined
 | 
| 429 |       elif defined_locally:
 | 
| 430 |         self.f.write('setconst FOO = "bar"')
 | 
| 431 |       else:
 | 
| 432 |         self.f.write('setconst global FOO = "bar"')
 | 
| 433 | 
 | 
| 434 |     elif node.keyword == Id.Assign_Declare:
 | 
| 435 |       # declare -rx foo spam=eggs
 | 
| 436 |       # export foo
 | 
| 437 |       # setconst foo
 | 
| 438 |       #
 | 
| 439 |       # spam = eggs
 | 
| 440 |       # export spam
 | 
| 441 | 
 | 
| 442 |       # Have to parse the flags
 | 
| 443 |       self.f.write('TODO ')
 | 
| 444 | 
 | 
| 445 |     # foo=bar spam=eggs -> foo = 'bar', spam = 'eggs'
 | 
| 446 |     n = len(node.pairs)
 | 
| 447 |     for i, pair in enumerate(node.pairs):
 | 
| 448 |       assert pair.lhs.tag == lhs_expr_e.LhsName
 | 
| 449 | 
 | 
| 450 |       left_spid = pair.spids[0]
 | 
| 451 |       self.cursor.PrintUntil(left_spid)
 | 
| 452 |       # Assume skipping over one Lit_VarLike token
 | 
| 453 |       self.cursor.SkipUntil(left_spid + 1)
 | 
| 454 | 
 | 
| 455 |       # Replace name.  I guess it's Lit_Chars.
 | 
| 456 |       self.f.write(pair.lhs.name)
 | 
| 457 |       op = new_assign_op_e if new_assign_op_e else '='
 | 
| 458 |       self.f.write(' %s ' % op)
 | 
| 459 | 
 | 
| 460 |       # foo=bar -> foo = 'bar'
 | 
| 461 |       #print('RHS', pair.rhs, file=sys.stderr)
 | 
| 462 |       if pair.rhs is None:
 | 
| 463 |         self.f.write("''")  # local i -> var i = ''
 | 
| 464 |       else:
 | 
| 465 |         self.DoWordAsExpr(pair.rhs, local_symbols)
 | 
| 466 | 
 | 
| 467 |       if i != n - 1:
 | 
| 468 |         self.f.write(',')
 | 
| 469 | 
 | 
| 470 |   def DoCommand(self, node, local_symbols, at_top_level=False):
 | 
| 471 |     if node.tag == command_e.CommandList:
 | 
| 472 |       # TODO: How to distinguish between echo hi; echo bye; and on separate
 | 
| 473 |       # lines
 | 
| 474 |       for child in node.children:
 | 
| 475 |         self.DoCommand(child, local_symbols)
 | 
| 476 | 
 | 
| 477 |     elif node.tag == command_e.SimpleCommand:
 | 
| 478 |       # How to preserve spaces between words?  Do you want to do it?
 | 
| 479 |       # Well you need to test this:
 | 
| 480 |       #
 | 
| 481 |       # echo foo \
 | 
| 482 |       #   bar
 | 
| 483 | 
 | 
| 484 |       # TODO: Need to print until the left most part of the phrase?  the phrase
 | 
| 485 |       # is a word, binding, redirect.
 | 
| 486 |       #self.cursor.PrintUntil()
 | 
| 487 | 
 | 
| 488 |       if node.more_env:
 | 
| 489 |         (left_spid,) = node.more_env[0].spids
 | 
| 490 |         self.cursor.PrintUntil(left_spid)
 | 
| 491 |         self.f.write('env ')
 | 
| 492 | 
 | 
| 493 |         # We only need to transform the right side, not left side.
 | 
| 494 |         for pair in node.more_env:
 | 
| 495 |           self.DoWordInCommand(pair.val, local_symbols)
 | 
| 496 | 
 | 
| 497 |       # More translations:
 | 
| 498 |       # - . to source
 | 
| 499 |       # - eval to sh-eval
 | 
| 500 | 
 | 
| 501 |       if node.words:
 | 
| 502 |         first_word = node.words[0]
 | 
| 503 |         ok, val, quoted = word.StaticEval(first_word)
 | 
| 504 |         word0_spid = word.LeftMostSpanForWord(first_word)
 | 
| 505 |         if ok and not quoted:
 | 
| 506 |           if val == '[':
 | 
| 507 |             last_word = node.words[-1]
 | 
| 508 |             # Check if last word is ]
 | 
| 509 |             ok, val, quoted = word.StaticEval(last_word)
 | 
| 510 |             if ok and not quoted and val == ']':
 | 
| 511 |               # Replace [ with 'test'
 | 
| 512 |               self.cursor.PrintUntil(word0_spid)
 | 
| 513 |               self.cursor.SkipUntil(word0_spid + 1)
 | 
| 514 |               self.f.write('test')
 | 
| 515 | 
 | 
| 516 |               for w in node.words[1:-1]:
 | 
| 517 |                 self.DoWordInCommand(w, local_symbols)
 | 
| 518 | 
 | 
| 519 |               # Now omit ]
 | 
| 520 |               last_spid = word.LeftMostSpanForWord(last_word)
 | 
| 521 |               self.cursor.PrintUntil(last_spid - 1)  # Get the space before
 | 
| 522 |               self.cursor.SkipUntil(last_spid + 1)  # ] takes one spid
 | 
| 523 |               return
 | 
| 524 |             else:
 | 
| 525 |               raise RuntimeError('Got [ without ]')
 | 
| 526 | 
 | 
| 527 |           elif val == '.':
 | 
| 528 |             self.cursor.PrintUntil(word0_spid)
 | 
| 529 |             self.cursor.SkipUntil(word0_spid + 1)
 | 
| 530 |             self.f.write('source')
 | 
| 531 |             return
 | 
| 532 | 
 | 
| 533 |       for w in node.words:
 | 
| 534 |         self.DoWordInCommand(w, local_symbols)
 | 
| 535 | 
 | 
| 536 |       # NOTE: This will change to "phrase"?  Word or redirect.
 | 
| 537 |       for r in node.redirects:
 | 
| 538 |         self.DoRedirect(r, local_symbols)
 | 
| 539 | 
 | 
| 540 |       # TODO: Print the terminator.  Could be \n or ;
 | 
| 541 |       # Need to print env like PYTHONPATH = 'foo' && ls
 | 
| 542 |       # Need to print redirects:
 | 
| 543 |       # < > are the same.  << is here string, and >> is assignment.
 | 
| 544 |       # append is >+
 | 
| 545 | 
 | 
| 546 |       # TODO: static_eval of simple command
 | 
| 547 |       # - [ -> "test".  Eliminate trailing ].
 | 
| 548 |       # - . -> source, etc.
 | 
| 549 | 
 | 
| 550 |     elif node.tag == command_e.Assignment:
 | 
| 551 |       self.DoAssignment(node, at_top_level, local_symbols)
 | 
| 552 | 
 | 
| 553 |     elif node.tag == command_e.Pipeline:
 | 
| 554 |       # Obscure: |& turns into |- or |+ for stderr.
 | 
| 555 |       # TODO:
 | 
| 556 |       # if ! true; then -> if not true {
 | 
| 557 | 
 | 
| 558 |       # if ! echo | grep; then -> if not { echo | grep } {
 | 
| 559 |       # }
 | 
| 560 |       # not is like do {}, but it negates the return value I guess.
 | 
| 561 | 
 | 
| 562 |       for child in node.children:
 | 
| 563 |         self.DoCommand(child, local_symbols)
 | 
| 564 | 
 | 
| 565 |     elif node.tag == command_e.AndOr:
 | 
| 566 |       for child in node.children:
 | 
| 567 |         self.DoCommand(child, local_symbols)
 | 
| 568 | 
 | 
| 569 |     elif node.tag == command_e.Sentence:
 | 
| 570 |       # 'ls &' to 'fork ls'
 | 
| 571 |       # Keep ; the same.
 | 
| 572 |       self.DoCommand(node.child, local_symbols)
 | 
| 573 | 
 | 
| 574 |     # This has to be different in the function case.
 | 
| 575 |     elif node.tag == command_e.BraceGroup:
 | 
| 576 |       # { echo hi; } -> do { echo hi }
 | 
| 577 |       # For now it might be OK to keep 'do { echo hi; }
 | 
| 578 |       #left_spid, right_spid = node.spids
 | 
| 579 |       (left_spid,) = node.spids
 | 
| 580 | 
 | 
| 581 |       self.cursor.PrintUntil(left_spid)
 | 
| 582 |       self.cursor.SkipUntil(left_spid + 1)
 | 
| 583 |       self.f.write('do {')
 | 
| 584 | 
 | 
| 585 |       for child in node.children:
 | 
| 586 |         self.DoCommand(child, local_symbols)
 | 
| 587 | 
 | 
| 588 |     elif node.tag == command_e.Subshell:
 | 
| 589 |       # (echo hi) -> shell echo hi
 | 
| 590 |       # (echo hi; echo bye) -> shell {echo hi; echo bye}
 | 
| 591 | 
 | 
| 592 |       (left_spid, right_spid) = node.spids
 | 
| 593 | 
 | 
| 594 |       self.cursor.PrintUntil(left_spid)
 | 
| 595 |       self.cursor.SkipUntil(left_spid + 1)
 | 
| 596 |       self.f.write('shell {')
 | 
| 597 | 
 | 
| 598 |       self.DoCommand(node.child, local_symbols)
 | 
| 599 | 
 | 
| 600 |       #self._DebugSpid(right_spid)
 | 
| 601 |       #self._DebugSpid(right_spid + 1)
 | 
| 602 | 
 | 
| 603 |       #print('RIGHT SPID', right_spid)
 | 
| 604 |       self.cursor.PrintUntil(right_spid)
 | 
| 605 |       self.cursor.SkipUntil(right_spid + 1)
 | 
| 606 |       self.f.write('}')
 | 
| 607 | 
 | 
| 608 |     elif node.tag == command_e.DParen:
 | 
| 609 |       # Just change (( )) to ( )
 | 
| 610 |       # Test it with while loop
 | 
| 611 |       self.DoArithExpr(node.child, local_symbols)
 | 
| 612 | 
 | 
| 613 |     elif node.tag == command_e.DBracket:
 | 
| 614 |       # [[ 1 -eq 2 ]] to (1 == 2)
 | 
| 615 |       self.DoBoolExpr(node.expr)
 | 
| 616 | 
 | 
| 617 |     elif node.tag == command_e.FuncDef:
 | 
| 618 |       # TODO: skip name
 | 
| 619 |       #self.f.write('proc %s' % node.name)
 | 
| 620 | 
 | 
| 621 |       # New symbol table for every function.
 | 
| 622 |       new_local_symbols = {}
 | 
| 623 | 
 | 
| 624 |       # Should be the left most span, including 'function'
 | 
| 625 |       self.cursor.PrintUntil(node.spids[0])
 | 
| 626 | 
 | 
| 627 |       self.f.write('proc ')
 | 
| 628 |       self.f.write(node.name)
 | 
| 629 |       self.cursor.SkipUntil(node.spids[1])
 | 
| 630 | 
 | 
| 631 |       if node.body.tag == command_e.BraceGroup:
 | 
| 632 |         # Don't add "do" like a standalone brace group.  Just use {}.
 | 
| 633 |         for child in node.body.children:
 | 
| 634 |           self.DoCommand(child, new_local_symbols)
 | 
| 635 |       else:
 | 
| 636 |         pass
 | 
| 637 |         # Add {}.
 | 
| 638 |         # proc foo {
 | 
| 639 |         #   shell {echo hi; echo bye}
 | 
| 640 |         # }
 | 
| 641 |         #self.DoCommand(node.body)
 | 
| 642 | 
 | 
| 643 |     elif node.tag == command_e.BraceGroup:
 | 
| 644 |       for child in node.children:
 | 
| 645 |         self.DoCommand(child, local_symbols)
 | 
| 646 | 
 | 
| 647 |     elif node.tag == command_e.DoGroup:
 | 
| 648 |       do_spid, done_spid = node.spids
 | 
| 649 |       self.cursor.PrintUntil(do_spid)
 | 
| 650 |       self.cursor.SkipUntil(do_spid + 1)
 | 
| 651 |       self.f.write('{')
 | 
| 652 | 
 | 
| 653 |       for child in node.children:
 | 
| 654 |         self.DoCommand(child, local_symbols)
 | 
| 655 | 
 | 
| 656 |       self.cursor.PrintUntil(done_spid)
 | 
| 657 |       self.cursor.SkipUntil(done_spid + 1)
 | 
| 658 |       self.f.write('}')
 | 
| 659 | 
 | 
| 660 |     elif node.tag == command_e.ForEach:
 | 
| 661 |       # Need to preserve spaces between words, because there can be line
 | 
| 662 |       # wrapping.
 | 
| 663 |       # for x in a b c \
 | 
| 664 |       #    d e f; do
 | 
| 665 | 
 | 
| 666 |       in_spid, semi_spid = node.spids
 | 
| 667 | 
 | 
| 668 |       if in_spid == const.NO_INTEGER:
 | 
| 669 |         #self.cursor.PrintUntil()  # 'for x' and then space
 | 
| 670 |         self.f.write('for %s in @Argv ' % node.iter_name)
 | 
| 671 |         self.cursor.SkipUntil(node.body.spids[0])
 | 
| 672 |       else:
 | 
| 673 |         self.cursor.PrintUntil(in_spid + 1)  # 'for x in' and then space
 | 
| 674 |         self.f.write('[')
 | 
| 675 |         for w in node.iter_words:
 | 
| 676 |           self.DoWordInCommand(w, local_symbols)
 | 
| 677 |         self.f.write(']')
 | 
| 678 |         #print("SKIPPING SEMI %d" % semi_spid, file=sys.stderr)
 | 
| 679 | 
 | 
| 680 |       if semi_spid != const.NO_INTEGER:
 | 
| 681 |         self.cursor.PrintUntil(semi_spid)
 | 
| 682 |         self.cursor.SkipUntil(semi_spid + 1)
 | 
| 683 | 
 | 
| 684 |       self.DoCommand(node.body, local_symbols)
 | 
| 685 | 
 | 
| 686 |     elif node.tag == command_e.ForExpr:
 | 
| 687 |       # Change (( )) to ( ), and then _FixDoGroup
 | 
| 688 |       pass
 | 
| 689 | 
 | 
| 690 |     elif node.tag == command_e.While:
 | 
| 691 |       cond = node.cond
 | 
| 692 |       if len(cond) == 1 and cond[0].tag == command_e.Sentence:
 | 
| 693 |         spid = cond[0].terminator.span_id
 | 
| 694 |         self.cursor.PrintUntil(spid)
 | 
| 695 |         self.cursor.SkipUntil(spid + 1)
 | 
| 696 | 
 | 
| 697 |       self.DoCommand(node.body, local_symbols)
 | 
| 698 | 
 | 
| 699 |     elif node.tag == command_e.If:
 | 
| 700 |       else_spid, fi_spid = node.spids
 | 
| 701 | 
 | 
| 702 |       # if foo; then -> if foo {
 | 
| 703 |       # elif foo; then -> } elif foo {
 | 
| 704 |       for arm in node.arms:
 | 
| 705 |         elif_spid, then_spid = arm.spids
 | 
| 706 |         if elif_spid != const.NO_INTEGER:
 | 
| 707 |           self.cursor.PrintUntil(elif_spid)
 | 
| 708 |           self.f.write('} ')
 | 
| 709 | 
 | 
| 710 |         cond = arm.cond
 | 
| 711 |         if len(cond) == 1 and cond[0].tag == command_e.Sentence:
 | 
| 712 |           sentence = cond[0]
 | 
| 713 |           self.DoCommand(sentence, local_symbols)
 | 
| 714 | 
 | 
| 715 |           # Remove semi-colon
 | 
| 716 |           semi_spid = sentence.terminator.span_id
 | 
| 717 |           self.cursor.PrintUntil(semi_spid)
 | 
| 718 |           self.cursor.SkipUntil(semi_spid + 1)
 | 
| 719 |         else:
 | 
| 720 |           for child in arm.cond:
 | 
| 721 |             self.DoCommand(child, local_symbols)
 | 
| 722 | 
 | 
| 723 |         self.cursor.PrintUntil(then_spid)
 | 
| 724 |         self.cursor.SkipUntil(then_spid + 1)
 | 
| 725 |         self.f.write('{')
 | 
| 726 | 
 | 
| 727 |         for child in arm.action:
 | 
| 728 |           self.DoCommand(child, local_symbols)
 | 
| 729 | 
 | 
| 730 |       # else -> } else {
 | 
| 731 |       if node.else_action:
 | 
| 732 |         self.cursor.PrintUntil(else_spid)
 | 
| 733 |         self.f.write('} ')
 | 
| 734 |         self.cursor.PrintUntil(else_spid + 1)
 | 
| 735 |         self.f.write(' {')
 | 
| 736 | 
 | 
| 737 |         for child in node.else_action:
 | 
| 738 |           self.DoCommand(child, local_symbols)
 | 
| 739 | 
 | 
| 740 |       # fi -> }
 | 
| 741 |       self.cursor.PrintUntil(fi_spid)
 | 
| 742 |       self.cursor.SkipUntil(fi_spid + 1)
 | 
| 743 |       self.f.write('}')
 | 
| 744 | 
 | 
| 745 |     elif node.tag == command_e.Case:
 | 
| 746 |       case_spid, in_spid, esac_spid = node.spids
 | 
| 747 |       self.cursor.PrintUntil(case_spid)
 | 
| 748 |       self.cursor.SkipUntil(case_spid + 1)
 | 
| 749 |       self.f.write('matchstr')
 | 
| 750 | 
 | 
| 751 |       # Reformat "$1" to $1
 | 
| 752 |       self.DoWordInCommand(node.to_match, local_symbols)
 | 
| 753 | 
 | 
| 754 |       self.cursor.PrintUntil(in_spid)
 | 
| 755 |       self.cursor.SkipUntil(in_spid + 1)
 | 
| 756 |       self.f.write('{')  # matchstr $var {
 | 
| 757 | 
 | 
| 758 |       # each arm needs the ) and the ;; node to skip over?
 | 
| 759 |       for arm in node.arms:
 | 
| 760 |         left_spid, rparen_spid, dsemi_spid, last_spid = arm.spids
 | 
| 761 |         #print(left_spid, rparen_spid, dsemi_spid)
 | 
| 762 | 
 | 
| 763 |         self.cursor.PrintUntil(left_spid)
 | 
| 764 |         # Hm maybe keep | because it's semi-deprecated?  You can use
 | 
| 765 |         # reload|force-reload {
 | 
| 766 |         # }
 | 
| 767 |         # e/reload|force-reload/ {
 | 
| 768 |         # }
 | 
| 769 |         # / 'reload' or 'force-reload' / {
 | 
| 770 |         # }
 | 
| 771 |         #
 | 
| 772 |         # Yeah it's the more abbreviated syntax.
 | 
| 773 | 
 | 
| 774 |         # change | to 'or'
 | 
| 775 |         for pat in arm.pat_list:
 | 
| 776 |           pass
 | 
| 777 | 
 | 
| 778 |         # Skip this
 | 
| 779 |         self.cursor.PrintUntil(rparen_spid)
 | 
| 780 |         self.cursor.SkipUntil(rparen_spid + 1)
 | 
| 781 |         self.f.write(' {')  # surround it with { }
 | 
| 782 | 
 | 
| 783 |         for child in arm.action:
 | 
| 784 |           self.DoCommand(child, local_symbols)
 | 
| 785 | 
 | 
| 786 |         if dsemi_spid != const.NO_INTEGER:
 | 
| 787 |           self.cursor.PrintUntil(dsemi_spid)
 | 
| 788 |           self.cursor.SkipUntil(dsemi_spid + 1)
 | 
| 789 |           # NOTE: indentation here will be off because ;; is likely indented
 | 
| 790 |           # with body.
 | 
| 791 |           self.f.write('}')
 | 
| 792 |         elif last_spid != const.NO_INTEGER:
 | 
| 793 |           self.cursor.PrintUntil(last_spid)
 | 
| 794 |           # NOTE: Indentation is also off here.  Arbitrarily put 4 spaces.
 | 
| 795 |           self.f.write('    }\n')
 | 
| 796 |         else:
 | 
| 797 |           raise AssertionError(
 | 
| 798 |               "Expected with dsemi_spid or last_spid in case arm")
 | 
| 799 | 
 | 
| 800 |       self.cursor.PrintUntil(esac_spid)
 | 
| 801 |       self.cursor.SkipUntil(esac_spid + 1)
 | 
| 802 |       self.f.write('}')  # strmatch $var {
 | 
| 803 | 
 | 
| 804 |     elif node.tag == command_e.NoOp:
 | 
| 805 |       pass
 | 
| 806 | 
 | 
| 807 |     elif node.tag == command_e.ControlFlow:
 | 
| 808 |       # No change for break / return / continue
 | 
| 809 |       pass
 | 
| 810 | 
 | 
| 811 |     elif node.tag == command_e.TimeBlock:
 | 
| 812 |       self.DoCommand(node.pipeline, local_symbols)
 | 
| 813 | 
 | 
| 814 |     else:
 | 
| 815 |       #log('Command not handled: %s', node)
 | 
| 816 |       raise AssertionError(node.__class__.__name__)
 | 
| 817 | 
 | 
| 818 |   def DoWordAsExpr(self, node, local_symbols):
 | 
| 819 |     style = _GetRhsStyle(node)
 | 
| 820 |     if style == word_style_e.SQ:
 | 
| 821 |       self.f.write("'")
 | 
| 822 |       self.DoWordInCommand(node, local_symbols)
 | 
| 823 |       self.f.write("'")
 | 
| 824 |     elif style == word_style_e.DQ:
 | 
| 825 |       self.f.write('"')
 | 
| 826 |       self.DoWordInCommand(node, local_symbols)
 | 
| 827 |       self.f.write('"')
 | 
| 828 |     else:
 | 
| 829 |       # "${foo:-default}" -> foo or 'default'
 | 
| 830 |       # ${foo:-default} -> @split(foo or 'default')
 | 
| 831 |       #                    @(foo or 'default')  -- implicit split.
 | 
| 832 | 
 | 
| 833 |       if word.IsVarSub(node):  # ${1} or "$1"
 | 
| 834 |         # Do it in expression mode
 | 
| 835 |         pass
 | 
| 836 |       # NOTE: ArithSub with $(1 +2 ) is different than 1 + 2 because of
 | 
| 837 |       # conversion to string.
 | 
| 838 | 
 | 
| 839 |       # For now, just stub it out
 | 
| 840 |       self.DoWordInCommand(node, local_symbols)
 | 
| 841 | 
 | 
| 842 |   def DoWordInCommand(self, node, local_symbols):
 | 
| 843 |     """
 | 
| 844 |     New reserved symbols:
 | 
| 845 |       echo == must be changed to echo '==' because = is a reserved symbol.
 | 
| 846 |       echo @$foo -> echo "@$foo" because @ is reserved
 | 
| 847 | 
 | 
| 848 |     Problems:
 | 
| 849 |     rm --verbose=true
 | 
| 850 |     rm '--verbose=true'  -- is this bad?
 | 
| 851 | 
 | 
| 852 |     Same with comma
 | 
| 853 |     foo, bar = 1
 | 
| 854 | 
 | 
| 855 |     # I guess we can allow this
 | 
| 856 |     ls --long foo,bar
 | 
| 857 | 
 | 
| 858 |     or force:
 | 
| 859 |     (foo, bar) = 1
 | 
| 860 | 
 | 
| 861 |     Maybe we need a clever 'pre-lex'
 | 
| 862 |     overwhelmingly the second char will be ' '
 | 
| 863 | 
 | 
| 864 |     foo/bar/foo.py
 | 
| 865 |     foo.py
 | 
| 866 |     ./hello
 | 
| 867 |     foo_bar
 | 
| 868 |     [a-zA-Z0-9]  / - . _  -- filename chars
 | 
| 869 | 
 | 
| 870 | 
 | 
| 871 |     first word:
 | 
| 872 |       var, const, export, setconst, global
 | 
| 873 |       func, proc, do, not, shell,
 | 
| 874 |       maybe: time, coproc, etc.
 | 
| 875 | 
 | 
| 876 |       =    -- generic expression, = 1+2
 | 
| 877 | 
 | 
| 878 |     non-filename char AFTER first word
 | 
| 879 |       cmd:
 | 
| 880 |           ' '     foo bar baz
 | 
| 881 |           '\n'    foo
 | 
| 882 |           '<'     foo < bar
 | 
| 883 |           '>'     foo > bar
 | 
| 884 |           !       ls !2 > !1
 | 
| 885 |           |       who | wc -l
 | 
| 886 |           |-       who |- wc -l
 | 
| 887 | 
 | 
| 888 |       expr:
 | 
| 889 |           =   foo = bar
 | 
| 890 |           ,   a, b = x
 | 
| 891 |           [   a[x] = 1
 | 
| 892 |           (   f(x)  for(  while(  if(
 | 
| 893 | 
 | 
| 894 |     1+2  -- I think this tries to run the command
 | 
| 895 |     """
 | 
| 896 |     # Are we getting rid of word joining?  Or maybe keep it but discourage and
 | 
| 897 |     # provide alternatives.
 | 
| 898 |     #
 | 
| 899 |     # You don't really have a problem with byte strings, those are b'foo', but
 | 
| 900 |     # that's in expression mode, not command mode.
 | 
| 901 | 
 | 
| 902 |     # Problems:
 | 
| 903 |     # - Tilde sub can't be quoted.  ls ~/foo/"foo" are incompatible with the
 | 
| 904 |     # rule.
 | 
| 905 |     # - Globs can't be quoted. ls 'foo'*.py can't be ls "foo*.py" -- it means
 | 
| 906 |     # something different.
 | 
| 907 |     # Might need to finish more of the globber to figure this out.
 | 
| 908 | 
 | 
| 909 |     # What about here docs words?  It's a double quoted part, but with
 | 
| 910 |     # different formatting!
 | 
| 911 |     if node.tag == word_e.CompoundWord:
 | 
| 912 | 
 | 
| 913 |       # UNQUOTE simple var subs
 | 
| 914 | 
 | 
| 915 |       # TODO: I think we have to print the beginning and the end?
 | 
| 916 | 
 | 
| 917 |       #left_spid = word.LeftMostSpanForWord(node)
 | 
| 918 |       #right_spid = word.RightMostSpanForWord(node)
 | 
| 919 |       #right_spid = -1
 | 
| 920 |       #print('DoWordInCommand %s %s' % (left_spid, right_spid), file=sys.stderr)
 | 
| 921 | 
 | 
| 922 |       # Special case for "$@".  Wow this needs pattern matching!
 | 
| 923 |       # TODO:
 | 
| 924 |       # "$foo" -> $foo
 | 
| 925 |       # "${foo}" -> $foo
 | 
| 926 | 
 | 
| 927 |       if (len(node.parts) == 1 and
 | 
| 928 |           node.parts[0].tag == word_part_e.DoubleQuotedPart):
 | 
| 929 |         dq_part = node.parts[0]
 | 
| 930 | 
 | 
| 931 |         # TODO: Double quoted part needs left and right IDs
 | 
| 932 |         left_spid, right_spid = dq_part.spids
 | 
| 933 |         assert right_spid != const.NO_INTEGER, right_spid
 | 
| 934 | 
 | 
| 935 |         if len(dq_part.parts) == 1:
 | 
| 936 |           part0 = dq_part.parts[0]
 | 
| 937 |           if part0.tag == word_part_e.SimpleVarSub:
 | 
| 938 |             vsub_part = dq_part.parts[0]
 | 
| 939 |             if vsub_part.token.id == Id.VSub_At:
 | 
| 940 |               # NOTE: This is off for double quoted part.  Hack to subtract 1.
 | 
| 941 |               self.cursor.PrintUntil(left_spid)
 | 
| 942 |               self.cursor.SkipUntil(right_spid + 1)  # " then $@ then "
 | 
| 943 |               self.f.write('@Argv')
 | 
| 944 |               return  # Done replacing
 | 
| 945 | 
 | 
| 946 |             # "$1" -> $1, "$foo" -> $foo
 | 
| 947 |             if vsub_part.token.id in (Id.VSub_Number, Id.VSub_Name):
 | 
| 948 |               self.cursor.PrintUntil(left_spid)
 | 
| 949 |               self.cursor.SkipUntil(right_spid + 1)
 | 
| 950 |               self.f.write(vsub_part.token.val)
 | 
| 951 |               return
 | 
| 952 | 
 | 
| 953 |           # Single arith sub, command sub, etc.
 | 
| 954 |           # On the other hand, an unquoted one needs to turn into
 | 
| 955 |           #
 | 
| 956 |           # $(echo one two) -> @[echo one two]
 | 
| 957 |           # `echo one two` -> @[echo one two]
 | 
| 958 |           #
 | 
| 959 |           # ${var:-'the default'} -> @$(var or 'the default')
 | 
| 960 |           #
 | 
| 961 |           # $((1 + 2)) -> $(1 + 2) -- this is OK unquoted
 | 
| 962 | 
 | 
| 963 |           elif part0.tag == word_part_e.BracedVarSub:
 | 
| 964 |             # Skip over quote
 | 
| 965 |             self.cursor.PrintUntil(left_spid)
 | 
| 966 |             self.cursor.SkipUntil(left_spid + 1)
 | 
| 967 |             self.DoWordPart(part0, local_symbols)
 | 
| 968 |             self.cursor.SkipUntil(right_spid + 1)
 | 
| 969 |             return
 | 
| 970 | 
 | 
| 971 |           elif part0.tag == word_part_e.CommandSubPart:
 | 
| 972 |             self.cursor.PrintUntil(left_spid)
 | 
| 973 |             self.cursor.SkipUntil(left_spid + 1)
 | 
| 974 |             self.DoWordPart(part0, local_symbols)
 | 
| 975 |             self.cursor.SkipUntil(right_spid + 1)
 | 
| 976 |             return
 | 
| 977 | 
 | 
| 978 |       # It's None for here docs I think.
 | 
| 979 |       #log("NODE %s", node)
 | 
| 980 |       #if left_spid is not None and left_spid >= 0:
 | 
| 981 |         #span = self.arena.GetLineSpan(span_id)
 | 
| 982 |         #print(span)
 | 
| 983 | 
 | 
| 984 |         #self.cursor.PrintUntil(left_spid)
 | 
| 985 |         #pass
 | 
| 986 | 
 | 
| 987 |       # TODO: 'foo'"bar" should be "foobar", etc.
 | 
| 988 |       # If any part is double quoted, you can always double quote the whole
 | 
| 989 |       # thing?
 | 
| 990 |       for part in node.parts:
 | 
| 991 |         self.DoWordPart(part, local_symbols)
 | 
| 992 | 
 | 
| 993 |       #if right_spid >= 0:
 | 
| 994 |         #self.cursor.PrintUntil(right_spid)
 | 
| 995 |         #pass
 | 
| 996 | 
 | 
| 997 |     else:
 | 
| 998 |       raise AssertionError(node.__class__.__name__)
 | 
| 999 | 
 | 
| 1000 |   def DoWordPart(self, node, local_symbols, quoted=False):
 | 
| 1001 |     span_id = word.LeftMostSpanForPart(node)
 | 
| 1002 |     if span_id is not None and span_id != const.NO_INTEGER:
 | 
| 1003 |       span = self.arena.GetLineSpan(span_id)
 | 
| 1004 |       #print(span)
 | 
| 1005 | 
 | 
| 1006 |       self.cursor.PrintUntil(span_id)
 | 
| 1007 | 
 | 
| 1008 |     if node.tag == word_part_e.ArrayLiteralPart:
 | 
| 1009 |       pass
 | 
| 1010 | 
 | 
| 1011 |     elif node.tag == word_part_e.EscapedLiteralPart:
 | 
| 1012 |       if quoted:
 | 
| 1013 |         pass
 | 
| 1014 |       else:
 | 
| 1015 |         # If unquoted \e, it should quoted instead.  ' ' vs. \<invisible space>
 | 
| 1016 |         # Hm is this necessary though?  I think the only motivation is changing
 | 
| 1017 |         # \{ and \( for macros.  And ' ' to be readable/visible.
 | 
| 1018 |         t = node.token
 | 
| 1019 |         val = t.val[1:]
 | 
| 1020 |         assert len(val) == 1, val
 | 
| 1021 |         if val != '\n':
 | 
| 1022 |           self.cursor.PrintUntil(t.span_id)
 | 
| 1023 |           self.cursor.SkipUntil(t.span_id + 1)
 | 
| 1024 |           self.f.write("'%s'" % val)
 | 
| 1025 | 
 | 
| 1026 |     elif node.tag == word_part_e.LiteralPart:
 | 
| 1027 |       # Print it literally.
 | 
| 1028 |       # TODO: We might want to do it all on the word level though.  For
 | 
| 1029 |       # example, foo"bar" becomes "foobar" in oil.
 | 
| 1030 |       spid = node.token.span_id
 | 
| 1031 |       if spid is None:
 | 
| 1032 |         #raise RuntimeError('%s has no span_id' % node.token)
 | 
| 1033 |         # TODO: Fix word.TildeDetect to construct proper tokens.
 | 
| 1034 |         print('WARNING:%s has no span_id' % node.token, file=sys.stderr)
 | 
| 1035 |       else:
 | 
| 1036 |         self.cursor.PrintUntil(spid + 1)
 | 
| 1037 | 
 | 
| 1038 |     elif node.tag == word_part_e.TildeSubPart:  # No change
 | 
| 1039 |       pass
 | 
| 1040 | 
 | 
| 1041 |     elif node.tag == word_part_e.SingleQuotedPart:
 | 
| 1042 |       # TODO:
 | 
| 1043 |       # '\n' is '\\n'
 | 
| 1044 |       # $'\n' is '\n'
 | 
| 1045 |       # TODO: Should print until right_spid
 | 
| 1046 |       # left_spid, right_spid = node.spids
 | 
| 1047 |       if node.tokens:  # Empty string has no tokens
 | 
| 1048 |         last_spid = node.tokens[-1].span_id
 | 
| 1049 |         self.cursor.PrintUntil(last_spid + 1)
 | 
| 1050 | 
 | 
| 1051 |     elif node.tag == word_part_e.DoubleQuotedPart:
 | 
| 1052 |       for part in node.parts:
 | 
| 1053 |         self.DoWordPart(part, local_symbols, quoted=True)
 | 
| 1054 | 
 | 
| 1055 |     elif node.tag == word_part_e.SimpleVarSub:
 | 
| 1056 |       spid = node.token.span_id
 | 
| 1057 |       op_id = node.token.id
 | 
| 1058 | 
 | 
| 1059 |       if op_id == Id.VSub_Name:
 | 
| 1060 |         self.cursor.PrintUntil(spid + 1)
 | 
| 1061 | 
 | 
| 1062 |       elif op_id == Id.VSub_Number:
 | 
| 1063 |         self.cursor.PrintUntil(spid + 1)
 | 
| 1064 | 
 | 
| 1065 |       elif op_id == Id.VSub_Bang:  # $!
 | 
| 1066 |         self.f.write('$BgPid')  # Job most recently placed in background
 | 
| 1067 |         self.cursor.SkipUntil(spid + 1)
 | 
| 1068 | 
 | 
| 1069 |       elif op_id == Id.VSub_At:  # $@
 | 
| 1070 |         self.f.write('$ifsjoin(Argv)')
 | 
| 1071 |         self.cursor.SkipUntil(spid + 1)
 | 
| 1072 | 
 | 
| 1073 |       elif op_id == Id.VSub_Pound:  # $#
 | 
| 1074 |         self.f.write('$Argc')
 | 
| 1075 |         self.cursor.SkipUntil(spid + 1)
 | 
| 1076 | 
 | 
| 1077 |       elif op_id == Id.VSub_Dollar:  # $$
 | 
| 1078 |         self.f.write('$Pid')
 | 
| 1079 |         self.cursor.SkipUntil(spid + 1)
 | 
| 1080 | 
 | 
| 1081 |       elif op_id == Id.VSub_Star:  # $*
 | 
| 1082 |         # PEDANTIC: Depends if quoted or unquoted
 | 
| 1083 |         self.f.write('$ifsjoin(Argv)')
 | 
| 1084 |         self.cursor.SkipUntil(spid + 1)
 | 
| 1085 | 
 | 
| 1086 |       elif op_id == Id.VSub_Hyphen:  # $*
 | 
| 1087 |         self.f.write('$Flags')
 | 
| 1088 |         self.cursor.SkipUntil(spid + 1)
 | 
| 1089 | 
 | 
| 1090 |       elif op_id == Id.VSub_QMark:  # $?
 | 
| 1091 |         self.f.write('$Status')
 | 
| 1092 |         self.cursor.SkipUntil(spid + 1)
 | 
| 1093 | 
 | 
| 1094 |       else:
 | 
| 1095 |         raise AssertionError(op_id)
 | 
| 1096 | 
 | 
| 1097 |     elif node.tag == word_part_e.BracedVarSub:
 | 
| 1098 |       left_spid, right_spid = node.spids
 | 
| 1099 | 
 | 
| 1100 |       # NOTE: Why do we need this but we don't need it in command sub?
 | 
| 1101 |       self.cursor.PrintUntil(left_spid)
 | 
| 1102 | 
 | 
| 1103 |       name_spid = node.token.span_id
 | 
| 1104 |       op_id = node.token.id
 | 
| 1105 | 
 | 
| 1106 |       parens_needed = True
 | 
| 1107 |       if node.bracket_op:
 | 
| 1108 |         # a[1]
 | 
| 1109 |         # These two change the sigil!  ${a[@]} is now @a!
 | 
| 1110 |         # a[@]
 | 
| 1111 |         # a[*]
 | 
| 1112 |         pass
 | 
| 1113 | 
 | 
| 1114 |       if node.prefix_op:
 | 
| 1115 |         # len()
 | 
| 1116 |         pass
 | 
| 1117 |       if node.suffix_op:
 | 
| 1118 |         # foo.trimLeft()
 | 
| 1119 |         # foo.trimGlobLeft()
 | 
| 1120 |         # foo.trimGlobLeft(longest=True)
 | 
| 1121 |         #
 | 
| 1122 |         # python lstrip() does something different
 | 
| 1123 | 
 | 
| 1124 |         # a[1:1]
 | 
| 1125 | 
 | 
| 1126 |         # .replace()
 | 
| 1127 |         # .replaceGlob()
 | 
| 1128 | 
 | 
| 1129 |         pass
 | 
| 1130 | 
 | 
| 1131 |       if op_id == Id.VSub_QMark:
 | 
| 1132 |         self.cursor.PrintUntil(name_spid + 1)
 | 
| 1133 | 
 | 
| 1134 |       if parens_needed:
 | 
| 1135 |         # Skip over left bracket and write our own.
 | 
| 1136 |         self.f.write('$(')
 | 
| 1137 |         self.cursor.SkipUntil(left_spid + 1)
 | 
| 1138 | 
 | 
| 1139 |         # Placeholder for now
 | 
| 1140 |         self.cursor.PrintUntil(right_spid)
 | 
| 1141 | 
 | 
| 1142 |         # Skip over right bracket and write our own.
 | 
| 1143 |         self.f.write(')')
 | 
| 1144 |       else:
 | 
| 1145 |         pass
 | 
| 1146 | 
 | 
| 1147 |       self.cursor.SkipUntil(right_spid + 1)
 | 
| 1148 | 
 | 
| 1149 |     elif node.tag == word_part_e.CommandSubPart:
 | 
| 1150 |       left_spid, right_spid = node.spids
 | 
| 1151 | 
 | 
| 1152 |       #self.cursor.PrintUntil(left_spid)
 | 
| 1153 |       self.f.write('$[')
 | 
| 1154 |       self.cursor.SkipUntil(left_spid + 1)
 | 
| 1155 | 
 | 
| 1156 |       self.DoCommand(node.command_list, local_symbols)
 | 
| 1157 | 
 | 
| 1158 |       self.f.write(']')
 | 
| 1159 |       self.cursor.SkipUntil(right_spid + 1)
 | 
| 1160 |       # change to $[echo hi]
 | 
| 1161 | 
 | 
| 1162 |     elif node.tag == word_part_e.ArithSubPart:
 | 
| 1163 |       left_spid, right_spid = node.spids
 | 
| 1164 | 
 | 
| 1165 |       # Skip over left bracket and write our own.
 | 
| 1166 |       self.f.write('$(')
 | 
| 1167 |       self.cursor.SkipUntil(left_spid + 1)
 | 
| 1168 | 
 | 
| 1169 |       # NOTE: This doesn't do anything yet.
 | 
| 1170 |       self.DoArithExpr(node.anode, local_symbols)
 | 
| 1171 |       # Placeholder for now
 | 
| 1172 |       self.cursor.PrintUntil(right_spid - 1)
 | 
| 1173 | 
 | 
| 1174 |       # Skip over right bracket and write our own.
 | 
| 1175 |       self.f.write(')')
 | 
| 1176 |       self.cursor.SkipUntil(right_spid + 1)
 | 
| 1177 | 
 | 
| 1178 |     else:
 | 
| 1179 |       raise AssertionError(node.__class__.__name__)
 | 
| 1180 | 
 | 
| 1181 |   def DoArithExpr(self, node, local_symbols):
 | 
| 1182 |     if node.tag == arith_expr_e.ArithBinary:
 | 
| 1183 |       # Maybe I should just write the left span and right span for each word?
 | 
| 1184 |       #self.f.write(str(node.left))
 | 
| 1185 | 
 | 
| 1186 |       if node.op_id == Id.Arith_Plus:
 | 
| 1187 |         # NOTE: Right isn't necessarily a word!
 | 
| 1188 |         r_id = word.LeftMostSpanForWord(node.right.w)
 | 
| 1189 |         #self.cursor.SkipUntil(r_id)
 | 
| 1190 |         #self.f.write('PLUS')
 | 
| 1191 | 
 | 
| 1192 |       #self.f.write(str(node.right))
 | 
| 1193 |     elif node.tag == arith_expr_e.ArithWord:
 | 
| 1194 |       self.DoWordInCommand(node.w, local_symbols)
 | 
| 1195 | 
 | 
| 1196 |     else:
 | 
| 1197 |       raise AssertionError(node.__class__.__name__)
 | 
| 1198 | 
 | 
| 1199 |   def DoBoolExpr(self, node):
 | 
| 1200 |     # TODO: switch on node.tag
 | 
| 1201 |     pass
 | 
| 1202 | 
 | 
| 1203 | # WordPart?
 | 
| 1204 | 
 | 
| 1205 | # array_item
 | 
| 1206 | #
 | 
| 1207 | # These get turned into expressions
 | 
| 1208 | #
 | 
| 1209 | # bracket_op
 | 
| 1210 | # suffix_op
 | 
| 1211 | # prefix_op
 |