OILS / display / ui.py View on Github | oilshell.org

602 lines, 316 significant
1# Copyright 2016 Andy Chu. All rights reserved.
2# Licensed under the Apache License, Version 2.0 (the "License");
3# you may not use this file except in compliance with the License.
4# You may obtain a copy of the License at
5#
6# http://www.apache.org/licenses/LICENSE-2.0
7"""
8ui.py - User interface constructs.
9"""
10from __future__ import print_function
11
12from _devbuild.gen.id_kind_asdl import Id, Id_t, Id_str
13from _devbuild.gen.syntax_asdl import (
14 Token,
15 SourceLine,
16 loc,
17 loc_e,
18 loc_t,
19 command_t,
20 command_str,
21 source,
22 source_e,
23)
24from _devbuild.gen.value_asdl import value_e, value_t
25from asdl import format as fmt
26from data_lang import j8_lite
27from display import pp_value
28from display import pretty
29from frontend import lexer
30from frontend import location
31from mycpp import mylib
32from mycpp.mylib import print_stderr, tagswitch, log
33import libc
34
35from typing import List, Tuple, Optional, Any, cast, TYPE_CHECKING
36if TYPE_CHECKING:
37 from _devbuild.gen import arg_types
38 from core import error
39 from core.error import _ErrorWithLocation
40
41_ = log
42
43
44def ValType(val):
45 # type: (value_t) -> str
46 """For displaying type errors in the UI."""
47
48 # TODO: consolidate these functions
49 return pp_value.ValType(val)
50
51
52def CommandType(cmd):
53 # type: (command_t) -> str
54 """For displaying commands in the UI."""
55
56 # Displays 'command.Simple' for now, maybe change it.
57 return command_str(cmd.tag())
58
59
60def PrettyId(id_):
61 # type: (Id_t) -> str
62 """For displaying type errors in the UI."""
63
64 # Displays 'Id.BoolUnary_v' for now
65 return Id_str(id_)
66
67
68def PrettyToken(tok):
69 # type: (Token) -> str
70 """Returns a readable token value for the user.
71
72 For syntax errors.
73 """
74 if tok.id == Id.Eof_Real:
75 return 'EOF'
76
77 val = tok.line.content[tok.col:tok.col + tok.length]
78 # TODO: Print length 0 as 'EOF'?
79 return repr(val)
80
81
82def PrettyDir(dir_name, home_dir):
83 # type: (str, Optional[str]) -> str
84 """Maybe replace the home dir with ~.
85
86 Used by the 'dirs' builtin and the prompt evaluator.
87 """
88 if home_dir is not None:
89 if dir_name == home_dir or dir_name.startswith(home_dir + '/'):
90 return '~' + dir_name[len(home_dir):]
91
92 return dir_name
93
94
95def _PrintCodeExcerpt(line, col, length, f):
96 # type: (str, int, int, mylib.Writer) -> None
97
98 buf = mylib.BufWriter()
99
100 buf.write(' ')
101
102 # TODO: Be smart about horizontal space when printing code snippet
103 # - Accept max_width param, which is terminal width or perhaps 100
104 # when there's no terminal
105 # - If 'length' of token is greater than max_width, then perhaps print 10
106 # chars on each side
107 # - If len(line) is less than max_width, then print everything normally
108 # - If len(line) is greater than max_width, then print up to max_width
109 # but make sure to include the entire token, with some context
110 # Print > < or ... to show truncation
111 #
112 # ^col 80 ^~~~~ error
113
114 buf.write(line.rstrip())
115
116 buf.write('\n ')
117 # preserve tabs
118 for c in line[:col]:
119 buf.write('\t' if c == '\t' else ' ')
120 buf.write('^')
121 buf.write('~' * (length - 1))
122 buf.write('\n')
123
124 # Do this all in a single write() call so it's less likely to be
125 # interleaved. See test/runtime-errors.sh errexit_multiple_processes
126 f.write(buf.getvalue())
127
128
129def _PrintTokenTooLong(loc_tok, f):
130 # type: (loc.TokenTooLong, mylib.Writer) -> None
131 line = loc_tok.line
132 col = loc_tok.col
133
134 buf = mylib.BufWriter()
135
136 buf.write(' ')
137 # Only print 10 characters, since it's probably very long
138 buf.write(line.content[:col + 10].rstrip())
139 buf.write('\n ')
140
141 # preserve tabs, like _PrintCodeExcerpt
142 for c in line.content[:col]:
143 buf.write('\t' if c == '\t' else ' ')
144
145 buf.write('^\n')
146
147 source_str = GetLineSourceString(loc_tok.line, quote_filename=True)
148 buf.write(
149 '%s:%d: Token starting at column %d is too long: %d bytes (%s)\n' %
150 (source_str, line.line_num, loc_tok.col, loc_tok.length,
151 Id_str(loc_tok.id)))
152
153 # single write() call
154 f.write(buf.getvalue())
155
156
157def GetLineSourceString(line, quote_filename=False):
158 # type: (SourceLine, bool) -> str
159 """Returns a human-readable string for dev tools.
160
161 This function is RECURSIVE because there may be dynamic parsing.
162 """
163 src = line.src
164 UP_src = src
165
166 with tagswitch(src) as case:
167 if case(source_e.Interactive):
168 s = '[ interactive ]' # This might need some changes
169 elif case(source_e.Headless):
170 s = '[ headless ]'
171 elif case(source_e.CFlag):
172 s = '[ -c flag ]'
173 elif case(source_e.Stdin):
174 src = cast(source.Stdin, UP_src)
175 s = '[ stdin%s ]' % src.comment
176
177 elif case(source_e.MainFile):
178 src = cast(source.MainFile, UP_src)
179 # This will quote a file called '[ -c flag ]' to disambiguate it!
180 # also handles characters that are unprintable in a terminal.
181 s = src.path
182 if quote_filename:
183 s = j8_lite.EncodeString(s, unquoted_ok=True)
184 elif case(source_e.SourcedFile):
185 src = cast(source.SourcedFile, UP_src)
186 # ditto
187 s = src.path
188 if quote_filename:
189 s = j8_lite.EncodeString(s, unquoted_ok=True)
190
191 elif case(source_e.ArgvWord):
192 src = cast(source.ArgvWord, UP_src)
193
194 # Note: _PrintWithLocation() uses this more specifically
195
196 # TODO: check loc.Missing; otherwise get Token from loc_t, then line
197 blame_tok = location.TokenFor(src.location)
198 if blame_tok is None:
199 s = '[ %s word at ? ]' % src.what
200 else:
201 line = blame_tok.line
202 line_num = line.line_num
203 outer_source = GetLineSourceString(
204 line, quote_filename=quote_filename)
205 s = '[ %s word at line %d of %s ]' % (src.what, line_num,
206 outer_source)
207
208 elif case(source_e.Variable):
209 src = cast(source.Variable, UP_src)
210
211 if src.var_name is None:
212 var_name = '?'
213 else:
214 var_name = repr(src.var_name)
215
216 if src.location.tag() == loc_e.Missing:
217 where = '?'
218 else:
219 blame_tok = location.TokenFor(src.location)
220 assert blame_tok is not None
221 line_num = blame_tok.line.line_num
222 outer_source = GetLineSourceString(
223 blame_tok.line, quote_filename=quote_filename)
224 where = 'line %d of %s' % (line_num, outer_source)
225
226 s = '[ var %s at %s ]' % (var_name, where)
227
228 elif case(source_e.VarRef):
229 src = cast(source.VarRef, UP_src)
230
231 orig_tok = src.orig_tok
232 line_num = orig_tok.line.line_num
233 outer_source = GetLineSourceString(orig_tok.line,
234 quote_filename=quote_filename)
235 where = 'line %d of %s' % (line_num, outer_source)
236
237 var_name = lexer.TokenVal(orig_tok)
238 s = '[ contents of var %r at %s ]' % (var_name, where)
239
240 elif case(source_e.Alias):
241 src = cast(source.Alias, UP_src)
242 s = '[ expansion of alias %r ]' % src.argv0
243
244 elif case(source_e.Reparsed):
245 src = cast(source.Reparsed, UP_src)
246 span2 = src.left_token
247 outer_source = GetLineSourceString(span2.line,
248 quote_filename=quote_filename)
249 s = '[ %s in %s ]' % (src.what, outer_source)
250
251 elif case(source_e.Synthetic):
252 src = cast(source.Synthetic, UP_src)
253 s = '-- %s' % src.s # use -- to say it came from a flag
254
255 else:
256 raise AssertionError(src)
257
258 return s
259
260
261def _PrintWithLocation(prefix, msg, blame_loc, show_code):
262 # type: (str, str, loc_t, bool) -> None
263 """Print an error message attached to a location.
264
265 We may quote code this:
266
267 echo $foo
268 ^~~~
269 [ -c flag ]:1: Failed
270
271 Should we have multiple locations?
272
273 - single line and verbose?
274 - and turn on "stack" tracing? For 'source' and more?
275 """
276 f = mylib.Stderr()
277 if blame_loc.tag() == loc_e.TokenTooLong:
278 # test/spec.sh parse-errors shows this
279 _PrintTokenTooLong(cast(loc.TokenTooLong, blame_loc), f)
280 return
281
282 blame_tok = location.TokenFor(blame_loc)
283 if blame_tok is None: # When does this happen?
284 f.write('[??? no location ???] %s%s\n' % (prefix, msg))
285 return
286
287 orig_col = blame_tok.col
288 src = blame_tok.line.src
289 line = blame_tok.line.content
290 line_num = blame_tok.line.line_num # overwritten by source.Reparsed case
291
292 if show_code:
293 UP_src = src
294
295 with tagswitch(src) as case:
296 if case(source_e.Reparsed):
297 # Special case for LValue/backticks
298
299 # We want the excerpt to look like this:
300 # a[x+]=1
301 # ^
302 # Rather than quoting the internal buffer:
303 # x+
304 # ^
305
306 # Show errors:
307 # test/parse-errors.sh text-arith-context
308
309 src = cast(source.Reparsed, UP_src)
310 tok2 = src.left_token
311 line_num = tok2.line.line_num
312
313 line2 = tok2.line.content
314 lbracket_col = tok2.col + tok2.length
315 # NOTE: The inner line number is always 1 because of reparsing.
316 # We overwrite it with the original token.
317 _PrintCodeExcerpt(line2, orig_col + lbracket_col, 1, f)
318
319 elif case(source_e.ArgvWord):
320 src = cast(source.ArgvWord, UP_src)
321 # Special case for eval, unset, printf -v, etc.
322
323 # Show errors:
324 # test/runtime-errors.sh test-assoc-array
325
326 #print('OUTER blame_loc', blame_loc)
327 #print('OUTER tok', blame_tok)
328 #print('INNER src.location', src.location)
329
330 # Print code and location for MOST SPECIFIC location
331 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
332 source_str = GetLineSourceString(blame_tok.line,
333 quote_filename=True)
334 f.write('%s:%d\n' % (source_str, line_num))
335 f.write('\n')
336
337 # Recursive call: Print OUTER location, with error message
338 _PrintWithLocation(prefix, msg, src.location, show_code)
339 return
340
341 else:
342 _PrintCodeExcerpt(line, blame_tok.col, blame_tok.length, f)
343
344 source_str = GetLineSourceString(blame_tok.line, quote_filename=True)
345
346 # TODO: If the line is blank, it would be nice to print the last non-blank
347 # line too?
348 f.write('%s:%d: %s%s\n' % (source_str, line_num, prefix, msg))
349
350
351def CodeExcerptAndPrefix(blame_tok):
352 # type: (Token) -> Tuple[str, str]
353 """Return a string that quotes code, and a string location prefix.
354
355 Similar logic as _PrintWithLocation, except we know we have a token.
356 """
357 line = blame_tok.line
358
359 buf = mylib.BufWriter()
360 _PrintCodeExcerpt(line.content, blame_tok.col, blame_tok.length, buf)
361
362 source_str = GetLineSourceString(line, quote_filename=True)
363 prefix = '%s:%d: ' % (source_str, blame_tok.line.line_num)
364
365 return buf.getvalue(), prefix
366
367
368class ctx_Location(object):
369
370 def __init__(self, errfmt, location):
371 # type: (ErrorFormatter, loc_t) -> None
372 errfmt.loc_stack.append(location)
373 self.errfmt = errfmt
374
375 def __enter__(self):
376 # type: () -> None
377 pass
378
379 def __exit__(self, type, value, traceback):
380 # type: (Any, Any, Any) -> None
381 self.errfmt.loc_stack.pop()
382
383
384# TODO:
385# - ColorErrorFormatter
386# - BareErrorFormatter? Could just display the foo.sh:37:8: and not quotation.
387#
388# Are these controlled by a flag? It's sort of like --comp-ui. Maybe
389# --error-ui.
390
391
392class ErrorFormatter(object):
393 """Print errors with code excerpts.
394
395 Philosophy:
396 - There should be zero or one code quotation when a shell exits non-zero.
397 Showing the same line twice is noisy.
398 - When running parallel processes, avoid interleaving multi-line code
399 quotations. (TODO: turn off in child processes?)
400 """
401
402 def __init__(self):
403 # type: () -> None
404 self.loc_stack = [] # type: List[loc_t]
405 self.one_line_errexit = False # root process
406
407 def OneLineErrExit(self):
408 # type: () -> None
409 """Unused now.
410
411 For SubprogramThunk.
412 """
413 self.one_line_errexit = True
414
415 # A stack used for the current builtin. A fallback for UsageError.
416 # TODO: Should we have PushBuiltinName? Then we can have a consistent style
417 # like foo.sh:1: (compopt) Not currently executing.
418 def _FallbackLocation(self, blame_loc):
419 # type: (Optional[loc_t]) -> loc_t
420 if blame_loc is None or blame_loc.tag() == loc_e.Missing:
421 if len(self.loc_stack):
422 return self.loc_stack[-1]
423 return loc.Missing
424
425 return blame_loc
426
427 def PrefixPrint(self, msg, prefix, blame_loc):
428 # type: (str, str, loc_t) -> None
429 """Print a hard-coded message with a prefix, and quote code."""
430 _PrintWithLocation(prefix,
431 msg,
432 self._FallbackLocation(blame_loc),
433 show_code=True)
434
435 def Print_(self, msg, blame_loc=None):
436 # type: (str, loc_t) -> None
437 """Print message and quote code."""
438 _PrintWithLocation('',
439 msg,
440 self._FallbackLocation(blame_loc),
441 show_code=True)
442
443 def PrintMessage(self, msg, blame_loc=None):
444 # type: (str, loc_t) -> None
445 """Print a message WITHOUT quoting code."""
446 _PrintWithLocation('',
447 msg,
448 self._FallbackLocation(blame_loc),
449 show_code=False)
450
451 def StderrLine(self, msg):
452 # type: (str) -> None
453 """Just print to stderr."""
454 print_stderr(msg)
455
456 def PrettyPrintError(self, err, prefix=''):
457 # type: (_ErrorWithLocation, str) -> None
458 """Print an exception that was caught, with a code quotation.
459
460 Unlike other methods, this doesn't use the GetLocationForLine()
461 fallback. That only applies to builtins; instead we check
462 e.HasLocation() at a higher level, in CommandEvaluator.
463 """
464 # TODO: Should there be a special span_id of 0 for EOF? runtime.NO_SPID
465 # means there is no location info, but 0 could mean that the location is EOF.
466 # So then you query the arena for the last line in that case?
467 # Eof_Real is the ONLY token with 0 span, because it's invisible!
468 # Well Eol_Tok is a sentinel with span_id == runtime.NO_SPID. I think that
469 # is OK.
470 # Problem: the column for Eof could be useful.
471
472 _PrintWithLocation(prefix, err.UserErrorString(), err.location, True)
473
474 def PrintErrExit(self, err, pid):
475 # type: (error.ErrExit, int) -> None
476
477 # TODO:
478 # - Don't quote code if you already quoted something on the same line?
479 # - _PrintWithLocation calculates the line_id. So you need to remember that?
480 # - return it here?
481 prefix = 'errexit PID %d: ' % pid
482 _PrintWithLocation(prefix, err.UserErrorString(), err.location,
483 err.show_code)
484
485
486def PrintAst(node, flag):
487 # type: (command_t, arg_types.main) -> None
488
489 if flag.ast_format == 'none':
490 print_stderr('AST not printed.')
491 if 0:
492 from _devbuild.gen.id_kind_asdl import Id_str
493 from frontend.lexer import ID_HIST, LAZY_ID_HIST
494
495 print(LAZY_ID_HIST)
496 print(len(LAZY_ID_HIST))
497
498 for id_, count in ID_HIST.most_common(10):
499 print('%8d %s' % (count, Id_str(id_)))
500 print()
501 total = sum(ID_HIST.values())
502 uniq = len(ID_HIST)
503 print('%8d total tokens' % total)
504 print('%8d unique tokens IDs' % uniq)
505 print()
506
507 for id_, count in LAZY_ID_HIST.most_common(10):
508 print('%8d %s' % (count, Id_str(id_)))
509 print()
510 total = sum(LAZY_ID_HIST.values())
511 uniq = len(LAZY_ID_HIST)
512 print('%8d total tokens' % total)
513 print('%8d tokens with LazyVal()' % total)
514 print('%8d unique tokens IDs' % uniq)
515 print()
516
517 if 0:
518 from osh.word_parse import WORD_HIST
519 #print(WORD_HIST)
520 for desc, count in WORD_HIST.most_common(20):
521 print('%8d %s' % (count, desc))
522
523 else: # text output
524 f = mylib.Stdout()
525
526 afmt = flag.ast_format # note: mycpp rewrite to avoid 'in'
527 if afmt in ('text', 'abbrev-text'):
528 ast_f = fmt.DetectConsoleOutput(f)
529 elif afmt in ('html', 'abbrev-html'):
530 ast_f = fmt.HtmlOutput(f)
531 else:
532 raise AssertionError()
533
534 if 'abbrev-' in afmt:
535 # ASDL "abbreviations" are only supported by asdl/gen_python.py
536 if mylib.PYTHON:
537 tree = node.AbbreviatedTree()
538 else:
539 tree = node.PrettyTree()
540 else:
541 tree = node.PrettyTree()
542
543 ast_f.FileHeader()
544 fmt.PrintTree(tree, ast_f)
545 ast_f.FileFooter()
546 ast_f.write('\n')
547
548
549def TypeNotPrinted(val):
550 # type: (value_t) -> bool
551 return val.tag() in (value_e.Null, value_e.Bool, value_e.Int,
552 value_e.Float, value_e.Str, value_e.List,
553 value_e.Dict, value_e.Obj)
554
555
556def _GetMaxWidth():
557 # type: () -> int
558 max_width = 80 # default value
559 try:
560 width = libc.get_terminal_width()
561 if width > 0:
562 max_width = width
563 except (IOError, OSError):
564 pass # leave at default
565
566 return max_width
567
568
569def PrettyPrintValue(prefix, val, f, max_width=-1):
570 # type: (str, value_t, mylib.Writer, int) -> None
571 """For the = keyword"""
572
573 encoder = pp_value.ValueEncoder()
574 encoder.SetUseStyles(f.isatty())
575
576 # TODO: pretty._Concat, etc. shouldn't be private
577 if TypeNotPrinted(val):
578 mdocs = encoder.TypePrefix(pp_value.ValType(val))
579 mdocs.append(encoder.Value(val))
580 doc = pretty._Concat(mdocs)
581 else:
582 doc = encoder.Value(val)
583
584 if len(prefix):
585 # If you want the type name to be indented, which we don't
586 # inner = pretty._Concat([pretty._Break(""), doc])
587
588 doc = pretty._Concat([
589 pretty.AsciiText(prefix),
590 #pretty._Break(""),
591 pretty._Indent(4, doc)
592 ])
593
594 if max_width == -1:
595 max_width = _GetMaxWidth()
596
597 printer = pretty.PrettyPrinter(max_width)
598
599 buf = mylib.BufWriter()
600 printer.PrintDoc(doc, buf)
601 f.write(buf.getvalue())
602 f.write('\n')