OILS / core / dev.py View on Github | oilshell.org

765 lines, 408 significant
1"""
2dev.py - Devtools / introspection.
3"""
4from __future__ import print_function
5
6from _devbuild.gen.option_asdl import option_i, builtin_i, builtin_t
7from _devbuild.gen.runtime_asdl import (cmd_value, scope_e, trace, trace_e,
8 trace_t)
9from _devbuild.gen.syntax_asdl import assign_op_e, Token
10from _devbuild.gen.value_asdl import (value, value_e, value_t, sh_lvalue,
11 sh_lvalue_e, LeftName)
12
13from core import error
14from core import optview
15from core import num
16from core import state
17from display import ui
18from data_lang import j8
19from frontend import location
20from osh import word_
21from data_lang import j8_lite
22from pylib import os_path
23from mycpp import mops
24from mycpp import mylib
25from mycpp.mylib import tagswitch, iteritems, print_stderr, log
26
27import posix_ as posix
28
29from typing import List, Dict, Optional, Any, cast, TYPE_CHECKING
30if TYPE_CHECKING:
31 from _devbuild.gen.syntax_asdl import assign_op_t, CompoundWord
32 from _devbuild.gen.runtime_asdl import scope_t
33 from _devbuild.gen.value_asdl import sh_lvalue_t
34 from core import alloc
35 from core.error import _ErrorWithLocation
36 from core import process
37 from core import util
38 from frontend.parse_lib import ParseContext
39 from osh.word_eval import NormalWordEvaluator
40 from osh.cmd_eval import CommandEvaluator
41
42_ = log
43
44
45class CrashDumper(object):
46 """Controls if we collect a crash dump, and where we write it to.
47
48 An object that can be serialized to JSON.
49
50 trap CRASHDUMP upload-to-server
51
52 # it gets written to a file first
53 upload-to-server() {
54 local path=$1
55 curl -X POST https://osh-trace.oilshell.org < $path
56 }
57
58 Things to dump:
59 CommandEvaluator
60 functions, aliases, traps, completion hooks, fd_state, dir_stack
61
62 debug info for the source? Or does that come elsewhere?
63
64 Yeah I think you should have two separate files.
65 - debug info for a given piece of code (needs hash)
66 - this could just be the raw source files? Does it need anything else?
67 - I think it needs a hash so the VM dump can refer to it.
68 - vm dump.
69 - Combine those and you get a UI.
70
71 One is constant at build time; the other is constant at runtime.
72 """
73
74 def __init__(self, crash_dump_dir, fd_state):
75 # type: (str, process.FdState) -> None
76 self.crash_dump_dir = crash_dump_dir
77 self.fd_state = fd_state
78
79 # whether we should collect a dump, at the highest level of the stack
80 self.do_collect = bool(crash_dump_dir)
81 self.collected = False # whether we have anything to dump
82
83 self.var_stack = None # type: List[value_t]
84 self.argv_stack = None # type: List[value_t]
85 self.debug_stack = None # type: List[value_t]
86 self.error = None # type: Dict[str, value_t]
87
88 def MaybeRecord(self, cmd_ev, err):
89 # type: (CommandEvaluator, _ErrorWithLocation) -> None
90 """Collect data for a crash dump.
91
92 Args:
93 cmd_ev: CommandEvaluator instance
94 error: _ErrorWithLocation (ParseError or error.FatalRuntime)
95 """
96 if not self.do_collect: # Either we already did it, or there is no file
97 return
98
99 self.var_stack, self.argv_stack, self.debug_stack = cmd_ev.mem.Dump()
100 blame_tok = location.TokenFor(err.location)
101
102 self.error = {
103 'msg': value.Str(err.UserErrorString()),
104 }
105
106 if blame_tok:
107 # Could also do msg % args separately, but JavaScript won't be able to
108 # render that.
109 self.error['source'] = value.Str(
110 ui.GetLineSourceString(blame_tok.line))
111 self.error['line_num'] = num.ToBig(blame_tok.line.line_num)
112 self.error['line'] = value.Str(blame_tok.line.content)
113
114 # TODO: Collect functions, aliases, etc.
115 self.do_collect = False
116 self.collected = True
117
118 def MaybeDump(self, status):
119 # type: (int) -> None
120 """Write the dump as JSON.
121
122 User can configure it two ways:
123 - dump unconditionally -- a daily cron job. This would be fine.
124 - dump on non-zero exit code
125
126 OILS_FAIL
127 Maybe counters are different than failure
128
129 OILS_CRASH_DUMP='function alias trap completion stack' ?
130 OILS_COUNTER_DUMP='function alias trap completion'
131 and then
132 I think both of these should dump the (path, mtime, checksum) of the source
133 they ran? And then you can match those up with source control or whatever?
134 """
135 if not self.collected:
136 return
137
138 my_pid = posix.getpid() # Get fresh PID here
139
140 # Other things we need: the reason for the crash! _ErrorWithLocation is
141 # required I think.
142 d = {
143 'var_stack': value.List(self.var_stack),
144 'argv_stack': value.List(self.argv_stack),
145 'debug_stack': value.List(self.debug_stack),
146 'error': value.Dict(self.error),
147 'status': num.ToBig(status),
148 'pid': num.ToBig(my_pid),
149 } # type: Dict[str, value_t]
150
151 path = os_path.join(self.crash_dump_dir,
152 '%d-osh-crash-dump.json' % my_pid)
153
154 # TODO: This should be JSON with unicode replacement char?
155 buf = mylib.BufWriter()
156 j8.PrintMessage(value.Dict(d), buf, 2)
157 json_str = buf.getvalue()
158
159 try:
160 f = self.fd_state.OpenForWrite(path)
161 except (IOError, OSError) as e:
162 # Ignore error
163 return
164
165 f.write(json_str)
166
167 # TODO: mylib.Writer() needs close()? Also for DebugFile()
168 #f.close()
169
170 print_stderr('[%d] Wrote crash dump to %s' % (my_pid, path))
171
172
173class ctx_Tracer(object):
174 """A stack for tracing synchronous constructs."""
175
176 def __init__(self, tracer, label, argv):
177 # type: (Tracer, str, Optional[List[str]]) -> None
178 self.arg = None # type: Optional[str]
179 if label in ('proc', 'module-invoke'):
180 self.arg = argv[0]
181 elif label in ('source', 'use'):
182 self.arg = argv[1]
183
184 tracer.PushMessage(label, argv)
185 self.label = label
186 self.tracer = tracer
187
188 def __enter__(self):
189 # type: () -> None
190 pass
191
192 def __exit__(self, type, value, traceback):
193 # type: (Any, Any, Any) -> None
194 self.tracer.PopMessage(self.label, self.arg)
195
196
197def _PrintShValue(val, buf):
198 # type: (value_t, mylib.BufWriter) -> None
199 """Print ShAssignment values.
200
201 NOTE: This is a bit like _PrintVariables for declare -p
202 """
203 # I think this should never happen because it's for ShAssignment
204 result = '?'
205
206 # Using maybe_shell_encode() because it's shell
207 UP_val = val
208 with tagswitch(val) as case:
209 if case(value_e.Str):
210 val = cast(value.Str, UP_val)
211 result = j8_lite.MaybeShellEncode(val.s)
212
213 elif case(value_e.BashArray):
214 val = cast(value.BashArray, UP_val)
215 parts = ['(']
216 for s in val.strs:
217 parts.append(j8_lite.MaybeShellEncode(s))
218 parts.append(')')
219 result = ' '.join(parts)
220
221 elif case(value_e.BashAssoc):
222 val = cast(value.BashAssoc, UP_val)
223 parts = ['(']
224 for k, v in iteritems(val.d):
225 # key must be quoted
226 parts.append(
227 '[%s]=%s' %
228 (j8_lite.ShellEncode(k), j8_lite.MaybeShellEncode(v)))
229 parts.append(')')
230 result = ' '.join(parts)
231
232 buf.write(result)
233
234
235def PrintShellArgv(argv, buf):
236 # type: (List[str], mylib.BufWriter) -> None
237 for i, arg in enumerate(argv):
238 if i != 0:
239 buf.write(' ')
240 buf.write(j8_lite.MaybeShellEncode(arg))
241
242
243def _PrintYshArgv(argv, buf):
244 # type: (List[str], mylib.BufWriter) -> None
245
246 # We're printing $'hi\n' for OSH, but we might want to print u'hi\n' or
247 # b'\n' for YSH. We could have a shopt --set xtrace_j8 or something.
248 #
249 # This used to be xtrace_rich, but I think that was too subtle.
250
251 for arg in argv:
252 buf.write(' ')
253 # TODO: use unquoted -> POSIX '' -> b''
254 # This would use JSON "", which CONFLICTS with shell. So we need
255 # another function.
256 #j8.EncodeString(arg, buf, unquoted_ok=True)
257
258 buf.write(j8_lite.MaybeShellEncode(arg))
259 buf.write('\n')
260
261
262class MultiTracer(object):
263 """ Manages multi-process tracing and dumping.
264
265 Use case:
266
267 TODO: write a shim for everything that autoconf starts out with
268
269 (1) How do you discover what is shelled out to?
270 - you need a MULTIPROCESS tracing and MULTIPROCESS errors
271
272 OILS_TRACE_DIR=_tmp/foo OILS_TRACE_STREAMS=xtrace:completion:gc \
273 OILS_TRACE_DUMPS=crash:argv0 \
274 osh ./configure
275
276 - Streams are written continuously, they are O(n)
277 - Dumps are written once per shell process, they are O(1). This includes metrics.
278
279 (2) Use that dump to generate stubs in _tmp/stubs
280 They will invoke benchmarks/time-helper, so we get timing and memory use
281 for each program.
282
283 (3) ORIG_PATH=$PATH PATH=_tmp/stubs:$PATH osh ./configure
284
285 THen the stub looks like this?
286
287 #!/bin/sh
288 # _tmp/stubs/cc1
289
290 PATH=$ORIG_PATH time-helper -x -e -- cc1 "$@"
291 """
292
293 def __init__(self, shell_pid, out_dir, dumps, streams, fd_state):
294 # type: (int, str, str, str, process.FdState) -> None
295 """
296 out_dir could be auto-generated from root PID?
297 """
298 # All of these may be empty string
299 self.out_dir = out_dir
300 self.dumps = dumps
301 self.streams = streams
302 self.fd_state = fd_state
303
304 self.this_pid = shell_pid
305
306 # This is what we consider an O(1) metric. Technically a shell program
307 # could run forever and keep invoking different binaries, but that is
308 # unlikely. I guess we could limit it to 1,000 or 10,000 artifically
309 # or something.
310 self.hist_argv0 = {} # type: Dict[str, int]
311
312 def OnNewProcess(self, child_pid):
313 # type: (int) -> None
314 """
315 Right now we call this from
316 Process::StartProcess -> tracer.SetChildPid()
317 It would be more accurate to call it from SubProgramThunk.
318
319 TODO: do we need a compound PID?
320 """
321 self.this_pid = child_pid
322 # each process keep track of direct children
323 self.hist_argv0.clear()
324
325 def EmitArgv0(self, argv0):
326 # type: (str) -> None
327
328 # TODO: Should we have word 0 in the source, and the FILE the $PATH
329 # lookup resolved to?
330
331 if argv0 not in self.hist_argv0:
332 self.hist_argv0[argv0] = 1
333 else:
334 # TODO: mycpp doesn't allow +=
335 self.hist_argv0[argv0] = self.hist_argv0[argv0] + 1
336
337 def WriteDumps(self):
338 # type: () -> None
339 if len(self.out_dir) == 0:
340 return
341
342 # TSV8 table might be nicer for this
343
344 metric_argv0 = [] # type: List[value_t]
345 for argv0, count in iteritems(self.hist_argv0):
346 a = value.Str(argv0)
347 c = value.Int(mops.IntWiden(count))
348 d = {'argv0': a, 'count': c}
349 metric_argv0.append(value.Dict(d))
350
351 # Other things we need: the reason for the crash! _ErrorWithLocation is
352 # required I think.
353 j = {
354 'pid': value.Int(mops.IntWiden(self.this_pid)),
355 'metric_argv0': value.List(metric_argv0),
356 } # type: Dict[str, value_t]
357
358 # dumps are named $PID.$channel.json
359 path = os_path.join(self.out_dir, '%d.argv0.json' % self.this_pid)
360
361 buf = mylib.BufWriter()
362 j8.PrintMessage(value.Dict(j), buf, 2)
363 json8_str = buf.getvalue()
364
365 try:
366 f = self.fd_state.OpenForWrite(path)
367 except (IOError, OSError) as e:
368 # Ignore error
369 return
370
371 f.write(json8_str)
372 f.close()
373
374 print_stderr('[%d] Wrote metrics dump to %s' % (self.this_pid, path))
375
376
377class Tracer(object):
378 """For OSH set -x, and YSH hierarchical, parsable tracing.
379
380 See doc/xtrace.md for details.
381
382 - TODO: Connect it somehow to tracers for other processes. So you can make
383 an HTML report offline.
384 - Could inherit SHX_*
385
386 https://www.gnu.org/software/bash/manual/html_node/Bash-Variables.html#Bash-Variables
387
388 Other hooks:
389
390 - Command completion starts other processes
391 - YSH command constructs: BareDecl, VarDecl, Mutation, Expr
392 """
393
394 def __init__(
395 self,
396 parse_ctx, # type: ParseContext
397 exec_opts, # type: optview.Exec
398 mutable_opts, # type: state.MutableOpts
399 mem, # type: state.Mem
400 f, # type: util._DebugFile
401 multi_trace, # type: MultiTracer
402 ):
403 # type: (...) -> None
404 """
405 trace_dir comes from OILS_TRACE_DIR
406 """
407 self.parse_ctx = parse_ctx
408 self.exec_opts = exec_opts
409 self.mutable_opts = mutable_opts
410 self.mem = mem
411 self.f = f # can be stderr, the --debug-file, etc.
412 self.multi_trace = multi_trace
413
414 self.word_ev = None # type: NormalWordEvaluator
415
416 self.ind = 0 # changed by process, proc, source, eval
417 self.indents = [''] # "pooled" to avoid allocations
418
419 # PS4 value -> CompoundWord. PS4 is scoped.
420 self.parse_cache = {} # type: Dict[str, CompoundWord]
421
422 # Mutate objects to save allocations
423 self.val_indent = value.Str('')
424 self.val_punct = value.Str('')
425 # TODO: show something for root process by default? INTERLEAVED output
426 # can be confusing, e.g. debugging traps in forkred subinterpreter
427 # created by a pipeline.
428 self.val_pid_str = value.Str('') # mutated by SetProcess
429
430 # Can these be global constants? I don't think we have that in ASDL yet.
431 self.lval_indent = location.LName('SHX_indent')
432 self.lval_punct = location.LName('SHX_punct')
433 self.lval_pid_str = location.LName('SHX_pid_str')
434
435 def CheckCircularDeps(self):
436 # type: () -> None
437 assert self.word_ev is not None
438
439 def _EvalPS4(self, punct):
440 # type: (str) -> str
441 """The prefix of each line."""
442 ps4 = self.mem.env_config.Get('PS4')
443 if ps4 is None:
444 ps4 = ''
445
446 # NOTE: This cache is slightly broken because aliases are mutable! I think
447 # that is more or less harmless though.
448 ps4_word = self.parse_cache.get(ps4)
449 if ps4_word is None:
450 # We have to parse this at runtime. PS4 should usually remain constant.
451 w_parser = self.parse_ctx.MakeWordParserForPlugin(ps4)
452
453 # NOTE: could use source.Variable, like $PS1 prompt does
454 try:
455 ps4_word = w_parser.ReadForPlugin()
456 except error.Parse as e:
457 ps4_word = word_.ErrorWord("<ERROR: Can't parse PS4: %s>" %
458 e.UserErrorString())
459 self.parse_cache[ps4] = ps4_word
460
461 # Mutate objects to save allocations
462 if self.exec_opts.xtrace_rich():
463 self.val_indent.s = self.indents[self.ind]
464 else:
465 self.val_indent.s = ''
466 self.val_punct.s = punct
467
468 # Prevent infinite loop when PS4 has command sub!
469 assert self.exec_opts.xtrace() # We shouldn't call this unless it's on
470
471 # TODO: Remove allocation for [] ?
472 with state.ctx_Option(self.mutable_opts, [option_i.xtrace], False):
473 with state.ctx_Temp(self.mem):
474 self.mem.SetNamed(self.lval_indent, self.val_indent,
475 scope_e.LocalOnly)
476 self.mem.SetNamed(self.lval_punct, self.val_punct,
477 scope_e.LocalOnly)
478 self.mem.SetNamed(self.lval_pid_str, self.val_pid_str,
479 scope_e.LocalOnly)
480 prefix = self.word_ev.EvalForPlugin(ps4_word)
481 return prefix.s
482
483 def _Inc(self):
484 # type: () -> None
485 self.ind += 1
486 if self.ind >= len(self.indents): # make sure there are enough
487 self.indents.append(' ' * self.ind)
488
489 def _Dec(self):
490 # type: () -> None
491 self.ind -= 1
492
493 def _ShTraceBegin(self):
494 # type: () -> Optional[mylib.BufWriter]
495 if not self.exec_opts.xtrace() or not self.exec_opts.xtrace_details():
496 return None
497
498 # Note: bash repeats the + for command sub, eval, source. Other shells
499 # don't do it. Leave this out for now.
500 prefix = self._EvalPS4('+')
501 buf = mylib.BufWriter()
502 buf.write(prefix)
503 return buf
504
505 def _RichTraceBegin(self, punct):
506 # type: (str) -> Optional[mylib.BufWriter]
507 """For the stack printed by xtrace_rich."""
508 if not self.exec_opts.xtrace() or not self.exec_opts.xtrace_rich():
509 return None
510
511 prefix = self._EvalPS4(punct)
512 buf = mylib.BufWriter()
513 buf.write(prefix)
514 return buf
515
516 def OnProcessStart(self, pid, why):
517 # type: (int, trace_t) -> None
518 """
519 In parent, Process::StartProcess calls us with child PID
520 """
521 UP_why = why
522 with tagswitch(why) as case:
523 if case(trace_e.External):
524 why = cast(trace.External, UP_why)
525
526 # There is the empty argv case of $(true), but it's never external
527 assert len(why.argv) > 0
528 self.multi_trace.EmitArgv0(why.argv[0])
529
530 buf = self._RichTraceBegin('|')
531 if not buf:
532 return
533
534 # TODO: ProcessSub and PipelinePart are commonly command.Simple, and also
535 # Fork/ForkWait through the BraceGroup. We could print those argv arrays.
536
537 with tagswitch(why) as case:
538 # Synchronous cases
539 if case(trace_e.External):
540 why = cast(trace.External, UP_why)
541 buf.write('command %d:' % pid)
542 _PrintYshArgv(why.argv, buf)
543
544 # Everything below is the same. Could use string literals?
545 elif case(trace_e.ForkWait):
546 buf.write('forkwait %d\n' % pid)
547 elif case(trace_e.CommandSub):
548 buf.write('command sub %d\n' % pid)
549
550 # Async cases
551 elif case(trace_e.ProcessSub):
552 buf.write('proc sub %d\n' % pid)
553 elif case(trace_e.HereDoc):
554 buf.write('here doc %d\n' % pid)
555 elif case(trace_e.Fork):
556 buf.write('fork %d\n' % pid)
557 elif case(trace_e.PipelinePart):
558 buf.write('part %d\n' % pid)
559
560 else:
561 raise AssertionError()
562
563 self.f.write(buf.getvalue())
564
565 def OnProcessEnd(self, pid, status):
566 # type: (int, int) -> None
567 buf = self._RichTraceBegin(';')
568 if not buf:
569 return
570
571 buf.write('process %d: status %d\n' % (pid, status))
572 self.f.write(buf.getvalue())
573
574 def OnNewProcess(self, child_pid):
575 # type: (int) -> None
576 """All trace lines have a PID prefix, except those from the root
577 process."""
578 self.val_pid_str.s = ' %d' % child_pid
579 self._Inc()
580 self.multi_trace.OnNewProcess(child_pid)
581
582 def PushMessage(self, label, argv):
583 # type: (str, Optional[List[str]]) -> None
584 """For synchronous constructs that aren't processes."""
585 buf = self._RichTraceBegin('>')
586 if buf:
587 buf.write(label)
588 if label in ('proc', 'module-invoke'):
589 _PrintYshArgv(argv, buf)
590 elif label in ('source', 'use'):
591 _PrintYshArgv(argv[1:], buf)
592 elif label == 'wait':
593 _PrintYshArgv(argv[1:], buf)
594 else:
595 buf.write('\n')
596 self.f.write(buf.getvalue())
597
598 self._Inc()
599
600 def PopMessage(self, label, arg):
601 # type: (str, Optional[str]) -> None
602 """For synchronous constructs that aren't processes.
603
604 e.g. source or proc
605 """
606 self._Dec()
607
608 buf = self._RichTraceBegin('<')
609 if buf:
610 buf.write(label)
611 if arg is not None:
612 buf.write(' ')
613 # TODO: use unquoted -> POSIX '' -> b''
614 buf.write(j8_lite.MaybeShellEncode(arg))
615 buf.write('\n')
616 self.f.write(buf.getvalue())
617
618 def OtherMessage(self, message):
619 # type: (str) -> None
620 """Can be used when receiving signals."""
621 buf = self._RichTraceBegin('!')
622 if not buf:
623 return
624
625 buf.write(message)
626 buf.write('\n')
627 self.f.write(buf.getvalue())
628
629 def OnExec(self, argv):
630 # type: (List[str]) -> None
631 buf = self._RichTraceBegin('.')
632 if not buf:
633 return
634 buf.write('exec')
635 _PrintYshArgv(argv, buf)
636 self.f.write(buf.getvalue())
637
638 def OnBuiltin(self, builtin_id, argv):
639 # type: (builtin_t, List[str]) -> None
640 if builtin_id in (builtin_i.eval, builtin_i.source, builtin_i.use,
641 builtin_i.wait):
642 return # These builtins are handled separately
643
644 buf = self._RichTraceBegin('.')
645 if not buf:
646 return
647 buf.write('builtin')
648 _PrintYshArgv(argv, buf)
649 self.f.write(buf.getvalue())
650
651 #
652 # Shell Tracing That Begins with _ShTraceBegin
653 #
654
655 def OnSimpleCommand(self, argv):
656 # type: (List[str]) -> None
657 """For legacy set -x.
658
659 Called before we know if it's a builtin, external, or proc.
660 """
661 buf = self._ShTraceBegin()
662 if not buf:
663 return
664
665 # Redundant with OnProcessStart (external), PushMessage (proc), and OnBuiltin
666 if self.exec_opts.xtrace_rich():
667 return
668
669 # Legacy: Use SHELL encoding, NOT _PrintYshArgv()
670 PrintShellArgv(argv, buf)
671 buf.write('\n')
672 self.f.write(buf.getvalue())
673
674 def OnAssignBuiltin(self, cmd_val):
675 # type: (cmd_value.Assign) -> None
676 buf = self._ShTraceBegin()
677 if not buf:
678 return
679
680 for i, arg in enumerate(cmd_val.argv):
681 if i != 0:
682 buf.write(' ')
683 buf.write(arg)
684
685 for pair in cmd_val.pairs:
686 buf.write(' ')
687 buf.write(pair.var_name)
688 buf.write('=')
689 if pair.rval:
690 _PrintShValue(pair.rval, buf)
691
692 buf.write('\n')
693 self.f.write(buf.getvalue())
694
695 def OnShAssignment(self, lval, op, val, flags, which_scopes):
696 # type: (sh_lvalue_t, assign_op_t, value_t, int, scope_t) -> None
697 buf = self._ShTraceBegin()
698 if not buf:
699 return
700
701 left = '?'
702 UP_lval = lval
703 with tagswitch(lval) as case:
704 if case(sh_lvalue_e.Var):
705 lval = cast(LeftName, UP_lval)
706 left = lval.name
707 elif case(sh_lvalue_e.Indexed):
708 lval = cast(sh_lvalue.Indexed, UP_lval)
709 left = '%s[%d]' % (lval.name, lval.index)
710 elif case(sh_lvalue_e.Keyed):
711 lval = cast(sh_lvalue.Keyed, UP_lval)
712 left = '%s[%s]' % (lval.name, j8_lite.MaybeShellEncode(
713 lval.key))
714 buf.write(left)
715
716 # Only two possibilities here
717 buf.write('+=' if op == assign_op_e.PlusEqual else '=')
718
719 _PrintShValue(val, buf)
720
721 buf.write('\n')
722 self.f.write(buf.getvalue())
723
724 def OnControlFlow(self, keyword, arg):
725 # type: (str, int) -> None
726
727 # This is NOT affected by xtrace_rich or xtrace_details. Works in both.
728 if not self.exec_opts.xtrace():
729 return
730
731 prefix = self._EvalPS4('+')
732 buf = mylib.BufWriter()
733 buf.write(prefix)
734
735 buf.write(keyword)
736 buf.write(' ')
737 buf.write(str(arg)) # Note: 'return' is equivalent to 'return 0'
738 buf.write('\n')
739
740 self.f.write(buf.getvalue())
741
742 def PrintSourceCode(self, left_tok, right_tok, arena):
743 # type: (Token, Token, alloc.Arena) -> None
744 """For (( )) and [[ ]].
745
746 Bash traces these.
747 """
748 buf = self._ShTraceBegin()
749 if not buf:
750 return
751
752 line = left_tok.line.content
753 start = left_tok.col
754
755 if left_tok.line == right_tok.line:
756 end = right_tok.col + right_tok.length
757 buf.write(line[start:end])
758 else:
759 # Print first line only
760 end = -1 if line.endswith('\n') else len(line)
761 buf.write(line[start:end])
762 buf.write(' ...')
763
764 buf.write('\n')
765 self.f.write(buf.getvalue())