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

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