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

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