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

766 lines, 409 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 core 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 == 'proc':
180 self.arg = argv[0]
181 elif label == 'source':
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 val = self.mem.GetValue('PS4')
443 if val.tag() == value_e.Str:
444 ps4 = cast(value.Str, val).s
445 else:
446 ps4 = ''
447
448 # NOTE: This cache is slightly broken because aliases are mutable! I think
449 # that is more or less harmless though.
450 ps4_word = self.parse_cache.get(ps4)
451 if ps4_word is None:
452 # We have to parse this at runtime. PS4 should usually remain constant.
453 w_parser = self.parse_ctx.MakeWordParserForPlugin(ps4)
454
455 # NOTE: could use source.Variable, like $PS1 prompt does
456 try:
457 ps4_word = w_parser.ReadForPlugin()
458 except error.Parse as e:
459 ps4_word = word_.ErrorWord("<ERROR: Can't parse PS4: %s>" %
460 e.UserErrorString())
461 self.parse_cache[ps4] = ps4_word
462
463 # Mutate objects to save allocations
464 if self.exec_opts.xtrace_rich():
465 self.val_indent.s = self.indents[self.ind]
466 else:
467 self.val_indent.s = ''
468 self.val_punct.s = punct
469
470 # Prevent infinite loop when PS4 has command sub!
471 assert self.exec_opts.xtrace() # We shouldn't call this unless it's on
472
473 # TODO: Remove allocation for [] ?
474 with state.ctx_Option(self.mutable_opts, [option_i.xtrace], False):
475 with state.ctx_Temp(self.mem):
476 self.mem.SetNamed(self.lval_indent, self.val_indent,
477 scope_e.LocalOnly)
478 self.mem.SetNamed(self.lval_punct, self.val_punct,
479 scope_e.LocalOnly)
480 self.mem.SetNamed(self.lval_pid_str, self.val_pid_str,
481 scope_e.LocalOnly)
482 prefix = self.word_ev.EvalForPlugin(ps4_word)
483 return prefix.s
484
485 def _Inc(self):
486 # type: () -> None
487 self.ind += 1
488 if self.ind >= len(self.indents): # make sure there are enough
489 self.indents.append(' ' * self.ind)
490
491 def _Dec(self):
492 # type: () -> None
493 self.ind -= 1
494
495 def _ShTraceBegin(self):
496 # type: () -> Optional[mylib.BufWriter]
497 if not self.exec_opts.xtrace() or not self.exec_opts.xtrace_details():
498 return None
499
500 # Note: bash repeats the + for command sub, eval, source. Other shells
501 # don't do it. Leave this out for now.
502 prefix = self._EvalPS4('+')
503 buf = mylib.BufWriter()
504 buf.write(prefix)
505 return buf
506
507 def _RichTraceBegin(self, punct):
508 # type: (str) -> Optional[mylib.BufWriter]
509 """For the stack printed by xtrace_rich."""
510 if not self.exec_opts.xtrace() or not self.exec_opts.xtrace_rich():
511 return None
512
513 prefix = self._EvalPS4(punct)
514 buf = mylib.BufWriter()
515 buf.write(prefix)
516 return buf
517
518 def OnProcessStart(self, pid, why):
519 # type: (int, trace_t) -> None
520 """
521 In parent, Process::StartProcess calls us with child PID
522 """
523 UP_why = why
524 with tagswitch(why) as case:
525 if case(trace_e.External):
526 why = cast(trace.External, UP_why)
527
528 # There is the empty argv case of $(true), but it's never external
529 assert len(why.argv) > 0
530 self.multi_trace.EmitArgv0(why.argv[0])
531
532 buf = self._RichTraceBegin('|')
533 if not buf:
534 return
535
536 # TODO: ProcessSub and PipelinePart are commonly command.Simple, and also
537 # Fork/ForkWait through the BraceGroup. We could print those argv arrays.
538
539 with tagswitch(why) as case:
540 # Synchronous cases
541 if case(trace_e.External):
542 why = cast(trace.External, UP_why)
543 buf.write('command %d:' % pid)
544 _PrintYshArgv(why.argv, buf)
545
546 # Everything below is the same. Could use string literals?
547 elif case(trace_e.ForkWait):
548 buf.write('forkwait %d\n' % pid)
549 elif case(trace_e.CommandSub):
550 buf.write('command sub %d\n' % pid)
551
552 # Async cases
553 elif case(trace_e.ProcessSub):
554 buf.write('proc sub %d\n' % pid)
555 elif case(trace_e.HereDoc):
556 buf.write('here doc %d\n' % pid)
557 elif case(trace_e.Fork):
558 buf.write('fork %d\n' % pid)
559 elif case(trace_e.PipelinePart):
560 buf.write('part %d\n' % pid)
561
562 else:
563 raise AssertionError()
564
565 self.f.write(buf.getvalue())
566
567 def OnProcessEnd(self, pid, status):
568 # type: (int, int) -> None
569 buf = self._RichTraceBegin(';')
570 if not buf:
571 return
572
573 buf.write('process %d: status %d\n' % (pid, status))
574 self.f.write(buf.getvalue())
575
576 def OnNewProcess(self, child_pid):
577 # type: (int) -> None
578 """All trace lines have a PID prefix, except those from the root
579 process."""
580 self.val_pid_str.s = ' %d' % child_pid
581 self._Inc()
582 self.multi_trace.OnNewProcess(child_pid)
583
584 def PushMessage(self, label, argv):
585 # type: (str, Optional[List[str]]) -> None
586 """For synchronous constructs that aren't processes."""
587 buf = self._RichTraceBegin('>')
588 if buf:
589 buf.write(label)
590 if label == 'proc':
591 _PrintYshArgv(argv, buf)
592 elif label == 'source':
593 _PrintYshArgv(argv[1:], buf)
594 elif label == 'wait':
595 _PrintYshArgv(argv[1:], buf)
596 else:
597 buf.write('\n')
598 self.f.write(buf.getvalue())
599
600 self._Inc()
601
602 def PopMessage(self, label, arg):
603 # type: (str, Optional[str]) -> None
604 """For synchronous constructs that aren't processes.
605
606 e.g. source or proc
607 """
608 self._Dec()
609
610 buf = self._RichTraceBegin('<')
611 if buf:
612 buf.write(label)
613 if arg is not None:
614 buf.write(' ')
615 # TODO: use unquoted -> POSIX '' -> b''
616 buf.write(j8_lite.MaybeShellEncode(arg))
617 buf.write('\n')
618 self.f.write(buf.getvalue())
619
620 def OtherMessage(self, message):
621 # type: (str) -> None
622 """Can be used when receiving signals."""
623 buf = self._RichTraceBegin('!')
624 if not buf:
625 return
626
627 buf.write(message)
628 buf.write('\n')
629 self.f.write(buf.getvalue())
630
631 def OnExec(self, argv):
632 # type: (List[str]) -> None
633 buf = self._RichTraceBegin('.')
634 if not buf:
635 return
636 buf.write('exec')
637 _PrintYshArgv(argv, buf)
638 self.f.write(buf.getvalue())
639
640 def OnBuiltin(self, builtin_id, argv):
641 # type: (builtin_t, List[str]) -> None
642 if builtin_id in (builtin_i.eval, builtin_i.source, builtin_i.wait):
643 return # These 3 builtins handled separately
644
645 buf = self._RichTraceBegin('.')
646 if not buf:
647 return
648 buf.write('builtin')
649 _PrintYshArgv(argv, buf)
650 self.f.write(buf.getvalue())
651
652 #
653 # Shell Tracing That Begins with _ShTraceBegin
654 #
655
656 def OnSimpleCommand(self, argv):
657 # type: (List[str]) -> None
658 """For legacy set -x.
659
660 Called before we know if it's a builtin, external, or proc.
661 """
662 buf = self._ShTraceBegin()
663 if not buf:
664 return
665
666 # Redundant with OnProcessStart (external), PushMessage (proc), and OnBuiltin
667 if self.exec_opts.xtrace_rich():
668 return
669
670 # Legacy: Use SHELL encoding, NOT _PrintYshArgv()
671 PrintShellArgv(argv, buf)
672 buf.write('\n')
673 self.f.write(buf.getvalue())
674
675 def OnAssignBuiltin(self, cmd_val):
676 # type: (cmd_value.Assign) -> None
677 buf = self._ShTraceBegin()
678 if not buf:
679 return
680
681 for i, arg in enumerate(cmd_val.argv):
682 if i != 0:
683 buf.write(' ')
684 buf.write(arg)
685
686 for pair in cmd_val.pairs:
687 buf.write(' ')
688 buf.write(pair.var_name)
689 buf.write('=')
690 if pair.rval:
691 _PrintShValue(pair.rval, buf)
692
693 buf.write('\n')
694 self.f.write(buf.getvalue())
695
696 def OnShAssignment(self, lval, op, val, flags, which_scopes):
697 # type: (sh_lvalue_t, assign_op_t, value_t, int, scope_t) -> None
698 buf = self._ShTraceBegin()
699 if not buf:
700 return
701
702 left = '?'
703 UP_lval = lval
704 with tagswitch(lval) as case:
705 if case(sh_lvalue_e.Var):
706 lval = cast(LeftName, UP_lval)
707 left = lval.name
708 elif case(sh_lvalue_e.Indexed):
709 lval = cast(sh_lvalue.Indexed, UP_lval)
710 left = '%s[%d]' % (lval.name, lval.index)
711 elif case(sh_lvalue_e.Keyed):
712 lval = cast(sh_lvalue.Keyed, UP_lval)
713 left = '%s[%s]' % (lval.name, j8_lite.MaybeShellEncode(
714 lval.key))
715 buf.write(left)
716
717 # Only two possibilities here
718 buf.write('+=' if op == assign_op_e.PlusEqual else '=')
719
720 _PrintShValue(val, buf)
721
722 buf.write('\n')
723 self.f.write(buf.getvalue())
724
725 def OnControlFlow(self, keyword, arg):
726 # type: (str, int) -> None
727
728 # This is NOT affected by xtrace_rich or xtrace_details. Works in both.
729 if not self.exec_opts.xtrace():
730 return
731
732 prefix = self._EvalPS4('+')
733 buf = mylib.BufWriter()
734 buf.write(prefix)
735
736 buf.write(keyword)
737 buf.write(' ')
738 buf.write(str(arg)) # Note: 'return' is equivalent to 'return 0'
739 buf.write('\n')
740
741 self.f.write(buf.getvalue())
742
743 def PrintSourceCode(self, left_tok, right_tok, arena):
744 # type: (Token, Token, alloc.Arena) -> None
745 """For (( )) and [[ ]].
746
747 Bash traces these.
748 """
749 buf = self._ShTraceBegin()
750 if not buf:
751 return
752
753 line = left_tok.line.content
754 start = left_tok.col
755
756 if left_tok.line == right_tok.line:
757 end = right_tok.col + right_tok.length
758 buf.write(line[start:end])
759 else:
760 # Print first line only
761 end = -1 if line.endswith('\n') else len(line)
762 buf.write(line[start:end])
763 buf.write(' ...')
764
765 buf.write('\n')
766 self.f.write(buf.getvalue())