OILS / core / dev.py View on Github | oils.pub

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